提起 View.post(), 相信不少童鞋一点都不陌生, 它用得最多的有两个功能, 使用简便而且实用:
1)在子线程中更新 UI. 从子线程中切换到主线程更新 UI, 不需要额外 new 一个 Handler 实例来实现.
2)获取 View 的宽高等属性值. 在 Activity 的 onCreate(),onStart(),onResume()等方法中调用 View.getWidth()等方法时会返回 0, 而通过 post 方法却可以解决这个问题.
本文将由从源码角度分析其原理, 由于篇幅原因会分 (上),(下) 两篇来进行讲解, 本篇将分析第 1)点. 在阅读文本之前, 希望读者是对 Handler 的 Looper 问题有一定了解的, 如果不了解请先阅读[朝花夕拾] Handler 篇.
本文的主要内容如下:
1, 在子线程中使用 View.post 更新 UI 功能使用示例
一般我们通过使用 View.post()实现在子线程中更新 UI 的示例大致如下:
- private Button mStartBtn;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_intent_service);
- mStartBtn = findViewById(R.id.start);
- new Thread(new Runnable() {
- @Override
- public void run() {
- mStartBtn.post(new Runnable() {
- @Override
- public void run() {
- // 处理一些耗时操作
- mStartBtn.setText("end");
- }
- });
- }
- }).start();
- }
第 7 行开启了一个线程, 第 10 行通过调用 post 方法, 使得在第 14 行实现了修改自身 UI 界面的显示(当然, 平时使用中不一定只能在 onCreate 中, 这里仅举例而已).
2,post 源码分析
在上述例子中, mStartBtn 是如何实现在子线程中通过 post 来更新 UI 的呢? 我们进入 post 源码看看.
- //====================View.java=================
- /**
- * <p>Causes the Runnable to be added to the message queue.
- * The runnable will be run on the user interface thread.</p>
- * ......
- */
- public boolean post(Runnable action) {
- final AttachInfo attachInfo = mAttachInfo;
- if (attachInfo != null) {
- return attachInfo.mHandler.post(action); //1
- }
- // Postpone the runnable until we know on which thread it needs to run.
- // Assume that the runnable will be successfully placed after attach.
- getRunQueue().post(action); //2
- return true;
- }
第 1~5 行的注释说, 该方法将 Runnable 添加到消息队列中, 该 Runnable 将在 UI 线程运行. 这就是该方法的作用, 添加成功了就会返回 true.
上述源码的执行逻辑, 关键点在 mAttachInfo 是否为 null, 这会导致两种逻辑:
1)mAttachInfo != null, 走代码1的逻辑.
2)mAttachInfo == null, 走代码2的逻辑.
当前 View 尚未 attach 到 Windows 时, 整个 View 体系还没有加载完, mAttachInfo 就会为 null, 表现在 Activity 中, 就是 onResume()方法还没有执行完. 反之, mAttachInfo 就不会为 null. 这部分内容会在下一篇文章中详细讲解, 这里先知道这个结论.
(1)mAttachInfo != null 的情况
对于第一种情况, 当看到代码1时, 应该会窃喜一下, 因为看到了老熟人 Handler, 这就是 Handler.post(Runnable)方法, 我们再熟悉不过了. 这里的 Runnable 会在哪个线程执行, 取决于该 Handler 实例化时使用的哪个线程的 Looper. 我们继续跟踪 mHandler 是在哪里实例化的.
- //=============View.AttachInfo===============
- /**
- * A Handler supplied by a view's {@link Android.view.ViewRootImpl}. This
- * handler can be used to pump events in the UI events queue.
- */
- final Handler mHandler;
- AttachInfo(IWindowSession session, IWindow Windows, Display display,
- ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
- Context context) {
- ......
- mViewRootImpl = viewRootImpl;
- mHandler = handler;
- ......
- }
我们发现 mHandler 是在实例化 AttachInfo 时传入的, 该实例就是前面 post 方法第 7 行的 mAttachInfo. 在 View 类中只有一处给它赋值的地方:
- //===============View.java=============
- 1 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
- 2 mAttachInfo = info;
- 3 ......
- 4
- }
现在的问题就变成了要追踪 dispatchAttachedToWindow 方法在哪里调用的, 即从哪里把 AttachInfo 传进来的. 这里我们先停住, 看看第二种情况.
(2)mAttachInfo == null 的情况
post 源码中第 11,12 行, 对代码2有说明: 推迟 Runnable, 直到我们知道需要它在哪个线程中运行. 代码2处, 看看 getRunQueue()的源码:
- //=============View.java============
- /**
- * Queue of pending runnables. Used to postpone calls to post() until this
- * view is attached and has a handler.
- */
- private HandlerActionQueue mRunQueue;
- /**
- * Returns the queue of runnable for this view.
- * ......
- */
- private HandlerActionQueue getRunQueue() {
- if (mRunQueue == null) {
- mRunQueue = new HandlerActionQueue();
- }
- return mRunQueue;
- }
getRunQueue()是一个单例模式, 返回 HandlerActionQueue 实例 mRunQueue.mRunQueue, 顾名思义, 表示该 view 的 HandlerAction 队列, 下面会讲到, HandlerAction 就是对 Runnable 的封装, 所以实际就是一个 Runnable 的队列. 注释中也提到, 它用于推迟 post 的调用, 直到该 view 被附着到 Windows 并且拥有了一个 handler.
HandlerActionQueue 的关键代码如下:
- //============HandlerActionQueue ========
- /**
- * Class used to enqueue pending work from Views when no Handler is attached.
- * ......
- */
- public class HandlerActionQueue {
- private HandlerAction[] mActions;
- private int mCount;
- public void post(Runnable action) {
- postDelayed(action, 0);
- }
- public void postDelayed(Runnable action, long delayMillis) {
- final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
- synchronized (this) {
- if (mActions == null) {
- mActions = new HandlerAction[4];
- }
- mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
- mCount++;
- }
- }
- ......
- public void executeActions(Handler handler) {
- synchronized (this) {
- final HandlerAction[] actions = mActions;
- for (int i = 0, count = mCount; i < count; i++) {
- final HandlerAction handlerAction = actions[i];
- handler.postDelayed(handlerAction.action, handlerAction.delay);
- }
- mActions = null;
- mCount = 0;
- }
- }
- ......
- private static class HandlerAction {
- final Runnable action;
- final long delay;
- public HandlerAction(Runnable action, long delay) {
- this.action = action;
- this.delay = delay;
- }
- ......
- }
- }
正如注释中所说, 该类用于在当前 view 没有 handler 附属时, 将来自 View 的挂起的作业 (就是 Runnable) 加入到队列中.
当开始执行 post()时, 实际进入到了第 14 行的 postDelay()中了. 第 15 行中, 将 Runnable 封装成了 HandlerAction, 在 39 行可以看到 HandlerAction 实际上就是对 Runnable 的封装. 第 21 行作用就是将封装后的 Runnable 加入到了数组中, 具体实现我们不深究了, 知道其作用就行, 而这个数组就是我们所说的队列. 这个类中 post 调用逻辑还是比较简单的, 就不啰嗦了.
代码2处执行的结果就是将 post 的参数 Runnable action 添加到 View 的全局变量 mRunQueue 中了, 这样就将 Runnable 任务存储下来了. 那么这些 Runnable 在什么时候开始执行呢? 我们在 View 类中搜索一下会发现, mRunQueue 的真正使用只有一处:
- //===========View.java============
- void dispatchAttachedToWindow(AttachInfo info, int visibility) {
- ......
- // Transfer all pending runnables.
- if (mRunQueue != null) {
- mRunQueue.executeActions(info.mHandler);
- mRunQueue = null;
- }
- ......
- onAttachedToWindow();
- ......
- }
这里我们又到 dispatchAttachedToWindow()方法了, 第一种情况也是到了这个方法就停下来了. 我们看看第 6 行, 传递的参数也是形参 AttachInfo info 的 mHandler. 进入到 HandlerActionQueue 类的 executeActions 可以看到, 这个方法的作用就是通过传进来的 Handler, 来 post 掉 mRunQueue 中存储的所有 Runnable, 该方法中的逻辑就不多说了, 比较简单. 这些 Runnable 最终在哪个线程运行, 就看这个 Handler 了.
到这里为止, 两种情况就殊途同归了, 最后落脚点都集中到了 dispatchAttachedToWindow 方法的 AttachInfo 参数的 mHandler 属性了. 所以现在的任务就是找到哪里调用了这个方法, mHandler 到底是使用的哪个线程的 Looper.
3,dispatchAttachedToWindow 方法的调用
要搞清这个方法的调用问题, 对于部分童鞋来说可能会稍微有点复杂, 所以这里单独用一小节来分析. 当然, 不想深入研究的童鞋, 直接记住本节最后的结论也是可以的, 不影响对 post 机制的理解.
这里需要对框架部分的代码进行全局搜索, 所以需要准备一套系统框架部分的源码, 以及源码阅读工具. 笔者这里用的是 Source Insight 来查找的 (不会使用童鞋可以学习一下, 使用非常广的源码阅读工具, 推荐阅读:[工利其器] 必会工具之(一)Source Insight 篇). 没有源码的童鞋, 也可以直接在线查找, 直接通过网站的形式来阅读源码(不知道如何操作的, 推荐阅读: 安卓本卓] Android 系统源码篇之(一) 源码获取, 源码目录结构及源码阅读工具简介第四点, AndroidXRef, 使用非常广).
全局搜索后的结果如下:
对于这个结果, 我们可以首先排除 "Boot-image-profile.txt" 和 "RecyclerView.java" 两个文件(原因不需多说吧... 如果真的不知道, 那就说明还完全没有到阅读这篇文章的时候), 跟这个方法调用相关的类就缩小到 View,ViewGroup 和 ViewRootImpl 类中. 在 View.java 中与该方法相关的只有如下两处, 显然可以排除掉 View.java.
ViewRootImpl 类中的调用如下:
- //=============ViewRootImpl.java===========
- final View.AttachInfo mAttachInfo;
- ......
- public ViewRootImpl(Context context, Display display) {
- ......
- mAttachInfo = new View.AttachInfo(mWindowSession,
- mWindow, display, this, mHandler, this, context);
- ......
- }
- private void performTraversals() {
- ......
- host.dispatchAttachedToWindow(mAttachInfo, 0);
- ......
- }
- ......
- final ViewRootHandler mHandler = new ViewRootHandler();
- ......
追踪 dispatchAttachedToWindow 方法的调用, 目的是为了找到 AttachInfo 的实例化, 从而找到 mHandler 的实例化, 这段代码中正好就实现了 AttachInfo 的实例化, 看起来有戏, 我们先放这里, 继续下看 ViewGroup 类中的调用.
在 ViewGroup 类中, 这个方法出现稍微多一点, 但是稍微观察可以发现, 根本没有找到 AttachInfo 实例化的地方, 要么直接使用的 View 类中的 mAttachInfo(因为 ViewGroup 是 View 的子类), 要么就是图一中通过传参得到. 而图一的方法, 也是重写的 View 的方法, 所以这个 AttachInfo info 实际也是来自 View. 这样一来我们也就排除了 ViewGroup 类中的调用了, 原始的调用不在这里面.
通过排除法, 最后可以断定, 最原始的调用其实就在 ViewRootImpl 类中. 如果研究过 View 的绘制流程, 那么就会清楚 View 体系的绘制流程 measure,layout,draw 就是从 ViewRootImpl 类的 performTraversals 开始的, 然后就是对 DecorView 下面的 View 树递归绘制的(如果对 View 的绘制流程不明白的, 推荐阅读我的文章:[朝花夕拾] Android 自定义 View 篇之(一)View 绘制流程). 这里的 dispatchAttachedToWindow 方法也正好从这里开始, 递归遍历实现各个子 View 的 attach, 中途在层层传递 AttachInfo 这个对象. 当然, 我们在前面介绍 View.post 源码时, 就看到过如下的注释:
- /**
- * A Handler supplied by a view's {@link Android.view.ViewRootImpl}. This
- * handler can be used to pump events in the UI events queue.
- */
- final Handler mHandler;
这里已经很明确说到了这个 mHandler 是 ViewRootImpl 提供的, 我们也可以根据这个线索, 来确定我们的推断是正确的. 有的人可能会吐槽了, 源码都直接给出了这个说明, 那为什么还要花这么多精力追踪 dispatchAttachedToWindow 的调用呢, 不是浪费时间吗? 答案是: 我们是在研究源码及原理, 仅仅限于别人的结论是不够的, 这是一个成长过程. 对于不想研究本节过程的童鞋, 记住结论即可.
结论: View 中 dispatchAttachedToWindow 的最初调用, 在 ViewRootImpl 类中; 重要参数 AttachInfo 的实例化, 也是在 ViewRootImpl 类中; 所有问题的核心 mHandler, 也来自 ViewRootImpl 类中.
4,mHandler 所在线程问题分析
通过上一节的分析, 现在的核心问题就转化为 mHandler 的 Looper 在哪个线程的问题了. 在第三节中已经看到 mHandler 实例化是在 ViewRootImpl 类实例的时候完成的, 且 ViewRootHandler 类中也没有指定其 Looper. 所以, 我们现在需要搞清楚, ViewRootImpl 是在哪里实例化的, 那么就清楚了 mHandler 所在线程问题.
现在追踪 ViewRootImpl 时会发现, 只有如下一个地方直接实例化了.
- //==========WindowManagerGlobal=========
- public void addView(View view, ViewGroup.LayoutParams params,
- Display display, Windows parentWindow) {
- ......
- ViewRootImpl root;
- ......
- root = new ViewRootImpl(view.getContext(), display);
- ......
- }
到这里, 我们就很难继续追踪了, 因为调用 addView 的地方太多了, 很难全局搜索, 我们先在这里停一会. 其实到这个 addView 方法时, 我们会看到里面有很多对 View view 参数的操作, 而 addView 顾名思义, 也是在修改 UI. 而对 UI 的修改, 只能发生主线程中, 否则会报错, 这是一个常识问题, 所以我们完全可以明确, addView 这个方法, 就是运行在主线程的. 我想, 这样去理解, 应该是完全没有问题的. 但是笔者总感觉还差点什么, 总觉得这里有点猜测的味道, 所以还想一探究竟, 看看这个 addView 方法是否真的就运行在主线程. 当然, 如果不愿意继续深入探究的童鞋, 记住本节最后的结论也没有问题.
既然现在倒着推导比较困难, 那就正着来推, 这就需要我们有一定的知识储备了, 需要知道 Android 的主线程, Activity 的启动流程, 以及 Windows 添加 view 的相关知识.
我们平时所说的主线程, 实际上指的就是 ActivityThread 这个类, 它里面有一个 main()函数:
- public static void main(String[] args) {
- ......
- ActivityThread thread = new ActivityThread();
- ......
- }
看到这里, 想必非常亲切了, Java 中程序启动的入口函数, 到这里就已经进入到 Android 的主线程了(对于 Android 的主线程是否就是 UI 线程这个问题, 业内总有些争议, 但官方文档很多地方的表述为主线程也就是 UI 线程, 既然如此, 我们也没有必要纠结了, 把这两者等同, 完全没有问题). 在 main 中, 实例了一个 ActivityThread(), 该类中有如下的代码:
- //========ActivityThread.java=========
- ......
- final H mH = new H();
- ......
- private class H extends Handler {
- public static final int LAUNCH_ACTIVITY = 100;
- public static final int RESUME_ACTIVITY = 107;
- public static final int RELAUNCH_ACTIVITY = 126;
- ......
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case LAUNCH_ACTIVITY:
- ......
- case RESUME_ACTIVITY:
- handleResumeActivity(...)
- ......
- case RELAUNCH_ACTIVITY:
- ......
- }
- ......
- final void handleResumeActivity(...) {
- ......
- ViewManager wm = a.getWindowManager();
- ......
- wm.addView(decor, l);
- ......
- }
其中定义了一个 Handler H, 现在毫无疑问, mH 使用的是主线程的 Looper 了. 如果清楚 Activity 的启动流程, 就会知道不同场景启动一个 Acitivty 时, 都会进入到 ActivityThread, 通过 mH 来 sendMessage, 从而直接或间接地在 handleMessage 回调方法中调用 handleResumeActivity(...), 显然, 这个方法就运行在主线程中了.
handleResumeActivity(...)的第 25 行会添加 DecorView, 即开始添加整个 View 体系了, 我们平时所说的 View 的绘制流程, 就是从这里开始的. 这里我们就需要了解 ViewManager,WindowManager,WindowManagerImpl 和 WindowManagerGlobal 类之间的关系了, 如下所示:
这里用到了系统源码中常用的一种设计模式 -- 桥接模式, 调用 WindowManagerImpl 中的方法时, 实际上是由 WindowManagerGlobal 对应方法来实现的. 所以第 25 行实际执行的就是 WindowManagerGlobal 的 addView 方法, 我们需要追踪的 ViewRootImpl 实例化就是在这个方法中完成的, 前面的源码显示了这一点.
结论: 这里的关键 mHandler 使用的 Looper 确实是来自于主线程.
5,mHandler 所用 Looper 线程问题分析状态图
上一节分析 mHandler 所用 Looper 所在线程问题, 其实就是伴随着启动 Activity 并绘制整个 View 的过程, 可以得到如下简略流程图:
通过这里的 dispatchAttachedToWindow 方法, 就将 mHandler 传递到了 View.post()这个流程中, 从而实现了从子线程中切换到主线程更新 UI 的功能.
6, 总结
到这里, 使用 View.post 方法实现在子线程中更新 UI 的源码分析就结束了. 我们可以看到, 实际上底层还是通过 Handler 从子线程切换到主线程, 来实现 UI 的更新, 由此可见 Handler 在子线程与主线程切换上的重要地位. 而整个分析流程其实主要是在做一件事, 确定核心 Handler 使用的是主线程的 Looper. 这其中还穿插了 ActivityThread,Activity 启动, WMS 添加 view,View 的绘制流程等相关知识点, 读者可以根据自己掌握的情况选择性地阅读. 当然, 源码中有很多知识点是环环相扣的, 各种知识点都需要平时多积累, 希望读者们遇到问题不要轻易放过, 这就是一个打怪升级的过程.
由于笔者经验和水平有限, 如有描述不当或不准确的地方, 请多多指教, 谢谢!
来源: https://www.cnblogs.com/andy-songwei/p/12013152.html