随心所欲换线程——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" />

只要点击按钮,就形成如下效果

handler 点击

发生的事情,如下

image-20201215101248583

其中可以在 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 面试 文章被收录于专栏

android面试

全部评论

相关推荐

有趣的牛油果开挂了:最近这个阶段收到些杂七杂八的短信是真的烦
点赞 评论 收藏
分享
感性的干饭人在线蹲牛友:🐮 应该是在嘉定这边叭,禾赛大楼挺好看的
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务