随心所欲换线程——handler原理详解
Hanlder 机制
在 android 中,常常需要切换线程,为什么?因为如果所有任务都在主线程,那么出现一些耗时操作时,比如从网上下载资源,很容易导致 UI 卡顿,也就是俗称的"ANR":UI10秒内无法交互
hanlder ,作为android 的消息机制,日常开发中必不可少要使用到,可以轻轻松松完成线程切换,而程序员无需管理,读取数据,更新 UI 是他最广泛的用途,了解 Handler,深入 Handler,是从新手到入门的必经之路
简单使用
现在,我们先来直观认识一下 handler
com/example/littlesongtest/MainActivity.java
public class MainActivity extends AppCompatActivity { static final int ADD_BTN = 1; private Button mBtnAdd; private Button mBtnNumber; private int number = 0; final Handler handler = new Handler() { @Override public void handleMessage(@NonNull Message msg) { //2收到事件进行判定 super.handleMessage(msg); switch (msg.what) { case ADD_BTN: btnAdd(); } } }; private void btnAdd() { number++; mBtnNumber.setText(number+""); //3 执行事件 } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBtnAdd = findViewById(R.id.btn_add); mBtnNumber = findViewById(R.id.btn_number); mBtnAdd.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { handler.sendEmptyMessage(ADD_BTN);//1 点击按钮,发送事件 } }); } }
布局文件很简单,两个 button
layout/activity_main.xml
<Button android:id="@+id/btn_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="点击" /> <Button android:id="@+id/btn_number" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="1" />
只要点击按钮,就形成如下效果
发生的事情,如下
其中可以在 handlerMessage 中做很多耗时操作,比如下载网络资源,读取数据库数据等,然后切换到主线程,进行 UI 刷新
你可能会奇怪,既然子线程做耗时操作,为什么不直接在子线程中刷新 UI 呢?
其实这是android的选择,技术上完全可以做到,但是这样会导致多子线程并发刷新 UI 的问题,势必要引入锁,而且开发者使用起来也很容易出错,所以干脆只在主线程刷新 UI,子线程耗时操作,这样各司其职,更加方便
上面代码的路线是:先在主线程sendEmptyMessage,然后到了 handler 中的handleMessage,判定msg.what,然后执行对应函数
那么sendEmptyMessage之后为啥跑到了 handler 中,msg 又是哪来的?,我们继续往下看。
原理
然而,android 开发并不这么简单,上面仅仅是执行了一次 handler,但是
假设有很多地方都执行了handler.sendEmptyMessage(),向 handler 派发任务,handler 如何处理?
MessageQueue
显然,handler 内部必然要有循环系统,能够持续的处理源源不断过来的任务,这就是 Looper与MessageQueue
打开 handler源码
final Looper mLooper; final MessageQueue mQueue;
可以发现其内部维护了这两个数据结构,当在主线程执行了 handler.sendEmptyMessage(或者 send,post)等方法时,其内部会调用sendEmptyMessageDelayed来构造 msg
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { Message msg = Message.obtain(); msg.what = what; return sendMessageDelayed(msg, delayMillis); }
将我们的参传给 msg.what,就是第一节中的”ADD_BTN“,(注意这里Message.obtain())将 handler的然后继续往下会调用到sendMessageAtTime,这里会构造一个MessageQueue
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) { MessageQueue queue = mQueue; if (queue == null) { RuntimeException e = new RuntimeException( this + " sendMessageAtTime() called with no mQueue"); Log.w("Looper", e.getMessage(), e); return false; } return enqueueMessage(queue, msg, uptimeMillis); }
再往下,就很显然,家境 msg 放入MessageQueue中
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
所以MessageQueue就是一个容器,存储发送进来的消息
那么,光保存还不够,谁来处理呢?
Looper
每个线程都有一个 Looper,Looper会不停的从 MessageQueue中查看是否有新消息,如果有,那么就会立即执行
首先,在一个线程中,我们使用 Loop.prepare()先创建一个 Looper,其中核心是 loop 方法,也是高频考点,由于源码较长,我将代码不重要的部分删掉了
public static void loop() { final Looper me = myLooper(); } me.mInLoop = true; final MessageQueue queue = me.mQueue; ... boolean slowDeliveryDetected = false; for (;;) { // 死循环 Message msg = queue.next(); if (msg == null) { return; //跳出循环 } ... try { msg.target.dispatchMessage(msg); //分发消息 if (observer != null) { observer.messageDispatched(token, msg); } dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } ... final long newIdent = Binder.clearCallingIdentity(); msg.recycleUnchecked(); } }
可以看到,里面有一个死循环,每次从MessageQueue中取出 msg,这就是为什么他会不断的查看 MessageQueue是否有新消息,只有MessageQueue中的 next 为 null,也即没有新消息了就会跳出
我们也可以手动结束循环,Looper 内置了 quit 方法,当执行 quit 时,会通知MessageQueue.next 返回 null,也就可以结束循环了。
而在循环中,Looper 获得了 msg后,会执行dispatchMessage对 msg 进行分发,
msg.target.dispatchMessage(msg);
这里的msg.target其实就是当前的 handler,在MessageQueue的 obtain 方法中
public static Message obtain(Handler h) { Message m = obtain(); m.target = h; return m; }
而 handler 是在主线程中的,也就是说,此时切换到了主线程,来看看dispatchMessage
public void dispatchMessage(@NonNull Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }
最后执行了handleMessage(msg);,是不是有点眼熟?就是一开始的那个handleMessage()!!!
所以从 msg 的路线来看,他经历了
Handler.send -> MessageQueue -> looper ->dispatchMessage -> handleMessage
其中 MessageQueue ,looper 是在子线程完成的,也就是 msg 的存储,处理过程
所以上面的原理用一句话来总结就是
handler 在主线程send 消息存到内部的 msg,多个 msg 存在MessageQueue,Loop 死循环不断取MessageQueue的 msg,取出一个就dispatchMessage分发,然后回到主线程执行handleMessage进行处理。
ThreadLocal
以上的原理其实已经能够逻辑自洽了,但是我想你还应该听说过一个东西——ThreadLocal
这个东西的名字就很有意思,有一个 Local,意思也非常明显:
每个线程都有一个ThreadLocal,而只有该线程能访问,其他线程无法访问,同时,该线程的所有点都可以访问
也就是说,ThreadLocal对于持有他的线程,是 global 的,对于其他线程,是 Local的
这种奇妙的特性,意味着,他在 handler 的机理中发挥了很大的作用
每一个handler都会维护一个 Looper,Looper 不断从MessageQueue取出 msg 来执行。handler 本身就是一个线程,handler 很多的时候,Looper 也很多,此时就需要一个统一管理 Looper 的工具,而 Looper 的作用域恰恰是对当前线程需要全部访问,而对其他线程不需要访问,Looper 中就维护了一个这样的列表
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
通过 set和 get 方法即可保存和取出想要的 Looper,因为ThreadLocal内部维护了一个 ThreadLocalMap,比如他的 set 方法,内部就是 构造了一个 map
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
可以看到value是保存在当前线程里面的,其他线程无法访问
我们知道线程实际上是共享内存的,只有栈,和一些变量是属于自己的,这是不是说 value 保存在 线程的栈中呢?不是的,毕竟栈太小了,只能保存一些变量,所以对象依然在堆内存中,只不过使用了一些其他的技巧设置成了仅自己可见,当然,本文介绍 handler,更加深入的留给读者自己探索吧
总结一下,ThreadLocal作为保存的 Looper 的一个载体,通过ThreadLocalMap完成数据存取,他的特性完全满足 Looper 的需要——在多线程下各个 looper 的信息不共享
最后,我们回过头来看 handler,handler 做了什么?
其实没有做什么,只是在主线程 send 了消息,然后在 handlerMessage 中处理,仅此而已,对于开发者来说,不得不说非常便利,最累的活已经被 Looper,MessageQueue 给做了
(图片来自网络)
常见的考点
面试官:handler 的一些常见使用方法?
小松:
上面的内容其实已经完成了 Handler 的原理,不过,handler 还有很多很方便的用法
在上文中,我们只用了sendEmptyMessage来发送消息,但是实际上,还有
post(Runnable)发送即时消息 postAtTime(Runnable,long) postDelayed(Runnable,long)发送延时消息 sendEmptyMessage(int)发送空消息 sendMessage(Message)发送即时消息 sendMessageAtTime(Message,long) sendMessageDelayed(Message,long)发送延时消息
如果没有亲身实践过,恐怕很难分清区别
此外,我们未必需要自定义 handler,通过post 线程也可以
new Thread(){ @Override public void run() { handler.post(new Runnable() { @Override public void run() { textView.setText("Hello Handler"); } }); } }.start();
面试官:主线程就是 UI 线程,你说非 UI 线程不能更新 UI,你确定?
小松:
是否确定,取决于android 是如何避免非 UI 线程更新 UI 的
比如下面的代码
@Override public void onCreate(Bundle savedInstanceState) { tvText = (TextView) findViewById(R.id.main_tv); new Thread(new Runnable() { @Override public void run() { tvText.setText("OtherThread"); } }).start(); }
虽然是在子线程中修改了tvText,但是不会报错,因为在每个 View 操作时会执行ViewRootImpl的方法checkThread(),此时才判定更新当前 UI 的是否是主线程,如果不是就抛异常
所以,只要在checkThread执行之前更新 UI,不管是什么线程都可以,而onResume被执行后ViewRootImpl才创建好,所以只要在onResume执行前更新 UI 即可,假设我们增加 sleep 方法,让 UI 更新等一下
tvText = (TextView) findViewById(R.id.main_tv); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } tvText.setText("OtherThread"); } }).start();
只需要等200 ms,ViewRootImp就会创建成功,此时抛出异常
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
所以,只要在checkThread()方法钱进行修改 UI,不管哪里都可以!
面试官:先 postDelayed一个 A,再 Post 一个 B,会发生什么?
小松:实际上,MessageQueue名为队列,实为链表,在上文中,为了简化逻辑,我将 queue简单看做为一个存储 msg 的容器,但是 queue 内部是有工作的
假设 postDelayed一个3 秒的 消息 A,此时 A 进入MessageQueue,由于是 delay 的,所以MessageQueue会调用nativePollOnce()阻塞自己,不再读数据,Looper拿不到数据,这样 Looper 就不能取 A,直到 3 秒后
如果这时候,再 Post 一个 B,那么 B 入队时发现 A 被阻塞,就会插入MessageQueue的头部,调用 nativeWake 唤醒线程,MessageQueue.next()开始执行,Looper 又可以获取数据,将 B 执行,此时队列只剩 A,如果 3 秒还没到,就再调用nativePollOnce()阻塞
这样,前面的阻塞不会耽误后面的执行,后面的执行也不会影响前面的阻塞
android面试