Hanlder 机制详解(线程间通讯)

概述

  1. 什么是Android消息机制
    Android中的消息机制主要是指Handler的运行机制以及Handler所附带的MessageQueue和Looper的工作过程,这三个实际上是一个整体。

  2. Handler的作用,为什么有Handler
    Handler的主要作用:负责跨线程通信。

    因为在主线程不能做耗时操作,而子线程不能更新UI,所以当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。

系统为什么不允许在子线程中访问UI?
因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态。
为什么系统不对UI控件的访问加上锁机制呢?
加上锁机制会让UI访问的逻辑变得复杂
锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
鉴于这两个缺点,最简单且高效的方法就是采用单线程模型来处理UI操作。


  1. 几个相关概念
  • Message:消息,被传递和处理的数据。其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,终由Handler处理。

  • Handler:处理者,负责Message的发送及处理。
    Handler的主要作用:(有两个主要作用)
    1. Handler.sendMessage(),在工作线程中发送消息;
    2. Handler.handleMessage(),在主线程中获取、并处理消息。

  • MessageQueue:消息队列,本质是一个单链表,用来存放Handler发送过来的消息,并按照FIFO规则执行。当然,存放Message并非实际意义的保存,而是将Message串联起来,等待Looper的抽取。

  • Looper:消息泵或循环器,通过Looper.loop()不断从MessageQueue中抽取Message。因此,一个MessageQueue需要一个Looper。

  • Thread:线程,负责调度整个消息循环,即消息循环的执行场所。
    一个Thread只能有一个Looper,一个MessageQueue,可以有多个Handler。

  1. 工作流程


    handler工作流程.jpg
  • Handler通过sendMessage()发送Message时,Looper的成员变量MessageQueue会通过enqueueMessage()向MessageQueue中添加一条消息。

此时Message会将Handler对象赋值给Message对象的target参数。

  • Looper通过loop()方法开启循环后,不断轮询调用MessageQueue的next()方法来获取Message。

  • 通过调用Messag目标Handler的dispatchMessage()方法去传递消息,目标Handler收到消息后调用handleMessage()方法处理消息

    简单来说,Handler通过sendMessage()方法将Message发送到Looper的成员变量MessageQueue中,之后Looper通过loop()方法不断循环遍历MessageQueue从中读取Message,最终回调给Handler处理。

消息机制分析

  1. ThreadLocal
  • 概念:
    ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。

  • 使用场景:

    • 一般来说,当某些数据是以线程为作用域并且不同线程具有 不同的数据副本的时候,就可以考虑采用ThreadLocal。
    • 复杂逻辑下的对象传递。
  • ThreadLocal 的set和get方法原理

    • set原理


      Threadlocal.set
    • get原理


      Threadlocal.get
  1. 消息队列
    消息队列(MessageQueue)主要包含了两个操作:插入和读取。尽管被称为队列,但是它的内部实现是通过一个单链表的数据结构来维护的消息列表,单链表在插入和删除上比较有优势。
  • 插入
    插入的方法是enqueueMessage,作用是往消息队列中插入一条消息。主要操作是单链表的插入操作。

插入的规则:
会根据message的延迟时间来进行插入,无延迟的会放到链表的头部,
有延迟的,延迟时间越长越靠后

  • 读取
    读取对应的方法是next,作用是从消息队列中取出一条消息并将其从消息队列中移除。next方法是一个无限循环的方法,如果消息队列没有消息,那么next方法会一直阻塞,当有新的消息到来时,next方法会返回这条信息并将其从单链表中移除。
  1. Looper
    Looper在Android的消息机制中扮演者消息循环的角色,具体来说,它会不停地从MessageQueue中查看是否有新的消息,如果有新消息就会立刻处理,否则就会一直阻塞在那里。
  • Looper的使用
    UI线程会自动创建Looper,子线程需自行创建。
    通过Looper.prepare()即可为当前线程创建一个Looper。
//子线程中需要自己创建一个Looper
new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();//为子线程创建Looper   
                Handler handler = new Handler();
                Looper.loop(); //开启消息轮询
            }
        }).start();

prepare()

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(线程本地存储区)中,它不是线程,作用是帮助Handler获得当前线程的Looper。

ThreadLocal.set()

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

获取当前线程,然后获取当前线程的ThreadLocalMap,通过map.set方法,将ThreadLocal和当前的Lopper对象存到map中。

ThreadLocal.get()

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

所以一个线程只会有一个ThreadLocal和一个Lopper,并且Looper是唯一的,不可被替换的。

在这里我们看到了Looper的构造方法

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

我们看到它会创建一个MessageQueue,然后将当前线程的对象保存起来。

  • 除了prepare(),还提供prepareMainLooper(),本质也是通过prepare(),主要是给主线程也就是ActivityThread创建Looper使用的。
  • 由于主线程Looper比较特殊,所以Looper提供了一个getMainLooper的方法,通过它可以在任何地方获取到主线程的Looper。
  • 无论是主线程还是子线程,Looper只能被创建一次,即一个Thread只有一个Looper。

  • Looper的退出
    • quit:直接退出Looper
    • quitSafely:只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。

在子线程中,如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。


  • loop方法原理


    looper原理.jpg
  1. Handler
    Handler的主要工作包含消息的发送和接收过程。
  • 发送有两种方式 send 和 post
//send方式的Handler创建
Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        //如UI操作
        mTextView.setText(new_str);
    }
};
//send
mHandler.sendEmptyMessage(0);
//post方式
private Handler mHandler = new Handler();
mHandler.post(new Runnable() {
    @Override
    public void run() {
        mTextView.setText(new_str);
    }
});

post方式最终还是通过一系列send方法实现的

public final boolean sendMessageAtFrontOfQueue(Message msg) {
    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, 0);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
       msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

从上边的代码可以发现,Handler发送消息的过程仅仅是向消息队列插入了一条信息,MessageQueue的next方法就会返回这条信息给Looper,Looper收到消息后就开始处理了,最终消息由Looper交给Handler处理,即Handler的dispatchMessage方法会被调用,这是Handler就进入了处理消息的阶段。

  • 处理消息
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

最终会调用Handler的handleMessage方法来处理消息。

知识点延伸

  1. Handler泄露的原因及正确写法
    如果直接在Activity中初始化一个Handler对象,会报如下错误:

This Handler class should be static or leaks might occur


原因是:
在Java中,非静态内部类会持有一个外部类的隐式引用,可能会造成外部类无法被GC;
比如这里的Handler,就是非静态内部类,它会持有Activity的引用从而导致Activity无法正常释放。

当一个消息是一个延迟消息的时候,这个Message会一直存在MessageQueue中,因为Message的target对应的就是Handler,然后Handler持有Activity的引用,所以就会导致了Activity无法被正常释放。

解决方法:
将 Handler 定义成静态的内部类,在内部持有Activity的弱引用,并在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)及时移除所有消息。

private static class MyHandler extends Handler {    
    //创建一个弱引用持有外部类的对象    
    private final WeakReference<MainActivity> content;    
    private MyHandler(MainActivity content) {        
        this.content = new WeakReference<MainActivity>(content);    
    }   
 
    @Override    
    public void handleMessage(Message msg) {        
        super.handleMessage(msg);        
        MainActivity activity= content.get();       
        if (activity != null) {            
            switch (msg.what) {                
                case 0: {                    
                    activity.notifyUI();                
                }           
            }        
        }    
    }
}
  1. Message可以如何创建?哪种效果更好,为什么?
    • 直接生成实例Message m = new Message
    • Message msg=Message.obtain();
    • Message msg=handler.obtainMessage();

而后两者是直接在消息池中取出一个Message实例,这样做就可以避免多生成Message实例。

  1. Looper死循环为什么不会导致应用卡死?

    • 主线程的主要方法就是消息循环,一旦退出消息循环,那么你的应用也就退出了,Looer.loop()方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生ANR异常。
    • 造成ANR的不是主线程阻塞,而是主线程的Looper消息处理过程发生了任务阻塞(即Looper中有消息,但是消息没有得到及时的处理),无法响应手势操作,不能及时刷新UI。
    • 阻塞与程序无响应没有必然关系,虽然主线程在没有消息可处理的时候是阻塞的,但是只要保证有消息的时候能够立刻处理,程序是不会无响应的。
  2. 为什么主线程可以直接new Handler()
    因为在ActivityMainThread的main()方法里调用了Looper.prepareMainLooper()方法。
    在其他线程如果要使用Handler,必须要调用Looper.prepare()方法。

  3. 子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?
    next()中的:nativePollOnce(ptr, nextPollTimeoutMillis); 阻塞线程
    quit()中的:nativeWake(mPtr); 唤醒线程

  4. 既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?
    这里主要关注 MessageQueue 的消息存取即可,看源码内部的话,在往消息队列里面存储消息时,会拿当前的 MessageQueue 对象作为锁对象,这样通过加锁就可以确保操作的原子性和可见性了。

消息的读取也是同理,也会拿当前的 MessageQueue 对象作为锁对象,来保证多线程读写的一个安全性。

参考:Android开发艺术探索
部分图片来源

https://juejin.im/post/5e61bf2de51d4526ea7f00bd

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342