sunxin's Studio.

关于Handler的一些问题

字数统计: 2k阅读时长: 8 min
2020/09/29 Share

Handler 源码分析

图示

流程


代码流程


问题分析

Handler内存泄露

场景模拟:使用Handler在子线程中发送消息到主线程中更新UI。

  1. 如果在发送消息的之前进行睡眠操作,在这时候立即关闭Activity,那么这个时候将会造成内存泄露,因为在Activity销毁的时候没有移除Handler消息,休眠结束后消息依旧会被发送。会造成空指针异常或者其他异常。这个时候如果在onDestroy中进行消息移除removeMessages,发现并没有什么作用。因为关闭的时候还在休眠状态,所以移除的消息为null。当休眠结束后消息还是会被正常发出。
  2. 针对休眠这种操作,只能在onDestroy中将Handler对象置为null,在发送消息的时候判空,这样解决一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

// 创建Handler 方式1
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
mTextView.setText(msg.obj.toString());
return false;
}
});

private void testHandlerLeak() {
// 开启子线程
new Thread() {
@Override
public void run() {
super.run();
Message message = Message.obtain();
message.what = 1;
message.obj = "hellooooo";
// 休眠3s
SystemClock.sleep(3000);
// 使用这种方式将会在onDestroy方法中移除掉
// mHandler.sendEmptyMessageDelayed(1, 3000);
if (mHandler != null) {
mHandler.sendMessage(message);
}
}
}.start();
}

@Override
protected void onDestroy() {
super.onDestroy();
// 如果上面使用休眠,那么在销毁Activity的时候移除的消息为null,内存泄露将依旧存在
mHandler.removeMessages(1);
mHandler = null;
}

为什么不能在子线程创建Handler

尝试在子线程中创建线程

1
2
3
4
5
6
7
new Thread() {
@Override
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
10
public 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
3
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

looper 是从ThreadLocal中获取的。因为在Android程序启动的时候,在ActivityThread中会在主线程中创建一个Looper放入到ThreadLocalMap中,进入ActivityThread中看一眼

1
Looper.prepareMainLooper();

1
2
3
4
5
6
7
8
9
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
1
2
3
4
5
6
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

走到这类可以看到,ThreadLocal把一个主线程当做key,Looper当做value,存入了ThreadLocalMap中。
所以,回头看一下,如果在子线程中创建Handler,从ThreadLocal以子线程为key是拿不到Looper的,所以就为空,报出了那个错误。

textview.setText(),只能在主线程执行,正确吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.textview);
// 开启子线程
new Thread() {
@Override
public void run() {
super.run();
SystemClock.sleep(1000);
mTextView.setText("看到了");
}
}.start();
}

如果不加睡眠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
6
void 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建Handler 方式1 推荐使用
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
mTextView.setText(msg.obj.toString());
return false;
}
});

// 创建Handler 方式2 报警告,不推荐使用
private Handler mHandler2 = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};

区别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
2
3
4
5
6
7
8
9
// 创建本地线程(主线程)
final ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Nullable
@Override
protected String initialValue() {
// 重写初始化返回方法,如果ThreadLocalMap拿不到值,在调用这个
return "嘻嘻";
}
};

ThreadLocal 内部使用一个Map保存信息,key为当前线程,value为泛型的类型,如果ThreadLocalMap获取不到值的时候,会调用初始化方法initialValue方法进行赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 从ThreadLocalMap中获取String值,key是主线程
System.out.println("主线程ThreadLocal:"+threadLocal.get());

new Thread(new Runnable() {
@Override
public void run() {
// 从子线程中拿值,获取不到,就会再次从初始化方法中获取,会获取到嘻嘻
String value = threadLocal.get();
System.out.println("从子线程中获取值:"+value); // 嘻嘻
// set一个值
threadLocal.set("哈哈哈");
System.out.println("从子线程中获取值: "+threadLocal.get());// 哈哈哈

// 使用完成需要移除
threadLocal.remove();
}
}).start();

为什么主线程用Looper死循环不会造成ANR

因为Looper在开启死循环的时候,一旦需要等待时,或者还没有到执行的时候,会调用NDK里面的JNI方法,释放当前的时间片,就不会造成ANR异常

为什么Handler构造方法里面的Looper不是直接new

如果在 Handler 构造方法里面直接 new Looper(), 可能是无法保证 Looper 唯一,只有用 Looper.prepare() 才能保证唯一性,具体可以看 prepare 方法

MessageQueue 为什么要放在 Looper 私有构造方法初始化?

因为一个线程只绑定一个 Looper ,所以在 Looper 构造方法里面初始化就可以保证 mQueue 也是唯一的 Thread 对应一个 Looper 对应一个 mQueue。

参考

移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架

CATALOG
  1. 1. Handler 源码分析
    1. 1.1. 图示
    2. 1.2. 问题分析
      1. 1.2.1. Handler内存泄露
      2. 1.2.2. 为什么不能在子线程创建Handler
      3. 1.2.3. textview.setText(),只能在主线程执行,正确吗?
      4. 1.2.4. new Handler() 两种写法的区别
      5. 1.2.5. ThreadLocal的用法与原理
    3. 1.3. 为什么主线程用Looper死循环不会造成ANR
    4. 1.4. 为什么Handler构造方法里面的Looper不是直接new
    5. 1.5. MessageQueue 为什么要放在 Looper 私有构造方法初始化?
    6. 1.6. 参考