子线程更新UI全解

发表于 2年以前  | 总阅读数:383 次

子线程更新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, 0, new 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(0, 0, 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(0, 0, 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 实现方式会直接让上面的“奇技淫巧”成为“不定时炸弹”。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ID-C1L-n432FPAhIuNcNeg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237270次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录