对Android Handler部分知识,予以记录。
线程间的通信,两个线程使用公共的变量或者公共的其他东西都可以进行通信,但是这种方式不是自主的,不能够自主切换线程执行,所以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对象,一个在主线程中创建,一个在子线程中创建,代码如下所示:
public class MainActivity extends AppCompatActivity {
private Handler handler1;
private Handler handler2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler1 = new Handler();
new Thread(new Runnable() {
@Override
public 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.set()会将当前ThreadLocal类与Looper set进ThreadLocalMap中,ThreadLocalMap是ThreadLocal中的静态类
而创建一个Looper会相应创建一个MessageQueue,并且获取当前线程
可以看到,首先判断sThreadLocal中是否存在Looper,如果没有则创建一个新的Looper设置进去。这样也就完全解释了为什么我们要先调用Looper.prepare()方法,才能创建Handler对象。同时也可以看出每个线程中最多只会有一个Looper对象。创建一个Looper对象会创建相应的MessageQueue,并且获取当前线程,故:Thread——>Looper——>MessageQueue是唯一对应的
所以Looper.prepare()的作用是创建一个新的Looper对象并设置到sThreadLocal的ThreadLocalMap中
Q:主线程中的Handler也没有调用Looper.prepare()方法,为什么就没有崩溃呢?
这是由于在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()方法。查看ActivityThread.java中的main()方法,代码如下所示:
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
// Install selective syscall interception
AndroidOs.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 certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);
// Call per-process mainline module initialization.
initializeMainlineModules();
Process.setArgV0("" );
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(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
可以看到,在第23行调用了Looper.prepareMainLooper()方法,而这个方法又会再去调用Looper.prepare()方法,注意此时传入的参数quitAllowed为false,代码如下所示:
我们应用程序的主线程开启的时候就会创建一个Looper对象,从而不需要再手动去调用Looper.prepare()方法了。
这样基本就将Handler的创建过程完全搞明白了,总结一下就是在主线程中可以直接创建Handler对象,而在子线程中需要先调用Looper.prepare()才能创建Handler对象。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler1 = new Handler();
new Thread(new Runnable() {
@Override
public 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.next
prev.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,这样也就完成了添加消息到队列头部的操作。
入队操作我们就已经看明白了,那出队操作是在哪里进行的呢?这个就需要看一看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–>MessageQueue–>Message–>Message.target–>mHandler–>Activity
原因首先我们知道程序启动时在主线程中会创建一个Looper对象,这个 Looper 里维护着一个 MessageQueue 消息队列,这个消息队列里会按时间顺序存放着Message ,然后Handler一般是通过内部类来创建的,内部类会持有外部类的引用,也就是 Handler持有Activity 的引用,而消息队列中的消息 target 是指向 Handler 的,也就等同消息持有 Handler的引用, 也就是说当消息队列中的消息如果还没有处理完,这些未处理的消息(也可以理解成延迟操作)是持有 Activity 的引用的,此时如果关闭 Activity ,是没办法回收的,从而就会导致内存泄露。
解决方案:Handler静态内部类 + 弱引用持有Activity
详细代码请看:内存溢出、内存泄漏与内存抖动
那么发送消息后,最终又是怎么调用到handleMessage的呢?接下来看一下Handler中dispatchMessage()方法的源码,如下所示:
在第101行进行判断,如果mCallback不为空,则调用mCallback的handleMessage()方法,否则直接调用Handler的handleMessage()方法,并将消息对象作为参数传递过去。这样我相信大家就都明白了为什么handleMessage()方法中可以获取到之前发送的消息了吧!
我们接下来继续分析一下,为什么使用异步消息处理的方式就可以对UI进行操作了呢?这是由于Handler总是依附于创建时所在的线程,比如我们的Handler是在主线程中创建的,而在子线程中又无法直接对UI进行操作,于是我们就通过一系列的发送消息、入队、出队等环节,最后调用到了Handler的handleMessage()方法中,这时的handleMessage()方法已经是在主线程中运行的,因而我们当然可以在这里进行UI操作了。整个异步消息处理流程的示意图如下图所示:
还是调用了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;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new Handler();
new Thread(new Runnable() {
@Override
public void run() {
handler.post(new Runnable() {
@Override
public void run() {
// 在这里进行UI操作
}
});
}
}).start();
}
}
虽然写法上简洁很多,但是原理是完全一样的,我们在Runnable对象的run()方法里更新UI,效果完全等同于在handleMessage()方法中更新UI。
原来就是调用了Handler中的post()方法
如果当前的线程不等于UI线程(主线程),就去调用Handler的post()方法,否则就直接调用Runnable对象的run()方法
epoll是linux系统一种高效的IO多路复用机制,采用事件驱动的方式实现。可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。
在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在linux新的内核中,有了一种替换它的机制,就是epoll。
在 select/poll中,通过遍历文件描述符,而epoll则是通过监听回调的的机制。
阻塞:通过调用 epoll_wait 方法,当前线程进入休眠,等待被唤醒。
唤醒:对 mWakeEventFd 发起写入操作,从而唤醒 nativePollOnce 中通过 epoll_wait 进入休眠的线程。
epoll_wait这里也是整个Android消息机制阻塞的真正位置,阻塞等待期间可以保证线程进入休眠状态,不占用CPU资源,同时监听所注册的事件。
1.减少用户态和内核态之间的文件句柄拷贝;
2.减少对可读可写文件句柄的遍历。
具体实现是:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。
什么是消息屏障?
当要处理的消息为消息屏障,则延迟执行后续的普通消息,优先执行异步消息,常用在绘制任务的处理。消息屏障使得消息处理有了优先级。
有些博文说它是同步屏障,其实两者是同一个东西。
主要目的是要解决线程切换问题,handler里的Message机制解决了线程间通信
MessageQueue是一个单向链表,next()调用nativePollOnce->lunx的epoll_wait()等待,实现阻塞时队列;
队列的出现解决了"处理消息"阻塞到"发送消息"的问题,由于队列是生产者消费者模式,而要使用队列需要至少两个线程与一个死循环;
为了循环取出队列里的消息
子线程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()去处理;
是的,如果主线程不进行looper.loop()阻塞,一下子执行完成,整个程序就直接结束了,不可能有机会去执行其他的任务了。
Android是事件为驱动的操作系统,事件过来就去handler里执行,没有事件就阻塞在那里显示界面;
sendMessage是生产者,handlerMessage是消费者;消息在队列中排队(MessageQueue),这样解决大量的消息过来的问题,不会造成主线程sendMessage阻塞,所有消息都会直接放在队列中排队等候执行;
ANR:Application Not Responding指的是应用程序无响应的错误,它表示应用程序在执行某个操作时长时间没有响应。在Android系统中,如果一个应用程序在主线程中执行了耗时的操作而导致主线程被阻塞,那么系统就会弹出一个对话框警告用户当前应用程序出现了ANR错误,并提示用户选择“等待”或“关闭应用程序”。
ANR通常是由于一些长时间的I/O操作、耗时的计算或者其他阻塞主线程的原因引起的。当主线程被阻塞时,应用程序的用户界面就会无响应,用户无法与应用程序进行交互,这就给用户带来了不好的体验。
ANR发生条件是:
Activity:应用在 5 秒内未响应用户的输入事件(如按键或者触摸)
BroadCastReceiver :BroadcastReceiver 未在 10 秒内完成相关的处理
Service:20 秒(均为前台)。Service 在20 秒内无法处理完成
如果Handler收到以上三个相应事件在规定时间内完成了,则移除消息,不会ANR;若没完成则会超时处理,弹出ANR对话框;
为了避免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()有事件时就分发,没事件就阻塞
内存泄漏:由于疏忽或错误造成程序未能释放已经不再使用的内存的情况
内存泄漏的原因:在Activity中,将Handler声明成非静态内部类或匿名内部类,这样Handle默认持有外部类Activity的引用。如果Activity在销毁时,Handler还有未执行完或者正在执行的Message,而Handler又持有Activity的引用,导致GC无法回收Activity,导致内存泄漏。如以下两种情形可能导致内存泄漏:
//匿名内部类
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//大量的操作,activity要销毁时还没结束
}
},1000);
//非静态内部类
private class MyHandler extends Handler {
@Override
public 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);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
}
2、在Activity销毁时,清空Handler中未执行或正在执行的Callback以及Message
@Override
protected void onDestroy() {
super.onDestroy();
//清空handler管道和队列
mHandler.removeCallbacksAndMessages(null);
}
内存抖动根本的解决方式是复用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个
Android是事件为驱动的操作系统,事件过来就去handler里执行(handler处理包括了创建服务,创建广播,结束服务,等等事件处理),如果没有事件过来就阻塞在那里,显示静止界面,有事件就去执行事件;
利用epoll机制可以定位到是哪个app接受这个事件,运用到红黑树,事件过来之后查找app来执行这个事件,事件带有一个标记,查找对应的app。
为了可以同时处理 native 侧消息。
为了可以同时处理键盘鼠标轨迹球等输入设备的消息。Android基于linux内核,是以事件为驱动的,将各种输入设备被抽象成了文件,监控这些文件描述符的I/O事件正是epoll的拿手好戏。