Android-异步消息处理机制(Handler,Looper,Message)

相信大部分开发android的人使用Handler在子线程上去进行ui的操作这种模式已经滚瓜烂熟了,但是当你不去深入研究它的原理,和理清它与Looper和Message之间的关系时,遇到问题和bug的时候你就会无从下手,手忙脚乱。

技术也是一门学问,只知其一不知其二,你永远只会停留在基础。送给自己也是送给大家的一句话:你若不想做,总会找到借口;你若真想做,总会找到方法!

开始进入正题,什么是异步消息处理机制?
就应用程序而已,android系统中java的应用程序和其他系统上相同,都是靠消息驱动来工作的,它们的大致工作原理是:

*有一个消息队列,可以往这个消息队列中不断投递消息(进)

*有一个消息循环,可以不断的从这个消息队列中取出消息,进行处理。(出)

事件源把待处理的消息加入到消息队列中(默认是加至队尾,但是也会遇到插队的情况出现,而且不是插中间,直接插入至队列头,这种是属于大哥级别的消息),处理线程从消息队列头不断取出消息分发给对应的target进行处理。这种实现模式在android上主要就是靠我们的Handler,Looper来实现。

为什么需要设计这样的一种机制来进行处理,因为我们都知道Android UI线程是不安全的,如果尝试在非ui线程上去进行ui的更新,这个时候程序是有可能会崩溃的。那么android推荐的处理方式是:在你的当前activity也就是ui线程上创建一个Handler,并实现它的callback:handleMessage,
当你需要在子线程上去进行ui更新的时候,创建一个Message,通过handler发送出去,在handleMessage中接收到这个message对象进行ui更新。

一.Handler

我们平时在开发的过程中都会选择直接去new一个Handler,如下代码:

private Handler mHandler=new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        return false;
    }
});

然后在线程中通过Message来组装需要传递到ui线程的数据,让handler来进行发送,代码如下:

new Thread(new Runnable() {
    @Override
    public void run() {
        //发送消息
        Message msg = Message.obtain();
        msg.what = 111;
        Bundle bundle=new Bundle();
        bundle.putString("huan","hello");
        msg.setData(bundle);
        mHandler.sendMessage(msg);
    }
}).start();

这样就可以在主线程的handler回调callback中接收到这个消息并进行处理:

@Override
public boolean handleMessage(Message msg) {
    if(msg.what==111){
        //取出数据
        String str = msg.getData().getString("huan");
        //...进行ui更新
    }
    return false;
}

这样一套基本的异步更新ui的流程就走完了。但是当写完这些代码的时候有没有想过为什么这样子做可以实现?表面上看我们紧紧就是new 了一个handler和一个messsage作为载体就达到了这样的效果,但是实际底层的实现却比这复杂的多。下面我们可以一起进入源码来看看到底里面是如何实现的。

先来看看handler的构造方法,
我们平时只实现了callback这一个参数,点进去一看发现其实是:

public Handler(Callback callback) {
    this(callback, false);
}

下面的 才是最终的一个实现:

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

可以看到其实当我们在new一个handler的时候,它里面进行了一个很重要的操作:

mLooper = Looper.myLooper();

我们最重要的Looper入场了,Looper.myLooper()方法获取了一个Looper对象,如果Looper对象为空,则会抛出一个运行时异常,所以想要一探究尽,我们得去看看这个Looper这个类,到底扮演着什么角色。

二.Looper

备注:至于Looper这个类到底有什么用,这里先不做解释,我们先跟着源码一步一步走下就会揭开它的面纱

当进入到Looper这个类的时候,查看myLooper()这个方法时发现就紧紧一行代码:

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

通过sThreadLocal这个对象来获取到looper,那既然是通过get方法来获取到这个looper,那必然有set方法来设置这个looper,但是我们自己只是new了一个handler,并没有对looper做任何的处理和操作,那必然是android系统自己在某个地方给我们做了某些操作,接下来继续深入查看Looper这个类,看看sThreadLocal这个对象到底是在哪儿进行申明的:

public final class Looper {
    private static final String TAG = "Looper";

    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

一查找发现,居然在类的开头进行了这个对象的初始化,并且给出了明确的注释:如果没有执行Looper的prepare()方法那么通过sThreadLocal.get()返回的就是null。所以,现在目的就明确了,跟着指示走下去看看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));
}

很明显的看到,原来是在prepare()方法中对sThreadLocal设置了一个新的looper,并且这里也进行了一个非空判断,如果已经执行过prepare来获取过一次looper,那么再次调用的时候就会抛出异常,这样的做法也是保证了一个线程中只有一个Looper的实例。

备注下:sThreadLocal是一个ThreadLocal对象,可以在一个线程中存储变量T

看到这里可能大家可能都会觉得很明了,清晰了,我们整理下整个思路流程就是:

  1. new一个handle来作为发送消息的载体

  2. 在handler的构造函数中通过Looper.myLooper()来获取到了这个Looper

  3. 在myLooper()这个方法其实也是通过sThreadLocal这个对象来进行获取的Looper

  4. 想要通过sThreadLocal这个对象来获取到looper,必须先执行Looper的prepare()方法

那么问题来了:
之前我们不是在new一个handler对象的时候,发现其实构造函数里面就是通过Looper.myLooper()方法来获取了一个looper,但是如果Looper对象为空,则会抛出一个运行时异常;并且我们至始至终都没用使用过这个looper对象,更不用说执行它的prepare方法了,那么为什么我们这样任性的使用handler,程序确没有奔溃?

一开始我就发现了这个细节,而且我们在new handler的时候大部分的时候都是在主线程中去操作的,那么猜想必然是android系统在应用程序启动的时候,开启主线程的时候就为这个线程创建了这了Looper对象,提前为我们执行了Looper.prepare()方法。
这个时候你也不用特意去研究android系统启动的过程就能很轻松的找到在Looper类的prepare()方法下面就是一个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);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

而且注释给的很详细了:在当前线程中初始化了一个Looper,并且作为我们应用程序的main looper,在我们应用程序创建的时候就会创建,所以不需要我们去手动的去调用这个方法。

查看源代码发现果然是这样的:在ActivityThread的main方法中

 public static final void main(String[] args) {      
        ......    
      
        Looper.prepareMainLooper();      
             
        ......      
      
        ActivityThread thread = new ActivityThread();      
        thread.attach(false);      
      
        ......     
        Looper.loop();      
      
        ......     
      
        thread.detach();      
        ......      
    }

这里小结下:我们应用程序在启动的时候会给我们创建一个main looper,并始终存在我们应用程序,所以不需要我们手动去调用Looper.prepare()方法,所以在主线程中的任意地方你都可以放肆的创建Handler,但是注意:如果是在子线程中创建Handler,
务必先调用Looper.prepare()才能创建Handler对象。

备注:在ActivityThread中不仅实现了Looper.prepareMainLooper()方法我们还看到有个Looper.loop()方法,这个方法有什么用,先不着急了解,我们留个伏笔。

三.消息循环

上面讲了hanlder创建的整个过程,以及如何得到looper的过程,但是始终还是不知道这个looper到底有什么用,是如何操作的?接下来回到我们之前的步骤中接着走下去,在sThreadLocal中设置了一个new Looper(),可以进入Looper的构造函数看看到底实现了什么:

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

这里new出了一个MessageQueue对象,也就是我们另一个角色扮演者:消息队列。所有通过handler发送的消息都会存储到这个消息队列来,因此一个Looper对象对应了一个MessageQueue。而在handler的构造函数中我们看到了:mQueue = mLooper.mQueue 也就是说Handler中的消息队列变量最终都会指向Looper的消息队列。

那就不难理解为什么我们通过

mHandler.sendMessage(msg);

这个代码就能将消息发送到Looper的消息队列中来,handler也为我们提供了一系列函数来帮助完成创建消息和插入消息队列的工作:

//从handler中创建一个消息码是what的消息
public final Message obtainMessage(int what)

//发送一个只含有消息码的消息
public final boolean sendEmptyMessage(int what)

//延时发送一个只含有消息码的消息
 public final boolean sendEmptyMessageDelayed(int what, long delayMillis)

//发送一个消息,默认添加至队列尾
public final boolean sendMessage(Message msg)

//发送一个消息,添加至队列头,优先级高
public final boolean sendMessageAtFrontOfQueue(Message msg)

接下来再进入sendMessage中看看如何操作的就明了了:

public final boolean sendMessage(Message msg)
{
    return sendMessageDelayed(msg, 0);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
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);
}

最后调用的是sendMessageAtTime这个方法,也就是拿到之前的MessageQueue然后进行了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指向了自己,也就是handler,然后又调用了queue.enqueueMessage(msg, uptimeMillis)
进去看看:

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) {
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    synchronized (this) {
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w("MessageQueue", e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

大致分析了下,这里主要就是将消息进行了一个时间排序,根据我们传入的uptimeMillis.根据时间的顺序调用msg.next.
消息进入队列已经完成,那么在什么时候进入处理消息,循环消息,就是我们之前埋下的伏笔:在ActivityThread中不仅实现了Looper.prepareMainLooper()方法我们还看到有个Looper.loop()方法

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    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;
        }

        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        msg.target.dispatchMessage(msg);

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }

        msg.recycleUnchecked();
    }
}

我们有时看源码的时候,没必要每一行都去理解它,抓重点就行:这个函数主要创建了一个死循环,不断的调用Message msg = queue.next();来从消息队列里面取出消息,并进行处理:msg.target.dispatchMessage(msg) ,而msg.target刚好是我们之前看handler的时候把自己赋值给了这个target;接下来继续看看这个target做了什么事:msg.target.dispatchMessage(msg);

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

在dispatchMessage方法中可以看到如果消息本身有callback,则直接交给msg的callback处理,否则mCallback不为null就会执行mCallback.handleMessage(msg);看到这儿相信大家已经豁然开朗了,这从我们刚开始讲的实现callback的handleMessage方法相对应,也就是说消息是从这里传递过去的。这样我们从开头到这儿刚好形成一个逻辑的闭环。一个消息链的发送和接收处理的完整过程我们再进行总结下:

1.无论是在android 应用程序主线程还是其他线程首先会执行Looper.prepare(),创建该线程的Looper对象,记住:一个线程对应一个Looper对象;然后在创建Looper的时候创建了一个MessagQueue消息队列管理消息的入栈和出栈,也是一个线程对应一个MessagQueue;

2.执行Looper.loop方法让该线程创建一个死循环,不断的调用Message msg = queue.next();来从消息队列里面取出消息,并进行处理:msg.target.dispatchMessage(msg)

3.创建一个Handler来进行消息的发送和接收处理,并在初始化的时候与该线程的Looper中的MessagQueue相关联;

4.通过handler发送message的时候,会将msg的target设置为handler自己,并且将该msg按照时间进行排序至消息队列

那么Handler,Looper,Message之间的关系就是:
handler负责不断发送message到MessageQueue;Looper将消息队列中的消息一个一个取出回调给dispatchMessage方法;最后消息回到handler所在的线程,通过handler的callback方法进行处理。

好了,到这里差不多把整个handler异步消息机制梳理完毕;后面还会出一篇文章讲解下Looper和handler的同步关系。以及在多线程中如何处理这个消息的传递;handlerThread的用法;

谢谢。

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

推荐阅读更多精彩内容