サブスレッド更新UIフルソリューション

1サブスレッド更新UI例外設計コンセプトと単純なソースコード分析

初心者は、次のように子スレッドのUIを更新するのを間違える可能性があります。

thread { imageView.setBackgroundColor(Color.RED) }

実行すると、アプリケーションは直接クラッシュして例外をスローします。これは、Android開発の鉄則でもあります。UIは子スレッドで更新できません

では、なぜAndroidは子スレッドにUIを更新させないのでしょうか?その理由は、最小画面リフレッシュレートが60Hzになったことです。つまり、画面は最大16msごとに更新されるため、UIの更新はできるだけ速くする必要があります。そうしないと、フレームが失われ、スタックします。その後、UI更新操作をロックできなくなります。頻繁にロックおよびロックを解除すると、UIのレンダリング時間が長くなる可能性がありますが、サブスレッドがロックせずにUIを更新できる場合、複数のスレッドが同時にUIを更新します。 、結果としてスレッドのセキュリティが低下し、最終的なUIが発生します。その影響は想像を絶するものであるため、Androidはサブスレッドを直接制限してUIを更新します。実際、Androidにはこの制限があるだけでなく、一般的なUIフレームワークは基本的にシングルスレッドモデルです。

デザインコンセプトを理解した後、ソースコードの観点から分析してみましょう。この記事のフレームワークのソースコードはAndroid11バージョンのものです。

まず、ログの観点から分析してみましょう。エラーログは次のとおりです。

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.");
   }
}

関数は1つだけです。現在のスレッドがmThreadと整合性があるかどうかを判断し、整合性がない場合は例外をスローします。次に、mThreadがViewRootImplコンストラクターで初期化されていることがわかります。

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

したがって、理由は明らかです。現在の呼び出しスレッドは、ViewRootImplのコンストラクターで初期化されたスレッドではなく、例外がスローされます。しかし、これは知られているだけであり、理由を知りたい場合は、ソースコードの分析を続ける必要があります。

2詳細なソースコード追跡

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;
   }
}

ビューのrequestLayout()は、その親レイアウトのrequestLayout()を呼び出します。ViewGropはこのメソッドをオーバーライドしないため、ビューのrequestLayout()を呼び出します。つまり、最上位レイヤーまで繰り返します。それでは、トップレベルのビューとは何かを見てみましょう。

アクティビティのトップレベルビュー

まず、onCreate()のsetContentView()メソッドから作成したレイアウトの親ビューを見てみましょう(分析を簡単にするために、アクティビティはandroidx.appcompat.app.AppCompatActivityではなくandroid.app.Activityから継承します)。

// 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);

したがって、PhoneWindowのsetContentView()に移動して、次のことを確認してください。

// 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のサブクラスであることを理解する必要があります。上記のソースコードは、DecorViewを作成し、generateDecor()を介してmDecorに割り当て、次にgenerateLayout()を介してViewGroupを作成し、mContentParentに割り当てます。これらの2つの方法に焦点を当てます。

// 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());
}

コンテキストを処理した後、新しい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;
}

上記の機能条件で判断した最終的なレイアウトファイルが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();
}

レイアウトファイルがビューとしてロードされ、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の子ビューです。これで、アクティビティで親レイアウトを表示するための完全なリンクトレースが完了しました。

再帰的な親レイアウトの概要の表示:開発者のxmlによって生成されたレイアウト-> mContentParent(FragmentLayout)->システムの組み込みレイアウトファイルによって生成されたビュー(LinearLayout)-> mDecor(DecorView)。

ViewParent of DecorView

DecorViewがトップレベルのビューであることがわかりましたが、問題は実際には解決されていません。DecorViewに親ビューがない場合、最後の再帰的なrequestLayout()は何もしないのと同じように終了しませんか?実際、親ビューを再帰的に検索することは不正確であると言ってきました。ViewParentは再帰的に検索されると言わなければなりませ。DecorViewには親ビューはありませんが、ViewParentはあります。ただし、このプロセスは、上記のように下から上に追跡することはできませんが、最初にアクティビティのライフサイクルのプロセスを上から下に理解することで取得できます。

まず、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);
            }
            ...
      }
   }
}

これらのコードでは、実際には1行だけを分析する必要があります:wm.addView(decor、l)、このメソッドの機能は、DecorViewをWindowManagerに追加することです。WindowManagerの実装クラスがWindowManagerImplであり、addViewのソースコードが次のとおりであることを確認します。

// 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#requestLayout()が呼び出され、親ビューの再帰的な検索が開始され、アクティビティの最上位のビューがDecorViewであることがわかります。

  2. DecorViewのViewParentはViewRootImplであるため、ViewRootImpl#requestLayout()が呼び出され、次にViewRootImpl#checkThread()が呼び出されます。

  3. ViewRootImplはメインスレッドで初期化されるため、子スレッドからチェックスレッドを呼び出すと例外がスローされます。

3子スレッドは例外なくビューを更新します

子スレッドがビューを更新する例外の理由を知っているので、子スレッドに例外がない状況があるかどうか疑問に思うのは自然なことです。

ユニバーサルビューのソリューション

View#requestLayout()のソースコードによると:

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

2つの条件:mParent!= nullおよびmParent.isLayoutRequested()== falseはmParent.requestLayout()のみを呼び出すため、これら2つの条件を破る方法を見つけることができます。

Activity#onResume()以前のビューを更新

アクティビティのライフサイクルに関連付けられたコールチェーンがあります:ActivityThread#handleResumeActivity()-> ActivityThread#performResumeActivity()-> Activity#performResume()-> Instrumentation#callActivityOnResume()-> Activity#onResume()、スペースとテーマの理由、さらに詳しく説明することなく。

呼び出しチェーンから、Activity#onResume()の後にViewRootImplが初期化されることがわかります。したがって、DecorViewに戻る前にView#requestLayout()を呼び出すときに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) }
}

子スレッドが更新される前のrequestLayout表示

まず、View#isLayoutRequested()のソースコードによると、mPrivateFlagsにPFLAG_FORCE_LAYOUTがあるかどうかに関連しています。

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

View#requestLayout()のソースコードによると、最初のレイヤーリクエストが取得されると、PFLAG_FORCE_LAYOUTがmPrivateFlagsに追加されます。

// 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;

スペース上の理由から、特定のビューレイアウトプロセスの概要を簡単に説明します。詳細なソースコードの追跡は実行されません。

ViewRootImpl#requestLayout()-> ViewRootImpl#scheduleTraversals()は最終的にViewRootImpl#performTraversals()を呼び出しますが、直接呼び出されることはありませんが、次のVSYNC、ViewRootImpl#performTraversals()-> ViewRootImpl#performLayout()までChoreographerを介して呼び出されます。 -> View#layout()であるため、mParent.isLayoutRequested()は次のVSYNCでのみfalseに割り当てられます。これは、ビューを更新するための子スレッドの即時実行に影響を与えることはできません。

したがって、メインスレッドでrequestLayout()を1回呼び出し、すぐに子スレッドを呼び出して、例外なくビューを更新できます。

サンプルコード:

// 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()
   }
}

WindowManager#addView()はLooper.prepare()の後に呼び出す必要があることに注意してください。そうしないと、エラーが報告されます。java.lang.RuntimeException:Looper.prepare()を呼び出さないスレッドThread[xxxx]内にハンドラーを作成できません。

その理由は、ViewRootImplの初期化時にHeadlerが作成され、Headlerの初期化時にLooper.prepare()が呼び出されるため、最初にHeadlerを初期化し、次にViewRootImplを初期化する必要があります。

ビュー固有のソリューション

ビューを更新すると、通常、View#requestLayout()とView#invalidate()の2つのメソッドが呼び出されます。後者のみが呼び出された場合、ソースコードをトレースして何が起こるかを確認できます。

// 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にこのビューを再描画させることを意味するため、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#scheduleTraversals()がViewRootImpl#requestLayout()と同じ方法で呼び出されていることがわかりますが、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#invalidate()は、特定のビューの特定の更新メソッドが特定の条件を満たす場合にのみ呼び出されます。ハードウェアアクセラレーションサブスレッドの更新が有効になっている場合は、クラッシュしません。この状況を1つずつ調査する必要があります。異なるバージョンの可能性によって制限されます。いくつかの例を挙げれば、異なる結果が得られます。

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()は呼び出されません。mDrawableWidthとmDrawableHeightへの変更は、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()をオーバーライドしません。mDrawableWidthおよびmDrawableHeightは常に-1であるため、requestLayout()は呼び出されません。

TextViewは固定サイズのテキストを更新します

TextView#checkForRelayout()はTextView#setText()で呼び出されます。

// 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

これらの2つのビューは、ルートHongmiaoがサブスレッドによってビューを更新するために使用します。SurfaceViewは画面レンダリングに独自のSurfaceを使用します。TextureViewはTextureView#lockCanvas()を介して一時的なSurfaceを使用することもできるため、View#requestLayout()をトリガーしません。

4まとめ

この記事では主に、子スレッドがUIを更新できず、UIを更新できるという基本原則に焦点を当て、アクティビティビューツリーの構築プロセスとUIを更新する基本的なプロセスを理解します。ただし、Androidの設計コンセプトによれば、サブスレッドのUIを更新するために使用しないでください。カスタムシステムは、特定のAPI実装方法を変更することが多く、上記の「魔法のトリック」が「無期限」になります。爆弾」。

おすすめ

転載: blog.csdn.net/ajsliu1233/article/details/124149904