Android - 消息机制与线程通信

时间:2021-12-17 09:12:38

以下资料摘录整理自老罗的Android之旅博客,是对老罗的博客关于Android底层原理的一个抽象的知识概括总结(如有错误欢迎指出)(侵删):
http://blog.csdn.net/luoshengyang/article/details/8923485
http://blog.csdn.net/luoshengyang/article/details/12957169

整理by Doing

消息机制
        Android应用程序是通过消息来驱动的,系统为每一个应用程序维护一个消息队列,应用程序的主线程不断地从这个消息队例中获取消息(Looper),然后对这些消息进行处理(Handler),这样就实现了通过消息来驱动应用程序的执行。
        当ActivityManagerService需要与应用程序进行并互时,如加载Activity和Service、处理广播待,会通过Binder进程间通信机制来知会应用程序,应用程序接收到这个请求时,它不是马上就处理这个请求,而是将这个请求封装成一个消息,然后把这个消息放在应用程序的消息队列中去,然后再通过消息循环来处理这个消息。这样做的好处就是消息的发送方只要把消息发送到应用程序的消息队列中去就行了,它可以马上返回去处理别的事情,而不需要等待消息的接收方去处理完这个消息才返回,这样就可以提高系统的并发性。实质上,这就是一种异步处理机制
 
Android应用程序有两种类型的线程:
  1. 带有消息队列,用来执行循环性任务:有消息时就处理;没有消息时就睡眠;(例子:主线程、android.os.HandlerThread)
  2. 没有消息队列,用来执行一次性任务:任务一旦执行完成便退出;(例子:java.lang.Thread)
 
带有消息队列的线程四要素:
Message(消息)、MessageQueue(消息队列)、Looper(消息循环)、Handler(消息发送和处理)
 
Message、 MessageQueue、 Looper和Handler的交互过程:
Android - 消息机制与线程通信
MessageQueue与Looper的关系
Android - 消息机制与线程通信
 
消息循环
        消息循环过程是由Looper类来实现(主线程中已默认创建)。Android应用程序进程在启动的时候,会在进程中加载ActivityThread类,并且执行这个类的main函数,应用程序的消息循环过程就是在这个main函数里面实现的。
        在消息处理机制中,消息都是存放在一个消息队列中去,而应用程序的主线程就是围绕这个消息队列进入一个无限循环的,直到应用程序退出。如果队列中有消息,应用程序的主线程就会把它取出来,并分发给相应的Handler进行处理;如果队列中没有消息,应用程序的主线程就会进入空闲等待状态,等待下一个消息的到来。
 
  • 创建Java层的Looper: 在Java层,创建了一个Looper对象,这个Looper对象是用来进入消息循环的,它的内部有一个消息队列MessageQueue对象mQueue:
Android - 消息机制与线程通信
Looper.prepare/prepareMainLooper:
  • Looper类的静态成员函数prepareMainLooper是专门应用程序的主线程调用的,应用程序的其它子线程都不应该调用这个函数来在本线程中创建消息循环对象,而应该调用prepare函数来在本线程中创建消息循环对象:其它地方能够方便地通过Looper类的getMainLooper函数来获得应用程序主线程中的消息循环对象,进而能够向应用程序主线程发送消息。
Android - 消息机制与线程通信
 
  • 创建JNI层的Looper对象:在JNI层,创建了一个NativeMessageQueue对象,这个NativeMessageQueue对象保存在Java层的消息队列对象mQueue的成员变量mPtr中; 在C++层,创建了一个Looper对象,保存在JNI层的NativeMessageQueue对象的成员变量mLooper中,这个对象的作用是,当Java层的消息队列中没有消息时,就使Android应用程序主线程进入等待状态,而当Java层的消息队列中来了新的消息后,就唤醒Android应用程序的主线程来处理这个消息:
 
        JNI层的Looper对象通过pipe系统调用来创建了一个管道。
        管道是Linux系统中的一种进程间通信机制,管道就是一个文件,在管道的两端,分别是两个打开文件文件描述符,这两个打开文件描述符都是对应同一个文件,其中一个是用来读的,别一个是用来写的,一般的使用方式就是,一个线程通过读文件描述符中来读管道的内容,当管道没有内容时,这个线程就会进入等待状态,而另外一个线程通过写文件描述符来向管道中写入内容,写入内容的时候,如果另一端正有线程正在等待管道中的内容,那么这个线程就会被唤醒:
  1. 一种进程/线程间通信机制
  2. 包含一个写端文件描述符和一个读端文件描述符
  3. Looper通过读端文件描述符等待新消息的到来
  4. Handler通过写端文件描述符通知Looper新消息的到来
Android - 消息机制与线程通信
 
        epoll:管道上的等待和唤醒的操作要借助Linux系统中的epoll机制,Looper利用epoll来监控消息队列是否有新的消息,也就是监控消息管道的读端文件描述符。 Linux系统中的epoll机制为处理大批量句柄而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率:
  1. 一种I/O多路复用技术,select/poll加强版
  2. epoll_create:创建一个epoll句柄
  3. epoll_ctl:设置要监控的文件描述符
  4. epoll_wait:等待监控的文件描述符发生IO事件
        为什么要用epoll?:Looper除了监控消息管道之外,还需要监控其它文件描述符,例如,用来接收键盘/触摸屏事件的文件描述符
 
消息的发送
  • 在ActivityThread.queueOrSendMessage函数中,把上面传进来的参数封装成一个Message对象msg,然后通过mH.sendMessage函数把这个消息对象msg加入到应用程序的消息队列中去。(mH为H类,继承于Handler类)
  • 在Looper::wake()中,通过打开文件描述符mWakeWritePipeFd往管道的写入一个"W"字符串。其实,往管道写入什么内容并不重要,往管道写入内容的目的是为了唤醒应用程序的主线程。
 
JAVA层常用的消息发送接口:
  • Handler.sendMessage:带一个Message参数,用来描述消息的内容
  • Handler.post:带一个Runnable参数,会被转换为一个Message参数
(Handler.sendMessage/post、Handler.sendMessageDelayed、Handler.sendMessageAtTime.....)
 
Message
Android - 消息机制与线程通信
 
消息的处理
调用Handler类的handleMessage函数来处理消息
 
总结
         A. Android应用程序的消息处理机制由消息循环、消息发送和消息处理三个部分组成的。
         B. Android应用程序的主线程在进入消息循环过程前,会在内部创建一个Linux管道(Pipe),这个管道的作用是使得Android应用程序主线程在消息队列为空时可以进入空闲等待状态,并且使得当应用程序的消息队列有消息需要处理时唤醒应用程序的主线程。
         C. Android应用程序的主线程进入空闲等待状态的方式实际上就是在管道的读端等待管道中有新的内容可读,具体来说就是是通过Linux系统的Epoll机制中的epoll_wait函数进行的。
         D. 当往Android应用程序的消息队列中加入新的消息时,会同时往管道中的写端写入内容,通过这种方式就可以唤醒正在等待消息到来的应用程序主线程。
         E. 当应用程序主线程在进入空闲等待前,会认为当前线程处理空闲状态,于是就会调用那些已经注册了的IdleHandler接口,使得应用程序有机会在空闲的时候处理一些事情。

消息在异步任务的应用
在主线程为什么要用异步任务?
-    主线程任务繁重
  • 执行组件生命周期函数
  • 执行业务逻辑
  • 执行用户交互
  • 执行UI渲染
-    主线程处理某一个消息时间过长时会产生ANR
  • Service生命周期函数 – 20s
  • Broadcast Receiver接收前台优先级广播函数 –10s
  • Broadcast Receiver接收后台优先级广播函数 – 60s
  • 影响输入事件处理的函数 – 5s
  • 影响进程启动的函数 – 10s
  • 影响Activity切换的函数– 2s
 
基于消息的异步任务接口:
  • android.os.HandlerThread:适合用来处于不需要更新UI的后台任务
  • android.os.AyncTask:适合用来处于需要更新UI的后台任务
 
HandlerThread
  • HandlerThread类继承了Thread类,因此,通过它可以在应用程序中创建一个子线程;其次,在它的run函数中,首先是调用Looper类的静态成员函数prepare来准备一个消息循环对象,然后会进入一个消息循环中,因此,这个子线程可以常驻在应用程序中,直到它接收收到一个退出消息为止。
  • Looper类的myLooper成员函数将这个子线程中的消息循环对象保存在HandlerThread类中的成员变量mLooper中, 这样,其它地方就可以方便地通过它的getLooper函数来获得这个消息循环对象了,有了这个消息循环对象后,就可以往这个子线程的消息队列中发送消息,通知这个子线程执行特定的任务了
android.os.HandlerThread
Android - 消息机制与线程通信
 
AsyncTask类
  • 内部实现ThreadPoolExecutor类线程池
  • 获取创建AsyncTask对象的当前所在线程的Handler进行消息发送和处理
 
源码分析:
  • 当第一次创建一个AsyncTask对象时,首先会创建一个线程池sExecutor(ThreadPoolExecutor类,是Java提供的多线程机制之一。)
ThreadPoolExecutor:
  1. ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
  2. BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
        各个参数的意义如下:
        corePoolSize -- 线程池的核心线程数量
        maximumPoolSize -- 线程池的最大线程数量
        keepAliveTime -- 若线程池的线程数数量大于核心线程数量,那么空闲时间超过keepAliveTime的线程将被回收
        unit -- 参数keepAliveTime使用的时间单位
        workerQueue -- 工作任务队列
        threadFactory -- 用来创建线程池中的线程
        ThreadPoolExecutor的运行机制:每一个工作任务用一个Runnable对象来表示,当我们要把一个工作任务交给这个线程池来执行的时候,就通过调用ThreadPoolExecutor的execute函数来把这个工作任务加入到线程池中去。此时,如果线程池中的线程数量小于corePoolSize,那么就会调用threadFactory接口来创建一个新的线程并且加入到线程池中去,再执行这个工作任务;如果线程池中的线程数量等于corePoolSize,但是工作任务队列workerQueue未满,则把这个工作任务加入到工作任务队列中去等待执行;如果线程池中的线程数量大于corePoolSize,但是小于maximumPoolSize,并且工作任务队列workerQueue已经满了,那么就会调用threadFactory接口来创建一个新的线程并且加入到线程池中去,再执行这个工作任务;如果线程池中的线程量已经等于maximumPoolSize了,并且工作任务队列workerQueue也已经满了,这个工作任务就被拒绝执行了。
 
  • 创建好了线程池后,再创建一个消息处理器:
private static final InternalHandler sHandler = new InternalHandler();  
    这行代码是在应用程序的主线程中执行的,因此,这个消息处理器sHandler内部引用的消息循环对象looper是应用程序主线程的消息循环对象
 
  • 创建AsyncTask对象,即执行AsyncTask类的构造函数:
[java] view plain copy
 Android - 消息机制与线程通信Android - 消息机制与线程通信
  1. public AsyncTask() {
  2. mWorker = new WorkerRunnable<Params, Result>() {
  3. public Result call() throws Exception {
  4. ......
  5. return doInBackground(mParams);
  6. }
  7. };
  8. mFuture = new FutureTask<Result>(mWorker) {
  9. @Override
  10. protected void done() {
  11. Message message;
  12. Result result = null;
  13. try {
  14. result = get();
  15. } catch (InterruptedException e) {
  16. android.util.Log.w(LOG_TAG, e);
  17. } catch (ExecutionException e) {
  18. throw new RuntimeException("An error occured while executing doInBackground()",
  19. e.getCause());
  20. } catch (CancellationException e) {
  21. message = sHandler.obtainMessage(MESSAGE_POST_CANCEL,
  22. new AsyncTaskResult<Result>(AsyncTask.this, (Result[]) null));
  23. message.sendToTarget();
  24. return;
  25. } catch (Throwable t) {
  26. throw new RuntimeException("An error occured while executing "
  27. + "doInBackground()", t);
  28. }
  29. message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
  30. new AsyncTaskResult<Result>(AsyncTask.this, result));
  31. message.sendToTarget();
  32. }
  33. };
  34. }
 
1)    WorkerRunnable对象mWorker:WorkerRunnable类实现了Callable接口,它的内部成员变量mParams用于保存从AsyncTask对象的execute函数传进来的参数列表:
  1. private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
  2. Params[] mParams;
  3. }
 
2)    FutureTask对象mFuture:实现了Runnable接口,可以作为一个工作任务通过调用AsyncTask类的execute函数添加到sExecuto线程池中去:
  1. public final AsyncTask<Params, Progress, Result> execute(Params... params) {
  2. ......
  3. mWorker.mParams = params;
  4. sExecutor.execute(mFuture);
  5. return this;
  6. }
    当mFuture加入到线程池中执行时,它调用的是mWorker对象的call函数, 在call函数里面,会调用AsyncTask类的doInBackground函数来执行真正的任务:
  1. mWorker = new WorkerRunnable<Params, Result>() {
  2. public Result call() throws Exception {
  3. ......
  4. return doInBackground(mParams);
  5. }
  6. };
 
当mWorker执行call完成工作任务的时候,mFuture对象中的done函数就会被被调用,根据任务的完成状况,执行相应的操作:例如,如果是因为异常而完成时,就会抛异常;如果是正常完成,就会把任务执行结果封装成一个AsyncTaskResult对象:
  1. private static class AsyncTaskResult<Data> {
  2. final AsyncTask mTask;
  3. final Data[] mData;
  4. AsyncTaskResult(AsyncTask task, Data... data) {
  5. mTask = task;
  6. mData = data;
  7. }
  8. }
    其中,成员变量mData保存的是任务执行结果,mTask指向前面创建的AsyncTask对象。
    最后把这个AsyncTaskResult对象封装成一个消息,并且通过消息处理器sHandler(InternalHandler类)加入到应用程序主线程的消息队列中:
  1. message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
  2. new AsyncTaskResult<Result>(AsyncTask.this, result));
  3. message.sendToTarget();
     这个消息最终就会在InternalHandler类的handleMessage函数中处理(主线程中):
  1. private static class InternalHandler extends Handler {
  2. @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
  3. @Override
  4. public void handleMessage(Message msg) {
  5. AsyncTaskResult result = (AsyncTaskResult) msg.obj;
  6. switch (msg.what) {
  7. case MESSAGE_POST_RESULT:
  8. // There is only one result
  9. result.mTask.finish(result.mData[0]);
  10. break;
  11. ......
  12. }
  13. }
  14. }
在这个函数里面,最终会调用前面创建的这个AsyncTask对象的finish函数来进一步处理(这个函数是在应用程序的主线程中执行的,因此,它可以操作应用程序的界面):
  1. private void finish(Result result) {
  2. ......
  3. onPostExecute(result);
  4. ......
  5. }
 
  • 在任务执行的过程当中,即执行doInBackground函数时候,可能通过调用publishProgress函数来将中间结果封装成一个消息发送到应用程序主线程中的消息队列中去,这个消息最终也是由InternalHandler类的handleMessage函数来处理的(这个函数是在应用程序的主线程中执行的,因此,它和前面的onPostExecute函数一样,可以操作应用程序的界面):
  1. protected final void publishProgress(Progress... values) {
  2. sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
  3. new AsyncTaskResult<Progress>(this, values)).sendToTarget();
  4. }
 

键盘(Keyboard)消息处理机制
        在系统启动的时候,SystemServer会启动窗口管理服务WindowManagerService,WindowManagerService在启动的时候就会通过系统输入管理器InputManager来总负责监控键盘消息。这些键盘消息一般都是分发给当前激活的Activity窗口来处理的,因此,当前激活的Activity窗口在创建的时候,会到WindowManagerService中去注册一个接收键盘消息的通道,表明它要处理键盘消息,而当InputManager监控到有键盘消息时,就会分给给它处理。当当前激活的Activity窗口不再处于激活状态时,它也会到WindowManagerService中去反注册之前的键盘消息接收通道,这样,InputManager就不会再把键盘消息分发给它来处理。
 
InputManager的初始化工作
        A. 在Java层中的WindowManagerService中创建了一个InputManager对象,由它来负责管理Android应用程序框架层的键盘消息处理;
        B. 在C++层也相应地创建一个InputManager本地对象来负责监控键盘事件;
        C. 在C++层中的InputManager对象中,分别创建了一个InputReader对象和一个InputDispatcher对象,前者负责读取系统中的键盘消息,后者负责把键盘消息分发出去;
        D. InputReader对象和一个InputDispatcher对象分别是通过InputReaderThread线程实例和InputDispatcherThread线程实例来实现键盘消息的读取和分发的。
        在Looper类中,会创建一个管道,当调用Looper类的pollOnce函数时,如果管道中没有内容可读,那么当前线程就会进入到空闲等待状态;当有键盘事件发生时,InputReader就会往这个管道中写入新的内容,这样就会唤醒前面正在等待键盘事件发生的线程。
 
应用程序注册键盘消息接收通道的过程
当InputManager监控到有键盘消息时,就会先找到当前被激活的窗口,然后找到其在InputManager中对应的键盘消息接收通道,通过这个通道在InputManager中的一端来通知在应用程序消息循环中的另一端,就把键盘消息分发给当前激活的Activity窗口了。
        A. 即将会被激活的Activity窗口,会通知InputManager,它是当前激活的窗口,因此,一旦发生键盘事件的时候,InputManager就把这个键盘事件抛给这个Activity处理;
        B. 应用程序会为这个Activity窗口和InputManager之间创建一个键盘消息接收通道,这个通道的一端由一个Server端的InputChannel构成,另一端由Client端的InputChannel构成,Server端的InputChannel注册在由InputManager所管理的InputDispatcher中,而Client端的InputChannel注册在由应用程序主线程的消息循环对象Looper中;
        C. 注册在InputDispatcher中的InputChannel由一个反向管道的读端和一个前向管道的写端组成,而注册在应用程序主线程的消息循环对象Looper中的InputChannel由这个前向管道的读端和反向管道的写端组成,这种交叉结构使得当有键盘事件发生时,InputDispatcher可以把这个事件通知给应用程序。
 
InputManager分发键盘消息给应用程序的过程
        A. 键盘事件发生,InputManager中的InputReader被唤醒,此前InputReader睡眠在/dev/input/event0这个设备文件上;
        B. InputReader被唤醒后,它接着唤醒InputManager中的InputDispatcher,此前InputDispatcher睡眠在InputManager所运行的线程中的Looper对象里面的管道的读端上;
        C. InputDispatcher被唤醒后,它接着唤醒应用程序的主线程来处理这个键盘事件,此前应用程序的主线程睡眠在Client端InputChannel中的前向管道的读端上;
        D. 应用程序处理处理键盘事件之后,它接着唤醒InputDispatcher来执行善后工作,此前InputDispatcher睡眠在Server端InputChannel的反向管道的读端上,注意这里与第二个线索处的区别。
 
应用程序注销键盘消息接收通道的过程
        当Activity窗口创建时,它会向InputManager注册键盘消息接收通道,而当Activity窗口销毁时,它就会向InputManager注销前面注册的键盘消息接收通道了。
        当我们按下键盘上的Back键时,当前激活的Activity窗口就会被失去焦点,但是这时候它还没有被销毁,它的状态被设置为Stopped;当新的Activity窗口即将要显示时,它会通知WindowManagerService,这时候WindowManagerService就会处理当前处理Stopped状态的Activity窗口了,要执行的操作就是销毁它们了,在销毁的时候,就会注销它们之前所注册的键盘消息接收通道。