本文主要分析 Handler机制和源码,线程切换的原理,下面是大致的目录:
- 子线程可以更新UI吗,为什么
- 常用的更新子线程更新切换方式
- Handler 源码解析
- handler.post原理
- runOnUiThread原理
- 主线程一直死循环取消息,为什么没有卡死
- 子线程间怎么发送消息
在onCreate()中开启子线程更新UI 有问题吗?
1 | public class MainActivity extends Activity { |
这样可以看到更新成功了,且没有抛出异常. 子线程能直接更新UI ? 其实是不能。
为什么不能在子线程更新UI?
因为Android的UI控件不是线程安全的,多个线程并发访问可能会导致UI 控件处于不可预期的状态,那既然这样,为什么不加锁呢?
缺点: 加锁会让UI访问的逻辑变的复杂,也会降低UI 的访问效率,因为锁的机制会阻塞某些线程的执行. 所有就采用单线程的方式来更新UI
1 | void checkThread() { |
但是上面为什么可以更新UI呢?
因为执行速度, 因为 ViewRootImpl 这个时候还没创建,这是在调用了 onResume 之后才创建的, ViewRootImpl关于UI 的操作都会 checkThread 如果不在主线程就会抛出异常。所以上面更新的时候还没执行到 checkThread 方法。
提到异步处理消息,我们常用的子线程更新UI 的方法有哪些呢?
- Handler.sendMessage()
- Handler的post()方法。
- Activity的runOnUiThread()方法。
- View.post(Runnable r)方法。
作为一个Android 开发,我们肯定会想到 Handler ,下面是一个最简单的但是不太规范的示例,这样我们就可以在子线程中做了处理然后更新 UI 了。
1 |
|
Handler 可以理解为处理器
首先看下构造方法 我们最常用的 new handler方法做了什么
1 | public Handler() { |
根据上面的构造方法可以看出来, 我们在创建Handler 时,如果不指定 callback 时,会默认为空, 如果没有指定 Looper 时,系统会自动 通过 Looper.myLooper() 帮我们指定当前线程的 Looper 。 如果 looper 对象为空,就会抛出异常。
我们看下 Looper.myLooper() 是怎么实现的呢
1 | public static Looper myLooper() { |
根据方法和资料我们可以知道, ThreadLocal 是所属与线程的,使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
根据经验,有get 肯定有set 的地方,我们看下 Looper 中的 ThreadLocal 的set 的地方
1 | public static void prepare() { |
根据上面可以看到, 一个线程中最多只能有一个 Looper ,在想使用到 Looper 的线程 中调用 prepare 方法就可以创建出 Looper了, 在 new handler 时就不会出现 looper 为空的情况了
但是有没有发现,我们在主线程就是没有调用 prepare 方法呀, 使用的时候也没有报错。这又是怎么回事呢?
通过看App 启动流程的代码可以发现, 在 ActivityThread 类中的 main 方法 这个类中的main方法就是整个App 的主线程的执行入口
在这个方法中,通过调用 Looper.prepareMainLooper() 去初始化了主线程的 looper
1 |
|
可以看到 prepare(false); 创建了一个不可以退出的 looper。
然后 main 方法中还通过调用 Looper.loop() 开启主线程的循环。
Looper.loop() 死循环,处理消息
直接看loop方法到底做了什么
1 | public static void loop() { |
中间省略部分源码, 跟着我们上面的分析,因为主线程在 App 初始化时在程序的住入口已经初始化过Looper 和开启了 loop 循环, 内容就是 从当前线程也就是主线程 的looper 关联的 MessageQueue 里不停的死循环 取出 Message 消息, 如果有消息就调用 msg.target.dispatchMessage(msg); 分发消息。
msg.target.dispatchMessage(msg);
处理消息。消息分发 但是这个 msg.target 又是什么东西呢
1 | 经过看源码 msg.target 的赋值是在这做的 Message 类中 |
msg.target其实就是初始化的handler,然后调用Handler的dispatchMessage();
从消息池中取出消息,如果没有的话就直接new一个Message对象,所以我们在写项目创建Message对象的时候尽量用handle.obtainMessage(),不要直接new Message(),复用会比较好。
知道 msg.target 就是 handler 了,那看下 handler 的 dispatchMessage 方法
1 |
|
handler.post 方法
1 |
|
MessageQueue
MessageQueue的 next 方法做了什么操作呢
1 | Message next() { |
也是死循环取消息 同一线程在同一时间只能处理一个消息,同一线程代码执行是不具有并发性,所以需要队列来保存消息和安排每个消息的处理顺序。
多个其他线程往UI线程发送消息,UI线程必须把这些消息保持到一个列表(它同一时间不能处理那么多任务),然后挨个拿出来处理,每一个Looper线程都会维护这样一个队列,而且仅此一个,这个队列的消息只能由该线程处理。
Message
Message 就没有太多可以说的,它就是一个消息的载体,用来保存消息的。
消息发送
常用的方法
1 |
|
主线程一直 loop 死循环为什么没有卡死
主线程的死循环一直运行是不是特别消耗 CPU 资源呢? 其实不然,这里就涉及到 Linux pipe/epoll机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。
这里采用的 epoll 机制,是一种 IO 多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步 I/O ,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量 CPU 资源。
其实这也是整个 Android 系统的做法,App 启动,然后就进入死循环,如果没有消息,就阻塞在哪些,AMS, WMS 等等 会通过binder抛过来一些消息,然后执行 onCreate 之类的方法,Activity 的生命周期的方法都是 msg,有消息过来就执行
runOnUiThread
1 |
|
可以看到代码 如果当前线程是主线程, 直接调用 Runnable 的run 方法,去执行, 如果不是主线程, 则调用 mHandler 的 post 方法 ,上面我们已经分析过 post 方法的原理了。 上面这个 mHandler 就是 Activity 的 Handler 也就是主线程的 Handler ,发送到主线程执行这个 Runnable。
子线程间怎么发送消息
根据上面的分析,我们在子线程中要使用handler 发送消息的话, 需要 手动在子线程的Handler 创建之前,调用 Looper.prepare 创建一个looper 来跟当前线程关联, 然后在创建完成 handler之后 调用 Looper.loop() 开启消息循环, 然后其他线程就可以通过这个线程创建出的 handler 往这个线程发送消息了。
总结
Handler 机制是现在各个公司面试必问的问题,掌握 Handler 原理对我们日常开发工作也是非常有帮助的。代码量也不大,比较好懂,作为一个 Android 开发工程师非常有必要掌握这些知识点。 欢迎交流学习。