美文网首页
Android 为什么不能再子线程更新UI

Android 为什么不能再子线程更新UI

作者: gc都无法回收的垃圾 | 来源:发表于2020-09-24 16:28 被阅读0次

    作为android开发人员,总是被要求着不能再子线程去更新UI,必须得再主线程更新UI,由于好奇,也由于看这些源码也可以提升自己,就去查了相关资料来学习(本文是自我学习记录的文章,欢迎讨论,若有不对还麻烦指正出来)
    先来看看下面的代码

    class PracticeActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_practice)
    
            Thread(Runnable {
                tv_text.text = "报错"
            }).start()
        }
    }
    

    在onCreate里面,实例化了一个Thread,并在里面进行了更新TextView的操作,按照常理来说,不能再子线程更新UI,那么会不会报错呢?
    运行一下


    onCreate里子线程更新UI

    并没有报错..这不符合常理啊?再试试在onResume里运行

    class PracticeActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_practice)
        }
    
        override fun onResume() {
            super.onResume()
            Thread(Runnable {
                tv_text.text = "报错吗"
            }).start()
        }
    }
    
    onResume里子线程更新UI

    啊这..依旧完美运行,难道我们以前报错都是假的吗?我学那么久的android都是白学的?
    不信邪,onPause里再试试!

    override fun onPause() {
            super.onPause()
            Thread(Runnable {
                tv_text.text = "还不报错?"
            }).start()
        }
    
     android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
            at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8052)
            at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)
    

    舒服了,这熟悉的异常,还是那个味道。
    那么这是为什么呢?

    先说结论
    在activity的onResume(包括)之前里的子线程是可以在子线程更新UI的,但是不能用耗时操作去更新,在onResume以后则无法在子线程更新UI(20.11.13 更正:因为activity的页面上的view,是在handleResumeactivity的方法里创建的,抛出 "Only the original thread that created a view hierarchy can touch its views."的原因是当前的Thread和创建view(ViewRootImpl)的Thread不是同一个,所以在activity的onResume之前,view还没创建,所以可以随意修改;因此,若在子线程创建一个视图,然后在主线程修改显示也是会报错的,比如在子线程创建一个dialog,然后在主线程show就会报错,比如在子线程创建一个Toast然后子线程show,也是可以显示且不会保存,不过在子线程创建的时候需要加Looper.prepare()和Looper.loop())。
    原因:不能再子线程更新UI的具体表现为,会抛出一个

    CalledFromWrongThreadException ("Only the original thread that created a view hierarchy can touch its views.")
    

    这个异常的抛出点在ViewRootImpl 里的(此处是在监测当前的所在的线程是否为创建此view的线程)

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

    这个ViewRootImpl是在哪里调用这个checkThread()方法的呢?

    public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();//检查线程
                mLayoutRequested = true;
                scheduleTraversals();
            }
        }
    public void invalidateChild(View child, Rect dirty) {
            invalidateChildInParent(null, dirty);
        }
    @Override
        public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
            checkThread();//检查线程
            ···
        }
    

    那么这些方法又是在哪里调用了呢?
    这得从更新View说起,就拿TextView.setTextView()开始
    在TextView.java里的setText方法里

    @UnsupportedAppUsage
    private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        ···//省略设置文本的方法
          if (mLayout != null) {  checkForRelayout(); }//接着看这个方法
    
    }
    
    private void checkForRelayout() {             
            ···
            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }
    
                // Dynamic height, but height has stayed the same,
                // so use our new text layout.
                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }
    
            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();
        } else {
            // Dynamic width, so we have no choice but to request a new
            // view layout with a new text layout.
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }
    

    在checkForRelayout()里无论走哪里的判断,最后都会走invalidate()方法,所以我们先来看看这个方法

    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) {
             ···
             // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
             final ViewParent p = mParent;//ViewParent是一个接口
    
            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也就是ViewParent,是一个接口类

    /**
     * Defines the responsibilities for a class that will be a parent of a View.
     * This is the API that a view sees when it wants to interact with its parent.
     * 
     */
    public interface ViewParent {
        public void requestLayout();
        ···
        public void invalidateChild(View child, Rect r);
    }
    

    这个接口主要就是为了当前view和父view进行交互的;那么接着看,这个mParent,到底是谁去实现这个接口的呢?
    在View.java里我们找到mParent复制的相关方法。

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

    这里是通过assignParent进行赋值的,那么又是什么时候、谁去赋值的呢?
    直接给出答案,ViewRootImpl;那么这个ViewRootImpl又是什么?

    /**
     * The top of a view hierarchy, implementing the needed protocol between View
     * and the WindowManager.  This is for the most part an internal implementation
     * detail of {@link WindowManagerGlobal}.
     *
     * {@hide}
     */
    @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
    public final class ViewRootImpl implements ViewParent,
            View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks
    

    根据头部信息翻译(机翻)一下:

    视图层次结构的顶部,实现视图之间所需的协议
    和WindowManager。这在很大程度上是一个内部实现
    {@link WindowManagerGlobal}的详细信息。
    

    简单的说就是ViewRootImpl实现了View和WindowManager之间的通讯协议(小声BB:这个也是绘制View三大流程的幕后黑手)。
    那么这个ViewRootImpl是在哪里初始化呢?
    在ActivityThread里的handleResumeActivity()里,简单的说,就是在Activity的onResume的时候。

    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
    ···
    //此处的方法里面调用了Activity.onResume()
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
       ···
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        //获取DecorView并添加到PhoneWindow上
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
        l.softInputMode |= forwardBit;
        ···
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                //添加到WindowManager里
                wm.addView(decor, l);
            } else {
                // The activity will get a callback for this {@link LayoutParams} change
                // earlier. However, at that time the decor will not be set (this is set
                // in this method), so no action will be taken. This call ensures the
                // callback occurs with the decor set.
                a.onWindowAttributesChanged(l);
            }
        }
     }
    

    在activity的setContentView时,DecorView 还没有被 WindowManager 正式添加到 Window 中,接着会调用到 ActivityThread 类的 handleResumeActivity 方法将顶层视图 DecorView 添加到 PhoneWindow 窗口,activity 的视图才能被用户看到。(补充知识:在activity.setContentView的时候创建了DecorView,但此时还未将DecorView 于WindowManager关联起来,是在这个流程里进行关联的)
    接着看wm,addView()

    ViewManager wm = a.getWindowManager();
    ···
    public WindowManager getWindowManager() {
        return mWindowManager;
    }
    
    mWindowManager = mWindow.getWindowManager();
    ···
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    

    然后进入PhoneWindow并没有getWindowManager()方法,所以进去父类Window.java查找

    public WindowManager getWindowManager() {
        return mWindowManager;
    }
    
    
    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
            ···
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
    
    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }
    

    所以此处也就是最终拿到的WindowManagerImpl,进去WindowManagerImpl看addView方法

    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
    

    最终调用的是WindowManagerGlobal 的addView方法

    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            ···
            ViewRootImpl root;
            ···
                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);
                } catch (RuntimeException e) {
                    // BadTokenException or InvalidDisplayException, clean up.
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                    throw e;
                }
            }
        }
    

    终于!!在此方法里创建了ViewRootImpl,并把相应的View设置到ViewRootImpl 里面去。
    然后进入ViewRootImpl的setView方法里

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        ···
    // Schedule the first layout -before- adding to the window
    // manager, to make sure we do the relayout before receiving
    // any other events from the system.
    requestLayout();
        ···
    view.assignParent(this);//将对应的view关联上相应的ViewRootImpl
        ···
    }
    
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();//此方法里会执行view的三大绘制流程:测量、布局、绘制,不过不在本文讨论范围
        }
    }
    
    View.java
    @UnsupportedAppUsage
    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");
        }
    }
    

    看到这里,是不是就和前面对应上了。
    总结:
    1.当View更新重绘时,也就是调用invalidate()的时候回去调用ViewParent的invalidateChild()方法。
    2.而这个ViewParent就是在Activity的OnResume的时候通过WindowManager(WindowManagerGlobal )创建的ViewRootImpl。
    3.所以在onCreate或者onStrat的时候,通过子线程去更新View是可以的,但是不能做耗时操作(比如sleep了2s,然后在setText,同样会报错,因为ViewParent为null的时候,就不会去调用invalidateChild()方法。
    4.在OnResume后,绑定了DecorView,并且为每个view都关联了 相应的ViewRootImpl后,invalidateChild()时就会判断是否在主线程。
    5.总的来说,为什么能在onCreate、onStart、onResume里面的子线程里直接进行UI更新,是因为此时还未创建ViewRootImpl,DecorView 还未与WindowManager绑定,所以无法进行ViewRootImpl的checkThread()操作。
    6.这些绑定创建流程不都是在resume里发送的吗?为毛onResume也可以在子线程更新?
    因为handleResumeActivity里的performResumeActivity()方法先与WindowManager.addView(decor, l)方法...也就是说onResume过后再进行创建ViewRootImpl。

    20.11.13 更正:现在发现写文章的时候说法有误,特此更正
    因为activity的页面上的view,是在handleResumeactivity的方法里创建的,抛出 "Only the original thread that created a view hierarchy can touch its views."的原因是当前的Thread和创建view(ViewRootImpl)的Thread不是同一个,所以在activity的onResume之前,view还没创建,所以可以随意修改;因此,若在子线程创建一个视图,然后在主线程修改显示也是会报错的,比如在子线程创建一个dialog,然后在主线程show就会报错,比如在子线程创建一个Toast然后子线程show,也是可以显示且不会保存,不过在子线程创建的时候需要加Looper.prepare()和Looper.loop()

    //此方式可行,且不会报错
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT).show();
            Looper.loop();
        }
    });
    thread.start();
    
    //子线程中调用    
    public void showDialog(){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //创建Looper,MessageQueue
                    Looper.prepare();
                    new Handler().post(new Runnable() {
                        @Override
                        public void run() {
                            builder = new AlertDialog.Builder(HandlerActivity.this);
                            builder.setTitle("子线程");
                            alertDialog = builder.create();
                            alertDialog.show();
                            alertDialog.hide();
                        }
                    });
                    //开始处理消息
                    Looper.loop();
                }
            }).start();
        }
    

    在子线程中调用showDialog方法,先调用alertDialog.show()方法,再调用alertDialog.hide()方法,hide方法只是将Dialog隐藏,并没有做其他任何操作(没有移除Window),然后再在主线程调用alertDialog.show();便会抛出Only the original thread that created a view hierarchy can touch its views异常了

     android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
            at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8052)
            at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)
    

    本文是自我学习记录的文章,欢迎讨论,若有不对还麻烦指正出来,谢谢~

    相关文章

      网友评论

          本文标题:Android 为什么不能再子线程更新UI

          本文链接:https://www.haomeiwen.com/subject/yaazyktx.html