我们都知道Android中只有主线程可以操作UI
有些爱刨根问底的同学可能就要问了:
为什么只有主线程可以更改UI呢?
回答这个问题前,我们都知道多线程操作资源时是存在并发这个问题的。而为了解决并发,我们会使用乐观锁、悲观锁、自旋锁等多种操作来避免多线程抢占资源时可能出现的死锁等问题。
Android本身就已经够复杂了,那为了让操作简单点,于是Android规定,只有主线程可以更新UI,这样就避免了添加锁,也让UI的绘制变得更加高效。
于此同时又带来一个新的问题,那子线程做了一些耗时操作就是想更新一下UI呢?
2022-09-16 13:03:31.258 5957-5984/com.example.testapt E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.example.testapt, PID: 5957
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8031)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1276)
at android.view.View.requestLayout(View.java:23120)
at android.view.View.requestLayout(View.java:23120)
at android.view.View.requestLayout(View.java:23120)
at android.view.View.requestLayout(View.java:23120)
at android.view.View.requestLayout(View.java:23120)
at android.view.View.requestLayout(View.java:23120)
at android.view.View.requestLayout(View.java:23120)
at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:360)
at android.view.View.requestLayout(View.java:23120)
at android.widget.TextView.checkForRelayout(TextView.java:8933)
at android.widget.TextView.setText(TextView.java:5755)
at android.widget.TextView.setText(TextView.java:5596)
at android.widget.TextView.setText(TextView.java:5553)
at com.example.testapt.FirstMainActivity$1.run(FirstMainActivity.java:45)
上图是在子线程中更新UI出现的报错,当然有些小伙伴会说我也在Thread中sleep了,为啥没有丝毫报错呢?(为了不打断本文的节奏,具体的原因后续更~)
现实中,做了耗时任务的子线程很多,比如网络加载,比如一些异步任务,当做完这些异步任务,该如何通知UI更新呢?
于是,Android推出了Handler机制,这也正是本文的重点。
一、主线程中定义Handler
handler的一个简单栗子如下:
public class FirstMainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
public static Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == 100) {
Log.i("FirstMainActivity", "接收到来自子线程的handler消息..." + msg.arg1);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new Thread() {
@Override
public void run() {
super.run();
Message message = new Message();
message.what = 100;
message.arg1 = 2008;
handler.sendMessage(message);
}
}.start();
}
});
}
}
我们可以看到这个handler是实例化在Activity中的,子线程处理好任务后,使用这个Handler抛出消息,并且还可以绑定一些数据发送,于此同时,在Activity中实例化了的handler在handleMessage中处理线程发送的数据,进行相应的数据处理。
通过sendMessage和handleMessage就丝滑地实现了现场的切换,是不是超级方便?
二、在子线程中定义Handler
又有爱折腾的Android要问了,如果我在子线程中实例化一个Handler呢?
我们尝试着在子线程中去handleMessage,主线程去sendMessage
public class FirstMainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Message message = new Message();
message.what = 100;
message.arg1 = 2008;
handler.sendMessage(message);
}
});
new Thread() {
@Override
public void run() {
super.run();
handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == 100) {
Log.i("FirstMainActivity", "接收到来自主线程的handler消息..." + msg.arg1);
}
}
};
}
}.start();
}
}
看上去只是将handler实例化的位置做了一个调换,这下我们却彻底run不起来了。
2022-09-16 13:18:27.237 7042-7076/com.example.testapt E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.example.testapt, PID: 7042
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:205)
at android.os.Handler.<init>(Handler.java:118)
at com.example.testapt.FirstMainActivity$2$1.<init>(FirstMainActivity.java:59)
at com.example.testapt.FirstMainActivity$2.run(FirstMainActivity.java:59)
程序直接报错如上:
Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
在初始化Handler对象之前,需要调用Looper.prepare()方法。
可能你又会疑惑了,那为啥主线程中就没这一大堆报错呢?
其实从报错中我们就可以看到,Handler的工作是依赖于Looper的。
Looper部分源码
//获取当前线程的Looper()
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
//准备looper
public static void prepare() {
prepare(true);
}
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));
}
可以看到Looper是通过ThreadLocal实现线程隔离,从而达到线程安全的。
插入:
ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程无法获取。因此Handler通过Looper再间接与线程绑定在一起。
我们再找找哪里在主线程中设置了Looper的
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
/**
* Returns the application's main looper, which lives in the main thread of the application.
*/
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
而在ActivityThread的main()函数入口
已经默认去给我们准备了主线程的Looper了。
同样我们需要在子线程中去初始化一下Looper
Looper.prepare();
然而此时我们发现子线程依然没有接收到任何消息。
在子线程和主线程中除了初始化Looper还做了什么预操作呢?
我们再看到Activity的main()函数
Looper.prepareMainLooper();
//省略部分代码
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
在调用了prepareMainLooper()之后,Looper还调用了loop()函数
@SuppressWarnings("AndroidFrameworkBinderIdentity")
public static void loop() {
final Looper me = myLooper();
//省略部分代码
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
可以看到利用for循环,轮询messageQueu里面的消息队列,因为开启了死循环,所以这个任务也是无法终止的,而后面的程序语句更是无法达到。
所以在main函数looper.loop()后又打印并抛出了一句异常
当我们回到子线程初始化handler之后,一起准备就绪后,也同样开启我们的looper.loop()轮询机制。
此时我们就能在子线程中接收到来自主线程分发的消息了。
然而这种Looper.loop()会让子线程一直处于等待的状态,主线程由于是UI线程,这种长期轮询等待消息的分发肯定是要一直开启。
而对于只处理一次耗时任务的线程来说,一直采用looper()循环无疑是消耗性能的。
所以注意,当我们在子线程中创建Looper后,在所有的任务执行完成后应该调用quit方法来终止消息循环。
三、handler的使用注意问题
既然handler如此好用,线程间传递消息如此顺滑,那么有没有什么要注意的事项呢?
message属于当前的handler,handler又属于当前的Looper,一个县城只有一个Looper和MessageQueue,而我们的主线程通常都是以Activity的形式展示在前台供用户交互。
如果我们的Message迟迟无法处理,或者有一个关于handler指向Activity的引用链无法释放,最终将导致已经被关闭的Activity无法被正常释放,从而导致内存泄露。
1、延时消息
在Activity销毁时移除所有Messages
public class FirstMainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private Handler handler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
//如果页面没到超时时间却关闭,此时引用链仍然存在,出现内存泄露
handler.postDelayed(new Runnable() {
@Override
public void run() {
binding.tvShow.setText("测试延时消息");
}
}, 5000);
}
}
2、内部类隐式调用
上述代码通过内部类的方式创建了handler对象,而内部类会隐式地持有外部类对象的引用,也就是FirstMainActivity,当执行postDelayed()方法时,该方法会将Handler装入一个Message,并且把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息。
那么当这个Activity退出时消息队列中还有未处理的消息,或者正在处理消息,而消息队列中的Message持有handler实例的引用,handler又持有Activity的引用,所以导致该Activity的内存资源无法被及时回收,引发内存泄露。
四、解决方案
1、移除所有消息
要想避免Handler引发内存泄露问题,需要我们在Activity关闭退出时移除消息队列中所有消息和所有的Runnable,主动断掉引用链,也就不会影响Activity的回收了。
在onDestroy()函数中调用
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
2、用弱引用+静态内部类
我们可以在初始化Handler时用静态内部类+弱引用,这样在gc时,activity就可以被回收了
private static class MyHandler extends Handler {
private WeakReference<FirstMainActivity> weakReference;
//弱引用的方式,在gc时,activity可被回收
public MyHandler(WeakReference<FirstMainActivity> weakReference) {
this.weakReference = weakReference;
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
}