当前位置:   article > 正文

如何做到在子线程更新 UI?_findviewbyid可以在子线程吗

findviewbyid可以在子线程吗

一般来讲,子线程是不能更新 UI 的,如果在子线程更新 UI,会报错。

但在某种情况下直接开启线程更新 UI 是不会报错的。

比如,在 onCreate 方法中,直接开启子线程更新 UI,这样是不会报错的。

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. setContentView(R.layout.activity_main)
  4. textView = findViewById(R.id.tv)
  5. thread {
  6. textView.text = "哈哈哈哈"
  7. }
  8. }

如果在子线程中假如延时,比如加一行Thread.sleep(2000)就会报错。

这是为什么呢?

有人会说,因为睡眠了 2 s,因此 UI 的线程检查机制就已经建立了,所以在子线程更新就会报错。

更新 UI 的线程检测是什么时候开始的

子线程更新的错误定位是 ViewRootImpl 中的 checkThread 方法和 requestLayout 方法。

  1. // ViewRootImpl 下 checkThread 的源码
  2. void checkThread() {
  3. if (mThread != Thread.currentThread()) {
  4. throw new CalledFromWrongThreadException(
  5. "Only the original thread that created a view hierarchy can touch its views.");
  6. }
  7. }
  8. //ViewRootImpl 下 requestLayout 的源码
  9. @Override
  10. public void requestLayout() {
  11. if (!mHandlingLayoutInLayoutRequest) {
  12. checkThread();
  13. mLayoutRequested = true;
  14. scheduleTraversals();
  15. }
  16. }

源码中可以看出,checkThread 就是进行线程检测的方法,而调用是在 requestLayout 方法中。

要想知道 requestLayout 是何时调用的,就要知道 ViewRootImpl 是如何创建的?

因为在 onCreate 中创建子线程访问 UI,是不报错的,这也说明在 onCreate中,ViewRootImpl 还未创建。

ViewRootImpl 是何时创建的。

在 ActivityThread 的 handleResumeActivity 中调用了 performResumeActivity 进行 onResume 的回调。

  1. @Override
  2. public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {
  3. // 代码省略...
  4. // performResumeActivity 最终会调用 Activity 的 onResume方法
  5. // 调用链如下: 会调用 r.activity.performResume。
  6. // performResumeActivity -> r.activity.performResume -> Instrumentation.callActivityOnResume(this) -> activity.onResume();
  7. final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
  8. // 代码省略...
  9. if (r.window == null && !a.mFinished && willBeVisible) {
  10. r.activity.mVisibleFromServer = true;
  11. mNumVisibleActivities++;
  12. if (r.activity.mVisibleFromClient) {
  13. // 注意这句,让 activity 显示,并且会最终创建 ViewRootImpl
  14. r.activity.makeVisible();
  15. }
  16. }
  17. }

进一步跟进 activity.makeVisible()

  1. void makeVisible() {
  2. if (!mWindowAdded) {
  3. ViewManager wm = getWindowManager();
  4. // 往 WindowManager 中添加 DecorView
  5. wm.addView(mDecor, getWindow().getAttributes());
  6. mWindowAdded = true;
  7. }
  8. mDecor.setVisibility(View.VISIBLE);
  9. }

WindowManager 是一个接口,它的实现类是 WindowManagerImpl

  1. // WindowManagerImpl 的 addView 方法
  2. @Override
  3. public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
  4. applyDefaultToken(params);
  5. // 最终调用了 WindowManagerGlobal 的 addView
  6. mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
  7. }
  8. // WindowManagerGlobal 的 addView
  9. public void addView(View view, ViewGroup.LayoutParams params,
  10. Display display, Window parentWindow) {
  11. // 省略部分代码
  12. // ViewRootImpl 对象的声明
  13. ViewRootImpl root;
  14. View panelParentView = null;
  15. synchronized (mLock) {
  16. // 省略部分代码
  17. // ViewRootImpl 对象的创建
  18. root = new ViewRootImpl(view.getContext(), display);
  19. view.setLayoutParams(wparams);
  20. mViews.add(view);
  21. mRoots.add(root);
  22. mParams.add(wparams);
  23. try {
  24. // 调用 ViewRootImpl 的 setView 方法
  25. root.setView(view, wparams, panelParentView);
  26. } catch (RuntimeException e) {
  27. // BadTokenException or InvalidDisplayException, clean up.
  28. if (index >= 0) {
  29. removeViewLocked(index, true);
  30. }
  31. throw e;
  32. }
  33. }
  34. }

由此可以看出,ViewRootImpl 是在 activity 的 onResume 方法调用后才由 WindowManagerGlobal 的 addView 方法创建。

那 requestLayout 是如何调用的呢?

在上面 WindowManagerGlobal 的 addView 方法中,创建完 ViewRootImpl 后,会调用它的 setView 的方法,在 setView 方法内部会调用 requestLayout

此时就会去检测 UI 更新时调用的线程了。

  1. // ViewRootImpl 的 setView
  2. public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
  3. synchronized (this) {
  4. if (mView == null) {
  5. mView = view;
  6. // 省略无关代码...
  7. // requestLayout 的调用
  8. requestLayout();
  9. // 省略无关代码...
  10. }
  11. }
  12. // requestLayout 方法
  13. @Override
  14. public void requestLayout() {
  15. if (!mHandlingLayoutInLayoutRequest) {
  16. checkThread();
  17. mLayoutRequested = true;
  18. scheduleTraversals();
  19. }
  20. }

而在 SheduleTranversals 方法中,会调用 TraversalRunnable 的 run方法,最终会在 performTraversals 方法中,调用 performMeasure performLayout performDraw 去开始 View 的绘制流程。

  1. void scheduleTraversals() {
  2. if (!mTraversalScheduled) {
  3. mTraversalScheduled = true;
  4. mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
  5. // TraversalRunnable 的 run 方法中,会开启 UI 的measure、layout、draw
  6. mChoreographer.postCallback(
  7. Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
  8. // 省略无关代码...
  9. }
  10. }
  11. final class TraversalRunnable implements Runnable {
  12. @Override
  13. public void run() {
  14. doTraversal();
  15. }
  16. }
  17. void doTraversal() {
  18. if (mTraversalScheduled) {
  19. // 省略部分代码
  20. performTraversals();
  21. }
  22. }
  23. private void performTraversals() {
  24. // Ask host how big it wants to be
  25. // 省略部分代码
  26. performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  27. performLayout(lp, mWidth, mHeight);
  28. performDraw();
  29. }

子线程更新 UI 实战

既然知道了子线程更新 UI 的检测是在 checkThread 方法中,那么有没有什么方法可以绕过呢?能否做到子线程更新 UI 呢?

答案是可以的。

我以一个简单的 demo 实验一下,下面先看效果。

代码如下:

  1. // MainActivity
  2. public class MainActivity extends AppCompatActivity {
  3. private View containerView;
  4. private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
  5. private TextView mTv2;
  6. private TextView mTv1;
  7. @Override
  8. protected void onCreate(Bundle savedInstanceState) {
  9. super.onCreate(savedInstanceState);
  10. setContentView(R.layout.activity_main);
  11. containerView = findViewById(R.id.container_layout);
  12. mTv1 = findViewById(R.id.text);
  13. mTv2 = findViewById(R.id.text2);
  14. // 开启线程,启动 GlobalLayoutListener
  15. Executors.newSingleThreadExecutor().execute(() -> initGlobalLayoutListener());
  16. }
  17. private void initGlobalLayoutListener() {
  18. globalLayoutListener = () -> {
  19. Log.e("caihua", "onGlobalLayout : " + Thread.currentThread().getName());
  20. ViewGroup.LayoutParams layoutParams = containerView.getLayoutParams();
  21. containerView.setLayoutParams(layoutParams);
  22. };
  23. this.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
  24. }
  25. public void updateUiInMain(View view) {
  26. mTv1.setText("主线程更新 UI");
  27. }
  28. public void updateUiInThread(View view) {
  29. new Thread(){
  30. @Override
  31. public void run() {
  32. SystemClock.sleep(2000);
  33. mTv2.setText("子线程更新 UI :" + Thread.currentThread().getName());
  34. }
  35. }.start();
  36. }
  37. }

原理:通过 ViewTreeObserver.OnGlobalLayoutListener 设置全局的布局监听,然后在 onGlobalLayout 方法中,调用 view 的 setLayoutParams 方法,setLayoutParams 方法内部会调用 requestLayout,这样就可以绕过线程检测。

为什么能绕过呢?

因为 setLayoutParams 中调用的 requestLayout 方法并不是 ViewRootImpl 中 requestLayout.

而 View 的 requestLayout 并不调用 checkThread 方法去检测线程。

源码如下↓

  1. // view.setLayoutParams 源码
  2. public void setLayoutParams(ViewGroup.LayoutParams params) {
  3. if (params == null) {
  4. throw new NullPointerException("Layout parameters cannot be null");
  5. }
  6. mLayoutParams = params;
  7. resolveLayoutParams();
  8. if (mParent instanceof ViewGroup) {
  9. ((ViewGroup) mParent).onSetLayoutParams(this, params);
  10. }
  11. // 调用 requestLayout 方法。
  12. requestLayout();
  13. }
  14. // View 的 requestLayout 方法
  15. public void requestLayout() {
  16. if (mMeasureCache != null) mMeasureCache.clear();
  17. if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
  18. ViewRootImpl viewRoot = getViewRootImpl();
  19. if (viewRoot != null && viewRoot.isInLayout()) {
  20. if (!viewRoot.requestLayoutDuringLayout(this)) {
  21. return;
  22. }
  23. }
  24. mAttachInfo.mViewRequestingLayout = this;
  25. }
  26. mPrivateFlags |= PFLAG_FORCE_LAYOUT;
  27. mPrivateFlags |= PFLAG_INVALIDATED;
  28. if (mParent != null && !mParent.isLayoutRequested()) {
  29. mParent.requestLayout();
  30. }
  31. if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
  32. mAttachInfo.mViewRequestingLayout = null;
  33. }
  34. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/258770
推荐阅读
相关标签
  

闽ICP备14008679号