Handler 源码分析
图示
问题分析
Handler内存泄露
场景模拟:使用Handler在子线程中发送消息到主线程中更新UI。
- 如果在发送消息的之前进行睡眠操作,在这时候立即关闭Activity,那么这个时候将会造成内存泄露,因为在Activity销毁的时候没有移除Handler消息,休眠结束后消息依旧会被发送。会造成空指针异常或者其他异常。这个时候如果在
onDestroy
中进行消息移除removeMessages
,发现并没有什么作用。因为关闭的时候还在休眠状态,所以移除的消息为null。当休眠结束后消息还是会被正常发出。 - 针对休眠这种操作,只能在
onDestroy
中将Handler对象置为null,在发送消息的时候判空,这样解决一下
1 |
|
为什么不能在子线程创建Handler
尝试在子线程中创建线程1
2
3
4
5
6
7new Thread() {
public void run() {
super.run();
new Handler();
}
}.start();
如果在华为手机上运行不会报错,可能是因为厂商对底层源码进行了修改,如果运行在Google的模拟器上,就会报错1
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
既然报错,就要找找报错的代码在哪里
直接new Handler点进去查看源码,走到这里1
2
3
4
5
6
7
8
9
10public Handler(@Nullable 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()");
}
...
}
通过上面关键代码看出,如果mLooper == null
就会抛出这个异常,为什么looper会为空呢,进入到Looper中看一下1
2
3public static Looper myLooper() {
return sThreadLocal.get();
}
looper 是从ThreadLocal中获取的。因为在Android程序启动的时候,在ActivityThread
中会在主线程中创建一个Looper放入到ThreadLocalMap
中,进入ActivityThread
中看一眼1
Looper.prepareMainLooper();
1 | public static void prepareMainLooper() { |
1 | private static void prepare(boolean quitAllowed) { |
走到这类可以看到,ThreadLocal把一个主线程当做key,Looper当做value,存入了ThreadLocalMap
中。
所以,回头看一下,如果在子线程中创建Handler,从ThreadLocal以子线程为key是拿不到Looper的,所以就为空,报出了那个错误。
textview.setText(),只能在主线程执行,正确吗?
1 |
|
如果不加睡眠1s,则可以更新成功,如果加上睡眠1s,那就会报错1
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
问题:为什么不加睡眠1s能更新成功呢?
答:因为更新太快,系统还没反应过来呢。🤣🤣 更新UI 的方法是在onCreate
方法中执行的,这个时候ViewRootImpl
还没有创建,所以checkThread
方法还没来得及执行。所以就更新成功了。因此,一旦加上了休眠,系统就反应过来了。就会进行线程检查,就会报出错误。
每个View进行更新的时候都会进行requestLayout()
,然后会checkThread
,判断更新的线程是否是主线程1
2
3
4
5
6void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
mThread 是主线程,这里会检查当前线程是否是主线程,那么为什么没有在 onCreate 里面没有进行这个检查呢?这个问题原因出现在 Activity 的生命周期中 , 在 onCreate 方法中, UI 处于创建过程,对用户来说界面还不可见,直到 onStart 方法后界面可见了,再到 onResume 方法后页面可以交互,从某种程度来讲, 在 onCreate 方法中不能算是更新 UI,只能说是配置 UI,或者是设置 UI 属性。 这个时候不会调用到 ViewRootImpl.checkThread () , 因为 ViewRootImpl 没有创建。 而在 onResume 方法后, ViewRootImpl 才被创建。 这个时候去交户界面才算是更新 UI。
setContentView 只是建立了 View 树,并没有进行渲染工作 (其实真正的渲染工作实在 onResume 之后)。也正是建立了 View 树,因此我们可以通过 findViewById() 来获取到 View 对象,但是由于并没有进行渲染视图的工作,也就是没有执行 ViewRootImpl.performTransversal。同样 View 中也不会执行 onMeasure (), 如果在 onResume() 方法里直接获取 View.getHeight() / View.getWidth () 得到的结果总是 0。(摘自参考)
new Handler() 两种写法的区别
1 | // 创建Handler 方式1 推荐使用 |
区别1:在于一个是接口的方法,一个是Handler内部的空方法
区别2:在于调用时机
在ActivityThread中开启Looper.prepareMainLooper();
在Looper.loop();
开启循环的时候,Looper轮询消息队列里的消息,如果取到消息,就会进行消息分发,交给Handler去处理。msg.target.dispatchMessage(msg);
target对象就是Handler对象,然后可以进入Handler中看看是如何分发消息的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* Handle system messages here.
*/
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
可以发现,方式2是最被冷落的方法,上面的方法都不执行,才能轮到他。方式1的返回值好像没什么作用,不管谁true or false
,过来都被return了。
ThreadLocal的用法与原理
1 | // 创建本地线程(主线程) |
ThreadLocal
内部使用一个Map保存信息,key为当前线程,value为泛型的类型,如果ThreadLocalMap获取不到值的时候,会调用初始化方法initialValue
方法进行赋值。
1 | // 从ThreadLocalMap中获取String值,key是主线程 |
为什么主线程用Looper死循环不会造成ANR
因为Looper在开启死循环的时候,一旦需要等待时,或者还没有到执行的时候,会调用NDK里面的JNI方法,释放当前的时间片,就不会造成ANR异常
为什么Handler构造方法里面的Looper不是直接new
如果在 Handler 构造方法里面直接 new Looper(), 可能是无法保证 Looper 唯一,只有用 Looper.prepare() 才能保证唯一性,具体可以看 prepare 方法
MessageQueue 为什么要放在 Looper 私有构造方法初始化?
因为一个线程只绑定一个 Looper ,所以在 Looper 构造方法里面初始化就可以保证 mQueue 也是唯一的 Thread 对应一个 Looper 对应一个 mQueue。