Android 消息处理

其实对于初学者来说Handler的使用很容易会很容易上手,但是对于其中的机制的理解,却不会那么简单,甚至1,2年经验的android开发也可能对其内部实现原理不是很了解。

其中我经过的认知的过程

  1. 初识Handler,认为没必要,直接执行方法不就好了么,为什么还要post出去,然后再在handleMessage中作处理,多此一举。

  2. 异步可使用Handler,在主线程更新ui

  3. 主线程中有个Looper不断循环,从MessageQueue中获取到Message,而Handler只是往MessageQueue中塞入Message

  4. 非主线程中也可以创建自己的Looper来处理消息,不过要自己调用Looper.prepare(),Looper.loop(),初始化和开始轮询。

  5. MessageQueue调用了jni层的方法来实现消息处理机制

笔者对整个android的消息处理机制的认知过程如上,经过的时间也很长,不是一蹴而就的。

今天就通过对其底层源码分析,和大家分享其内部实现原理,主要是第5步的认知,涉及到了jni层。这里主要介绍主线程中的Looper。

  注意:android源码2.3.1

我们从头开始
frameworks/base/core/java/android/app/ActivityThread

...
...
public static final void main(String[] args) {
    ...
     Looper.prepareMainLooper();
        if (sMainThreadHandler == null) {
            sMainThreadHandler = new Handler();
        }
      ...
      ...
        Looper.loop();
    ...
}

我们这里以ActivityThread为入口来分析消息处理机制,每个app启动的时候,都会创建自己的ActivityThread,并且在main方法中也会创建主线程中的Looper。

这里如果对app整个启动流程不是特别清楚的话,建议先看下
http://www.jianshu.com/p/9da5bb46835c

frameworks/base/core/java/android/os/Looper

...
private Looper() {
        mQueue = new MessageQueue();
        mRun = true;
        mThread = Thread.currentThread();
    }
...
 public static final void prepare() {
        ...
        sThreadLocal.set(new Looper());
    }
    public static final void prepareMainLooper() {
        prepare();
        setMainLooper(myLooper());
       ...
    }
  ...
  ...
   public static final Looper myLooper() {
        return (Looper)sThreadLocal.get();
    }
  ...

首先我们要理解ThreadLocal这个类的作用
http://www.cnblogs.com/alphablox/archive/2013/01/20/2869061.html

简单来说,就是为了实现每个线程中有对应一个Looper。

上面代码主要做了

  1. 创建Looper,MessageQueue
  2. 把Looper与当前主线程关联

mQueue其实就是MessageQueue,在Looper的构造方法中创建。
下面分析下MessageQueue。

frameworks/base/core/java/android/os/MessageQueue

...
private native void nativeInit();
...
MessageQueue() {
        nativeInit();
    }
...

创建MessageQueue,其实主要调用了native方法。

其实在我们分析android源码的时候,要找到的对应的native方法,其实是一件很麻烦的事情,因为我们只知道方法名,并不知道对应的.c文件或者.cpp文件是哪个,这样就造成了我们阅读的障碍,这里有个比较笨的方法就是使用grep '**' -R .来查询文件内容。但是毕竟android源码比较庞大,这样做其实还是很麻烦的,我这里暂时也没有更好的办法,主要还是参考了别人的文章,找到了对应的文件。

frameworks/base/core/jni/android_os_MessageQueue

...
NativeMessageQueue::NativeMessageQueue() {
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        mLooper = new Looper(false);
        Looper::setForThread(mLooper);
    }
}
...
static void android_os_MessageQueue_setNativeMessageQueue(JNIEnv* env, jobject messageQueueObj,
        NativeMessageQueue* nativeMessageQueue) {
    env->SetIntField(messageQueueObj, gMessageQueueClassInfo.mPtr,
             reinterpret_cast<jint>(nativeMessageQueue));
}
...
static void android_os_MessageQueue_nativeInit(JNIEnv* env, jobject obj) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
  ...
    android_os_MessageQueue_setNativeMessageQueue(env, obj, nativeMessageQueue);
}
...
static JNINativeMethod gMessageQueueMethods[] = {
    { "nativeInit", "()V", (void*)android_os_MessageQueue_nativeInit },
    ...
};
...

简单地介绍下上面代码

  1. 注册对应的jni方法,如nativeInit.
  2. java层调用nativeInit后创建,NativeMessageQueue
  3. NativeMessageQueue中创建了Looper,如java层也是跟线程绑定,但是这个jni层的Looper,跟java层的 Looper没有关系。
  4. 利用JNIEnv方法给java 层中的MessageQueue对象的mPtr变量设置为NativeMessageQueue的地址。

下面是对jni层Looper的解析
frameworks/base/libs/utils/Looper

Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks),
        mResponseIndex(0) {
      ...
      int result = pipe(wakeFds);
      ...
      mWakeReadPipeFd = wakeFds[0];
      mWakeWritePipeFd = wakeFds[1];
      ...
      mEpollFd = epoll_create(EPOLL_SIZE_HINT);
      ...
}

其实Looper的代码很多,这里我就贴出了关键的几行,但是涉及到的知识点却很多。

  1. pipe,创建读写管道
    http://www.cnblogs.com/kunhu/p/3608109.html
  2. epoll_create,管理io
    http://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html

其实我对这2者也并不是很熟悉。但是我知道以下几点,我认为就足够了。

  1. pipe创建读写2个通道
  2. epoll监听io
  3. epoll_wait会io阻塞,等到pipe管道中写入内容后,阻塞结束。

以上其实都是ActivityThread中调用Looper.prepareMainLooper之后的流程。主要是进行一些初始化,创建一些基础的对象。

下面我们来分析Looper.loop(),消息轮询。

frameworks/base/core/java/android/os/Looper

...
 public static final void loop() {
        Looper me = myLooper();
        MessageQueue queue = me.mQueue;
        while (true) {
            Message msg = queue.next(); // might block
             ...
                msg.target.dispatchMessage(msg);
             ...
                msg.recycle();
            }
        }
    }
...

这段代码就是开启while死循环,不断从MessageQueue中获取Message,一看最重要的肯定是queue.next()获取Message这个方法。
继续跟入。
frameworks/base/core/java/android/os/MessageQueue

  ...
  final Message next() {
      ...
       for (;;) {
            ...
            nativePollOnce(mPtr, nextPollTimeoutMillis);
            ...
        }
      ...
  }
  ...

next方法去获取Message,最终还是调用nativePollOnce方法,传入的2个参数,这里介绍下。

  1. mPtr,是MessageQueue的一个变量,在前面其实已经介绍过了,其实是指向jni层的NativeMessageQueue的地址。

  2. nextPollTimeoutMillis其实是epoll_wait时,如果pipe中没有内容写入的等待时间,举个例子,如果nextPollTimeoutMillis为1000,那么这1秒中如果pipe中一直没有东西写入,那么,epoll_wait那么会阻塞1秒,然后继续运行,如果为0的话,epoll_wait就不会阻塞,如果为-1的话,如果pipe中没有东西写入,就会一直阻塞。

下面我们来看下nativePollOnce的具体实现。

frameworks/base/core/jni/android_os_MessageQueue

...
void NativeMessageQueue::pollOnce(int timeoutMillis) {
    mLooper->pollOnce(timeoutMillis);
}
...

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jint ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(timeoutMillis);
}
...

其实在前面那一步就已经介绍了,ptr是NativeMessageQueue指针,在方法中强转为NativeMessageQueue对象后,调用他的pollOnce,
然后再调用NativeMessageQueue中的Looper的pollOnce方法

frameworks/base/libs/utils/Looper

...
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
    for (;;) {
        while (mResponseIndex < mResponses.size()) {
         ...
        result = pollInner(timeoutMillis);
    }
}

int Looper::pollInner(int timeoutMillis) {
    ...
    ...
     int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    ...
     for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeReadPipeFd) {
            if (epollEvents & EPOLLIN) {
                awoken();
            }
            ...
         }
        ...
      }
      ..
}
...
void Looper::awoken() {
...
    do {
        nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
    } while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
}
...
  1. epoll_wait阻塞,等待timeoutMillis毫秒或者pipe中有输入
  2. 如果有消息过来了,其实会先往pipe中输入,解除epoll_wait阻塞
  3. for循环eventCount,判断是否是当前线程的管道的fd,如果是,就调用awoken方法,read一下把东西读出来

其实到这里了,已经把大部分重要的代码贴出,下面我们来过一遍,走下整个流程,来梳理一下。

  1. Looper初始化,这里就不过多介绍了。
  2. 调用loop,不断从MessageQueue取数据。
    第一次调用时,nextPollTimeoutMillis=0,epoll_wait没有造成阻塞,马上运行到下面,由于刚开始,还没有消息过来eventCount=0,所以
    java层中。
    frameworks/base/core/java/android/os/MessageQueue
...
final Message next() {
    ...
    for (;;) {
    ...
    nativePollOnce(mPtr, nextPollTimeoutMillis);
            synchronized (this) {
                ...
                final Message msg = mMessages;
                if (msg != null) {
                    final long when = msg.when;
                    if (now >= when) {
                        mBlocked = false;
                        mMessages = msg.next;
                        msg.next = null;
                        if (Config.LOGV) Log.v("MessageQueue", "Returning message: " + msg);
                        return msg;
          else{
                    nextPollTimeoutMillis = -1;
          }
    ...
    }
}
...

调用nativePollOnce其实主要作用就是在jni层中epoll_wait等待pipe管道有新的输入,即有新的 Message。

刚刚开始,没有阻塞,所以nativePollOnce马上返回。

Message其实是一个链表结构。

mMessages变量始终指向第一个Message.

由于刚开始还没有Message,所有mMessages = null

进入else,设置nextPollTimeoutMillis = -1;
由于还处于for之中,所以继续调用native方法nativePollOnce,
前面我已经介绍了,当nextPollTimeoutMillis=-1时,其实在epoll_wait会一直阻塞。

这时,我又不得不提到这篇优秀的文章
http://www.jianshu.com/p/9da5bb46835c

这里我并不清楚第一个Message消息是哪里传过来的,但是在App启动时Activity的生命周期中大部分都是利用handleMessage来处理的,姑且当作是第一个Message吧。

我们都知道是通过Handler来发送Message,由于Handler中发送 Message方法很多,但是最后都是调用sendMessageAtTime

frameworks/base/core/java/android/os/Handler

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

这里的mQueue其实就是我们在Looper中创建的MessageQueue.
frameworks/base/core/java/android/os/MessageQueue

...
final boolean enqueueMessage(Message msg, long when) {
       ...
        final boolean needWake;
        synchronized (this) {
          ...
            Message p = mMessages;
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked; // new head, might need to wake up
            } else {
                Message prev = null;
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
                msg.next = prev.next;
                prev.next = msg;
                needWake = false; // still waiting on head, no need to wake up
            }
        }
        if (needWake) {
            nativeWake(mPtr);
        }
        return true;
    }
...

enqueueMessage其实这个方法很重要,它对Message先进行了排序,要早运行的排在了前面,比较晚运行的排在队列后面。

前面介绍了mMessages其实是指向Message链表的第一个Message.
这时候来了个新的Message,由于是刚刚开始mMessage = null;
所以进入了if()中的语句。

这里有个变量需要特别解释,needWake,顾名思义,是否需要唤醒。
就整篇文章而言,其实阻塞的就一个地方,就是epoll_wait,所以needWake指的需不需要唤醒,也就是指的这里。
mBlocked代表,当前的epoll_wait是否正在阻塞,如果是阻塞的,
mBlocked = true,所以needWake = mBlocked = true,也就是需要唤醒。
如果非阻塞那么,needWake = false.
最终是

if (needWake) {
            nativeWake(mPtr);
        }

前面已经介绍了,由于nextPollTimeoutMillis=-1,epoll_wait正在阻塞,所以needWake = true;所以这个时候需要唤醒。

这里我就 不介绍跳转过程了,最终调用jni层的Looper中的wake方法

frameworks/base/libs/utils/Looper

...
void Looper::wake() {
...
    do {
        nWrite = write(mWakeWritePipeFd, "W", 1);
    } while (nWrite == -1 && errno == EINTR);
  ...
}
...

前面其实我已经介绍过了epoll_wait有2种方式不再阻塞

  1. 等待传入的timeout参数时间到
  2. pipe管道中有新的输入

由于这个时候的timeout=-1代表一直阻塞,所以只能通过往pipe管道中写入东西,才能不阻塞epoll_wait.如上write一个内容。其实写的内容是什么并不重要,如上,其实就是写了一个W字符。

这个时候,epoll_wait不再阻塞,继续往下运行。
frameworks/base/libs/utils/Looper

...
int Looper::pollInner(int timeoutMillis) {
    ...
     int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    ...
     for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeReadPipeFd) {
            if (epollEvents & EPOLLIN) {
                awoken();
            } 
          ...
        } 
    ...
    ...

}
...
void Looper::awoken() {
    ...
     do {
        nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
    } while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
}
...

这个时候进入awoken方法,把写入内容读出。没有造成阻塞。

也就是java层中的MessageQueue中的next方法中的nativePollOnce没有造成阻塞,继续运行。

frameworks/base/core/java/android/os/MessageQueue

  ...
  final Message next() {
        ...
      
        for (;;) {
            ...
            nativePollOnce(mPtr, nextPollTimeoutMillis);
            ...
             final Message msg = mMessages;
                if (msg != null) {
                    final long when = msg.when;
                    if (now >= when) {
                        mBlocked = false;
                        mMessages = msg.next;
                        msg.next = null;
                        if (Config.LOGV) Log.v("MessageQueue", "Returning message: " + msg);
                        return msg;
                    } else {
                        nextPollTimeoutMillis = (int) Math.min(when - now, Integer.MAX_VALUE);
                    }
                } else {
                    nextPollTimeoutMillis = -1;
                }
            ...  
      }
        ...
  }
  ...

这时nativePollOnce阻塞结束,继续运行,if (now >= when) 是为了判断这个消息是否延迟,对应我们Handler中延迟发送的一些方法,如postDelay()等。这里直接认为没有延时。

在Message链表中把第一个Message移除返回。
最后调用Handler的handleMessage,这不是重点我就直接略过了。

等到Message都处理完了之后,获取到Message 为null。nextPollTimeoutMillis又被置为-1,然后又在epoll_wait中进行阻塞。

这里对延时发送Message的没有详细讲解,主要还是2点。

  1. 在添加Message的时候进行时间排序了,把Message插入到对应的位置。
  2. 主要还是利用nextPollTimeoutMillis,在epoll_wait的时候阻塞,等待。

这里我就不再详细赘述了。

问题

在分析源码的时候,我遇到了一个问题,整个消息机制,其实就是一个死循环,一直在执行,当没有消息的时候epoll_wait阻塞。
为什么epoll_wait在主线程阻塞不会造成卡顿,或者ANR?

29125917-E9AB-4187-AA9C-A685980FE347.png

我这里就不再过多赘述了。

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

推荐阅读更多精彩内容