Android异步消息处理机制源码分析

声明:本文是参考了以下几位大神的文章,自己按照自己的思维习惯整理的笔记,并添加一些相关的内容。如有不正确的地方,欢迎留言指出,谢谢!
郭霖博客
鸿洋博客
任玉刚《Android开发艺术探索》

一. Andoid消息机制概述

Android规定访问UI只能在主线程进行,如果在子线程中访问UI,那么程序就会抛出异常。ViewRootImpl对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);
  }
}

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

为什么系统不对UI控件的访问加上锁机制?

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

回想一下,实际开发中,在子线程进行UI操作,通常有以下几种方法:

  1. 调用 runOnUiThread() 方法,通过匿名内部类实现run()方法进行UI操作
  2. 使用AsyncTask
  3. 在主线程中创建Handler对象,子线程创建Message对象,然后通过 sendMessage() 发送创建的Message,主线程通过handleMessage() 处理接收到的Message
  4. Handler的 post() 方法
  5. View的 post() 方法

其实,这么多的方法,其内部的实现原理都是一样的,都是通过接下来我们要说的Android异步消息处理机制来实现的。

二. Android异步消息处理机制分析

Android的异步消息处理机制实际上就是Handle的运行机制,因此本节主要围绕Handler的工作过程来分析,主要包括 HandlerMessageMessageQueueLooper 这四个类。另外,为了更好地理解Looper这个类,还会介绍 ThreadLocal 这个类。

1. ThreadLocal的工作原理

ThreadLocal是一个线程内部的 数据存储类,通过它可以在指定的线程中存储数据,数据存储后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。

ThreadLocal的使用场景:

  1. 当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候
  2. 复杂逻辑下的对象传递。比如监听器的传递,有时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性,在这种情况下,我们又需要监听器能够贯穿整个线程的执行过程,就可以采用ThreadLocal,让监听器作为线程内部的全局对象而存在,在线程内部只要通过get方法就可以获取到监听器。

下边通过例子看一下ThreadLocal的真正含义:

  //定义一个ThreadLocal对象,存储一个Boolean类型的数据
ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<Boolean>();

//在主线程中设置值为true
mBooleanThreadLocal.set(true);
System.out.println("[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());

//新建子线程1,设置值为false
new Thread("Thread#1"){
    @Override
    public void run() {
        
        mBooleanThreadLocal.set(false);
        System.out.println("[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
    };
}.start();

//新建子线程2,不设置
new Thread("Thread#2"){
    @Override
    public void run() {
        
        System.out.println("[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
    };
}.start();
        
        

上面代码中,在主线程中设置mBooleanThreadLocal的值为true,在子线程1中设置其值为false,在子线程2不设置其值。然后分别在3个线程中通过get方法获取mBooleanThreadLocal的值,根据前边对ThreadLocal的描述,这个时候,主线程应该是true,子线程1中应该是false,而子线程2中由于没有设置值,所以应该是null。运行结果如下:

[Thread#main]mBooleanThreadLocal=true
[Thread#1]mBooleanThreadLocal=false
[Thread#1]mBooleanThreadLocal=null

从运行结果可以看出,虽然在不同线程中访问的是同一个ThreadLocal对象,但是它们通过ThreadLocal获取的值却不一样,这就是ThreadLocal的奇妙之处。

ThreadLocal之所以有这么奇妙的效果,是因为不同线程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找出对应的value值。很显然,不同线程中的数组是不同的。

2. MessageQueue的工作原理

MessageQueue主要包含两个操作:插入和读取。对应的方法为 enqueueMessage()next(),其中enqueueMessage的作用就是往消息队列插入一条消息,而next的作用是从消息队列中取出一条消息并将其从消息队列删除。故读取操作本身会伴随着删除操作。

尽管MessageQueue叫消息队列,当时它的内部实现并不是用的队列,实际上它是通过一个单链表的数据结构来维护消息列表,单链表在插入和删除上比较有优势。

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

观察上面的代码,可以发现,其实所谓的入队其实是将所有的消息按时间进行排序(通过比较when参数)。具体的操作方法就是根据时间顺序调用msg.next(),从而为每一个消息指定它的下个消息是什么。

next()方法有点长,这里不贴出来。next()方法是一个无限循环方法,如果消息队列中没有消息,那么next方法就会一直阻塞。当有新消息到来时,next方法会返回这条消息并将其从单链表移除。

3. Looper的工作原理

Looper在Android消息机制中扮演着消息循环的角色,负责创建一个MessageQueue,然后进入一个死循环不断从该MessageQueue中读取消息。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(true));
}

可以看到,首先判断sThreadLocal中是否已经存在Looper了,如果还没有则创建一个新的Looper设置进去。如果sThreadLocal已经有了looper,则抛出异常,这也说明Looper.prepare()不能被调用两次,同时保证了一个线程中只有一个Looper实例

再来看看Looper的构造方法:

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

在Looper的构造方法中,创建了一个MessageQueue。由于Looper只能被调用一次,这也就保证了一个线程只能有一个Looper实例,一个Looper只能对应一个MessageQueue

Looper最重要的方法是looper方法,只有调用了looper后,消息系统才会真正地起作用。下面看看loop()源码:

public static final 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;
    
    for(;;) {//死循环
        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.target.dispatchMessage(msg);
            if (me.mLogging!= null) me.mLogging.println(
                    "<<<<< Finished to    " + msg.target + " "
                    + msg.callback);
            //释放消息占用资源
            msg.recycle();
        }
    }
}

looper()方法会陷入一个死循环,不断地调用MessageQueue的 next() 方法去获取消息队列中的消息。每当有一个消息出队,就将它传递到 msg.targetdispatchMessage() 方法,这里的msg.target就是Handler(下边会分析为什么msg.target是一个Handler对象)。如果消息队列为空,则会阻塞

另外,看看myLooper方法

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

在looper()方法中,会先调用myLooper()方法判断looper是否为空。故,looper()必须在调用prepare()后才能调用。

Looper提供了quit 和 quitSafely来退出一个Looper,二者区别是:quit会直接退出,而quitSafely只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全退出。

public void quit() {
    mQueue.quit(false); 
}

public void quitSafely() {
    mQueue.quit(true); 
}

从源码可以看到,退出Looper的两个方法都调用了MessageQueue的quit()方法。

void quit(boolean safe) {
    ...
    
    synchronized (this) {
        if (mQuitting) {
            return;
        }
        mQuitting = true;   
        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }

        ...
    }
}

通过源码,发现,最终,分别调用的是MessageQueue的 removeAllFutureMessagesLocked()removeAllMessagesLocked() 方法。removeAllMessagesLocked()方法主要是把MessageQueue消息池中所有的消息全部清空,无论是延迟消息还是非延迟消息;而removeAllFutureMessagesLocked()方法只会清空MessageQueue消息池中所有的延迟消息,并将消息池中所有非延迟消息派发出去让Handler去处理完后才停止Looper循环。quitSafely相比于quit方法安全的原因在于清空消息之前会派发所有的非延迟消息。

在子线程中,如果手动为其创建了Looper,那么在所有的事情处理完成后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待状态。

4. Handler的工作原理

Handle 的工作主要包含消息的发送和接收过程。消息的发送可以通过 post() 的一系列方法以及 send() 的一系列方法来实现。使用Handler之前,我们都是初始化一个实例,所以想来看看Handle的构造方法,看其如何与MessageQueue联系上的,它在子线程中发送的消息(一般发送消息都在非UI线程)怎么发送到MessageQueue中的。

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

可以看到,构造方法中会调用Looper.myLooper()来获取当前的Looper对象,如果Looper为空,则会抛出异常。然后又获取了这个Looper实例中保存的MessageQueue(消息队列),这样就保证了handler的实例与我们Looper实例中MessageQueue关联上了。

然后看看我们最常用的 sendMessage() 方法:

public final boolean sendMessage(Message msg){
    return sendMessageDelayed(msg, 0);
}
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
    Message msg = Message.obtain();
    msg.what = what;
    return sendMessageDelayed(msg, delayMillis);
}
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() 。其实,Handler有很多发送消息的方法,其中除了sendMessageAtFrontOfQueue()方法之外,其它的发送消息方法最终都会辗转调用到sendMessageAtTime()方法中。

sendMessageAtTime()方法接收两个参数,其中msg参数就是我们发送的Message对象,而uptimeMillis参数则表示发送消息的时间,它的值等于自系统开机到当前时间的毫秒数再加上延迟时间。

sendMessageAtTime()方法又调用了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 赋值为this,也就是把当前的handler作为msg的target属性,最终会调用queue的enqueueMessage()方法入队列。如果大家还记得Looper的looper()方法,该方法中会取出每一个msg,然后交给msg.target.dispatchMessage(msg)通过处理消息,上文说这里的msg.target就是一个Handler对象就是在这里注入的。

接下来看看dispatchMessage()方法:

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

该方法中,先进行判断,如果mCallback不为空,则调用mCallback的handleMessage()方法,否则直接调用Handler的handleMessage()方法。而Handler的handleMessage是一个空方法,所以一般我们会继承Handler并重写handlerMessage()方法来实现我们处理消息的逻辑。

5. 总结

一个最标准的异步消息处理线程应该是这样写的:

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

分析一下,为什么使用异步消息处理的方式就可以对UI进行操作了呢?这是由于Handler总是依附于创建时所在的线程,比如我们的Handler是在主线程中创建的,而在子线程中又无法直接对UI进行操作,于是我们就通过一系列的发送消息、入队、出队等环节,最后调用到了Handler的handleMessage()方法中,这时的handleMessage()方法已经是在主线程中运行的,因而我们当然可以在这里进行UI操作了。整个异步消息处理流程的示意图如下图所示:

Android异步消息处理机制流程图

总结一下:

  1. 首先Looper.prepare()在本线程中保存一个Looper实例,然后该实例中保存一个MessageQueue对象;因为Looper.prepare()在一个线程中只能调用一次,一个线程只会有一个Looper和一个MessageQueue
  2. Looper.loop()会让当前线程进入一个无限循环,不断从MessageQueue队列中通过next()方法获取消息,然后回调msg.target.dispatchMessage(msg)方法进行处理
  3. Handler的构造方法,会首先得到当前线程中保存的Looper实例,进而与Looper实例中的MessageQueue进行关联
  4. Handler的sendMessage()方法,会给msg的target赋值为handler自身,然后加入MessageQueue中
  5. 在构造Handler实例时,我们会重写handleMessage()方法,也就是msg.target.dispatchMessage(msg)最终调用的方法

三. 子线程进行UI操作几种方式的分析

本文开始的时候就归纳了5种在子线程中进行UI操作的方法,前边分析了整个Android异步消息处理机制的流程,也就是我们比较常用的通过重写Handler的handleMessage()方法这一流程。下边,我们对其它几种方式进行分析。

1. Handler的post()方法

Handler的post()方法如下:

public final boolean post(Runnable r){
   return  sendMessageDelayed(getPostMessage(r), 0);
}

调用了sendMessageDelayed()方法去发送一条消息,并且还使用了getPostMessage()方法将Runnable对象转换成Message。

private final Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

这个方法中,将消息的callback字段值指定为传入的Runnable对象。在Handler的dispatchMessage()方法中原来有做一个检查,如果Message的callback等于null才会去调用handleMessage()方法,否则就调用handleCallback()方法。那我们快来看下handleCallback()方法中的代码吧:

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

非常粗暴,直接调用一开始传入的Runnable对象的run()方法。因此,在子线程中通过Handler的post()方法进行UI操作可以这么写:

public class MainActivity extends Activity {
    private Handler handler;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler = new Handler();
        new Thread(new Runnable() {
            @Override
            public void run() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        // 在这里进行UI操作
                    }
                });
            }
        }).start();
    }
}

2. View的post()方法

看一下View中的post()源码:

public boolean post(Runnable action) {
    Handler handler;
    if (mAttachInfo != null) {
        handler = mAttachInfo.mHandler;
    } else {
        ViewRoot.getRunQueue().post(action);
        return true;
    }
    return handler.post(action);
}

直接调用了上边分析过的Handler的post()方法

3. Activity的runOnUiThread()方法

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

也很简单,先判断当前线程是不是主线程,如果不是,调用handler的post()方法。否则直接调用Runnable的run()方法。

4. AsyncTask机制

由于AsyncTask后边会专门写一篇文章介绍,这里就不展开了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容