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、新连接放入连接池,并返回
最后一步好理解吧,走到这里说明就要用这个新连接了,那么就把它存到连接池,返回这个连接。
这个拦截器确实麻烦,大家好好梳理下吧,我也再来个图:
饿汉单例为什么是线程安全的?
保证一个实例很简单,只要每次返回同一个实例就可以,关键是如何保证实例化过程的线程安全
?
这里先回顾下类的初始化
。
在类实例化之前,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
就代表一直阻塞。