提起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================= 1 /**
  * <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); //①
     }
     // 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); //②
     return true;
 }

第1~5行的注释说,该方法将Runnable添加到消息队列中,该Runnable将在UI线程运行。这就是该方法的作用,添加成功了就会返回true。

上述源码的执行逻辑,关键点在mAttachInfo是否为null,这会导致两种逻辑:

1)mAttachInfo != null,走代码①的逻辑。

2)mAttachInfo == null,走代码②的逻辑。

当前View尚未attach到Window时,整个View体系还没有加载完,mAttachInfo就会为null,表现在Activity中,就是onResume()方法还没有执行完。反之,mAttachInfo就不会为null。这部分内容会在下一篇文章中详细讲解,这里先知道这个结论。

(1)mAttachInfo != null的情况

对于第一种情况,当看到代码①时,应该会窃喜一下,因为看到了老熟人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 window, 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) {
         mAttachInfo = info;
         ......
 }

现在的问题就变成了要追踪dispatchAttachedToWindow方法在哪里调用的,即从哪里把AttachInfo传进来的。这里我们先停住,看看第二种情况。

(2)mAttachInfo == null的情况

post源码中第11、12行,对代码②有说明:推迟Runnable,直到我们知道需要它在哪个线程中运行。代码②处,看看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被附着到Window并且拥有了一个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调用逻辑还是比较简单的,就不啰嗦了。

代码②处执行的结果就是将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, Window parentWindow) {
       ......
       ViewRootImpl root;
       ......
       root = new ViewRootImpl(view.getContext(), display);
       ......
 }

到这里,我们就很难继续追踪了,因为调用addView的地方太多了,很难全局搜索,我们先在这里停一会。其实到这个addView方法时,我们会看到里面有很多对View view参数的操作,而addView顾名思义,也是在修改UI。而对UI的修改,只能发生主线程中,否则会报错,这是一个常识问题,所以我们完全可以明确,addView这个方法,就是运行在主线程的。我想,这样去理解,应该是完全没有问题的。但是笔者总感觉还差点什么,总觉得这里有点猜测的味道,所以还想一探究竟,看看这个addView方法是否真的就运行在主线程。当然,如果不愿意继续深入探究的童鞋,记住本节最后的结论也没有问题。

既然现在倒着推导比较困难,那就正着来推,这就需要我们有一定的知识储备了,需要知道Android的主线程,Activity的启动流程,以及Window添加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的绘制流程等相关知识点,读者可以根据自己掌握的情况选择性地阅读。当然,源码中有很多知识点是环环相扣的,各种知识点都需要平时多积累,希望读者们遇到问题不要轻易放过,这就是一个打怪升级的过程。

由于笔者经验和水平有限,如有描述不当或不准确的地方,请多多指教,谢谢!

Android多线程之(一)View.post()源码分析——在子线程中更新UI的更多相关文章

  1. Android在子线程中更新UI(二)

    MainActivity如下: package cc.testui2; import android.os.Bundle; import android.view.View; import andro ...

  2. android 不能在子线程中更新ui的讨论和分析

    问题描写叙述 做过android开发基本都遇见过 ViewRootImpl$CalledFromWrongThreadException,上网一查,得到结果基本都是仅仅能在主线程中更改 ui.子线程要 ...

  3. Android在子线程中更新UI(一)

    MainActivity如下: package cc.testui1; import android.os.Bundle; import android.os.Handler; import andr ...

  4. Android开发UI之在子线程中更新UI

    转自第一行代码-Android Android是不允许在子线程中进行UI操作的.在子线程中去执行耗时操作,然后根据任务的执行结果来更新相应的UI控件,需要用到Android提供的异步消息处理机制. 代 ...

  5. Android子线程中更新UI的4种方法

    方法一:用Handler 1.主线程中定义Handler: Handler mHandler = new Handler() { @Override public void handleMessage ...

  6. 老问题:Android子线程中更新UI的3种方法

    在Android项目中经常有碰到这样的问题,在子线程中完成耗时操作之后要更新UI,下面就自己经历的一些项目总结一下更新的方法: 方法一:用Handler 1.主线程中定义Handler: Handle ...

  7. Android 在子线程中更新UI

    今天在做练习时,在一个新开启的线程中调用“Toast.makeText(MainActivity.this, "登陆成功",Toast.LENGTH_SHORT).show();” ...

  8. C# 多线程多文件批量下载---子线程中更新UI 实例

    代码1: using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;usi ...

  9. Android:在子线程中更新UI的三种方式

    ①使用Activity中的runOnUiThread(Runnable) ②使用Handler中的post(Runnable) 在创建Handler对象时,必须先通过Context的getMainLo ...

随机推荐

  1. Hbase与Oracle的比较

    http://blog.csdn.net/lucky_greenegg/article/details/47070565 转自:http://www.cnblogs.com/chay1227/arch ...

  2. 一文教您如何通过 Java 压缩文件,打包一个 tar.gz Filebeat 采集器包

    欢迎关注笔者的公众号: 小哈学Java, 专注于推送 Java 领域优质干货文章!! 个人网站: https://www.exception.site/essay/create-tar-gz-by-j ...

  3. Linux系统移植的重要文件

    移植linux内核的关键文件:             arch/arm/mach-s5p6818/cpu.c                         cpu_init_machine()   ...

  4. jquery微信浏览器阻止页面拖动

    jquery微信浏览器阻止页面拖动<pre>function bodyScroll(event) { event.preventDefault();} document.body.addE ...

  5. [干货]AspNetCore熟练应用CancellationToken,CTO会对你刮目相看

    背景 已经有很多文章记录了 web程序中采用异步编程的优势和.Net异步编程的用法, 异步编程虽然不能解决查询数据库的瓶颈, 但是利用线程切换,能最大限度的弹性利用工作线程, 提高了web服务的响应能 ...

  6. thinkphp 5.1 去掉 .html 后缀

    thinkphp 5.1 去掉 .html 后缀  

  7. pat 1013 Battle Over Cities(25 分) (并查集)

    1013 Battle Over Cities(25 分) It is vitally important to have all the cities connected by highways i ...

  8. 如何解决UNMOUNTABLE BOOT VALUME

    Windows error:UNMOUNTABLE BOOT VALUME 解决方法:Windows 修复工具 chkdsk命令 chkdsk D:/f ps:chkdsk 磁盘名 /f

  9. 【Java】面向对象之多态

    生活中,比如动物中跑的动作,小猫.小狗和大象,跑起来是不一样的.再比如飞的动作,昆虫.鸟类和飞机,飞起来也是不一样的.可见,同一类的事物通过不同的实际对象可以体现出来的不同的形态.多态,描述的就是这样 ...

  10. C语言|博客作业05

    这个作业属于哪个课程 C语言程序设计II 这个作业的要求在哪里 https://edu.cnblogs.com/campus/zswxy/CST2019-1/homework/9825 我在这个课程的 ...