Handler相关的学习笔记(源码解析)

一、Android的消息机制

Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueen和Looper做支撑。MessageQueen作为消息的存储单元,Looper以无限遍历的形式查询MessageQueen中是否有新消息,Looper是与当前线程绑定的,在Handler内部获取Looper可以通过ThreadLocal获取,但是线程是默认没有Looper的,需要使用Handler就需要为线程创建Looper(UI线程除外,UI线程会默认创建初始化Looper,在ActivityThread被创建时候创建(Main方法中))

handler机制原理图解

二、Handler、MeaasgeQueen、Looper和Thread之间的绑定

  1. Handler绑定当前线程的Looper和MessageQueen
    先看一下Handler里边的源码,当我们实现下面一个这样的handler时,构造函数都做了什么。

    handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Log.d("TAG233", "handleMessage: "+msg.obj);                    
        }
    };
    

从Handler的构造函数进去,发现最后到达了这个构造方法,并且里边有这样的一段代码:

public Handler(Callback callback, boolean async) {
    //……

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    
    //……
}

首先在Handler内部,它获取了和当前线程绑定的Looper,之后又获取了和Looper绑定的MessageQueen,这个时候会发现有一段抛出异常的代码,因为在线程创建的初期,是不会主动的去创建Looper的(UI线程除外),要创建Looper,需要调用Looper.prepare()实现(具体的源码分析会在下面写),如果没有调用这个方法,那么当前线程是不会存在这个Looper的,自然也不会存在对应的MeaasgeQueen,这个时候如果直接使用handler,系统就会抛出异常。
获取MessageQueen的方式是十分简单的,在拿到Looper之后拿Looper里的MeaasgeQueen即可。

那么Handler如何保证自己获取到的Looper是当前线程的Looper呢?
在Handler的构造方法里,有一句mLooper = Looper.myLooper(),这个myLooper里面十分简单

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

也就是说,Looper是从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在Handler的机制里,担当的就是一个存储Looper的任务,ThreadLocal里有一个Entry,这个Entry是 Entry(ThreadLocal<?> k, Object v),也就是说Looper是作为value存储在ThreadLocal里的,通过当前线程t获取到ThreadLocalMap,然后通过ThreadLocalMap里的Entry,获取到Looper并返回。

到这里,Handler和Looper、MeaasgeQueen的绑定的源码看完啦~

  1. Looper和当前线程的绑定
    看完了Handler的绑定,我们知道,Handler里获取当前线程的Looper是从ThreadLocal里拿的,创建Looper又是要调用Looper.prepared()方法来创建Looper并完成和当前线程的绑定,那么现在先来看看Looper.prepared()都做了什么。
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));
}

prepare()方法里很简单,就是往ThreadLocal里添加Looper来完成绑定,当然,从代码中也能够看出来,创建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);
}

对应前边Handler获取和当前线程绑定的get方法,思路就比较清晰了,同样通过当前线程t获取ThreadLocalMap,通过setMap实现key为当前ThreadLocal,value就是这个Looper啦。

但是这个ThreadLocal又是哪里来的?在Looper里找到了这样一句代码解释了这个问题:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

看完了ThreadLocal里的问题,但是还是没有解决Looper到底怎么和当前线程完成的绑定,再看Looper的构造方法:

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

这下子没问题了,原来Looper在创建的时候,在构造函数里,通过Thread.currentThread()获取到了当前线程,并用mThread引用,所以当我们在ThreadLocal里存储Looper时,这个Looper就已经和这个线程绑定好了,所以Handler在ThreadLocal里get这个Looper时,就已经是绑定了当前线程的。再看mQueue是直接new MeaasgeQueen()了,也完成了消息队列的绑定。

总结一下,Looper在Looper.prepare()时就完成了和当前线程、MeaasgeQueen的绑定,之后存储在ThreadLocal里,Handler通过ThreadLocal获取到这个Looper,完成了一系列的绑定。

三、Handler的工作原理

Handler的工作主要包含消息的发送和接收的过程,消息的发送是通过一系列的send的方法来完成的。

当我们需要发送一条消息时,最常用的方式就是使用sendMessage(Message msg)方法,接下来看一下源码:
从sendMessage开始,发送消息依次经过了sendMessage->sengMessageDelay()->sendMessageAtTime(),中间的过程十分简单,就不贴代码了,主要来看一下sendMessageAtTime()这个方法。

public boolean sendMessageAtTime(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);
}

这里主要做了一个对消息队列的判断,如果为空则抛出异常,重点看一下enqueueMessage都做了什么

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

在这段代码里,可以看到 msg.target = this; 这行代码,这行代码指定了当前这条消息是由这个handler发送的,这也就是为什么一个Handler发送消息时,其他的Handler不会接受到消息的原因。

在Looper中,Looper.loop()方法里如果检测到MessageQueen中有消息,就会取出这条消息,之后通过msg.target.dispatchMessage(msg)分发消息,由于Message中设置了target,保证了这个Handler发出的消息不会被其他的Handler接收到。也由于dispatchMessage()是在Looper中执行的,msg.target实质上就是一个handler,所以在这里,就成功的将代码逻辑切换到指定的线程执行了。

在看queue.enqueueMessage(msg, uptimeMillis),这句代码的作用其实就是向MessageQueen里插入消息了。

从上面的代码也可以看出,Handler接收的消息其实是由dispatchMessage分发的,来看看它的源码:

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

从源码可以看出,最后无论如何,都是通过调用handleMessage()来传递消息完成最后的处理的,而handleMessage则是由我们自己重写的一个方法,也就是最后的处理消息的逻辑。

那么CallBack是什么呢?
在源码的注释中,CallBack是用来创建一个Handler实例但是并不需要派生Handler的子类,其实就是另一种使用Handler的方式。在日常的开发中,我们都是派生一个Handler的子类并且重写它的HandleMessage()方法来处理消息,当我们不想派生子类时,就可以通过 Handler handler = new Handler(callback) 来实现。

好啦,目前对Handler的分析就先告一段落~
最近在面试经常问到Handler,总觉得自己答的不够详细,希望这篇文章过后能有所进步~

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

推荐阅读更多精彩内容