六、Android 消息机制之Handler

从开发的角度来讲,Handler 是 Android 消息机制的上层接口。因此我们主要讨论的是 Handler 的运行机制。

那么首先回答个问题,为什么要有 Handler 机制?

0. 为什么要有 Handler 机制?

回答这个问题,首先我们得知道 Handler 有什么作用。

作用: Handler 的主要作用是将一个 任务 切换到 Handler 所在的线程中去执行。

而 Android 规定访问 UI 这个 任务 只能在主线程中进行,如果在子线程中访问 UI,就会抛出异常。每次操作 UI 时会进行验证,这个验证是通过 ViewRootImpl 类里面的 checkThread 方法 完成的。

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

只能在主线程就只能在主线程访问呗,那为什么还要这个 Handler 呢,因为 Android 又建议在主线程中不能进行耗时操作,否则会导致 ANR 。那考虑这样一种情况,比如我们要从服务端拉取一些数据并将其显示在 UI 上,这个必须在子线程中进行拉取动作,拉取完毕后又不能在子线程中访问 UI,那我该如何将访问 UI 的工作切换到主线程中呢?对,就是 Handler。

因此系统之所以提供 Handler,主要原因就是为了 解决在子线程中无法访问 UI 的矛盾。

Q 为什么不允许在子线程中访问 UI?

因为 Android 的 UI 控件不是线程安全的,如果在 多线程中并发访问 可能会导致 UI 控件处于不可预期的状态。

Q 那为什么不对 UI 控件的访问加上 锁机制呢?

  • 首先加锁会让 UI 访问的逻辑变得复杂
  • 其次锁机制会降低 UI 访问的效率,因为锁机制会阻塞某些线程的执行。

1. Looper、MessageQueue、Message、Handler

消息机制的模型主要由以上四个类组成:

  • Message:消息 分为 硬件产生的消息(如按钮、触摸等)软件生成的消息
  • MessageQueue:消息队列 ,内部采用 单链表 的数据结构来存储 消息列表
  • Handler:消息处理者,主要用于向 消息队列 发送各种消息事件 和 处理相应消息事件。
  • Looper:消息循环,不断循环执行(Looper.loop),用于从 消息队列中取出 消息,并按分发机制将取出的消息分发给 消息处理者。

2. 示意图

下图完整类图取自 http://gityuan.com/2015/12/26/handler-message-framework/

消息机制Main.jpg

下图为简化版本:


消息机制.png

针对简化版做个解释:

  • 1、整个消息机制从 Looper 开始。我们都知道使用Handler 时,如果没有 Looper 会抛出异常, 这个在源码中很清楚。所以整个消息机制这个大机器启动的源头就是 Looper, 通过调用 Looper.prepare() 我们会 new 一个 Looper 对象,并存放在 ThreadLocal 里,这个 ThreadLocal 稍后解释。而在 Looper 的构造函数中,会 new MessageQueue 对象。

  • 2、调用 Looper.loop() 启动整个消息机制。 在调用 loop 方法后, Looper 就开始了自己的 无限循环之路, 会一直从 MessageQueue 中取 Message, 这个操作对应的代码是 loop 方法里的 queue.next(), MessageQueue 中的 next 也是一个 无限循环,如果 MessageQueue 中没有消息, 那么 next 方法就会阻塞在这里,相应的 Looper 也会阻塞。 当有新消息到来时, 会唤醒他们。 至此,整个消息机制已经运转起来了,就等 消息 发过来了。

  • 3、Handler 发送消息。 首先我们得 new 一个 handler 才能发送消息,在我们 new handler 的时候,会将当前线程的 Looper 取出来,同时 得到 Looper 里的 MessageQueue。 有了消息队列,我们就能往队列里插入数据了。 handler 的消息发送最终都是调用的 MessageQueue.enqueueMessage() 方法。 这样我们就把一个 Message 发送到了 MessageQueue 里。噢,对了,Message 是自己构建的,这个就不说了。

  • 4、此时 消息队列里有了 Message,那么 MessageQueue 的next 就会把刚刚那个 消息返回给Looper, Looper 收到 Message 后, 会开始 消息的分发,就是调用 Message.target.dispatchMessage(msg), 这个 Message 就是刚刚发送的 Message,那 target 就是 Message 里持有的 Handler 对象。因此 这个消息的处理 又回到了 Handler 这里。 那么 Message 是什么时候持有的 Handler 对象呢,没错,就是在 Handler 发送消息时,即调用 enqueueMessage 的时候。这个方法内部 第一行代码就是 msg.target = this; ,这样就把 Handler 赋给了 Message 的 target 变量。

上述是整个 消息机制的 大致流程,嗯,这么长估计没人看,我自己都不想看。
看过这么一大段后,对于 Handler 的主要作用 想必还是一头雾水。 还记得 Handler 的主要作用吗?
Handler主要作用:将一个任务切换到 Handler 所在的线程中去执行。

了解流程后,我知道了一个 消息 怎么发给消息队列, 然后 Handler 会自己处理这个消息。但是这个线程是怎么切换的,完全不知道。

上述简化图中有个灰色的块块, ThreadLocal。 对,它就是关键。

3. ThreadLocal 工作原理

ThreadLocal 是一个线程内部的数据存储类。 还是自己去找定义看吧,这里就不写了,我们还是关心它有什么用。

它的主要作用就是:可以在不同的线程中维护一套数据的副本并且彼此互不干扰。

这又是什么意思,别慌,这里有个例子可以方便理解。

public class ThreadLocalSample {

    private ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<>();

    public void test() {
        //1、在主线程中设置 mThreadLocal 的值为 true
        mThreadLocal.set(true);
        System.out.println(Thread.currentThread().getName() + " : mThreadLocal = " + mThreadLocal.get());

        new Thread("Thread#1") {
            @Override
            public void run() {
                //1、在子线程1中设置 mThreadLocal 的值为 false
                mThreadLocal.set(false);
                System.out.println(Thread.currentThread().getName() + " : mThreadLocal = " + mThreadLocal.get());
            }
        }.start();

        new Thread("Thread#2") {
            @Override
            public void run() {
                //1、在子线程2中 不设置 mThreadLocal,那么get得到的值应该为 null
                System.out.println(Thread.currentThread().getName() + " : mThreadLocal = " + mThreadLocal.get());
            }
        }.start();

    }

    public static void main(String[] args) {
        new ThreadLocalSample().test();
    }

}

这段代码执行的结果如下:

threadlocal_code.png

从结果可知,虽然在不同的线程访问的是同一个 ThreadLocal 对象,但是他们通过 ThreadLocal 获取到的值却是不一样的。

这是为什么呢?

3.1 ThreadLocal.Values

ThreadLoacal 内部有个 静态内部类 Values,Values 内部维护的是一个 Object [ ] ,当我们通过 ThreadLoacal 进行 set() 方法调用时,实际是在 Values.put 。 当然不同版本的api,实现不一样,比如最新版本把Values改为了ThreadLocalMap,并且内部维护的是 Entry [ ] 数组。但是原理都一样,这里就 Values 分析简单点。

/**
     * Sets the value of this variable for the current thread. If set to
     * {@code null}, the value will be set to null and the underlying entry will
     * still be present.
     *
     * @param value the new value of the variable for the caller thread.
     */
    public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }

可以看到,当我们调用 set 方法时, 会先取得当前的 Thread, 然后把 值 put 进去。 那么中间那句 Values values = values(currentThread); 是怎么回事呢。 看 Thread 源码。

    /**
     * Normal thread local values.
     */
    ThreadLocal.Values localValues;

你可以看到 Thread 的成员变量里持有 ThreadLocal.Values 。 所以当我们 set 时,会先从当前线程那里获取到 Values 对象, 也就是说我们实际是在给 每个线程的 Values 赋值。那么 values(currentThread) 做了什么呢。

    /**
     * Gets Values instance for this thread and variable type.
     */
    Values values(Thread current) {
        return current.localValues;
    }

很简单,就是返回 线程的 localValues 变量。 那么当我们第一次 set 时, 这个 Values 肯定为空, 那么就会调用 values = initializeValues(currentThread); 来进行初始化。

那么 ThreadLocal 类里的 initializeValues 又做了什么呢。

    /**
     * Creates Values instance for this thread and variable type.
     */
    Values initializeValues(Thread current) {
        return current.localValues = new Values();
    }

没错,直接 new Values(),并且把它赋给 Thread 类的 localValues 变量。 这样当前线程就拥有了 ThreadLocal.Values ,当下次set时,就会从这个线程中取出这个 Values 并对它进行赋值。 每个线程的 Values 是不一样的。 那 get 就不用说了,也是从当前线程中取出这个 Values ,然后获取相应的值,具体可自行查看源码。

到这里 ThreadLocal 就分析的差不多了,想必有了个大概印象,针对上述例子给个图理解。

ThreadLocalValues.png

4. Looper 中的ThreadLocal

分析过 ThreadLocal 后,看 Looper 就不一样了。
再来看一看 Looper.prepare() 方法。

    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));
    }

sThreadLocal 就是 ThreadLocal 的引用,如下

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

这段我们说过,会new 一个 looper, 同时会把这个 looper 赋值给 ThreadLocal,我们知道 ThreadLocal 调用 set 方法的时候,会先获取当前线程,然后调用 Values.put , 那么这个时候,我们就把 主线程 与 Looper 绑定在一起了。

Handler 的构造方法如下:

    public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

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

我们可以看到首先会获取 Looper 对象, 而 Looper.myLooper() 的实现很简单,就一句话 return sThreadLocal.get(); 这样,如果你是在主线程中 new 的 handler, 那么你也就会在 主线程中 处理消息了。

Handler工作过程.png

5. Handler 中的dispatchMessage 消息分发

这个方法比较简单,看源码就能看懂。

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
  • Message 里的 callback 是个 Runnable, 如果不为空的话,就会调用 handleCallback , 这里面也就一句话 message.callback.run();
  • mCallback 是 Handler类 的一个内部接口
    public interface Callback {
        public boolean handleMessage(Message msg);
    }
  • handleMessage, 当我们自己 new handle 时会重写这个方法,用来处理 message, 这个方法在 Handler 里是空实现.
    /**
     * Subclasses must implement this to receive messages.
     */
    public void handleMessage(Message msg) {
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容