前言
转载请声明,转自【https://www.cnblogs.com/andy-songwei/p/11438492.html】,谢谢。
Handler的身影总是时不时出现在工作,笔试,面试中,可见其对于Android的重要性。一年前写过一篇文章【朝花夕拾】Handler篇,随着这一年来对Handler更多的认识和理解,本文对Handler知识点做的一些补充。
一、为什么要引入Handler
Handler的主要作用是切换线程,将线程切换到Handler所使用的Looper所在线程中去,我们大部分的开发者通常使用Handler是用于子线程通知主线程更新UI,我们需要明确的是更新UI只是Handler的其中一个作用而已。
那么为什么只能在主线程中更新UI,而不能在子线程中完成呢?因为Android系统规定,只能在主线程中访问UI,如果在子线程中访问UI,程序就会报错。在访问UI的时候,系统会调用ViewRootImpl类中的checkThread方法,如下所示:
1 //=======ViewRootImpl.java======= 2 final Thread mThread; 3 ...... 4 public ViewRootImpl(Context context, Display display) { 5 mThread = Thread.currentThread(); 6 } 7 ...... 8 void checkThread() { 9 if (mThread != Thread.currentThread()) { 10 throw new CalledFromWrongThreadException( 11 "Only the original thread that created a view hierarchy can touch its views."); 12 } 13 } 14 ......
我们知道ViewRootImpl是View体系根View DecorView与Activity的PhoneWindow之间的纽带,最初ViewRootImpl实例化的时候,是在主线程中完成的。所以,上述checkThread方法可以用来判断当前是否为主线程,不是则报异常,该异常应该比较常见的了。
那么,系统为什么不允许在子线程中访问UI呢?这是因为UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态。那么为什么系统不对UI控件的访问加上锁机制呢?主要是因为如果加上锁机制会有两个缺点:1)使访问逻辑变得复杂。2)降低访问UI的效率,因为锁机制会阻塞某些线程的执行。所以,最简单且高效的方法就是采用单线程模型来处理UI操作,对于开发者来说也不是很麻烦,只需要通过Handler切换一下UI访问的执行线程即可。
所以Handler的出现,就解决了子线程中不能访问UI的问题。
二、Handler回调所在线程问题
在对Handler理解不深入的时候,一直没有认真注意过new一个Handler后,回调方法所在的线程问题,总以为任何时候都可以在回调方法中更新UI。事实上,之所以会有这样的错误认识,是因为我们使用Handler的时候基本上都用于更新UI了,就犯了经验主义错误。实际上,回调方法所在线程,和发送消息的handler使用的Looper所在的线程一致。下面我们先通过一些实验开看看结果。
1、在子线程中使用main Looper的情况
1 private void testHandler() { 2 Log.i("songzheweiwang", "thread1=" + Thread.currentThread()); 3 new Thread(new Runnable() { 4 @Override 5 public void run() { 6 new Handler(Looper.getMainLooper()).post(new Runnable() { 7 @Override 8 public void run() { 9 Log.i("songzheweiwang", "thread2=" + Thread.currentThread()); 10 } 11 }); 12 } 13 }).start(); 14 }
在主线程中调用如上方法,对应的log如下,可见回调方法是在主线程中:
1 08-31 12:48:49.342 9414-9414/com.example.demos I/songzheweiwang: thread1=Thread[main,5,main] 2 08-31 12:48:49.373 9414-9414/com.example.demos I/songzheweiwang: thread2=Thread[main,5,main]
2、在子线程中使用子线程Looper的情况
1 private void testHandler() { 2 Log.i("songzheweiwang", "thread1=" + Thread.currentThread()); 3 new Thread(new Runnable() { 4 @Override 5 public void run() { 6 Looper.prepare(); 7 new Handler().post(new Runnable() { 8 @Override 9 public void run() { 10 Log.i("songzheweiwang", "thread2=" + Thread.currentThread()); 11 } 12 }); 13 Looper.loop(); 14 } 15 }).start(); 16 }
在主线程中调用该方法,得到的log如下,可见回调方法是在当前子线程中:
1 08-31 12:53:49.718 9655-9655/com.example.demos I/songzheweiwang: thread1=Thread[main,5,main] 2 08-31 12:53:49.719 9655-9750/com.example.demos I/songzheweiwang: thread2=Thread[Thread-7,5,main]
上述示例采用的是post方式,sendMessage方式也是一样的结果,这里就不举例了。我们平始使用Handler多数情况下是在主线程中new Handler的,默认情况下使用的是main Looper,然后在子线程中用该Handler实例来post或者sendMessage,所以默认情况下回调方法就是运行在主线程中,我们在该方法中访问UI就没有报错。
三、消息循环Looper
上一节示例中,第6行和13行红色部分展示了在子线程中,Handler使用子线程Looper的使用范例。如果去掉这两行代码,系统会报如下异常:
Looper.loop(),就是用来开启线程的消息循环,没有该行代码就无法收到消息,其作用在上一篇文章中说过,这里不赘述了。Looper.prepare(),是获取当前线程的Looper,如果没有Looper会报上述异常,源码如下所示:
1 public Handler(Callback callback, boolean async) { 2 ...... 3 mLooper = Looper.myLooper(); 4 if (mLooper == null) { 5 throw new RuntimeException( 6 "Can't create handler inside thread that has not called Looper.prepare()"); 7 } 8 ...... 9 }
Looper.myLooper()方法用于获取当前线程中的Looper,如果当前线程下没有Looper,就会报异常。每一个线程下都有自己专属的Looper,由TheadLocal来进行管理,至于ThreadLocal,有兴趣的可以自行研究。
我们是否会有疑问,平时在new Handler的时候,也没有去调用Looper.prepare()方法,为什么没有报错呢?
这是因为咱们new Handler的时候经常就是在主线程中完成的,会默认使用主线程的Looper。我们平时经常提到主线程,其实就是AcitivityThread,是在Zygote创建应用程序进程时调用其main方法启动的。主线程启动的时候会创建自己的Looper,源码如下:
1 public static void main(String[] args) { 2 ...... 3 Looper.prepareMainLooper(); 4 5 ActivityThread thread = new ActivityThread(); 6 thread.attach(false); 7 8 if (sMainThreadHandler == null) { 9 sMainThreadHandler = thread.getHandler(); 10 } 11 ...... 12 Looper.loop(); 13 ..... 14 }
Looper.prepareMainLooper()里面也是调用的Looper.prepare()方法,所以在当前应用进程创建的时候,系统就为该进程的主线程创建好了Looper,后续在主线程中实例化Handler的时候,就默认为使用该Looper了。
四、一些Tip
1、当有Handler发送Message时,会通过MessageQueue类的enqueueMessage方法将Message加入到Handler内部的MessageQueue中,我们一般称之为消息队列。但实际上它的数据结构为单链表,而不是队列,因为队列是先进先出,中间不能插入和删除元素,但是单链表可以。Message会根据post/sendMessage时指定处理的时间来在插入到链表中,或者通过quit方法将消息从链表中移除。
2、取Message的方法MessageQueue.next()方法是个死循环,没有消息时会阻塞。Looper.loop()也是个死循环,而且调用了MessageQueue.next()方法,当MessageQueue没有消息时也会阻塞,而当有消息加入时就会立即处理。让这两个死循环终止的唯一条件就是Looper执行quit/quitSafety方法让自己退出,这样会将消息队列标记为退出状态,否者会一直死循环下去。
3、在执行Looper.loop()的时候,会判断当前线程是否已经创建过Looper了,如果已经创建过,就会报异常,如下源码说明了这一点:
1 public static void prepare() { 2 prepare(true); 3 } 4 private static void prepare(boolean quitAllowed) { 5 if (sThreadLocal.get() != null) { 6 throw new RuntimeException("Only one Looper may be created per thread"); 7 } 8 sThreadLocal.set(new Looper(quitAllowed)); 9 }
通过这种方式,就保证了一个线程中,只会实例Looper一次,也就是只会有一个Looper实例。
1 private Looper(boolean quitAllowed) { 2 mQueue = new MessageQueue(quitAllowed); 3 mThread = Thread.currentThread(); 4 }
实例Looper的时候,会新建一个MessageQueue,那么一个线程中就只会维护一个MessageQueue。
参考资料
本文主要参考了任玉刚的《Android开发艺术探索》