Android Handler 通信 - 彻底了解 Handler 的通信过程

相关文章链接:

1. Android Framework - 学习启动篇
2. Android Handler 通信 - 源码分析与手写 Handler 框架
3. Android Handler 通信 - 彻底了解 Handler 的通信过程

相关源码文件:

/frameworks/base/core/java/android/os/Handler.java
/frameworks/base/core/java/android/os/MessageQueue.java
/frameworks/base/core/java/android/os/Looper.java

framework/base/core/jni/android_os_MessageQueue.cpp
system/core/libutils/Looper.cpp
system/core/include/utils/Looper.h

在 Android 应用开发过程中,跨进程通信一般是 binder 驱动 ,关于 binder 驱动的源码分析,大家感兴趣可以看看我之前的一些文章。这里我们来聊聊线程间的通信 Handler,关于 handler 的通信原理我想大家应该都是倒背如流。但有一些比较细节的东西大家可能就未必了解了:

  • 基于 Handler 可以做性能检测
  • 基于 Handler 可以做性能优化
  • 基于 Handler 竟然也可以做跨进程通信?

关于 Handler 的基础原理篇,大家有不了解的可以看看 《Android Handler 通信 - 源码分析与手写 Handler 框架》 ,本文这里我们先来分析一下 Handler 是怎么处理消息延迟的,这是我在头条面试碰到的一个题目。首先我们先自己思考一下:如果我们要延迟 2s 再去处理这个消息,自己实现会怎样去处理?(思考五分钟)我们一起来看看源码:

// MessageQueue.java 中的 next 方法源码
Message next() {
        // 判断 native 层的 MessageQueue 对象有没有正常创建
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        // 消息执行需要等待的时间
        int nextPollTimeoutMillis = 0;
        for (;;) {
            // 执行 native 层的消息延迟等待,调 next 方法第一次不会进来
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                // 获取当前系统的时间
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                ...
                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;
                        // 标记为已在使用状态
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // 如果队列里面没有消息,等待时间是 -1
                    nextPollTimeoutMillis = -1;
                }
                // 有没有空闲的 IdleHandler 需要执行,一般我们没关注这个功能
                // 后面内容有专门解释,这里目前分析是 == 0 ,跳出
                if (pendingIdleHandlerCount <= 0) {
                    mBlocked = true;
                    continue;
                }
                ...
            }
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }

通过源码分析我们发现消息的处理过程,是通过当前消息的执行时间与当前系统时间做比较,如果小于等于当前系统时间则立即返回执行该消息,如果大于当前系统时间则调用 nativePollOnce 方法去延迟等待被唤醒,当消息队列里面为空时则设置等待的时间为 -1。关于 IdleHandler 、异步消息和消息屏障的源码已被我忽略,大家待会可以看文章后面的分析。我们跟进到 Native 层的 android_os_MessageQueue_nativePollOnce 方法。

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    // 通地址转换成 native 层的 MessageQueue 对象
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    // 调用 native 层 Looper 对象的 pollOnce 方法
    mLooper->pollOnce(timeoutMillis);
}

inline int pollOnce(int timeoutMillis) {
    return pollOnce(timeoutMillis, NULL, NULL, NULL); 【5】
}

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        ...
        if (result != 0) {
            ...
            return result;
        }
        // 再处理内部轮询
        result = pollInner(timeoutMillis); 【6】
    }
}

int Looper::pollInner(int timeoutMillis) {
    ...
    int result = POLL_WAKE;
    mResponses.clear();
    mResponseIndex = 0;
    mPolling = true; //即将处于idle状态
    // fd最大个数为16
    struct epoll_event eventItems[EPOLL_MAX_EVENTS]; 
    // 等待事件发生或者超时,在 nativeWake() 方法,向管道写端写入字符,则该方法会返回;
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    ...
    return result;
}

通过源码分析我们能发现 Looper.loop() 方法并不是一个死循环那么简单,如果真是一个简单的死循环那得多耗性能,其实 native 层是调用 epoll_wait 进入等待的,timeoutMillis 这里有三种情况分别为:0 ,-1 和 >0 。如果是 -1 那么该方法会一直进入等待,如果是 0 那么该方法会立即返回,如果是 >0 该方法到等待时间就会立即返回,关于 epoll_wait 的使用和原理介绍,大家可以看看之前的内容。接下来我们看看唤醒方法:

boolean enqueueMessage(Message msg, long when) {
  ...
  synchronized (this) {
    if (mQuitting) {
      ...
      // We can assume mPtr != 0 because mQuitting is false.
      // 如果需要唤醒,调用 nativeWake 方法唤醒
      if (needWake) {
        nativeWake(mPtr);
      }
    }
    return true;
}

void NativeMessageQueue::wake() {
    mLooper->wake();
}

void Looper::wake() {
    uint64_t inc = 1;
    // 向管道 mWakeEventFd 写入字符1 , 写入失败仍然不断执行
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            ALOGW("Could not write wake signal, errno=%d", errno);
        }
    }
}

我们来总结一下延迟消息的处理过程:首先我们在 native 层其实也有 Handler.cpp 、MessageQueue.cpp 和 Looper.cpp 对象,但他们并不是与 Java 层一一对应的,只有 MessageQueue.java 和 MessageQueue.cpp 有关联。当我们上层计算好延迟时间后调用 native 层 nativePollOnce 方法,其内部实现采用 epoll 来处理延迟等待返回(6.0版本)。

当有新的消息插入时会调用 native 层的 nativeWake 方法,这个方法很简单就是向文件描述符中写入一个最简单的 int 数据 -1,目的是为了唤醒之前的 epoll_wait 方法,其实也就是唤醒 nativePollOnce 的等待。关于 Handler.cpp 、MessageQueue.cpp 和 Looper.cpp 对象,如果大家感兴趣可以看看之前的文章。

由此可见大公司的一个简单面试题就能过滤到很多人,我以前也经常听到同学抱怨,你为什么要问我这些问题,开发中又用不上。那么接下带大家来看看开发中能用上的,但我们可能并不熟悉的一些源码细节。

  • IdleHandler

我们在实际的开发过程中为了不阻塞主线程的其它任务执行,可能要等主线程空闲下来再去执行某个特定的方法,比如我们写了一个性能监测的工具,触发了某些条件要不定时的去收集某些信息,这个肯定是不能去影响主线程的,否则我们发现加了某些性能监测工具,反而会引起整个应用性能更差甚至引起卡顿。那如何才能知道主线程是否空闲了?

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
  @Override
  public boolean queueIdle() {
    // 开始执行一些其它操作,可能不耗时也可能稍微耗时
    // 比如跨进程访问,比如查询数据库,比如收集某些信息,比如写日志等等
    return false;
  }
});

这个代码很简单,只是我们平时可能比较少接触,我们来看看源码:

public void addIdleHandler(@NonNull IdleHandler handler) {
  if (handler == null) {
    throw new NullPointerException("Can't add a null IdleHandler");
  }
  synchronized (this) {
    mIdleHandlers.add(handler);
  }
}

Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration

        for (;;) {
            synchronized (this) {
                //  目前这里分析有内容
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // 循环遍历所有的 IdleHandler 回调
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                // 回调执行 queueIdle 方法
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                // 执行完函数返回是不是需要空闲时一直回调
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }
  • 主线程方法耗时

在实际的应用开发过程中,我们可能会发现界面会有卡顿的问题,尤其是在一些复杂的场景下,比如直播间各种礼物动画效果或者复杂列表滑动等等,当然引起卡顿的原因会有很多,比如机型的不同,在我们的手机上丝丝般顺滑,在测试的手机上却像没吃饭一样,当然还与 cpu 使用率、主线程执行耗时操作和磁盘 I/O 操作等等都有关。实际过程中要排查卡顿其实是比较复杂的,这里我们主要来监听排查是不是主线程有方法耗时:

    @Override
    public void onCreate() {
        super.onCreate();
        Looper.getMainLooper().setMessageLogging(new Printer() {
            long currentTime = -1;

            @Override
            public void println(String x) {
                if (x.contains(">>>>> Dispatching to ")) {
                    currentTime = System.currentTimeMillis();
                } else if (x.contains("<<<<< Finished to ")) {
                    long executeTime = System.currentTimeMillis() - currentTime;
                    // 原则上是 16 毫秒,一般没办法做到这么严格
                    if (executeTime > 60) {
                        Log.e("TAG", "主线程出现方法耗时");
                    }
                }
            }
        });
    }

当然这么做只能监测到主线程有方法耗时而引起卡顿,可能目前这些代码无法跟踪到是哪个方法耗时引起的卡顿,但基于这些代码去实现是哪个方法引起的耗时应该是 soeasy ,这里我们先不做过多的延伸主要来看下原理:

    /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the 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;

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            // 在执行方法之前做打印
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            // 执行消息的分发
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            // 执行方法之后再做打印
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
            ...
        }
    }

原理其实是非常简单的,关于异步消息、消息屏障、跨进程通信以及监听具体的某个方法耗时,在后面的源码分析中会陆陆续续的提到,大家感兴趣可以持续关注。国庆快乐~

视频地址:https://pan.baidu.com/s/1fLq65bUnwswwrEa20RKpsw
视频密码:gkpv

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

推荐阅读更多精彩内容