查看原文
其他

子线程更新UI全解

搜狐焦点 满鹏飞 搜狐技术产品 2022-12-21


 
 

本文字数:17700

预计阅读时间:45分钟

子线程更新UI全解

目录

  • 子线程更新 UI 异常设计理念及简单源码解析

  • 深入源码追踪

    • Activity 的顶层 View

    • DecorView 的 ViewParent

    • ViewRootImpl 的 requestLayout

  • 子线程更新 View 不发生异常的情况

    • 针对通用 View 的方案

    • 针对特定 View 的方案

  • 总结


子线程更新 UI 异常设计理念及简单源码解析

初学者可能会犯在子线程更新 UI 的错误,例如:

thread { imageView.setBackgroundColor(Color.RED) }

一旦运行,应用会直接崩溃并抛出异常,这也是我们 Android 开发的一条铁律:_在子线程中不能更新 UI_。

那么为什么 Android 不让子线程更新 UI 呢?原因在于现在屏幕刷新率最低是 60Hz,意味着最多每 16ms 就会刷新一次屏幕,所以 UI 更新要尽可能快速,否则会丢帧导致卡顿。那么 UI 更新操作就不能加锁,频繁的加锁释放锁可能会延长 UI 渲染时间,但是不加锁如果允许子线程更新 UI 会导致多个线程对 UI 同时更新,造成线程不安全而导致 UI 最终效果无法想象,所以 Android 直接限制了子线程更新 UI,实际上不只是 Android 有这种限制,常见的 UI 框架基本都是单线程模型。

了解了设计理念,我们从源码的角度来分析一下,本文 Framework 源码均来自 Android 11 版本。

首先我们先从 Log 的角度分析,错误日志是:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
   at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8798)
   at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1606)
   at android.view.View.requestLayout(View.java:25390)
   ...
   at android.view.View.setBackgroundColor(View.java:23617)

可以看到从View#setBackgroundColor()起层层调用之后会到达ViewRootImpl#checkThread(),然后抛出了异常,ViewRootImpl#checkThread() 方法是:

// android.view.ViewRootImpl
void checkThreadcheckThread() {
   if (mThread != Thread.currentThread()) {
      throw new CalledFromWrongThreadException(
               "Only the original thread that created a view hierarchy can touch its views.");
   }
}

仅有一个功能:判断当前线程跟 mThread 是否一致,如果不一致就抛出异常。继而可以看到 mThread 是在 ViewRootImpl 构造方法中被初始化的:

// android.view.ViewRootImpl
 public ViewRootImpl(Context context, Display display, IWindowSession session,
      boolean useSfChoreographer) 
{
   ...
   mThread = Thread.currentThread();
   ...
}

所以原因很清楚了:当前调用的线程不是 ViewRootImpl 的构造方法中初始化的线程就会抛出异常。但是这仅仅知其然,想要知其所以然还得继续深入源码进行分析。

深入源码追踪

imageView.setBackgroundColor() 开始,根据调用链可以得到对 View#requestLayout() 的调用:

// android.view.View#setBackgroundDrawable
if (requestLayout) {
   requestLayout();
}

那么重点看一下 View#requestLayout() 的源码:

// android.view.View
public void requestLayout() {
   if (mMeasureCache != null) mMeasureCache.clear();

   // 如果 View 树正在 Layout 流程时有 View 调用 requestLayout(),则将此 View 加入到 ViewRootImpl 的队列中
   if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
      // Only trigger request-during-layout logic if this is the view requesting it,
      // not the views in its parent hierarchy
      ViewRootImpl viewRoot = getViewRootImpl();
      if (viewRoot != null && viewRoot.isInLayout()) {
            if (!viewRoot.requestLayoutDuringLayout(this)) {
               return;
            }
      }
      mAttachInfo.mViewRequestingLayout = this;
   }

   mPrivateFlags |= PFLAG_FORCE_LAYOUT;
   mPrivateFlags |= PFLAG_INVALIDATED;

   // 如果当前 View 存在 ViewParent,且 isLayoutRequested() 为 false 则调用 ViewParent 的 requestLayout()
   if (mParent != null && !mParent.isLayoutRequested()) {
      mParent.requestLayout();
   }
   if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
      mAttachInfo.mViewRequestingLayout = null;
   }
}

View 的 requestLayout() 会调用其父布局的 requestLayout(),ViewGrop 并没有重写这个方法,所以还是调用的 View 的 requestLayout(),即一直递归到最上层。所以我们看一下最上层的 View 是什么。

Activity 的顶层 View

首先我们先从 onCreate() 中的 setContentView() 方法看我们创建的布局的父 View 是谁(为了分析简单,我们的 Activity 继承自 android.app.Activity,而非 androidx.appcompat.app.AppCompatActivity):

// android.app.Activity
public void setContentView(@LayoutRes int layoutResID) {
   getWindow().setContentView(layoutResID);
   initWindowDecorActionBar();
}

getWindow()得到的是 attach()中创建的 PhoneWindow 对象:

// android.app.Activity#attach
mWindow = new PhoneWindow(this, window, activityConfigCallback);

所以去 PhoneWindowsetContentView() 中一探究竟:

// com.android.internal.policy.PhoneWindow
public void setContentView(int layoutResID) {
   if (mContentParent == null) {
      installDecor();
   } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
      mContentParent.removeAllViews();
   }

   if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
      // 共享元素动画相关
      final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
               getContext());
      transitionTo(newScene);
   } else {
      mLayoutInflater.inflate(layoutResID, mContentParent);
   }
}

我们传入的 layoutResID 通过 mLayoutInflater.inflate(layoutResID, mContentParent) 将 xml 布局加载到 mContentParent 中,那么就要看看 mContentParent 是怎么创建出来的,即installDecor()

// com.android.internal.policy.PhoneWindow
private void installDecor() {
   mForceDecorInstall = false;
   if (mDecor == null) {
      mDecor = generateDecor(-1);
      mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
      mDecor.setIsRootNamespace(true);
      if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
      }
   } else {
      mDecor.setWindow(this);
   }
   if (mContentParent == null) {
      mContentParent = generateLayout(mDecor);
      ...
   }
}

首先需要了解 DecorView 是一个 FrameLayout 的子类,上述源码通过 generateDecor() 创建出一个 DecorView 赋值给 mDecor,然后通过 generateLayout() 创建出一个 ViewGroup 赋值给 mContentParent,所以我们重点关注这两个方法:

// com.android.internal.policy.PhoneWindow
protected DecorView generateDecor(int featureId) {
   Context context;
   if (mUseDecorContext) {
      Context applicationContext = getContext().getApplicationContext();
      if (applicationContext == null) {
            context = getContext();
      } else {
            context = new DecorContext(applicationContext, this);
            if (mTheme != -1) {
               context.setTheme(mTheme);
            }
      }
   } else {
      context = getContext();
   }
   return new DecorView(context, featureId, this, getAttributes());
}

处理完 context 之后就直接 new 了一个 DecorView 对象,所以继续看 generateLayout()

// com.android.internal.policy.PhoneWindow
protected ViewGroup generateLayout(DecorView decor) {
   ...
   // 前面会根据不同的 window feature 使用不同的布局文件,比如 FEATURE_NO_TITLE 就是没有标题栏的布局
   mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

   ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
   ...
   return contentParent;
}

假设通过上面 feature 条件判断最后的布局文件是 R.layout.screen_simple,源码为:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">

    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />

    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />

</LinearLayout>

可以看到该布局是一个 LinearLayout 布局,包括一个 id 为 action_mode_bar_stub 的 用 ViewStub 引用的 ActionBar,一个 id 为 @android:id/content 的 FrameLayout。

继续跟踪 onResourcesLoaded() 方法,看看布局文件和 DecorView 的关系:

// com.android.internal.policy.PhoneWindow
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
   ...
   final View root = inflater.inflate(layoutResource, null);
   if (mDecorCaptionView != null) {
      if (mDecorCaptionView.getParent() == null) {
            addView(mDecorCaptionView,
                  new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
      }
      mDecorCaptionView.addView(root,
               new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
   } else {

      // Put it below the color views.
      addView(root, 0new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
   }
   mContentRoot = (ViewGroup) root;
   initializeElevation();
}

可以看到将布局文件加载成 View 然后添加到 DecorView 中。然后继续看 generateLayout() 剩下的代码是 findViewById(ID_ANDROID_CONTENT)

// android.view.Window
public <T extends View> T findViewById(@IdRes int id) {
   return getDecorView().findViewById(id);
}

ID_ANDROID_CONTENT 的值是 com.android.internal.R.id.content,这个 id 实际对应的就是上面 xml 文件中的 id 为 @android:id/content 的 FrameLayout,所以 mContentParent 就是那个 LinearLayout 的子 View,至此我们完成了对 Activity 中 View 父布局的完整链路追踪。

View 递归父布局小结:开发者的 xml 生成的布局 -> mContentParent(FragmentLayout)-> 系统内置布局文件生成的 View(LinearLayout)-> mDecor(DecorView)。

DecorView 的 ViewParent

虽然我们已经得到 DecorView 是顶层 View,但是问题没有真正解决:如果 DecorView 没有父 View,最后递归 requestLayout() 岂不是就此终结相当于什么都没干?其实我们一直说递归查找父 View 的说法是不准确的,应该说递归查找 _ViewParent_,DecorView 虽然没有父 View 了,但是它依然有 _ViewParent_。但是这个过程不能像上面那样自下而上追溯,而是自上而下先了解了 Activity 生命周期的流程才能得到。

我们先看看 ActivityThread#handleResumeActivity() 的源码:

// com.android.internal.app.ActivityThread
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
      String reason) 
{
   ...
   final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
   ...
   final Activity a = r.activity;
   ...
   boolean willBeVisible = !a.mStartedActivity;
   ...
   if (r.window == null && !a.mFinished && willBeVisible) {
      r.window = r.activity.getWindow();
      View decor = r.window.getDecorView();
      decor.setVisibility(View.INVISIBLE);
      ViewManager wm = a.getWindowManager();
      WindowManager.LayoutParams l = r.window.getAttributes();
      ...
      if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
               a.mWindowAdded = true;
               wm.addView(decor, l);
            }
            ...
      }
   }
}

这些代码真正需要我们分析的只有一行:wm.addView(decor, l),这个方法的作用是将 DecorView 添加到 WindowManager 中。找到 WindowManager 的实现类是 WindowManagerImpladdView 的源码如下:

// android.view.WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
   applyDefaultToken(params);
   mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
            mContext.getUserId());
}

继续追踪 mGlobal.addview() 的源码:

// android.view.WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
      Display display, Window parentWindow, int userId) 
{

   ViewRootImpl root;
   View panelParentView = null;

   synchronized (mLock) {
      ...
      root = new ViewRootImpl(view.getContext(), display);

      view.setLayoutParams(wparams);

      mViews.add(view);
      mRoots.add(root);
      mParams.add(wparams);

      // do this last because it fires off messages to start doing things
      try {
            root.setView(view, wparams, panelParentView, userId);
      } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
               removeViewLocked(index, true);
            }
            throw e;
      }
   }
}

可以看到实例化了一个 ViewRootImpl 对象,并且将 DecorView 传入了 setView() 中,那么继续追踪:

// android.view.ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
      int userId) 
{
   synchronized (this) {
      if (mView == null) {
         ...
         view.assignParent(this);
         ...
      }
   }
}

这个方法非常长,但是我们只是为了追踪 DecorView 的 ViewParent,所以只需要追踪一行 view.assignParent(this),DecorView 没有重写,一致追踪到 View 的该方法:

// android.view.View
void assignParent(ViewParent parent) {
   if (mParent == null) {
      mParent = parent;
   } else if (parent == null) {
      mParent = null;
   } else {
      throw new RuntimeException("view " + this + " being added, but"
               + " it already has a parent");
   }
}

所以问题解决,DecorView 的 ViewParent 是 ViewRootImpl。

ViewRootImpl 的 requestLayout

终于可以回归正题,看看 ViewRootImpl 的 requestLayout() 做了些什么:

// android.view.ViewRootImpl
public void requestLayout() {
   if (!mHandlingLayoutInLayoutRequest) {
      checkThread();
      mLayoutRequested = true;
      scheduleTraversals();
   }
}

终于看到了我们熟悉的 checkThread(),回归到最初简要分析时的结论了:当前调用的线程不是 ViewRootImpl 的构造方法中初始化的线程就会抛出异常。

那么 ViewRootImpl 初始化的方法在什么线程呢,ActivityThread#handleResumeActivity() 导致了 ViewRootImpl 的初始化,又因为 ActivityThread 所在线程是主线程,所以 ViewRootImpl 初始化的方法在主线程。

其实通过深入源码分析得到的链路很清晰:

  1. 子线程更新 View 会调用 View#requestLayout(),然后开始递归查找父 View,找到了 Activity 的顶层 View 是 DecorView。

  2. DecorView 的 ViewParent 是 ViewRootImpl,所以调用了 ViewRootImpl#requestLayout(),继而调用了 ViewRootImpl#checkThread()

  3. ViewRootImpl 在主线程初始化,因此子线程调用检查线程会抛出异常。

子线程更新 View 不发生异常的情况

知道了子线程更新 View 发生异常的原因,自然就会想是否有子线程不发生异常的情况。

针对通用 View 的方案

根据 View#requestLayout() 的源码:

// android.view.View#requestLayout
if (mParent != null && !mParent.isLayoutRequested()) {
   mParent.requestLayout();
}

两个条件:mParent != nullmParent.isLayoutRequested() == false 都满足才会调用 mParent.requestLayout(),所以可以想办法打破这两个条件。

Activity#onResume() 及以前更新 View

有一条关联 Activity 生命周期的调用链是:ActivityThread#handleResumeActivity() -> ActivityThread#performResumeActivity() -> Activity#performResume() -> Instrumentation#callActivityOnResume() -> Activity#onResume(),因为篇幅和主题原因,不多赘述。

由调用链可知 ViewRootImpl 在 Activity#onResume() 之后初始化,所以如果在此之前调用 View#requestLayout() 递归到 DecorView 时不满足 mParent != null 而不会调用到 ViewRootImpl#requestLayout()

示例代码:

// com.demo.MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   thread { imageView.setBackgroundColor(Color.RED) }
}

在子线程更新 View 之前先 requestLayout

首先根据 View#isLayoutRequested() 的源码可得与 mPrivateFlags 是否存在 PFLAG_FORCE_LAYOUT 有关:

// android.view.View
public boolean isLayoutRequested() {
   return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}

而根据 View#requestLayout() 的源码,可得第一层请求时就会在 mPrivateFlags 加入 PFLAG_FORCE_LAYOUT

// android.view.View#requestLayout
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
   mParent.requestLayout();
}

那么是什么时候 mPrivateFlags 去掉 PFLAG_FORCE_LAYOUT 呢?是在 View#layout() 里:

// android.view.View#layout
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;

具体的 View 布局流程因为篇幅原因简单概述不做深入源码追踪了:

ViewRootImpl#requestLayout() -> ViewRootImpl#scheduleTraversals() 会最终调用到 ViewRootImpl#performTraversals(),但并不是直接调用的,而是通过 Choreographer 等到下一个 VSYNC 时才调用,ViewRootImpl#performTraversals() -> ViewRootImpl#performLayout() -> View#layout(),所以 mParent.isLayoutRequested() 在下一个 VSYNC 时才会被赋值为 false,无法影响到马上执行的子线程更新 View。

因此我们可以先主线程调用一次requestLayout(),马上调用子线程更新 View 就不会发生异常了。

示例代码:

// com.demo.MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   imageView.setOnClickListener {
      imageView.requestLayout()
      thread { imageView.setBackgroundColor(Color.RED) }
   }
}

子线程初始化 ViewRootImpl

ViewRootImpl 初始化在 WindowManagerGlobal#addView() 中,外部能访问到的是 WindowManagerGlobal 是 WindowManager 的代理类,外部通过 WindowManager#addView() 去调用即可。那么只要在子线程初始化 ViewRootImpl,线程检查时就不会报错了。

示例代码:

// com.demo.MainActivity#onCreate
button.setOnClickListener {
   thread {
         Looper.prepare()
         val imageView = ImageView(this)
         windowManager.addView(imageView, WindowManager.LayoutParams())
         imageView.setBackgroundColor(Color.RED)
         Looper.loop()
   }
}

注意要在 Looper.prepare() 之后调用 WindowManager#addView(),否则会报错:java.lang.RuntimeException: Can't create handler inside thread Thread[xxxx] that has not called Looper.prepare()

原因是 ViewRootImpl 初始化的时候会创建一个 Headler,而 Headler 初始化的时候会调用 Looper.prepare(),所以这里要先初始化 Headler,再初始化 ViewRootImpl。

针对特定 View 的方案

更新 View 一般会调用两个方法:View#requestLayout()View#invalidate(),如果只调用后者我们可以跟踪一下源码看看会发生什么:

// android.view.View
public void invalidate(boolean invalidateCache) {
   invalidateInternal(00, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
      boolean fullInvalidate) 
{
   ...
   if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
            || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
            || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
            || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
      ...
      final AttachInfo ai = mAttachInfo;
      final ViewParent p = mParent;
      if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
      }
   }
}

p.invalidateChild(this, damage) 表示使 ViewParent 重绘这个 View,所以跟踪一下 ViewGroup 的源码:

// android.view.ViewGroup
public final void invalidateChild(View child, final Rect dirty) {
   final AttachInfo attachInfo = mAttachInfo;
   if (attachInfo != null && attachInfo.mHardwareAccelerated) {
      // HW accelerated fast path
      onDescendantInvalidated(child, child);
      return;
   }
   ...
}

首先就会判断是否开启了硬件加速,如果开启了会进入硬件加速逻辑:

// android.view.ViewGroup
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
   ...
   if (mParent != null) {
      mParent.onDescendantInvalidated(this, target);
   }
}

又是向上递归,我们轻车熟路去找 ViewRootImpl#onDescendantInvalidated() 的实现:

// android.view.ViewRootImpl
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
   if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
      mIsAnimating = true;
   }
   invalidate();
}

@UnsupportedAppUsage
void invalidate() {
   mDirty.set(00, mWidth, mHeight);
   if (!mWillDrawSoon) {
      scheduleTraversals();
   }
}

可以看到最后跟 ViewRootImpl#requestLayout() 一样调用到了 ViewRootImpl#scheduleTraversals(),但是却没有调用ViewRootImpl#checkThread()

所以我们得到了一个结论:在硬件加速的情况下只调用 View#invalidate() 不会触发线程检查。

那么在非硬件加速的时候呢?还得返回去看看 ViewGroup#invalidateChild()

// android.view.ViewGroup#invalidateChild
do {
   ...
   parent = parent.invalidateChildInParent(location, dirty);
   ...
while (parent != null);

循环调用 ViewParent#invalidateChildInParent(),所以去 ViewRootImpl#invalidateChildInParent() 中一探究竟:

// android.view.ViewRootImpl
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
   checkThread();
   ...
}

第一行就直接检查线程了,所以非硬件加速的情况下只调用 View#invalidate() 依然会触发线程检查。

在某些特定 View 的特定更新方法满足特定条件下会只调用 View#invalidate(),如果开启了硬件加速子线程更新就不会崩溃,这种情况需要一一探索,而且受限于版本不同可能会有不同的结果,仅仅举几个例子:

imageView.setImageDrawable(ColorDrawable(Color.RED))

// android.widget.ImageView
public void setImageDrawable(@Nullable Drawable drawable) {
   if (mDrawable != drawable) {
      ...
      final int oldWidth = mDrawableWidth;
      final int oldHeight = mDrawableHeight;

      updateDrawable(drawable);

      if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
      }
      invalidate();
   }
}

如果不修改 Drawable 的固有宽高不变就不会调用 requestLayout()mDrawableWidthmDrawableHeight 的改变在 updateDrawable()中。

 // android.widget.ImageView
 private void updateDrawable(Drawable d) {
    ...
    if (d != null) {
       ...
       mDrawableWidth = d.getIntrinsicWidth();
       mDrawableHeight = d.getIntrinsicHeight();
       ...
    } else {
       mDrawableWidth = mDrawableHeight = -1;
    }
 }
 // android.graphics.drawable.Drawable
 public int getIntrinsicWidth() {
      return -1;
  }

  public int getIntrinsicHeight() {
      return -1;
  }

ColorDrawable 并未重写 getIntrinsicWidth()getIntrinsicHeight()mDrawableWidthmDrawableHeight 一直都是 -1,所以并未调用 requestLayout()

TextView 在固定尺寸下更新文本

TextView#setText() 中会调用 TextView#checkForRelayout()

// android.widget.TextView
private void checkForRelayout() {
   if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
         || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
         && (mHint == null || mHintLayout != null)
         && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
      // 上述三个条件为:
      // TextView 的宽度是固定的
      // 没有设置提示文本,或者提示文本已经被渲染完成
      // TextView 的宽度大于 0

      int oldht = mLayout.getHeight();
      int want = mLayout.getWidth();
      int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

      makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
               mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
               false);

      if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
         // 不是跑马灯模式
         if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
               && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
            // TextView 的高度是固定的
            autoSizeText();
            invalidate();
            return;
         }

         if (mLayout.getHeight() == oldht
               && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
            // 没有改变高度
            autoSizeText();
            invalidate();
            return;
         }
      }

      requestLayout();
      invalidate();
   } else {
      nullLayouts();
      requestLayout();
      invalidate();
   }
}

可以看到满足源码中注释的条件就不会触发 View#requestLayout()

SurfaceView 和 TextureView

这两个 View 是根红苗正用来子线程更新 View 的,SurfaceView 使用自带 Surface 去做画面渲染,TextureView 同样可以通过 TextureView#lockCanvas() 使用临时的 Surface,所以都不会触发 View#requestLayout()

总结

本文主要着眼于子线程不能更新 UI 和能更新 UI 的底层原理,了解了 Activity View 树的构建流程、更新 UI 的基础流程。但是根据 Android 的设计理念,还是不应使用在子线程中更新 UI,定制化系统常常更改特定的 API 实现方式会直接让上面的“奇技淫巧”成为“不定时炸弹”。



也许你还想看

(▼点击文章标题或封面查看)

深入排查 FLAG 导致的 Activity 无法正常启动

2021-12-23

遵循 Google 应用指南的 Retrofit + Coroutine 封装

2021-08-12

JetPack Compose从初探到实战

2021-06-17

Android与HEIF格式图片适配方法

2021-12-02

Android自定义ViewGroup的那些事儿

2021-10-14


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存