文章目录
- 前言
- 一、Handler源码分析
- 1、创建Handler
- 2、发送消息
- 3、取消息
- 4、消息处理
- 5、线程切换的方法(Handler异步消息处理机制流程)
- handler.sendMessage()
- handler.post()
- View.post()
- Activity中的runOnUiThread()
- 二、Handler高频面试题
- 1、为什么要有Handler?
- 2、为什么要有MessageQueue?
- 3、为什么要有Looper?
- 4、主线程的Looper和子线程Looper有什么不同?
- 5、一个线程可以有几个Handler,几个looper?
- 6、主线程会为什么会一直阻塞?
- 7、ANR是什么,发生条件
- 8、Looper的死循环为什么不会让主线程卡死(或ANR)?:
- 9、为什么Handler会造成内存泄露?
- 10、内存抖动如何解决?
- 11、安卓中的Looper.loop()阻塞为什么不会有问题?
前言
线程间的通信,两个线程使用公共的变量或者公共的其他东西都可以进行通信,但是这种方式不是自主的,不能够自主切换线程执行,所以Handler的最终目的是为了线程间的切换,线程异步消息处理
Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。
如果非要在子线程中更新UI,那会出现什么情况呢?
android.view.ViewRoot$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views.
很容易抛一个CalledFromWrongThreadException异常。
如果在子线程访问UI线程,Android提供了以下的方式:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
Handler
一、Handler源码分析
1、创建Handler
创建两个Handler对象,一个在主线程中创建,一个在子线程中创建,代码如下所示:
public class MainActivity extends AppCompatActivity {private Handler handler1;private Handler handler2;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);handler1 = new Handler();new Thread(new Runnable() {@Overridepublic void run() {handler2 = new Handler();}}).start();}
}
运行程序,你会发现,在子线程中创建的Handler(Handler2)是会导致程序崩溃的:
我们尝试在子线程中先调用一下Looper.prepare(),运行程序成功,不再报运行时异常
那这加上Looper.prepare()运行成功是为什么呢?此时我们分析一下Handler(基于android13 API 33)源码:
在第224行调用了Looper.myLooper()方法获取了一个Looper对象,如果Looper对象为空,则会抛出一个运行时异常。也就是我们上述出现的异常。
什么时候Looper对象会为空呢?接着看Looper.myLooper()中的代码:
sThreadLocal是一个关于Looper的ThreadLocal类
接着查找sThreadLocal查看是在哪里给sThreadLocal设置Looper,发现是Looper.prepare()方法
可以看到,首先判断sThreadLocal中是否存在Looper,如果没有则创建一个新的Looper设置进去。这样也就完全解释了为什么我们要先调用Looper.prepare()方法,才能创建Handler对象。同时也可以看出每个线程中最多只会有一个Looper对象。创建一个Looper对象会创建相应的MessageQueue,并且获取当前线程,故:Thread——>Looper——>MessageQueue是唯一对应的
所以Looper.prepare()的作用是创建一个新的Looper对象并设置到sThreadLocal中
Q:主线程中的Handler也没有调用Looper.prepare()方法,为什么就没有崩溃呢?
这是由于在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()方法。查看ActivityThread中的main()方法,代码如下所示:
public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");// Install selective syscall interceptionAndroidOs.install();// CloseGuard defaults to true and can be quite spammy. We// disable it here, but selectively enable it later (via// StrictMode) on debug builds, but using DropBox, not logs.CloseGuard.setEnabled(false);Environment.initForCurrentUser();// Make sure TrustedCertificateStore looks in the right place for CA certificatesfinal File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());TrustedCertificateStore.setDefaultUserDirectory(configDir);// Call per-process mainline module initialization.initializeMainlineModules();Process.setArgV0("<pre-initialized>");Looper.prepareMainLooper();// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.// It will be in the format "seq=114"long startSeq = 0;if (args != null) {for (int i = args.length - 1; i >= 0; --i) {if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {startSeq = Long.parseLong(args[i].substring(PROC_START_SEQ_IDENT.length()));}}}ActivityThread thread = new ActivityThread();thread.attach(false, startSeq);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();}if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}// End of event ActivityThreadMain.Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");
}
可以看到,在第20行调用了Looper.prepareMainLooper()方法,而这个方法又会再去调用Looper.prepare()方法,代码如下所示:
我们应用程序的主线程开启的时候就会创建一个Looper对象,从而不需要再手动去调用Looper.prepare()方法了。
这样基本就将Handler的创建过程完全搞明白了,总结一下就是在主线程中可以直接创建Handler对象,而在子线程中需要先调用ooper.prepare()才能创建Handler对象。
2、发送消息
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);handler1 = new Handler();new Thread(new Runnable() {@Overridepublic void run() {Message message = Message.obtain();message.arg1 = 1;Bundle bundle = new Bundle();bundle.putString("data", "data");message.setData(bundle);handler1.sendMessage(message);}}).start();
}
这里Handler到底是把Message发送到哪里去了呢?为什么之后又可以在Handler的handleMessage()方法中重新得到这条Message呢?看来又需要通过阅读源码才能解除我们心中的疑惑了:
调用了sendMessageDelayed:
接着调用了sendMessageAtTime,这个方法的源码如下所示:
sendMessageAtTime()方法接收两个参数,其中msg参数就是我们发送的Message对象,而uptimeMillis参数则表示发送消息的时间,它的值等于自系统开机到当前时间的毫秒数再加上延迟时间,如果你调用的不是sendMessageDelayed()方法,延迟时间就为0,然后将这两个参数都传递到MessageQueue的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) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = mBlocked;} else {// Inserted within the middle of the queue. Usually we don't have to wake// up the event queue unless there is a barrier at the head of the queue// and the message is the earliest asynchronous message in the queue.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; // invariant: p == prev.nextprev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {nativeWake(mPtr);}}return true;
}
MessageQueue并没有使用一个集合把所有的消息都保存起来,它只使用了一个mMessages对象表示当前待处理的消息。然后观察上面的代码我们就可以看出,所谓的入队其实就是将所有的消息按时间来进行排序,这个时间就是msg.when。具体的操作方法就根据时间的顺序调用msg.next,从而为每一个消息指定它的下一个消息是什么。当然如果你是通过sendMessageAtFrontOfQueue()方法来发送消息的,它也会调用enqueueMessage()来让消息入队,只不过时间为0,这时会把mMessages赋值为新入队的这条消息,然后将这条消息的next指定为刚才的mMessages,这样也就完成了添加消息到队列头部的操作。
3、取消息
入队操作我们就已经看明白了,那出队操作是在哪里进行的呢?这个就需要看一看Looper.loop()方法的源码了,如下所示:
此方法最后进入了一个死循环,然后不断地调用loopOnce()方法,这个方法作用为
Poll and deliver single message, return true if the outer loop should continue.
轮询并传递单个消息,如果外部循环应该继续,则返回true。
它的简单逻辑就是如果当前MessageQueue中存在mMessages(即待处理消息),就将这个消息出队,然后让下一条消息成为mMessages,否则就进入一个阻塞状态,一直等到有新的消息入队。每当有一个消息出队,就将它传递到msg.target的dispatchMessage()方法中,那这里msg.target又是什么呢?其实就是Handler,查看Message类源码:
PS:这里msg.target通过target将Handler存入Message,是为了解决在多个Hander的情况无法找到处理当前消息的Handler问题。实际上是一种架构设计上的妥协,我们常见的Hander内存泄漏问题也是源于此。最终导致Activity无法及时回收:
Thread–>Looper–>MessageQue–>Message.target–>mHandler–>Activity
4、消息处理
那么发送消息后,最终又是怎么调用到handleMessage的呢?接下来看一下Handler中dispatchMessage()方法的源码,如下所示:
在第101行进行判断,如果mCallback不为空,则调用mCallback的handleMessage()方法,否则直接调用Handler的handleMessage()方法,并将消息对象作为参数传递过去。这样我相信大家就都明白了为什么handleMessage()方法中可以获取到之前发送的消息了吧!
5、线程切换的方法(Handler异步消息处理机制流程)
handler.sendMessage()
我们接下来继续分析一下,为什么使用异步消息处理的方式就可以对UI进行操作了呢?这是由于Handler总是依附于创建时所在的线程,比如我们的Handler是在主线程中创建的,而在子线程中又无法直接对UI进行操作,于是我们就通过一系列的发送消息、入队、出队等环节,最后调用到了Handler的handleMessage()方法中,这时的handleMessage()方法已经是在主线程中运行的,因而我们当然可以在这里进行UI操作了。整个异步消息处理流程的示意图如下图所示:
handler.post()
还是调用了sendMessageDelayed()方法去发送一条消息啊,并且还使用了getPostMessage()方法将Runnable对象转换成了一条消息,我们来看下这个方法的源码:
在这个方法中将消息的callback字段的值指定为传入的Runnable对象。咦?这个callback字段看起来有些眼熟啊,在Handler的dispatchMessage()方法中原来有做一个检查,如果Message的callback等于null才会去调用handleMessage()方法,否则就调用handleCallback()方法。那我们快来看下handleCallback()方法中的代码吧:
竟然就是直接调用了一开始传入的Runnable对象的run()方法。因此在子线程中通过Handler的post()方法进行UI操作就可以这么写:
public class MainActivity extends Activity {private Handler handler;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);handler = new Handler();new Thread(new Runnable() {@Overridepublic void run() {handler.post(new Runnable() {@Overridepublic void run() {// 在这里进行UI操作}});}}).start();}
}
虽然写法上简洁很多,但是原理是完全一样的,我们在Runnable对象的run()方法里更新UI,效果完全等同于在handleMessage()方法中更新UI。
View.post()
原来就是调用了Handler中的post()方法
Activity中的runOnUiThread()
如果当前的线程不等于UI线程(主线程),就去调用Handler的post()方法,否则就直接调用Runnable对象的run()方法
二、Handler高频面试题
1、为什么要有Handler?
主要目的是要解决线程切换问题,handler里的Message机制解决了线程间通信
2、为什么要有MessageQueue?
MessageQueue是一个单向链表,next()调用nativePollOnce->lunx的epoll_wait()等待,实现阻塞时队列;
队列的出现解决了"处理消息"阻塞到"发送消息"的问题,由于队列是生产者消费者模式,而要使用队列需要至少两个线程与一个死循环;
- 一个线程负责生产消息;
- 一个线程消费消息;
- 死循环需要取出放入队列里的消息;
3、为什么要有Looper?
为了循环取出队列里的消息
4、主线程的Looper和子线程Looper有什么不同?
子线程Looper是可以退出的,主线程不行
5、一个线程可以有几个Handler,几个looper?
多个handler,每个handler都会配一个MessageQueue
Lopper和MessageQueue绑定,为防止创建多个messageQueue,Looper创建也只能被调用一次;
一个Looper,放在ThreadLocalMap中;
假如Looper对象由Handler创建,每创建一个Handler就有一个Looper,那么调用Looper.loop()时开启死循环;在外边调用Looper的地方就会阻塞
一个线程可以有多个Handler,并且每一个Handler都可以处理消息队列中的消息。每个Handler在创建时会与当前线程的消息队列相关联,因此可以通过Handler向该线程的消息队列发送消息。
需要注意的是,不同的Handler可能会被关联到相同的Looper(消息循环器)上,也可能不同的Handler使用各自独立的Looper来实现消息处理。例如,一个Activity可能会创建多个Handler对象,其中一些Handler会在主线程上执行,而另一些Handler则会在新建的子线程上执行,它们分别使用了不同的Looper来处理消息队列中的消息。
因此,可以说一个线程可以拥有多个Handler,这取决于应用程序设计的具体情况和需要。但是,由于在Android中每个线程都只有一个消息队列,因此多个Handler之间处理消息时可能会存在竞争和同步问题,需要开发者进行合理的规划和处理,以避免出现不必要的问题。
looper的生命周期是当前线程的生命周期长度,如何保证一个线程中只有一个Looper,可以通过线程ThreadLocal,ThreadLocal中会有一个ThreadLocalMap保存一个Looper,通过调用ThreadLocal的get()来判断是否能获取到Looper,如果能得到说明已经有了Looper直接返回一个异常通知已经有了Looper
Looper.prepare():保证只有一个Looper。存入Looper,存Looper时ThreadLocalMap的key为ThreadLocal,value为Looper;
sThreadLocal为ThreadLocal类
进入ThreadLocal类:获取当前线程:Thread.currentThread()
进入Thread类
ThreadLocalMap:类似于HashMap;每个Thread对象都有一个对应的ThreadLocalMap;
Looper.loop():循环提取消息并最终调用handlerMessage()去处理;
6、主线程会为什么会一直阻塞?
是的,如果主线程不进行looper.loop()阻塞,一下子执行完成,整个程序就直接结束了,不可能有机会去执行其他的任务了。
Android是事件为驱动的操作系统,事件过来就去handler里执行,没有事件就阻塞在那里显示界面;
sendMessage是生产者,handlerMessage是消费者;消息在队列中排队(MessageQueue),这样解决大量的消息过来的问题,不会造成主线程sendMessage阻塞,所有消息都会直接放在队列中排队等候执行;
7、ANR是什么,发生条件
ANR:Application Not Responding指的是应用程序无响应的错误,它表示应用程序在执行某个操作时长时间没有响应。在Android系统中,如果一个应用程序在主线程中执行了耗时的操作而导致主线程被阻塞,那么系统就会弹出一个对话框警告用户当前应用程序出现了ANR错误,并提示用户选择“等待”或“关闭应用程序”。
ANR通常是由于一些长时间的I/O操作、耗时的计算或者其他阻塞主线程的原因引起的。当主线程被阻塞时,应用程序的用户界面就会无响应,用户无法与应用程序进行交互,这就给用户带来了不好的体验。
ANR发生条件是:
Activity:应用在 5 秒内未响应用户的输入事件(如按键或者触摸)
BroadCastReceiver :BroadcastReceiver 未在 10 秒内完成相关的处理
Service:20 秒(均为前台)。Service 在20 秒内无法处理完成如果Handler收到以上三个相应事件在规定时间内完成了,则移除消息,不会ANR;若没完成则会超时处理,弹出ANR对话框;
为了避免ANR错误,开发人员可以采取以下措施:
- 将耗时的操作放在子线程中执行,避免在主线程中执行。
- 使用异步任务或线程池等机制来执行耗时的操作,从而避免阻塞主线程。
- 在主线程中使用Handler或者AsyncTask等机制来更新UI界面。
- 优化应用程序的代码,减少不必要的计算和I/O操作。
8、Looper的死循环为什么不会让主线程卡死(或ANR)?:
我们的UI线程(主线程)其实是ActivityThread所在的线程,而一个线程只会有一个Looper;
ActivityThread.java的main函数是一个APP进程的入口,如果不一直循环,则在main函数执行完最后一行代码后整个应用进程就会退出;
android是以事件为驱动的操作系统,当有事件来时,就去做对应的处理,没有时就显示静态界面;
App进程的入口为ActivityThread.java的main()函数,注意ActivityThread不是一个线程;
应用的UI主线程实际是调用ActivityThread.java的main()函数执行时所在的线程,而这个线程对我们不可见,但是这就是主线程:
在ActivityThread.java的main()函数中,会调用Looper.prepareMainLooper()Looper.prepareMainLooper()会创建一个Looper并放到主线程的变量threadLocals中进行绑定,threadLocals是一个ThreadLocal.ThreadLocalMap在ActivityThread.java的main()函数结尾,开启Looper.loop()进行死循环,不让main函数结束,从而让App进程不会结束;Android系统是以事件作为驱动的操作系统,当有事件来时,就去做对应处理,没有事件时,就显示当前界面,不做其他多余操作(浪费资源)在Looper.loop()的死循环中,不仅要取用户发的事件,还要取系统内核发的事件(如屏幕亮度改变等等)在调用Looper.loop()时,从MessageQueue.next()中获取事件,若没有则阻塞,有则分发MessageQueue其实不是一个队列,用epoll机制实现了阻塞。
在Looper.prepareMainLooper()时,会调用c++函数:
epoll_create()将App注册进epoll机制的红黑树中得到fd的值,
epoll_ctl()给每个App注册事件类型并监听fd值是否改变Linux中事件都会被写入文件中,如触摸屏幕事件会写入到:dev/input/event0文件中),fd有改变时唤醒epoll_wait,
epoll_wait()有事件时就分发,没事件就阻塞
9、为什么Handler会造成内存泄露?
内存泄漏:由于疏忽或错误造成程序未能释放已经不再使用的内存的情况
内存泄漏的原因:在Activity中,将Handler声明成非静态内部类或匿名内部类,这样Handle默认持有外部类Activity的引用。如果Activity在销毁时,Handler还有未执行完或者正在执行的Message,而Handler又持有Activity的引用,导致GC无法回收Activity,导致内存泄漏。如以下两种情形可能导致内存泄漏
1、在Activity内将Handler声明成匿名内部类
//匿名内部类private Handler mHandler = new Handler() {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);}};
new Handler().postDelayed(new Runnable() {@Overridepublic void run() {//大量的操作,activity要销毁时还没结束}},1000);
2、在Activity内将Handler声明成非静态内部类:
//非静态内部类private class MyHandler extends Handler{@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);}}private MyHandler mHandler = new MyHandler();
内存泄露的本质:长生命周期持有短生命周期,造成短生命周期得不到释放就会造成内存泄露;线程的生命周期长,Activity的生命周期短,Activity运行完Handler得不到释放;
1.在Activity中实例化Handler导致了,Activity中持有handler对象;2.Message和Handler的持有是由于在Lopper中进行循环遍历的时候,Message需要被执行,所以要使用handler的handleMessage()3.MessageQueue是Message的集合对象,所以造成持有关系;4.Looper又和MessageQueue进行了绑定,造成了Looper对MessageQueue的持有;最终:线程----->Looper----->MessageQueue----->Message----->Handler------>Activity,一系列持有造成的内存泄露
内存泄露两大解决方案:
1、静态内部类 + 弱引用
private static class MyHandler extends Handler {//弱引用,在垃圾回收时,activity可被回收private WeakReference<MainActivity> mWeakReference;public MyHandler(MainActivity activity) {mWeakReference = new WeakReference<>(activity);}@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);}}
2、在Activity销毁时,清空Handler中未执行或正在执行的Callback以及Message
@Overrideprotected void onDestroy() {super.onDestroy();//清空handler管道和队列mHandler.removeCallbacksAndMessages(null);}
10、内存抖动如何解决?
内存抖动根本的解决方式是复用handler.obtainMessage();
Message的创建方式有两种:
1.new Message()
2.obtainMessage()
内存抖动是为啥?因为短时间创建大量的对象并销毁。
使用obtainMessage创建一个Message,会有复用的作用,涉及到一个回收池,回收池中存放的是Message,会有一定的数量,使用单项链表MessageQueue来存放这些Message。每个message对象指向下一个Message对象
obtainMessage()创建对象是从回收池中获取,没有的才会进行创建,回收池中获取一个Message需要将管理回收池的列表同这个取出来的Message的关联进行切断,所以需要将此获取的Message的next引用置为空。并且sPool变量的引用将会变成下一个Message,同时单向列表的size-1
public static Message obtain() {synchronized (sPoolSync) {if (sPool != null) {Message m = sPool;sPool = m.next;m.next = null;m.flags = 0; // clear in-use flag sPoolSize--; return m; } return new Message();}
}
new Message()产生的对象是不会进回收池的。
从Looper的回收池中取Message;MessageQueue是一个单向链表,MessageQueue不是一个单纯的对象,而是一个链表集合,最大长度固定50个
11、安卓中的Looper.loop()阻塞为什么不会有问题?
Android是事件为驱动的操作系统,事件过来就去handler里执行(handler处理包括了创建服务,创建广播,结束服务,等等事件处理),如果没有事件过来就阻塞在那里,显示静止界面,有事件就去执行事件;
利用epoll机制可以定位到是哪个app接受这个事件,运用到红黑树,事件过来之后查找app来执行这个事件,事件带有一个标记,查找对应的app。