美文网首页
【Android面试题】高级UI面试题——如何更新UI,为什么子

【Android面试题】高级UI面试题——如何更新UI,为什么子

作者: 小城哇哇 | 来源:发表于2023-08-30 21:38 被阅读0次

    如何更新UI,为什么子线程不能更新UI?

    这道题想考察什么?

    1. 是否了解为什么子线程不能更新UI的概念与真实场景使用,是否熟悉为什么子线程不能更新UI的本质区别?

    考察的知识点

    1. 为什么子线程不能更新UI的概念在项目中使用与基本知识

    考生应该如何回答

    1.你工作这么些年,有注意到子线程不能更新UI?

    答:

    其实Android开发者都在说,子线程不能更新UI,难道一定就不能在子线程更新UI吗,那也未必:

    下面代码是可以的,运行结果并无异常,可以正常的在子线程中更新了TextView控件

    @Override
    protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            tv = findViewById(R.id.tv);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    tv.setText("Test");
                }
            }).start();
    }
    

    但是假如让线程休眠1000ms,就会发生错误:

    Only the original thread that created a view hierarchy can touch its views.
    

    上面报错的意思是只有创建视图层次结构的原始线程才能更新这个视图,也就是说只有主线程才有权力去更新UI,其他线程会直接抛异常的;
    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7905)的异常路径可以看到抛出异常的最终在ViewRootImlcheckThread方法里,ViewRootImlView的根类,其控制着View的测量、绘制等操作,那么现在我们转到ViewRootImpl.java源码观察:

    @Override
    public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                mLayoutRequested = true;
                scheduleTraversals();
            }
    }
    
    void checkThread() {
            if (mThread != Thread.currentThread()) {
                throw new CalledFromWrongThreadException(
              "Only the original thread that created a view hierarchy can touch its views.");
            }
    }
    

    上面的scheduleTraversals()函数里是对View进行绘制操作,而在绘制之前都会检查当前线程是否为主线程mThread,如果不是主线程,就抛出异常;这样做法就限制了开发者在子线程中更新UI的操作;
    但是为什么最开始的在onCreate()里子线程对UI的操作没有报错呢,可以设想一下是因为ViewRootImp此时还没有创建,还未进行当前线程的判断;
    现在,我们寻找ViewRootImp在何时创建,从Activity启动过程中寻找源码,通过分析可以查看ActivityThread.java源码,并找到handleResumeActivity方法:

    final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume) {
            ···
            // TODO Push resumeArgs into the activity for consideration
            ActivityClientRecord r = performResumeActivity(token, clearHide);
            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();
                    a.mDecor = decor;
                    l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                    l.softInputMode |= forwardBit;
                    if (a.mVisibleFromClient) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    }
    
                } else if (!willBeVisible) {
                    if (localLOGV) Slog.v(
                        TAG, "Launch " + r + " mStartedActivity set");
                    r.hideForNow = true;
                }
            ···
    }
    

    可以看到内部执行了performResumeActivity方法:

    public final ActivityClientRecord performResumeActivity(IBinder token, boolean clearHide) {
        if (r != null && !r.activity.mFinished) { 
            r.activity.performResume(); 
        省略...
    } 
    

    会发现在内部调用了ActivityperformResume方法,可以肯定应该是要回调生命周期的onResume方法了:

        final void performResume() {
            ···
            mCalled = false;
            // mResumed is set by the instrumentation
            mInstrumentation.callActivityOnResume(this);
            if (!mCalled) {
                throw new SuperNotCalledException(
                    "Activity " + mComponent.toShortString() +
                    " did not call through to super.onResume()");
            }
            ···
        }
    

    接着然后又调用了InstrumentationcallActivityOnResume方法:

        public void callActivityOnResume(Activity activity) {
            activity.mResumed = true;
            activity.onResume();
            
            if (mActivityMonitors != null) {
                synchronized (mSync) {
                    final int N = mActivityMonitors.size();
                    for (int i=0; i<N; i++) {
                        final ActivityMonitor am = mActivityMonitors.get(i);
                        am.match(activity, activity, activity.getIntent());
                    }
                }
            }
        }
    

    然后就可以看到执行了 activity.onResume()方法,也就是回调了Activity生命周期的onResume方法;现在让我们回头看看handleResumeActivity方法,会执行这段代码:

    ···
    r.activity.mVisibleFromServer = true;
                    mNumVisibleActivities++;
                    if (r.activity.mVisibleFromClient) {
                        r.activity.makeVisible();
                    }
    

    发现在内部调用了ActivitymakeVisible方法:

    void makeVisible() {
            if (!mWindowAdded) {
                ViewManager wm = getWindowManager();
                wm.addView(mDecor, getWindow().getAttributes());
                mWindowAdded = true;
            }
            mDecor.setVisibility(View.VISIBLE);
        }
    

    源码内部调用了WindowManageraddView方法,而WindowManager方法的实现类是WindowManagerImp类,直接找WindowManagerImpaddView方法:

        @Override
        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyDefaultToken(params);
            mGlobal.addView(view, params, mDisplay, mParentWindow);
        }
    

    最终然后又执行了WindowManagerGlobaladdView方法,在该方法中,终于看到了ViewRootImpl的创建;

    public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {          
            ···
                root = new ViewRootImpl(view.getContext(), display);
                view.setLayoutParams(wparams);
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
            ···
        }
    

    真相大白:

    刚刚源码分析可得知,ViewRootImpl对象是在onResume方法回调之后才创建,那么就说明了为什么在生命周期的onCreate方法里,甚至是onResume方法里都可以实现子线程更新UI,因为此时还没有创建ViewRootImpl对象,并不会进行是否为主线程的判断;

    个人理解

    必须要在主线程更新UI,实际是为了提高界面的效率和安全性,带来更好的流畅性;你反推一下,假如允许多线程更新UI,但是访问UI是没有加锁的,一旦多线程抢占了资源,那么界面将会乱套更新了,体验效果就不言而喻了;所以在Android中规定必须在主线程更新UI,很合理吧

    总结源码分析

    第一点:子线程可以在ViewRootImpl还没有创建之前更新UI的

    第二点:访问UI是没有加对象锁的,在子线程环境下更新UI,会造成各种未知风险的

    第三点:Android开发者一定要在主线程更新UI操作,这个是职业习惯哦

    最后

    有需要以上面试题的朋友可以关注一下哇哇,以上都可以分享!!!

    相关文章

      网友评论

          本文标题:【Android面试题】高级UI面试题——如何更新UI,为什么子

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