Android的Handler消息处理机制

这篇文章借鉴了任玉刚老师的《Android开发艺术探索》和Android6.0的源码。因为这只是我个人的一个学习记录,所以理解有错的地方希望大家可以指出来共同学习。(有个小建议就是希望大家可以在阅读源码的情况下学习这篇文章,互相比较会事半功倍

消息处理机制的本质:一个线程开启循环模式持续监听并依次处理其他线程给它发送的消息。

其中开启循环模式使用的是Looper结构;为了保证每个线程都有唯一的Looper,因此使用了ThreadLocal类存储Looper;读取的消息对象类是Message;线程一般需要处理多个消息,所以需要MessageQueue结构存储未处理的消息。

它们的类图关系如下:


Handler.png

首先从Handler的创建开始,创建一个Handler最后都会调用下面的两个构造函数之一,它们之间的区别在于是否指定Looper。(下面所有源码中的 ‘...’表示省略的部分,详细的大家可以去看源码。)

public Handler(Callback callback, boolean async) {
    ...
    mLooper = Looper.myLooper();  // 获取当前线程的Looper
    ...
    mQueue = mLooper.mQueue;  // 指定跟Looper绑定的消息队列
    mCallback = callback;  // 处理消息的回调接口,可为null
    mAsynchronous = async;
}

public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;  // 设置指定的Looper
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

Looper的工作原理

之前Handler的构造函数中使用 Looper.myLooper() 方法获取默认的Looper,源码如下

public static @Nullable Looper myLooper() {
    // 从sThreadLocal变量中获取当前线程的Looper对象
    return sThreadLocal.get();  
}

知道Looper怎么获取了,那么又是什么时候存储的呢?首先看下Looper类的构造函数,为Looper指定了消息队列和线程对象。消息队列和线程是一一对应的关系,多个Handler共享一个消息队列,从而实现消息共享。

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

其实如果是你自己创建的线程要使用消息循环,那么需要做一些准备工作,否则会报异常,最简单的示例代码如下

new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();  // 为当前线程创建一个Looper
        // 该handler没有指定Looper,所以默认持有此线程的Looper,也就是通过该handler发送的消息都会由此线程的Looper处理。
        Handler handler = new Handler();
        Looper.loop();  // 开启消息循环
    };
}).start();

然后看到 Looper.perpare() 方法,发现该方法做的事情就是新建一个当前线程的Looper并存储在sThreadLocal变量中,关于ThreadLocal类对Looper的存储和获取细节下面会讲到。

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

这时候你可能会发现,在主线程使用Handler的时候好像并没有开启消息循环呀,还不是一样能用Handler进行消息通信。这是因为主线程的消息循环在应用开始时就会调用 prepareMainLooper() 方法进行初始化。

/**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. The main looper for your application
 * is created by the Android environment, so you should never need
 * to call this function yourself.  See also: {@link #prepare()}
 */
public static void prepareMainLooper() {
    prepare(false);  // 存储当前线程的Looper变量
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        // 将当前线程的Looper设置为主线程的Looper ,方便后续调用
        sMainLooper = myLooper();         }
}

你也可以通过getMainLooper()方法在任意线程获取主线程的Looper。

准备工作完成后,就可以使用Looper.loop()方法开启消息循环,这是一个无限循环,唯一的退出条件是消息队列的next()方法返回为null,下面是关键代码

public static void loop() {
    final Looper me = myLooper();
    ...
    final MessageQueue queue = me.mQueue;
    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();
    for (;;) {
        // 在没有消息时,一般会在此处阻塞线程
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        ...
        // 调用Message对应Handler的方法进行消息分发
        msg.target.dispatchMessage(msg);
        ...
        // Message的回收,将该对象的相关信息置空后放入池中
        msg.recycleUnchecked();          
    }
}

MessageQueue的工作原理

说完了消息循环方式,接下来就是消息队列中消息的存储和取出实现了。

MessageQueue类是一个链表队列结构,消息的存储通过enqueueMessage()方法实现(插入链表头部),消息的取出通过next()方法实现(从链表尾部删除并返回)。下面只展示 next()方法的实现,其它细节代码大家可以自行查看源码。

nativePollOnce()是一个native函数,内部涉及epoll机制的处理(在没有消息处理时阻塞在管道的读端)。

Message next() {
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }
    int pendingIdleHandlerCount = -1; // 第一次迭代
    int nextPollTimeoutMillis = 0;  // 等待时间
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }
        // 队列没有消息时阻塞,两种情况该方法会返回,一是等到有消息就返回;
        // 第二种时等待了nextPollTimeoutMillis时间之后,nativePollOnce方法也会返回。
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                do {  // 循环找到一条不是异步而且target(Handler)不为空的message
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // 下一条消息还没到时间,设置一个时间间隔,到时后唤醒
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 或取消息
                    mBlocked = false;
                    if (prevMsg != null) {  
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    return msg;  // 返回拿到的消息
                }
            } else {
                // 没有消息,对该值复位
                nextPollTimeoutMillis = -1;
            }
            ...
    }
}

Message的工作原理

Message的默认获取方法,一般通过obtain()方法获取Message对象

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {  // 消息池不为null时,取出一个消息对象返回
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();  // 否则创建新的消息
}

其它重载的obtain()方法的区别在于指定了某些参数,Message的重要参数如下

  • public int what;识别同一Handler的不同Message
  • public int arg1; public int arg2;传递整型数值时使用
  • public Object obj;传递对象数据时使用,该对象类需要实现Parcelable接口
  • Bundle data;传递Bundle类型数据时使用,通过setData()getData()方法进行修改和访问
  • Handler target;指定处理消息的Handler,通过setTarget()getTarget()方法进行修改和访问,也可以通过Handler.obtainMessage()方法默认指定。

Handler的工作原理

Looper的工作原理一节中有一个消息的分发函数,源码如下

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);  // callback是一个Runnable对象
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) return;
        }
        handleMessage(msg);  // 子类需要重写的消息处理逻辑实现
    }
}

private static void handleCallback(Message message) {
    message.callback.run();
}

ThreadLocal类的工作原理

刚刚在上面提到Looper变量的存储是通过ThreadLocal类执行的,而ThreadLocal类是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储后,只有在指定线程内可以获取到存储的数据,对于其它线程来说则无法获取到数据。

这里我只简述下ThreadLocal的存储和获取原理,更详细的内部类Values的各种方法大家可以去查看源码。

public void set(T value) {
    Thread currentThread = Thread.currentThread();  // 获取当前线程对象
    Values values = values(currentThread);  // 获取指定线程的Values对象
    if (values == null) {
        values = initializeValues(currentThread);  // 不存在时进行初始化
    }
    values.put(this, value);  // 存储
}

public T get() {
    // Optimized for the fast path.
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values != null) {
        Object[] table = values.table;  // 存储的值就是存储在该对象数组中
        int index = hash & values.mask;
        if (this.reference == table[index]) {
            return (T) table[index + 1];  // 存储位置是在索引的下一位,原因可以去查看put方法
        }
    } else {
        values = initializeValues(currentThread);
    }
    return (T) values.getAfterMiss(this);
}

最终一般调用Handler子类重写的 handleMessage() 方法进行消息处理。

Handler中还有一个很重要的方法是 sendMessage(),最后调用的是 sendMessageAtTime() 方法,主要逻辑就是将消息入队到该Handler相关的Looper中的消息队列中。

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);  // 进行消息的入队
}

参考

《Android开发艺术探索》 - 任玉刚

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

推荐阅读更多精彩内容