Android消息机制和应用
版权声明:转载必须注明本文转自严振杰的博客: http://blog.yanzhenjie.com
本文主要讲的是Android消息机制的Java层,Android消息机制对Android开发者来说是一个基础知识,网络上介绍Android消息机制的文章很多,为了本文不显得多余,我争取从不同的角度来做一个解析,包括一些基础和源码分析。
我们知道Android的消息机制主要指Handler
、MessageQueue
和Looper
的运作机制、要想完全搞清楚Android的消息机制势必要先理解Binder IPC
机制、Linux pipe/epoll
机制,由于篇幅会过长,本文不会深入讲IPC
机制和pipe/epoll
机制,涉及到以上两点时会讲述它的基本作用和原理。
本文主要介绍的内容
本文主要要介绍的类:
- ThreadLocal
- Handler、Looper和MessageQueue
- HandlerThread
- ActivityThread
- ApplicationThread
- IntentService
读完本文后可以了解到的主要内容:
- 子线程为什么不能直接
new Handler()
? Handler
是如何切换线程的?Looper.loop()
死循环为什么不会导致主线程发生ANR?- ANR是如何发生的?
Activity
的生命周期是如何在Looper.loop()
死循环外执行的?
ThreadLocal
看到ThreadLocal
的第一感觉就是该类和线程有关,确实如此,但是要注意它不是线程,否则它就该叫LocalThread
了。
ThreadLocal
是用来存储指定线程的数据的,当某些数据的作用域是该指定线程并且该数据需要贯穿该线程的所有执行过程时就可以使用ThreadnLocal
存储数据,当某线程使用ThreadnLocal
存储数据后,只有该线程可以读取到存储的数据,除此线程之外的其他线程是没办法读取到该数据的。
一些读者看完上面这段话应该还是不理解ThreadLocal
的作用,我们举个栗子:
ThreadLocal<Boolean> local = new ThreadLocal<>();// 设置初始值为true.local.set(true);Boolean bool = local.get();Logger.i("MainThread读取的值为:" + bool);new Thread() { @Override public void run() { Boolean bool = local.get(); Logger.i("SubThread读取的值为:" + bool); // 设置值为false. local.set(false); }}.start():// 主线程睡1秒,确保上方子线程执行完毕再执行下面的代码。Thread.sleep(1000);Boolean newBool = local.get();Logger.i("MainThread读取的新值为:" + newBool);
读者朋友先不要往下看,请接合上面方的ThreadLocal
介绍猜测一下打印结果。
我想读者朋友应该都猜中了结果,第一条Log无可置疑,因为设置了值为true
,因此打印结果是:
MainThread读取的值为:true
对于第二条Log,根据上方介绍,某线程使用ThreadLocal
存储的数据,只能被该线程读取,因此第二条Log的结果是:
SubThread读取的值为:null
紧接着在子线程中设置了ThreadLocal
的值为false
,然后第三条Log将被打印,原理同上,子线程中设置了ThreadLocal
的值并不影响主线程的数据:
MainThread读取的值为:true
实验结果证实:就算是同一个ThreadLocal
对象,任一线程对其的set()
和get()
方法的操作都是相互独立互不影响的。
这就是开篇第一个问题的答案的基础知识,但是要完全理解第一问题还需要后面的内容的铺垫,因此我们先不揭晓第一个问题的完整答案。
Handler、Looper和MessageQueue
Handler
和Looper
组成了一个生产者消费者模式,Handler
作为生产者向MessageQueue
添加产物Message
,Looper
作为消费者,在Looper#loop()
方法的死循环中从MessageQueue#next()
循环取出Message
进行消费。
Handler
我们从Handler
的使用开始一步步翻阅源码做个简单的解析。我们在日常开发中使用Handler最多的场景是子线程向主线程发送消息更新UI,一般我们是这样向主线程发送消息的:
Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { // 这里是主线程 ... }};class UIThread implements Runnable { @Override public void run() { // 这里是主线程 ... }}class SubThread extends Thread { private Handler mHandler; SubThread(Handler handler) { this.mHandler = handler; } @Override public void run() { ... // 耗时操作。 // 第一种方法 mHandler.obtainMessage(1).sendToTarget(); ... // 耗时操作。 // 第二种方法 Message message = new Message(); message.what = 2; message.obj = ... mHandler.sendMessage(message); ... // 耗时操作。 // 第三种方法 mHandler.post(new UIThread()); }}
上述代码看起来比较笨拙,但是列出了Handler向主线程发送消息的常用方法,不过这都是Handler
为了使发送消息更加简单而提供的封装方法,他们都会产生一个Message
对象,包括Handler#post(Runnable)
最终也是剩成一个Message
:
public class Handler {...public final boolean sendMessage(Message msg){ return sendMessageDelayed(msg, 0);}public final boolean post(Runnable r){ return sendMessageDelayed(getPostMessage(r), 0);}private static Message getPostMessage(Runnable r) { Message message = Message.obtain(); message.callback = r; return message;}public final boolean sendMessageDelayed(Message msg, long delay) { if (delay < 0) { delay = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delay);}
最终它们都走向了Handler#enqueueMessage()
然后转而调用MessageQueue#enqueueMessage()
,其目的都是为了向MessageQueue
添加一个Message
:
public class Handler {...boolean sendMessageAtTime(Message msg, long uptime) { MessageQueue queue = mQueue; if (queue == null) { return false; } return enqueueMessage(queue, msg, uptimeMillis);}boolean enqueueMessage(MessageQueue queue, Message msg, long uptime) { msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis);}
看了上述代码,勤奋好学的我们得抛出一个问题,MessageQueue
中的消息是如何被读取的呢?可想而知的是,MessageQueue
中有消息进入,肯定是有消息出去的,想必是有成对的方法提供出来吧。
MessageQueue
我们在MessageQueue
的源码中发现了成对出现的进出方法(以下代码经过大量精简,读者应该自行翻阅一下源码,以免产生误导):
public class MessageQueue {...Message mMessages;boolean needWake;/** * 消息进入的方法。 */boolean enqueueMessage(Message msg, long when) { msg.when = when; Message p = mMessages; if (p == null || when == 0 || when < p.when) { msg.next = p; mMessages = msg; } else { Message prev; for (;;) { prev = p; p = p.next; if (p == null || when < p.when) { break; } } msg.next = p; prev.next = msg; } // 上面省去了对needWake的赋值逻辑 if (needWake) { // 唤醒阻塞 nativeWake(mPtr); } return true;}/** * 消息出去的方法。 */Message next() { int nextPollTimeMls = 0; for (;;) { // 尝试阻塞 nativePollOnce(ptr, nextPollTimeMls); Message prevMsg = null; Message msg = mMessages; if (msg != null && msg.target == null) { do { prevMsg = msg; msg = msg.next; } while (msg != null); } if (msg != null) { if (now < msg.when) { nextPollTimeMls = Math.min(msg.when - now, MAX_VALUE); } else { if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; return msg; } } else { nextPollTimeMls = -1; } nextPollTimeMls = 0; }}
从上述两个方法结合更多源码可以看出,MessageQueue
采用的是单向链表数据结构,mMessage
是链表的第一个元素,Message
的next
字段保存链表的下一个元素。
消息出去的方法在本文中非常重要,因此我们主要看一下MessageQueue#next()
方法,next()
里面有一个for(;;)
循环,循环体内调用了nativePollOnce(long, int)
方法,这是一个Native方法,实际作用是通过Native层的MessageQueue
阻塞当前调用栈线程nextPollTimeMls
毫秒的时间。
下面是nextPollTimeMls
取值的不同情况的阻塞表现:
- 小于0,一直阻塞,直到被唤醒
- 等于0,不会阻塞
- 大于0,最长阻塞
nextPollTimeMls
毫秒,期间如被唤醒会立即返回
MessageQueue
中有一个nativeWake(long)
的Native方法,可以唤醒nativePollOnce()
的阻塞。
现在回到next()
方法体中,我们看到循环开始前nextPollTimeMls
的值是0
,那么nativePollOnce()
方法将会立刻返回,此时尝试取出下一个Message
元素,如果没有下一个元素,nextPollTimeMls
的值被修改为-1
,此时nativePollOnce()
进入阻塞状态,等待下一个Message
的进入并唤醒阻塞,然后取出Message
对象返回。
Looper
现在知道消息从哪里出去的,那么接下来就看看Looper
是如何消费,Looper
类的代码很少,我贴出关键部分的代码(经过大量精简):
public class Looper {...static ThreadLocal<Looper> sThreadLocal = ...;private static Looper sMainLooper;MessageQueue mQueue;// 获取当前线程的的Looperpublic static Looper myLooper() { return sThreadLocal.get();}// 初始化当前线程的Looperpublic static void prepare() { if (myLooper() == null) { sThreadLocal.set(new Looper()); }}// 初始化主线程的Looperpublic static void prepareMainLooper() { if (sMainLooper == null) { prepare(); sMainLooper = myLooper(); }}// 获取主线程的Looperpublic static Looper getMainLooper() { return sMainLooper;}// Looper的构造方法中初始化MessageQueueprivate Looper() { mQueue = new MessageQueue();}// 循环处理当前线程的消息队列中的消息public static void loop() { final Looper nowLooper = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue nowQueue = nowLooper.mQueue; for (;;) { Message msg = nowQueue.next(); // 从消息队列读取下一条消息 if (msg == null) { // 如果读取到空消息,退出循环,退出该方法 return; } ... // 通过Handler分发消息 msg.target.dispatchMessage(msg); ... }}
上述代码中,在Looper
的构造方法中,初始化了Looper
的MessageQueue
对象;初始化Looper
和获取Looper
的方法使用到了ThreadLocal
,在ThreaqLocal
中我们介绍了ThreadLocal#get()
只能获取到当前线程保存的数据;在Looper#loop()
方法中首先判断了当前线程的Looper
是否为空,为空就抛出运行时异常,中断当前操作,不为空则进入死循环读取消息队列中的消息,把消息发回发送消息的Handler
去分发。
因此到这里我们可以得出第一个问题的答案了:
子线程为什么不能直接
new Handler()
?
我们回到Handler
中看看调用Handler
的空构造发生了什么:
public class Handler {...final MessageQueue mQueue;final Callback mCallback;public Handler() { this(null, false);}public Handler(Callback callback, boolean async) { mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; mCallback = callback;}
我们看到调用new Handler()
时会判断Looper.myLooper()
方法获取当前线程的Looper
,如果为空则会抛出运行时异常中断当前线程,不为空则拿当前线程的Looper
对象中的MessageQueue
对象,等待Handler#sendMessage()
等方法向消息队列中添加消息。
因此,在子线程中直接new Handler()
时,当前子线程的Looper
对象势必为空,为空则不能继续消费Handler
产生的Message
了,自然得抛出一个异常。
Handler/Looper运行机制梳理
简单的学习了Handler
、MessageQueue
和Looper
后我们不难发现,当某个线程要使用Android的Handler
消息机制时,首先要调用Looper#prepare()
静态方法为当前线程生成一个Looper
对象,紧接着调用Looper#loop()
静态方法后,会拿出该线程的Looper
对象的MessageQueue
开始循环调用MessageQueue#next()
方法获取消息队列的下一个Message
并处理。
根据上面MessageQueue
中的分析,当MessageQueue
中没有下一个Message
时,next()
方法会调用MessageQueue#nativePollOnce()
阻塞当前线程,直到下一个Message
被加入并通过MessageQueue#nativeWake()
唤醒阻塞,此时便可以拿出下一个Message
返回给Looper
,Looper
通过msg.target.dispatchMessage(msg)
分发消息。
为了理解的更加全面,接下来看看Handler#dispatchMessage(Message)
方法:
public class Handler {...final MessageQueue mQueue;final Callback mCallback;public interface Callback { public boolean handleMessage(Message msg);}public Handler() { this(null, false);}public Handler(Callback callback) { this(callback, false);}public Handler(Callback callback, boolean async) { mLooper = Looper.myLooper(); ... mQueue = mLooper.mQueue; mCallback = callback;}// Looper中调用的分发方法public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); }}// 处理Runnable的Messageprivate static void handleCallback(Message message) { message.callback.run();}// 处理非Runnable的Messagepublic void handleMessage(Message msg) {}
看到这里也就知道了,文章开头的Handler
的接受消息为什么那么用了。在Handler#dispatchMessage(Message)
中,首先判断了该Message
是否是Runnable
,如果是,则直接执行Runnable#run()
方法,如果不是则看当前Handler
是否有Callback
对象,如果有的话就回调到Callback#handleMessage(Message)
方法去,如果没有则调用Handler#handleMessage(Message)
方法。
明白了以上的流程,到了我们得出第二个问题的答案的时候了:
Handler
是如何切换线程的?
上面提到了,当某个线程要使用Android的消息机制时,首先必须要调用Looper#prepare()
方法为当前线程生成一个Looper
对象,然后在该线程中调用Looper#loop()
拿出该线程的Looper
对象的MessageQueue
开始循环处理其中的消息,如果消息队列为空,那么该线程就会被MessageQueue#nativePollOnce()
阻塞起来,只要该队列中进来消息时,该线程同时被MessageQueue#nativeWake()
唤醒。其他线程要向该线程发送消息时,只要拿到该线程的Looper
并在其他线程实例化Handler
,在其他线程中使用Handler
发送消息即可向该线程的MessageQueue
中添加一个消息,此时该线程的Looper#loop()
方法即可获取到消息并在该线程中处理了。
HandlerThread
根据以上原理我们来模拟一下使用流程,先封装一段代码:
public class HandlerThread extends Thread { private Looper mLooper; private Handler mHandler; public HandlerThread() { } @Override public void run() { Looper.prepare(); mLooper = Looper.myLooper(); Looper.loop(); } public Looper getLooper() { return mLooper; } public Handler getHandler() { if (mHandler == null) { mHandler = new Handler(getLooper()); } return mHandler; } public void quit() { if (mLooper != null) { mLooper.quit(); } }}
上述类中,在线程运行时,初始化了该线程的Looper
,并作为成员变量保存了起来,然后调用Looper#loop()
静态方法让该Looper
去处理发送到该线程的消息。
怎么用这个封装类呢?首先初始化一下让这个线程运行起来:
public class AbcThread implements Runnable {...HandlerThread thread = new HandlerThread();thread.start();
此时该线程应该阻塞在Looper#Loop()
处,如果我们发送一个Runnable
在这个线程执行,那么我们可以:
public class AbcThread implements Runnable {...thread.getHandler().post(new Runnable() { @Ovvride public void run() { // 该处代码执行在 HandleThread#run() 方法中 }});
另一种使用方式:
public class AbcThread implements Runnable {...Looper looper = thread.getLooper();Handler mHandler = new Handler(looper) { @Override public void handleMessage(Message msg) { // 该处代码执行在 HandlerThread#run() 方法中 }};Message message = new Message();...mHandler.sendMessage(message);
上述代码都是伪代码,读者明白原理即可,不要照抄。Android SDK中也提供了HandlerThread
类,原理和上述类相同,但是考虑更加全面,具体读者可以查看SDK源代码。
ActivityThread
到此很多人也应该明白了Android主线程的工作原理,下面我们通过ActivityThread
深入理解Android主线程的工作原理。
ActivityThread
是Android应用程序的入口,也就是任何一个进程的主线程入口。为了不增加读者理解上的复杂度,此处我们以单进程为例。对于单进程来说,ActivityThread
就是Android应用的主线程启动的类,我摘抄了一段最主要代码:
public final class ActivityThread {...public static void main(String[] args) { Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread(); thread.attach(false); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");}
我们看到了Java应用程序的main(String[])
方法,Android系统把每一个应用当做一个Java应用来看待,从main(String[])
方法中我们可以看到初始化了主线程Looper
并调用主线程Looper#loop()
循环处理发送到主线程的消息。
此时,我们可以得出第三个问题的答案了:
Looper.loop()
死循环为什么不会导致主线程发生ANR?
我们知道,如果main()
方法执行完,那么也意味着主线程生命周期结束,主线程即退出,应用程序也随即死掉。当然我们是绝不希望应用在手机上点一下icon后没啥反应,所以就要保证主线程一直或者。
那么如何保证能主线程一直存活呢?就是让该方法的代码一直执行下去的,也就出现了Looper#loop()
死循环,简单来说它保证了代码一直在执行,在消息队列中没有消息的时候该线程被MessageQueue#nativePollOnce()
阻塞,但是该线程还是活着的,所以我们的主线程活着意味着我们的应用程序是正在运行的。
因此,Looper.loop()
中的死循环和阻塞保证了主线程一直在运行,而不是挂掉,它运行过程就是主线程的运行过程,因此Looper.loop()
中的死循环和MessageQueue#nativePollOnce()
不会导致主线程发生ANR。
主线程内
MessageQueue#nativePollOnce()
一直阻塞,是否会特别消耗CPU资源呢?这里其实是利用了Linux pipe/epoll
机制,当MessageQueue#nativePollOnce()
阻塞时,此时主线程会释放CPU资源进入休眠状态,直到下一条消息被加入消息队列,并调用MessageQueue#nativeWake()
后,通过往pipe
管道写端写入数据来唤醒主线程工作。因此主线程在阻塞时,其实是处于休眠状态,并不会消耗大量CPU资源。
现在该总结第四个问题的答案了:
ANR是怎么发生的呢?
其实Android所有的UI操作都通过Handler
来发消息操作的,包括屏幕刷新,各种点击事件,Activity的生命周期等。因此当Looper.loop()
取到任一消息后,处理该消息的时间过长,影响到屏幕刷新速率,此时造成UI卡顿现象,乃至发生ANR。
主线程的运行机制大概如上,其中涉及到Linux pipe/epoll
机制可能是读者比较陌生的,但是只要知道它的作用即可理解这个过程。综上所述,我们在日常开发中,想从子线程往主线程发送消息时一般这样做:
private Handler mHandler = new Handler(Looper.getMainLooper());...mHandler.post(new Runnable(){ ...});
这样就可以让Runnable#run()
运行在主线程啦。
ApplicationThread
机智的读者们又发现了另一个问题,既然是死循环一直在执行,那么屏幕刷新,各种点击事件和Activity的生命周期等又是如何在主线程执行的?如果是其他地方发送了消息到MainLooper
后被执行,而主线程执行句柄一直在死循环和阻塞中,是否开启了新的线程来发送消息?也就是我们要探索的第五个问题:
Activity
的生命周期是如何在Looper.loop()
死循环外执行的?
答案是它确实开启了新线程来执行,其实在上文ActivityThread
中我们注意到:
public final class ActivityThread {...private ApplicationThread mAppThread = new ApplicationThread();public static void main(String[] args) { ... ActivityThread thread = new ActivityThread(); // 建立Binder通道,创建新线程 thread.attach(false); ...}private void attach(boolean system) { if (!system) { ... IActivityManager mgr = ActivityManager.getService(); mgr.attachApplication(mAppThread); ... } ...}
看了源码我们注意到,当Android应用启动时代码会执行到attach(boolean)
中system
为false
的情况下。比较明显的是mAppThrea
是ApplicationThread
类型,我们跟踪源码看一下ActivityManager.getService()
返回的IActivityManager
的实现类是什么:
public class ActivityManager {...public static IActivityManager getService() { return IActMngService.get();}private static final Singleton<IActivityManager> IActMngService = new Singleton<IActivityManager>() { @Override protected IActivityManager create() { IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE); IActivityManager am = IActivityManager.Stub.asInterface(b); return am; }};
其中Singleton
是帮助方便开发者实现的单例模式的类,ActivityManager#getService()
最终拿到的是ActivityManagerService
,既Binder
的客户端(C),也就是我们常说的AMS,它在SystemServer#run()
时被创建,并且运行一个独立的进程中。
:
private class ApplicationThread extends IApplicationThread.Stub { @Override public final void scheduleLaunchActivity(IBinder token, ...) { ... sendMessage(H.LAUNCH_ACTIVITY, token, ...); } public final void scheduleResumeActivity(IBinder token, ...) { ... sendMessage(H.RESUME_ACTIVITY, token, } public final void schedulePauseActivity(IBinder token, ...) { ... sendMessage(H.PAUSE_ACTIVITY, token, ...); } public final void scheduleStopActivity(IBinder token, ...) { ... sendMessage(H.STOP_ACTIVITY, token, ...); } public final void scheduleDestroyActivity(IBinder token, ...) { ... sendMessage(H.DESTROY_ACTIVITY, token, ...); } ...}
显而易见的是,ApplicationThread
是Binder
的服务端,也就是说这里远程方法被调用时,都运行在非主线程中,而是Binder
的线程池的线程中。
我们看到的上述方法都是在执行Activity
的生命周期,当ApplicationThread
的远程方法被调用时,就调用sendMessage()
发送一条消息,先来看一下源代码:
public final class ActivityThread {...final H mH = new H();private void sendMessage(int what, ...) { Message msg = Message.obtain(); msg.what = what; ... mH.sendMessage(msg);}private class H extends Handler { public static final int LAUNCH_ACTIVITY = 100; public static final int PAUSE_ACTIVITY = 101; ... public void handleMessage(Message msg) { switch (msg.what) { case LAUNCH_ACTIVITY: { ... handleLaunchActivity(...); break; } case PAUSE_ACTIVITY: { ... handlePauseActivity(...); break; } ... } }
在ApplicationThread
中调用sendMessage()
后最终H
类会收到消息并做出响应。根据我们前面的分析,H
被调用午餐构造时会获取Looper.myLooper()
作为自己的Looper
,那么H
又是ActivityThread
被实例化时初始化的,因此H
持有主线程的Looper
,这样一来H#handleMessage()
就回到主线程运行啦。
上方代码中,当H#handleMessage()
运行时,通过handleLaunchActivity()
和handlePauseActivity()
等方法反射创建Activity
实例,然后调用其生命周期方法。
上述就是Android消息机制Java层的所有内容,如果有没讲清楚的地方,还请读者朋友留言指教。
IntentService
接下来我们学习一个应用实例,IntentService
的本质也是四大组件之一的Service
,它与普通Service
不同的是,IntentService#onStart()
执行后会将操作发送到子线程去执行,其内部使用的就是我们上面提到的HandleThread
:
public abstract class IntentService extends Service { private volatile Looper mServiceLooper; private volatile ServiceHandler mServiceHandler; private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { onHandleIntent((Intent)msg.obj); stopSelf(msg.arg1); } } @Override public void onCreate() { super.onCreate(); HandlerThread thread = new HandlerThread(); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); } @Override public void onStart(@Nullable Intent intent, int startId) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; mServiceHandler.sendMessage(msg); } @Override public void onDestroy() { mServiceLooper.quit(); } protected abstract void onHandleIntent(@Nullable Intent intent);}
该例中使用HandlerThread
的方式与我们上面用的方式一致,先是启动HandlerThread
线程,然后使用该子线程的Looper
重新实例化了一个Handler
,等到onStart()
响应时,将操作通过新实例化的Handler
发送到HandlerThread
这个子线程去操作,由于消息队列在不设置优先级和执行时间时,遵循FIFO
的原则,也就是说它是以单线程的形式一个个的执行队列中的任务。
因此,如果我们平常使用的Service
中有耗时任务,不妨把它换成IntentService
来试试。
本文就到这里,觉得有帮助的朋友,请给我一个评论或者点赞,鼓励和批评都是我前进的动力,谢谢读者朋友耐心看完,告辞!
本文参考了以下链接:
- https://www.zhihu.com/question/34652589/answer/90344494
- https://pqpo.me/2017/05/03/learn-messagequeue/
版权声明:转载必须注明本文转自严振杰的博客: http://blog.yanzhenjie.com
更多相关文章
- Android 的消息队列模型
- Android进程 与 消息模型
- AndroidUI线程机制
- Android Handler机制7之消息发送
- 【Android 开发】: Android 消息处理机制之四: Android 消息循环
- 【Android 开发】: Android 消息处理机制之一: Handler 与 Messa
- AndroidAndroid程序提示和消息button响应事件
- Android异步处理系列文章四篇之一使用Thread+Handler实现非UI线
- Android消息循环