Android消息机制

消息机制概述

Android消息机制主要是指Handler的运行机制以及Handler所附带MessageQueue和Looper的工作过程。
Handler主要作用是将一个任务切换到某个指定的线程中去执行,像访问UI只能在主线程中进行,ViewRootImpl的checkThread方法会对UI操作进行验证。

系统为什么不允许子线程访问UI?

  • 多线程中并发访问可能会导致UI控件处于不可预期的状态。
  • 如果对UI控件的访问加上锁,会使逻辑变得复杂,也会降低UI访问的效率。

基于这两点原因就采用了最简单,最高效的采用单线程模式来处理UI操作。

Handler工作架构


消息机制里需要频繁创建消息对象(Message),因此消息对象需要使用享元模式来缓存,以避免重复分配&回收内存。
Message内部维护一个长度为50的链表去管理被回收的Message的对象,调用obtain方法时会优先从消息池中取Message对象,若是没有可复用的Message,就会创建该Message,之后回收时会被存放在消息池中,下次调用obtain时就可以被复用。

ThreadLocal的工作原理

ThreadLocal的使用场景

是一个线程内部的数据存储类,可以在指定线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据。
使用场景:

  • 当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑用ThreadLocal。
    对于Handler来说,需要获取当前线程的Looper,不同线程有不同的Looper对象,这里就用到了ThreadLocal。
  • 复杂逻辑下的对象传递
    可用于监听器的传递。比如一个线程过于复杂,需要监听器贯穿整个线程的执行过程。ThreadLocal可以让监听器作为线程内的全局对象而存在。
    private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();
    
    mBooleanThreadLocal.set(true);
        Log.i(TAG,"mBooleanThreadLocal: "+mBooleanThreadLocal.get());

        new Thread("Thread1"){
            @Override
            public void run() {
                mBooleanThreadLocal.set(false);
                Log.i(TAG,"Thread1 mBooleanThreadLocal: "+mBooleanThreadLocal.get());
            }
        }.start();

        new Thread("Thread2"){
            @Override
            public void run() {
                Log.i(TAG,"Thread2 mBooleanThreadLocal: "+mBooleanThreadLocal.get());
            }
        }.start();

最终结果:
09-28 10:55:30.614 3517-3517/com.example.dell.myapplication I/MainActivity: mBooleanThreadLocal: true
09-28 10:55:30.618 3517-3565/com.example.dell.myapplication I/MainActivity: Thread1 mBooleanThreadLocal: false
09-28 10:55:30.630 3517-3566/com.example.dell.myapplication I/MainActivity: Thread2 mBooleanThreadLocal: null

可以看到一个神奇的结果,在一个类中访问同一个全局变量,但是结果却都不一样。这个也就证明了它在指定线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据。之所以会是这个结果,是因为不同线程访问同一个ThreadLocal的get方法时会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找对应的value值,不同的线程ThreadLocalMap是不同的。

ThreadLocal的源码分析

set方法

     public void set(T value) {
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap对象
        // ThreadLocal.ThreadLocalMap threadLocals
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 创建ThreadLocalMap对象
            createMap(t, value);
     }
     
     
     private void set(ThreadLocal key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                 // 获取Entry的ThreadLocal对象
                ThreadLocal k = e.get();

                if (k == key) {
                    // 找到了就覆盖原有的value值
                    e.value = value;
                    return;
                }

                if (k == null) {
    //如果当前桶位置key为null,特殊处理,替换并清除过期的Entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            //找到一个为null的桶,将传入的Entry放入当前桶
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //没有清理出可用的桶而且容量超过阈值,重新Hash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

get方法

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 找到当前ThreadLocal对应的实体对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        // 返回初始值
        return setInitialValue();
    }
    
    private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

由此可以看出:它们所操作的对象都是当前线程的threadLocals对象的table数组,因此在不同的线程中访问同一个ThreadLocal的set和get方法,它们对threadLocals对象的读/写操作仅限于各自线程内部。

消息队列的工作原理

消息队列一般指的是MessageQueue,MessageQueue最主要有两个操作,插入和读取。插入对应enqueueMessage方法,往消息队列中插入一条消息。读取对应next方法,从消息队列中取出一条消息并将其从消息队列中移除。
MessageQueue内部不是消息队列,是单链表的数据结构。
通过源码可以看到next方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法就会阻塞到这,当有新消息到来时,next方法会返回这条消息并将其从单链表中移除。

Looper的工作原理

Looper会不断从MessageQueue中查看是否有新消息,有新消息立即处理,否则会一直阻塞。

在子线程中创建Handler会报错

Handler的工作需要Looper,没有Looper会报错。这也就是子线程中创建Handler报错的原因,主线程创建时会默认创建Looper的。

Looper的关键方法

为线程创建Looper

Looper.prepare() 为当前线程创建一个Looper
Looper.prepareMainLooper() 创建主线程Looper

开启消息循环方法

Looper.Loop()

获取Looper对象

Looper.myLooper()获取当前线程的Looper对象
Looper.getMainLooper()获取主线程的Looper对象

退出Looper

Looper.quit() 直接退出Looper
Looper.quitSafely() 设定退出标记,把消息队列中已有的消息处理完毕后才安全退出。
Looper退出后,通过Handler发送的消息会失败。

子线程中创建Looper需要注意的地方

在所有事情完成后应该调用quit方法来终止消息循环,不然子线程会一直处于等待状态,调用了子线程会立刻终止。

Looper.loop()

只有调用了loop方法,消息循环系统才会起作用。
这个方法是个死循环,只有调用MessageQueue的next方法为null时才会退出循环。
loop方法会调用MessageQueue的next方法来获取新消息,而next是一个阻塞操作,当没有消息时,next方法会一直阻塞在那里,这也会导致loop方法一直阻塞在那里。

创建多个Handler,发送消息怎么知道哪个Handler去处理消息

Message的target指向的就是发送这条消息的Handler对象。这样就会实现这个Handler发送这条消息,消息又回到这个Handler的handleMessage方法进行处理。

Handler

创建Handler时应该和一个Looper进行绑定,主线程默认已经创建Looper了,子线程需要自己创建Looper。

子线程中创建Handler需要手动创建Looper对象。

new Thread(new Runnable() {
            @Override
            public void run() {
                //调用Looper.prepare()方法
                Looper.prepare();

                //在子线程中创建Handler
                mHandlerThread = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        Log.e("sub thread", "---------> msg.what = " + msg.what);
                    }
                };
                // 注意这个方法要在loop之前不然无效
                //仅仅是将消息入队到调用发送消息的那个handler对象的消息队列中
                mHandlerThread.sendEmptyMessage(1);
                //调用Looper.loop()方法
                Looper.loop();
            }
        }).start();

整体流程

  1. 调用sendXXX,postXXX方法,会调用enqueueMessage()将消息加入到消息队列(数据结构是单向链表,链表在插入和删除上比较有优势)中,enqueueMessage会根据时间的先后顺序进行存储。
  2. 主线程的ActivityThread.main()会执行Looper.prepareMainLooper()和Looper.loop()启动一个死循环。在循环里面检测消息队列的消息(queue.next()),没有消息就会调用nativePollOnce阻塞线程,释放cpu资源,线程休眠。有消息取出消息,处理消息。
  3. 在相应回调中处理该消息。注意是下面三种回调。
    整个模式其实就是一个生产者-消费者模式,源源不断的生产消息,处理消息,没有消息时进行休眠。
public void dispatchMessage(Message msg) {
        // 这里的callback指的是post(Runnable r)传下来的Runnable对象
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            // 这里的Callback指的是handler的带参构造参数,public Handler(Callback callback)
            // 这种方式是不想派生子类
           /**Handler mHandlerThread = new Handler(new Handler.Callback() {
           @Override
           public boolean handleMessage(Message msg) {
               Log.e("sub thread", "---------> msg.what = " + msg.what);
                return false;
            }
        });**/
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            // 最后,调用Handler的handleMessage处理消息
            handleMessage(msg);
        }
    }

主线程消息循环模型

AMS以进程间通信的方式完成ActivityThread的请求后回调ApplicationThread的Binder方法,然后ApplicationThread会向H发送消息,H收到消息会将ApplicationThread中的逻辑切换到ActivityThread中执行,即切换到主线程中执行。

Message

分为三类:
同步消息,屏障消息,异步消息。
同步消息和异步消息没什么不同,但是加了同步屏障就有区别。
屏障消息的target为null,屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。

MessageQueue的next方法,遇到屏障消息,就直接循环消息列表,遍历移动msg的位置,直到移动到的msg是异步message则退出循环。

常见点

如何在子线程中创建Handler

两种方式,其实就是一种在子线程中创建Looper即可,让消息处理轮询起来。线程退出注意loop的quit方法,停止轮询。

Looper、handler、线程间的关系

一个线程对应一个Looper,多个Handler。
如何保证唯一的Looper?
使用ThreadLocal来存储,对应一个线程一个Looper对象。

post方法发送消息,run方法执行在哪个线程

looper所在线程决定的。

sendMessageDelayed方法怎么执行的

  1. 每个消息的处理时间(when)不一样(SystemClock.uptimeMillis() + delayMill)
  2. 消息入队时,根据消息的处理时间(when)做插入排序,队头的消息就是最需要执行的消息。
  3. 当消息队列为空时(无消息时线程会阻塞),消息入队需要唤醒线程。
  4. 当消息队列不为空时(一般不需要唤醒),只有当开启同步屏障后第一个异步消息需要唤醒(开启同步屏障时会在队首插入一个占位消息,此时消息队列不为空,但是线程可能是阻塞的)。

Looper.loop方法是个死循环为啥不会导致主线程阻塞

线程执行完内部代码,线程生命周期就会终止,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出。并且主线程无消息时会休眠,有消息再被唤醒。
没有消息时,主线程会释放CPU资源进入休眠状态。这里采用的epoll机制。
ANR定义:如果主线程被长时间阻塞,导致无法响应用户的操作,即造成ANR。
ANR根本原因不是线程在睡眠,而是消息队列被其他耗时消息阻塞,导致消息没有及时处理。

阻塞、唤醒、延时入队

阻塞:
消息的阻塞在MessageQueue的next方法(调用Looper的loop方法,不断从消息队列中取消息,取消息方法就是next),阻塞调用的方法是nativePollOnce(ptr,nextPollTimeoutMillis),nextPollTimeoutMillis是阻塞时间,为-1时候,一直阻塞,直到被唤醒,为0不需要阻塞,大于0为阻塞的时间,时间到了,唤醒线程。
唤醒:
消息的唤醒在MessageQueue的enqueueMessage方法(往消息队列中插入消息),唤醒的方法是nativeWake(),底层是epoll机制,通过往pipe管道写端写入数据来唤醒主线程工作,然后继续往后执行,取消息,以及分发消息。
延时入队:
发送延时消息,延时的消息为当前时间加上需要延时的时间,然后保存在Message的when变量中,消息入队在enqueueMessage方法中,里面会根据时间的先后顺序将Message插入到mMessages单链表中,时间到了,会取出消息,然后处理消息。

内存泄漏

handler发送的消息在当前handler的消息队列中,如果此时activity被finish掉了,那么消息队列的消息依旧会由handler进行处理,若此时handler声明为内部类(非静态内部类),我们知道内部类天然持有外部类的实例引用,那么就会导致activity无法回收,进而导致activity泄露。
解决方案:
静态内部类
静态内部类不持有外部类的引用,所以使用静态的handler不会导致activity的泄露。
弱引用
弱引用修饰对象的生命周期决定。

消息分发

Handler.callback和handleMessage方法都存在,但callback返回true,handleMessage还会执行么?
源码分析对应整体流程小节的dispatchMessage源码。
注意这几个调用时机:

  • 如果使用post(Runnable r)就相当于设置了Message.Callback,就会优先调用Runnable的run方法。
  • 如果这两种方式同时存在
Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        //do something
        super.handleMessage(msg);
    }
};

Handler handler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        return true;
    }
});

会先执行Handler.Callback,如果返回true不会执行匿名内部类handleMessage方法。但是如果返回false就还会执行匿名内部类handleMessage方法。

  • 如果不存在Handler.Callback
    只会执行匿名内部类handleMessage方法。

主线程的死循环一直运行是不是特别消耗CPU资源

及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里。此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

IdleHandler机制

IdleHandler是什么?怎么使用,能解决什么问题?
可以在主线程空闲时执行任务,而不影响其他任务的执行。

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override
    public boolean queueIdle() {
        //此处添加处理任务
        //主线程
        return false;
    }
});

返回true表示可以反复执行该方法,即执行后还可以再次执行;返回false表示执行完该方法后会移除该IdleHandler,即只执行一次。
如果有多个任务延迟加载,可以调用多次addIdleHandler,但是不优雅。可以配合队列使用,一次添加多个任务再去调用一次addIdleHandler。

源码分析IdleHandler
在消息队列为空或者需要执行的消息还未到时间时,即消息队列空闲时才去执行的。

Handler同步屏障机制

一般发消息都是同步消息,message的target对应handler,但是也有特殊情况,就是异步消息,屏障消息的target为null,并且插入到消息队列头部。同步屏障不会自动移除,使用完成之后需要手动进行移除,不然会造成同步消息无法被处理。
使用
开启同步屏障

int barrier= mMyHandler.getLooper().getQueue().postSyncBarrier()

// 创建Handler构造方法的设置被设为隐藏
Message msg = mMyHandler.obtainMessage();
msg.setAsynchronous(true);
mMyHandler.sendMessage(msg);

移除消息屏障

mMyHandler.getLooper().getQueue().removeSyncBarrier(barrier);
再调用Handler移除message

同步屏障就是阻碍同步消息,只让异步消息通过。消息机制在处理消息的时候,优先处理异步消息。

使用场景:
ViewRootImpl.java

@UnsupportedAppUsage
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //开启同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //发送异步消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

UI更新相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。

主线程的消息是哪来的

是App进程中的其他线程通过Handler发送给主线程。

Activity的生命周期是怎么实现在死循环体外能够执行起来的

ActivityThread的内部类H继承于Handler,通过handler消息机制,简单说Handler机制用于同一个进程的线程间通信。Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。
AMS操纵Activity,Activity具体执行生命周期,会通过Handler给到ActivityThread中执行。

举例说明消息传递流程


暂停Activity,流程如下:

  • 线程1的AMS中调用线程2的ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题)
  • 线程2通过binder传输到App进程的线程4;
  • 线程4通过handler消息机制,将暂停Activity的消息发送给主线程;
  • 主线程在looper.loop()中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()方法,再经过方法的调用,最后便会调用到Activity.onPause(),当onPause()处理完后,继续循环loop下去。

主线程Looper,主线程Handler,MessageQueue都是在哪创建的,如何获取主线程对应的Looper

1.主线程Looper是在ActivityThread的main方法中创建的,调用Looper的prepareMainLooper方法。

public static void prepareMainLooper() {
    // 在这里new了一个Looper对象,并将这个对象装进ThreadLocal中
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            // ThreadLocal对象调用get方法
            sMainLooper = myLooper();
        }
    }

2.在main方法中创建ActivityThread.H,这个继承了Handler。
3.MessageQueue是在Looper的构造方法中创建的。
4.获取主线程的Looper可以调用Looper的getMainLooper方法。
获取线程中的Looper,可以调用Looper的myLooper方法。
myLooper方法的实质就是ThreadLocal调用get方法。

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

推荐阅读更多精彩内容