Android异步消息处理机制深度解析

对于Android的异步消息处理机制,大家一定都不陌生,异步消息处理机制一个常见的应用场景就是在子线程中更新UI,我们都知道,Android的UI是线程不安全的,如果在子线程中直接更新UI,便会导致程序崩溃。对于该问题,常见的解决方法是,在UI线程新建一个Handler并覆写其handleMessage方法,在子线程中获取Message对象,通过Message对象的arg,obj字段以及setData()方法携带一些数据,之后利用UI线程的Handler将消息发送出去,最后便可以在handleMessage方法中获取到刚刚发送的消息并进行相应的处理了,示例代码如下:

public class MainActivity extends ActionBarActivity {

    private Handler handler1=new Handler(){
        @Override
        public void handleMessage(Message msg){
            //to do sth
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable(){
            @Override
            public void run() {
                Message msg=Message.obtain();
                msg.what=1;
                Bundle bundle = new Bundle();  
                bundle.putString("info", "info");  
                msg.setData(bundle);  
                handler1.sendMessage(msg);
            }
        }).start();
    }

}

这种方法相信大家都已经用得很熟了,注意,此时我们是在UI线程创建Handler的,那么现在我们尝试在子线程创建Handler,看看与之前的UI线程创建Handler有何区别,示例代码如下:

public class MainActivity extends ActionBarActivity {

    private Handler handler2=null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable(){
            @Override
            public void run() {
                handler2=new Handler();
            }
        }).start(); 
    }

}

结果,系统竟然报错了,错误提示信息为Can't create handler inside thread that has not called Looper.prepare() 。意思是,不能在一个没有调用Looper.prepare()的线程内创建Handler。那么我们依据系统的意思,在创建Handler之前调用Looper.prepare()试试看:

new Thread(new Runnable(){
    @Override
    public void run() {
        Looper.prepare();
        handler2=new Handler();
    }
}).start(); 

果不其然,这回终于不报错了。但是,仅仅解决问题是不够的,我们更应该去探究问题背后所隐藏的原理,只有这样我们的能力才会有一个质的提升,所以下面,我将带领大家从源代码级别深入地探究Android的异步消息处理机制

OK,话不多说,让我们赶快进入这美妙的探索之旅吧~~~

前面已经讲到,在子线程中创建Handler时,如果事先不调用一下Looper.prepare(),系统便会报错。那么我们为什么一定要先去调一下Looper.prepare()呢?看来,只有Handler的构造函数以及Looper.prepare()的源代码能够告诉我们答案了,我们先从Handler的构造函数看起,Handler的构造函数源代码如下:

public Handler() {  
    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 = null;  
} 

我们从mLooper = Looper.myLooper()这行代码看起,如果拿到的mLooper对象为空的话,便会抛出一个运行时异常,提示信息正是刚刚的“Can't create handler inside thread that has not called Looper.prepare()”,那么mLooper对象何时为空呢,这就要去看Looper.myLooper()中的代码了,Looper.myLooper()的代码如下:

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

显然,这个方法是从sThreadLocal对象中拿出当前的Looper对象,如果拿不到的话则返回null。现在我们可以大胆猜测下在什么地方给sThreadLocal对象设置Looper的了,没错,就是在Looper.prepare()方法中,我们赶紧去看一下Looper.prepare()方法的源码:

public static final void prepare() {  
    if (sThreadLocal.get() != null) {  
        throw new RuntimeException("Only one Looper may be created per thread");  
    }  
    sThreadLocal.set(new Looper());  
}  

在Looper.prepare()中,会先去尝试获取sThreadLocal中的Looper对象,如果当前能够获取到Looper对象,则抛出运行时异常“Only one Looper may be created per thread”,这也说明了每个线程最多只能有一个Looper对象。如果获取不到Looper对象,则给sThreadLocal设置Looper对象。
说到这里,很多朋友可能都会有疑惑:为什么主线程中没有调用Looper.prepare(),却依然能够正常地创建Handler呢?
这是因为程序在启动时,系统已经自动帮我们调用了Looper.prepare()了,具体可以参照ActivityThread中的main()方法,这里就不去详述了,感兴趣的朋友可以去自行查阅。
我们继续去分析Handler中的源代码:

  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 = null; 

可以看到,在利用Looper.myLooper()获取到Looper对象并赋值给Handler的成员变量mLooper之后,又将 mLooper的mQueue赋值给Handler的成员变量mQueue,由此可见,Handler中拥有着Looper和MessageQueue两个成员变量,Handler的构造函数的主要目的就是初始化这两个成员变量,同时,一个Looper对应着一个MessageQueue。
分析完Handler的构造函数以及Looper.prepare()的源代码,我们再来研究一下消息的发送流程,先温习一下消息发送的代码:

Message msg=Message.obtain();
msg.what=1;
Bundle bundle = new Bundle();  
bundle.putString("info", "info");  
msg.setData(bundle);  
handler1.sendMessage(msg);

看到这里,我们不禁要问:Handler到底将消息发到哪里?为什么之后在handleMessage中又可以收到之前发送的消息?
我们知道,Handler有很多发送Message的方法,其中,除了sendMessageAtFrontOfQueue方法,其他方法都会辗转调用到sendMessageAtTime方法,sendMessageAtTime方法的源代码如下:

public boolean sendMessageAtTime(Message msg, long uptimeMillis)  
{  
    boolean sent = false;  
    MessageQueue queue = mQueue;  
    if (queue != null) {  
        msg.target = this;  
        sent = queue.enqueueMessage(msg, uptimeMillis);  
    }  
    else {  
        RuntimeException e = new RuntimeException(  
            this + " sendMessageAtTime() called with no mQueue");  
        Log.w("Looper", e.getMessage(), e);  
    }  
    return sent;  
}  

可以看到,sendMessageAtTime方法有两个参数,第一个参数为msg,代表着我们发送的消息,第二个参数为uptimeMillis,代表着发送消息的时间,其值为自系统开机到当前时间的毫秒数加上延迟时间,如果不是调用的sendMessageDelayed方法,则延迟时间为0.之后,将当前Handler的MessageQueue对象(即mQueue)取出,判断MessageQueue对象是否为空,若不为空,则将当前消息的target属性指向当前发送消息的Handler对象(即this),最后,调用我们MessageQueue对象的enqueueMessage方法让消息进入消息队列中。
这里要稍微解释下MessageQueue,顾名思义,MessageQueue就是一个消息队列,其内部会以一个队列的形式存储我们发送的消息,并提供了消息的入队和出队方法。
接下来就要分析较为关键的enqueueMessage方法了,enqueueMessage方法会将消息放入消息队列中,并按照消息发送的时间对消息进行排序,enqueueMessage方法的源代码如下:

final boolean enqueueMessage(Message msg, long when) {  
    if (msg.when != 0) {  
        throw new AndroidRuntimeException(msg + " This message is already in use.");  
    }  
    if (msg.target == null && !mQuitAllowed) {  
        throw new RuntimeException("Main thread not allowed to quit");  
    }  
    synchronized (this) {  
        if (mQuiting) {  
            RuntimeException e = new RuntimeException(msg.target + " sending message to a Handler on a dead thread");  
            Log.w("MessageQueue", e.getMessage(), e);  
            return false;  
        } else if (msg.target == null) {  
            mQuiting = true;  
        }  
        msg.when = when;  
        Message p = mMessages;  
        if (p == null || when == 0 || when < p.when) {  
            msg.next = p;  
            mMessages = msg;  
            this.notify();  
        } else {  
            Message prev = null;  
            while (p != null && p.when <= when) {  
                prev = p;  
                p = p.next;  
            }  
            msg.next = prev.next;  
            prev.next = msg;  
            this.notify();  
        }  
    }  
    return true;  
}  

enqueueMessage方法有两个参数,第一个是入队的消息,第二个是该消息发送的时间。前面提到的sendMessageAtFrontOfQueue方法也会调用到enqueueMessage方法,但传入的消息发送时间为0。在enqueueMessage方法的内部,会将当前入队消息的when字段设置为传入的消息发送时间,取出当前的队头消息并赋给变量p,之后判断如果当前队头消息为空或消息发送时间为0或消息发送时间小于队头消息的时间,则将当前入队消息的next指针指向队头消息,之后将队头消息重新赋值为入队消息,从而完成了将入队消息插入到消息队列头部的操作。如果不满足上述的三种情况,则按照消息发送时间的先后顺序寻找入队消息在队列中的插入位置,之后将入队消息插入即可。
上面是对消息的入队操作的分析,那么出队操作在哪里呢?我们就要去分析一下Looper.loop()的源码了:

public static final void loop() {  
    Looper me = myLooper();  
    MessageQueue queue = me.mQueue;  
    while (true) {  
        Message msg = queue.next(); // might block  
        if (msg != null) {  
            if (msg.target == null) {  
                return;  
            }  
            if (me.mLogging!= null) me.mLogging.println(  
                    ">>>>> Dispatching to " + msg.target + " "  
                    + msg.callback + ": " + msg.what  
                    );  
            msg.target.dispatchMessage(msg);  
            if (me.mLogging!= null) me.mLogging.println(  
                    "<<<<< Finished to    " + msg.target + " "  
                    + msg.callback);  
            msg.recycle();  
        }  
    }  
}  

在Looper.loop()中,会进入一个死循环,不断调用当前MessageQueue的next()方法取出下一条待处理的消息,如果当前没有待处理的消息则阻塞。之后,当消息不为空且消息的target字段不为空的话,调用消息的target字段的dispatchMessage方法,注意,此时消息的target字段就是当初发送这条消息的Handler对象。
接下来,我们进入到Handler的dispatchMessage方法的源代码中:

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

在Handler的dispatchMessage方法中,首先会去判断消息的callback字段是否为空,若不为空,则调用handleCallback方法对消息进行处理,若为空,则再去判断Handler的mCallback字段是否为空(Handler的无参构造函数中mCallback被设置为空),若不为空,则调用mCallback的handleMessage方法,若mCallback字段为空,则直接调用 handleMessage方法。
至此,我们已经从源代码级别回答了上面提出的两个问题:Handler到底将消息发到哪里?为什么之后在handleMessage中又可以收到之前发送的消息?相信大家一定都有很深的理解了吧_
下面介绍一个Android异步消息处理线程的最标准的写法,此写法引自Android官方文档:

class LooperThread extends Thread {  
      public Handler mHandler;  
      public void run() {  
          Looper.prepare();  
          mHandler = new Handler() {  
              public void handleMessage(Message msg) {  
                  // process incoming messages here  
              }  
          };  
          Looper.loop();  
      }  
  }  

现在,我们来思考一个非常关键的问题:handleMessage方法何时运行在主线程中,何时运行在子线程中?
有不少朋友可能会说,handleMessage方法的定义位于主线程中,其就会在主线程中执行,handleMessage方法的定义位于子线程中,其就会在子线程中执行。
事实真的是这样吗?
其实不然,我先举个反例。HandlerThread就是一个典型的反例。我们在主线程中定义Handler并覆写其handleMessage方法,在定义Handler时,我们传入一个已经启动的HandlerThread对象的Looper作为参数,那么,我们此时的handleMessage方法是运行在子线程中的,但此时我们handleMessage方法的定义是位于主线程中的。
这是怎么回事呢?明明handleMessage方法的定义是位于主线程中的,怎么会运行在子线程里面呢?看来还得再次分析一下我们的源码,不过这一次,我们需要对源码进行逆向分析。
首先,我们知道,handleMessage方法是在dispatchMessage方法中被调用的,而dispatchMessage方法又是在Looper.loop()中调用的,也就是说,如果Looper.loop()运行在主线程,handleMessage方法也会运行在主线程,如果Looper.loop()运行在子线程,handleMessage方法也会运行在子线程。那么我们的Looper.loop()到底是运行在主线程,还是在子线程呢?其实,这就要看我们定义的Handler用的是哪个线程的Looper了,如果我们定义的Handler用的是主线程的Looper,那么它使用的MessageQueue自然也是主线程Looper对应的MessageQueue,通过该Handler发送的消息会进入该MessageQueue中,之后会在主线程的Looper对应的Looper.loop()方法中不断取出该MessageQueue中的消息进行处理,注意,此时我们主线程的Looper对应的Looper.loop()方法也是运行在主线程中的,所以此时我们的handleMessage方法也是运行在主线程中的。定义的Handler用的是子线程的Looper的分析过程同上。总结一下,如果创建Handler时用的是主线程的Looper,则相应的handleMessage方法会运行在主线程中,如果创建Handler时用的是子线程的Looper,则相应的handleMessage方法会运行在子线程中。
看到这里,相信大家已经对Android的异步消息处理机制有了一个非常深入的理解了,如果对文中内容有疑惑的话请在博客下方留言,我会尽可能地解答,谢谢大家,望大家多多支持!

参考:http://blog.csdn.net/guolin_blog/article/details/9991569

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

推荐阅读更多精彩内容