补:《Android面试题思考与解答》2021年3月刊(二)

简介: 回来啦,《Android面试题思考与解答21年3月刊》送给大家。

onStart可见的解释?可见进程


从另外的角度看,这个可见 可以指的是 可见进程。这就涉及到进程的分类。


为了确定在内存不足时应该终止哪些进程,Android 会根据每个进程中运行的组件以及这些组件的状态,将它们放入“重要性层次结构”。这些进程类型包括(按重要性排序):前台进程,可见进程,服务流程,缓存进程


这些进程是什么意思呢?


  • 前台进程是用户目前执行操作所需的进程。比如 正在用户的互动屏幕上运行一个 Activity(其 onResume() 方法已被调用)
  • 可见进程是正在进行用户当前知晓的任务。比如 正在运行的 Activity 在屏幕上对用户可见,但不在前台(其 onPause() 方法已被调用)
  • 服务流程包含一个已使用 startService() 方法启动的 Service。
  • 缓存进程是目前不需要的进程。比如 当前不可见的一个或多个 Activity 实例(onStop() 方法已被调用并返回)


所以Activity的生命周期又可以通过进程分为:


可见进程(onStart)——> 前台进程(onResume)——> 可见进程(onPause)——> 缓存进程(onStop)


这些进程有什么用呢?


我们都知道,在Android系统中有很多很多运行中的APP,也就代表了不同的进程。

当内存不够时(达到了某个阈值),系统首先会通过onTrimMemory()回调方法告诉应用,让应用自己来处理低内存情况下的减少内存操作。这之后,如果内存还是很紧张,那么就会开始对一些进程的杀除,以释放内存。这里就需要判断进程的优先级了,从低优先级开始按顺序终止进程。


所以,进程的分类作用就在这了。优先级的高低其实就代表了 终止进程的顺序,也代表了对用户的影响程度。


当然实际代码中,进程优先级是有数字表示的,也就是ADJ,而上面说的进程类型都有相应的进程优先级数字范围。比如:


public final class ProcessList {
    //可见进程
    static final int VISIBLE_APP_ADJ = 100;
    // 前台进程
    static final int FOREGROUND_APP_ADJ = 0;
    // 服务进程
    static final int SERVICE_ADJ = 500;
    // 缓存进程
    static final int CACHED_APP_MIN_ADJ = 900;
    //...
}


再回到我们的问题上来:


其中,可见进程这里也出现了可见的概念,给出的解释是:用户知晓


当我们点击一个页面,我们知道这个页面将要显示出来,也知道之前的页面在这个页面后面。所以这些页面和进程都是我们所知晓的,只是不在前台。


所以onStart表示的可见,也可以理解为可见进程,意思是这个Activity所在的进程任务已经被创建并显示,我们知晓它,只是没在前台。


介绍下okhttp中的设计模式


  • 外观模式。通过okHttpClient这个外观去实现内部各种功能。
  • 建造者模式。构建不同的Request对象。
  • 工厂模式。通过OkHttpClient生产出产品RealCall。
  • 享元模式。通过线程池、连接池共享对象。
  • 责任链模式。将不同功能的拦截器形成一个链。


具体讲解可以看之前的文章:


https://mp.weixin.qq.com/s/eHLXxjvMgII6c_FVRwwdjg


介绍下okhttp的拦截器


  • addInterceptor(Interceptor),这是由开发者设置的,会按照开发者的要求,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。


  • RetryAndFollowUpInterceptor,这里会对连接做一些初始化工作,以及请求失败的重试工作,重定向的后续请求工作。


  • BridgeInterceptor,这里会为用户构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包。


  • CacheInterceptor,这里主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。


  • ConnectInterceptor,这里主要就是负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec。


  • networkInterceptors,这里也是开发者自己设置的,所以本质上和第一个拦截器差不多,但是由于位置不同,用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。


  • CallServerInterceptor,这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。


okhttp的连接池工作流程,说说ConnectInterceptor。


连接拦截器,之前说了是关于TCP连接的。


object ConnectInterceptor : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val exchange = realChain.call.initExchange(chain)
    val connectedChain = realChain.copy(exchange = exchange)
    return connectedChain.proceed(realChain.request)
  }
}


代码看着倒是挺少的,但其实这里面很复杂很复杂,不着急,我们慢慢说。这段代码就执行了一个方法就是initExchange方法:


internal fun initExchange(chain: RealInterceptorChain): Exchange {
    val codec = exchangeFinder.find(client, chain)
    val result = Exchange(this, eventListener, exchangeFinder, codec)
    return result
  }
  fun find(
    client: OkHttpClient,
    chain: RealInterceptorChain
  ): ExchangeCodec {
    try {
      val resultConnection = findHealthyConnection(
          connectTimeout = chain.connectTimeoutMillis,
          readTimeout = chain.readTimeoutMillis,
          writeTimeout = chain.writeTimeoutMillis,
          pingIntervalMillis = client.pingIntervalMillis,
          connectionRetryEnabled = client.retryOnConnectionFailure,
          doExtensiveHealthChecks = chain.request.method != "GET"
      )
      return resultConnection.newCodec(client, chain)
    } 
  }


好像有一点眉目了,找到一个ExchangeCodec类,并封装成一个Exchange类。


  • ExchangeCodec:是一个连接所用的编码解码器,用于编码HTTP请求和解码HTTP响应。
  • Exchange:封装这个编码解码器的一个工具类,用于管理ExchangeCodec,处理实际的 I/O。


明白了,这个连接拦截器(ConnectInterceptor)就是找到一个可用连接呗,也就是TCP连接,这个连接就是用于HTTP请求和响应的。你可以把它可以理解为一个管道,有了这个管道,才能把数据丢进去,也才可以从管道里面取数据。


而这个ExchangeCodec,编码解码器就是用来读取和输送到这个管道的一个工具,相当于把你的数据封装成这个连接(管道)需要的格式。我咋知道的?我贴一段ExchangeCodec代码你就明白了:


//Http1ExchangeCodec.java
  fun writeRequest(headers: Headers, requestLine: String) {
    check(state == STATE_IDLE) { "state: $state" }
    sink.writeUtf8(requestLine).writeUtf8("\r\n")
    for (i in 0 until headers.size) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n")
    }
    sink.writeUtf8("\r\n")
    state = STATE_OPEN_REQUEST_BODY
  }


这里贴的是Http1ExchangeCodec的write代码,也就是Http1的编码解码器。


很明显,就是将Header信息一行一行写到sink中,然后再由sink交给输出流,具体就不分析了。只要知道这个编码解码器就是用来处理连接中进行输送的数据即可。


然后就是这个拦截器的关键了,连接到底是怎么获取的呢?继续看看:


private fun findConnection(): RealConnection {
    // 1、复用当前连接
    val callConnection = call.connection 
    if (callConnection != null) {
        //检查这个连接是否可用和可复用
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          toClose = call.releaseConnectionNoEvents()
        }
      return callConnection
    }
   //2、从连接池中获取可用连接
    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      val result = call.connection!!
      eventListener.connectionAcquired(call, result)
      return result
    }
    //3、从连接池中获取可用连接(通过一组路由routes)
    if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        return result
      }
    route = localRouteSelection.next()
    // 4、创建新连接
    val newConnection = RealConnection(connectionPool, route)
    newConnection.connect
    // 5、再获取一次连接,防止在新建连接过程中有其他竞争连接被创建了
    if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) { 
      return result
    }
    //6、还是要使用创建的新连接,放入连接池,并返回
    connectionPool.put(newConnection)
    return newConnection
  }


获取连接的过程很复杂,为了方便看懂,我简化了代码,分成了6步。


  • 1、检查当前连接是否可用。


怎么判断可用的?主要做了两个判断 1)判断是否不再接受新的连接 2)判断和当前请求有相同的主机名和端口号。


这倒是很好理解,要这个连接是连接的同一个地方才能复用是吧,同一个地方怎么判断?就是判断主机名和端口号


还有个问题就是为什么有当前连接??明明还没开始连接也没有获取连接啊,怎么连接就被赋值了?


还记得重试和重定向拦截器吗?对了,就是当请求失败需要重试的时候或者重定向的时候,这时候连接还在呢,是可以直接进行复用的。


  • 2和3、从连接池中获取可用连接


第2步和第3步都是从连接池获取连接,有什么不一样吗?


connectionPool.callAcquirePooledConnection(address, call, null, false)
connectionPool.callAcquirePooledConnection(address, call, routes, false)


好像多了一个routes字段?


这里涉及到HTTP/2的一个技术,叫做 HTTP/2 CONNECTION COALESCING(连接合并),什么意思呢?


假设有两个域名,可以解析为相同的IP地址,并且是可以用相同的TLS证书(比如通配符证书),那么客户端可以重用相同的TCP连接从这两个域名中获取资源。


再看回我们的连接池,这个routes就是当前域名(主机名)可以被解析的ip地址集合,这两个方法的区别也就是一个传了路由地址,一个没有传。


继续看callAcquirePooledConnection代码:


internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
    if (address.url.host == this.route().address.url.host) {
      return true 
    }
    //HTTP/2 CONNECTION COALESCING
    if (http2Connection == null) return false
    if (routes == null || !routeMatchesAny(routes)) return false
    if (address.hostnameVerifier !== OkHostnameVerifier) return false
    return true 
  }


1)判断主机名、端口号等,如果请求完全相同就直接返回这个连接。2)如果主机名不同,还可以判断是不是HTTP/2请求,如果是就继续判断路由地址,证书,如果都能匹配上,那么这个连接也是可用的。


  • 4、创建新连接


如果没有从连接池中获取到新连接,那么就创建一个新连接,这里就不多说了,其实就是调用到socket.connect进行TCP连接。


  • 5、再从连接池获取一次连接,防止在新建连接过程中有其他竞争连接被创建了


创建了新连接,为什么还要去连接池获取一次连接呢?因为在这个过程中,有可能有其他的请求和你一起创建了新连接,所以我们需要再去取一次连接,如果有可以用的,就直接用它,防止资源浪费。


其实这里又涉及到HTTP2的一个知识点:多路复用


简单的说,就是不需要当前连接的上一个请求结束之后再去进行下一次请求,只要有连接就可以直接用。


HTTP/2引入二进制数据帧和流的概念,其中帧对数据进行顺序标识,这样在收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。同样是因为有了序列,服务器就可以并行的传输数据,这就是流所做的事情。


所以在HTTP/2中可以保证在同一个域名只建立一路连接,并且可以并发进行请求。


  • 6、新连接放入连接池,并返回


最后一步好理解吧,走到这里说明就要用这个新连接了,那么就把它存到连接池,返回这个连接。


这个拦截器确实麻烦,大家好好梳理下吧,我也再来个图:


21.png


饿汉单例为什么是线程安全的?


保证一个实例很简单,只要每次返回同一个实例就可以,关键是如何保证实例化过程的线程安全

这里先回顾下类的初始化


在类实例化之前,JVM会执行类加载


而类加载的最后一步就是进行类的初始化,在这个阶段,会执行类构造器<clinit>方法,其主要工作就是初始化类中静态的变量,代码块。


<clinit>()方法是阻塞的,在多线程环境下,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>(),其他线程都会被阻塞。换句话说,<clinit>方法被赋予了线程安全的能力。


再结合我们要实现的单例,就很容易想到可以通过静态变量的形式创建这个单例,这个过程是线程安全的,所以我们得出了第一种单例实现方法:


private static Singleton singleton = new Singleton();
public static Singleton getSingleton() {
      return singleton;
}


很简单,就是通过静态变量实现唯一单例,并且是线程安全的。


看似比较完美的一个方法,也是有缺点的,就是有可能我还没有调用getSingleton方法的时候,就进行了类的加载,比如用到了反射或者类中其他的静态变量静态方法。所以这个方法的缺点就是有可能会造成资源浪费,在我没用到这个单例的时候就对单例进行了实例化。


在同一个类加载器下,一个类型只会被初始化一次,一共有六种能够触发类初始化的时机:

  • 1、虚拟机启动时,初始化包含 main 方法的主类;
  • 2、new等指令创建对象实例时
  • 3、访问静态方法或者静态字段的指令时
  • 4、子类的初始化过程如果发现其父类还没有进行过初始化
  • 5、使用反射API 进行反射调用时
  • 6、第一次调用java.lang.invoke.MethodHandle实例时


这种我不管你用不用,只要我这个类初始化了,我就要实例化这个单例,被类比为 饿汉方法。(是真饿了,先实例化出来放着吧,要吃的时候就可以直接吃了)


缺点就是 有可能造成资源浪费(到最后,饭也没吃上,饭就浪费了)


但其实这种模式一般也够用了,因为一般情况下用到这个实例的时候才会去用这个类,很少存在需要使用这个类但是不使用其单例的时候。


当然,话不能说绝了,也是有更好的办法来解决这种可能的资源浪费


kotlin 单例为什么这么简单?


object Singleton


没了?嗯,没了。


这里涉及到一个kotlin中才有的关键字:object(对象)


关于object主要有三种用法:


  • 对象表达式


主要用于创建一个继承自某个(或某些)类型的匿名类的对象。


window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*……*/ }
    override fun mouseEntered(e: MouseEvent) { /*……*/ }
})


  • 对象声明


主要用于单例。也就是我们今天用到的用法。


object Singleton


我们可以通过Android Studio 的 Show Kotlin Bytecode 功能,看到反编译后的java代码:


public final class Singleton {
   public static final Singleton INSTANCE;
   private Singleton() {
   }
   static {
      Singleton var0 = new Singleton();
      INSTANCE = var0;
   }
}


很显然,跟我们上一节写的饿汉差不多,都是在类的初始化阶段就会实例化出来单例,只不过一个是通过静态代码块,一个是通过静态变量。


  • 伴生对象


类内部的对象声明可以用 companion 关键字标记,有点像静态变量,但是并不是真的静态变量。


class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}
//使用
MyClass.create()


反编译成Java代码:


public final class MyClass {
   public static final MyClass.Factory Factory = new MyClass.Factory((DefaultConstructorMarker)null);
   public static final class Factory {
      @NotNull
      public final MyClass create() {
         return new MyClass();
      }
      private Factory() {
      }
      // $FF: synthetic method
      public Factory(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}


其原理还是一个静态内部类,最终调用的还是这个静态内部类的方法,只不过省略了静态内部类的名称。


要想实现真正的静态成员需要 @JvmField 修饰变量。


静态内部类单例的实现原理


有什么办法可以不浪费这个实例呢?也就是达到 按需加载 单例?


这就要涉及到另外一个知识点了,静态内部类的加载时机。


刚才说到类的加载时候,初始化过程只会加载静态变量和代码块,所以是不会加载静态内部类的。


静态内部类是延时加载的,意思就是说只有在明确用到内部类时才加载。只使用外部类时不加载。


根据这个信息,我们就可以优化刚才的 饿汉模式,改成静态内部类模式(java和kotlin版本)


private static class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }
    public static Singleton getSingleton() {
        return SingletonHolder.INSTANCE;
    }


companion object {
        val instance = SingletonHolder.holder
    }
    private object SingletonHolder {
        val holder = SingletonDemo()
    }


同样是通过类的初始化<clinit>()方法保证线程安全,并且在此之上,将单例的实例化过程向后移,移到静态内部类。所以就变成了当调用getSingleton方法的时候才会去初始化这个静态内部类,也就是才会实例化静态单例。


如此一整,这种方法就完美了...吗?好像也有缺点啊,比如我调用getSingleton方法创建实例的时候想传入参数怎么办呢?


可以,但是需要一开始就设置好参数值,无法通过调用getSingleton方法来动态设置参数。比如这样写:


private static class SingletonHolder {
        private static String test="123";
        private static Singleton INSTANCE = new Singleton(test);
    }
    public static Singleton getSingleton() {
        SingletonHolder.test="12345";
        return SingletonHolder.INSTANCE;
    }


最终实例化进去的test只会是123,而不是12345。因为只要你开始用到SingletonHolder内部类,单例INSTANCE就会最开始完成了实例化,即使你赋值了test,也是单例实例化之后的事了。


这个就是 静态内部类方法的缺点了。如果不用动态传参数,那么这个方法已经足够了。


双重校验单例方式的原理


加锁怎么加,也是个问题。


首先肯定的是,我们加的锁肯定是类锁,因为要针对这个类进行加锁,保证同一时间只有一个线程进行单例的实例化操作。


那么类锁就有两种加法了,修饰静态方法和修饰类对象:


//方法1,修饰静态方法
    public synchronized static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
//方法2,代码块修饰类对象
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }


方法2这种方式就是我们常说的双重校验的模式。


比较下两种方式其实区别也就是在这个双重校验,首先判断单例是否为空,如果为空再进入加锁阶段,正常走单例的实例化代码。


那么,为什么要这么做呢?


  • 第一个判断,是为了性能。当这个singleton已经实例化之后,我们再取值其实是不需要再进入加锁阶段的,所以第一个判断就是为了减少加锁。把加锁只控制在第一次实例化这个过程中,后续就可以直接获取单例即可。
  • 第二个判断,是防止重复创建对象。当两个线程同时走到synchronized这里,线程A获得锁,进入创建对象。创建完对象后释放锁,然后线程B获得锁,如果这时候没有判断单例是否为空,那么就会再次创建对象,重复了这个操作。


到这里,看似问题都解决了。


等等,new Singleton()这个实例化过程真的没问题吗?


在JVM中,有一种操作叫做指令重排


JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,会将指令进行重新排序,但是这种重新排序不会对单线程程序产生影响。


简单的说,就是在不影响最终结果的情况下,一些指令顺序可能会被打乱。


再看看在对象实例化中的指令主要有这三步操作:


  • 1、分配对象内存空间
  • 2、初始化对象
  • 3、instance指向刚分配的内存地址


如果我们将第二步和第三步重排一下,结果也是不影响的:


  • 1、分配对象内存空间
  • 2、instance指向刚分配的内存地址
  • 3、初始化对象


这种情况下,就有问题了:


当线程A进入实例化阶段,也就是new Singleton(),刚完成第二步分配好内存地址。这时候线程B调用了getSingleton()方法,走到第一个判空,发现不为空,返回单例,结果用的时候就有问题了,对象都没有初始化完成。


这就是指令重排有可能导致的问题。


所以,我们需要禁止指令重排,volatile 登场。


volatile 主要有两个特性:


  • 可见性。也就是写操作会对其他线程可见。
  • 禁止指令重排。


所以再加上volatile 对变量进行修饰,这个双重校验的单例模式也就完整了。


private volatile static Singleton singleton;


Handler被设计出来的原因?有什么用?


一种东西被设计出来肯定就有它存在的意义,而Handler的意义就是切换线程。


作为Android消息机制的主要成员,它管理着所有与界面有关的消息事件,常见的使用场景有:


  • 跨进程之后的界面消息处理。


比如Activity的启动,就是AMS在进行进程间通信的时候,通过Binder线程 将消息发送给ApplicationThread的消息处理者Handler,然后再将消息分发给主线程中去执行。


  • 网络交互后切换到主线程进行UI更新


当子线程网络操作之后,需要切换到主线程进行UI更新。


总之一句话,Hanlder的存在就是为了解决在子线程中无法访问UI的问题。


为什么建议子线程不访问(更新)UI?


因为Android中的UI控件不是线程安全的,如果多线程访问UI控件那还不乱套了。


那为什么不加锁呢?


  • 会降低UI访问的效率。本身UI控件就是离用户比较近的一个组件,加锁之后自然会发生阻塞,那么UI访问的效率会降低,最终反应到用户端就是这个手机有点卡。
  • 太复杂了。本身UI访问时一个比较简单的操作逻辑,直接创建UI,修改UI即可。如果加锁之后就让这个UI访问的逻辑变得很复杂,没必要。


所以,Android设计出了 单线程模型 来处理UI操作,再搭配上Handler,是一个比较合适的解决方案。


子线程访问UI的 崩溃原因 和 解决办法?


崩溃发生在ViewRootImpl类的checkThread方法中:


void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }


其实就是判断了当前线程 是否是 ViewRootImpl创建时候的线程,如果不是,就会崩溃。


而ViewRootImpl创建的时机就是界面被绘制的时候,也就是onResume之后,所以如果在子线程进行UI更新,就会发现当前线程(子线程)和View创建的线程(主线程)不是同一个线程,发生崩溃。


解决办法有三种:


  • 在新建视图的线程进行这个视图的UI更新,主线程创建View,主线程更新View。
  • ViewRootImpl创建之前进行子线程的UI更新,比如onCreate方法中进行子线程更新UI。
  • 子线程切换到主线程进行UI更新,比如Handler、view.post方法。


MessageQueue是干嘛呢?用的什么数据结构来存储数据?


看名字应该是个队列结构,队列的特点是什么?先进先出,一般在队尾增加数据,在队首进行取数据或者删除数据。


Hanlder中的消息似乎也满足这样的特点,先发的消息肯定就会先被处理。但是,Handler中还有比较特殊的情况,比如延时消息。


延时消息的存在就让这个队列有些特殊性了,并不能完全保证先进先出,而是需要根据时间来判断,所以Android中采用了链表的形式来实现这个队列,也方便了数据的插入。


来一起看看消息的发送过程,无论是哪种方法发送消息,都会走到sendMessageDelayed方法


public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        return enqueueMessage(queue, msg, uptimeMillis);
    }


sendMessageDelayed方法主要计算了消息需要被处理的时间,如果delayMillis为0,那么消息的处理时间就是当前时间。


然后就是关键方法enqueueMessage


boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; 
                prev.next = msg;
            }
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }


不懂得地方先不看,只看我们想看的:


  • 首先设置了Message的when字段,也就是代表了这个消息的处理时间
  • 然后判断当前队列是不是为空,是不是即时消息,是不是执行时间when大于表头的消息时间,满足任意一个,就把当前消息msg插入到表头。
  • 否则,就需要遍历这个队列,也就是链表,找出when小于某个节点的when,找到后插入。


好了,其他内容暂且不看,总之,插入消息就是通过消息的执行时间,也就是when字段,来找到合适的位置插入链表。


具体方法就是通过死循环,使用快慢指针p和prev,每次向后移动一格,直到找到某个节点p的when大于我们要插入消息的when字段,则插入到p和prev之间。或者遍历到链表结束,插入到链表结尾。


所以,MessageQueue就是一个用于存储消息、用链表实现的特殊队列结构。


延迟消息是怎么实现的?


总结上述内容,延迟消息的实现主要跟消息的统一存储方法有关,也就是上文说过的enqueueMessage方法。


无论是即时消息还是延迟消息,都是计算出具体的时间,然后作为消息的when字段进程赋值。


然后在MessageQueue中找到合适的位置(安排when小到大排列),并将消息插入到MessageQueue中。


这样,MessageQueue就是一个按照消息时间排列的一个链表结构。


MessageQueue的消息怎么被取出来的?


刚才说过了消息的存储,接下来看看消息的取出,也就是queue.next方法。


Message next() {
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
            }
        }
    }


奇怪,为什么取消息也是用的死循环呢?


其实死循环就是为了保证一定要返回一条消息,如果没有可用消息,那么就阻塞在这里,一直到有新消息的到来。


其中,nativePollOnce方法就是阻塞方法,nextPollTimeoutMillis参数就是阻塞的时间。


那什么时候会阻塞呢?两种情况:


  • 1、有消息,但是当前时间小于消息执行时间,也就是代码中的这一句:


if (now < msg.when) {
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}


这时候阻塞时间就是消息时间减去当前时间,然后进入下一次循环,阻塞。


  • 2、没有消息的时候,也就是上述代码的最后一句:


if (msg != null) {} 
    else {
    // No more messages.
    nextPollTimeoutMillis = -1;
    }


-1就代表一直阻塞。


目录
相关文章
|
7月前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
193 1
|
7月前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
82 0
|
7月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
74 8
|
7月前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
95 3
|
7月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
85 6
|
7月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
52 0
|
7月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
51 0
|
7月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
89 2
|
7月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
205 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
7月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
82 1

热门文章

最新文章

  • 1
    如何修复 Android 和 Windows 不支持视频编解码器的问题?
  • 2
    Android历史版本与APK文件结构
  • 3
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 4
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 5
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 6
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 7
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 8
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
  • 9
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 10
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
  • 1
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
    24
  • 2
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    33
  • 3
    Android历史版本与APK文件结构
    121
  • 4
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    29
  • 5
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
    23
  • 6
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
    57
  • 7
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    37
  • 8
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
    73
  • 9
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    118
  • 10
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
    29