美文网首页Android技术文章
从源码的角度来分析子线程真的不能更新UI吗?

从源码的角度来分析子线程真的不能更新UI吗?

作者: zkxok | 来源:发表于2017-07-17 22:37 被阅读128次

    我们初学Android的时候就知道“子线程里面是不能直接更新UI的”,那么真的是如此吗?我们来看下面这段代码

    public class MainActivity extends AppCompatActivity {
        TextView tv;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
             tv = (TextView) findViewById(R.id.tv);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    tv.setText("我是子线程,我更新UI了");
                }
            }).start();
        }
    }
    

    布局文件很简单,里面就只有一个TextView。这段代码在子线程里面更新了UI,理论上应该是会报错的。让我们来看一看实际的运行结果:

    子线程更新UI.png

    居然没有报错,TextView也显示出来为我们设置的值。惊不惊喜?意不意外?

    1.jpg

    分析

    子线程为啥不能更新UI?

    子线程不能更新UI,这句基本上没错的,但也有例外情况,我们上面这个例子就是。不过我们还得先分析下在这绝大多数情况下,子线程为啥不能更新UI,然后再来分析在这极少数例外的情况下,子线程为啥可以更新UI

    1、设计层面上

    Android系统为啥不允许子线程中更新UI呢?这是因为Android的UI控件不是线程安全的。如果多线程中并发访问可能会导致UI控件处于不可预期的状态。你可能会问为啥不对UI控件加锁呢?因为加锁会导致UI访问的逻辑变得复杂,同时会降低UI访问的效率,锁机制会阻塞某些线程的执行。鉴于以上问题。最简单高效的办法就是采用单线程来处理UI操作,我们只需要用Handler来切换一下线程就可以了。

    2、代码层面上

    下面这段代码是ViewRootImpl的一个方法,其实我们在调用UI控件
    setText等更新UI的方法时,会调用到ViewRootImpl的这个方法,这个方法就是去检查当前线程是不是主线程(mThread是主线程),只有那么几行代码而已的,如果当前线程不是主线程,就会抛出异常。这也就是子线程不能更新UI的代码层面上的原因?

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

    然而我们在子线程更新UI为什么不报错?

    那么出现上面子线程更新UI居然不报错的原因是啥呢?不是说子线程不能更新UI吗?当访问UI时,ViewRootImpl会调用checkThread方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在MainActivity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢?唯一的解释就是执行onCreate方法的那个时候ViewRootImpl还没创建,无法去检查当前线程。有了这个想法,那么我们就去验证下,这个想法是否正确,怎么验证呢?当然是看ViewRootImpl实在何处创建的。
    我们可以找到ActivityThread的handleResumeActivity方法

     final void handleResumeActivity(IBinder token,
                boolean clearHide, boolean isForward, boolean reallyResume) {
            // If we are getting ready to gc after going to the background, well
            // we are back active so skip it.
            unscheduleGcIdler();
            mSomeActivitiesChanged = true;
    
            // TODO Push resumeArgs into the activity for consideration
            ActivityClientRecord r = performResumeActivity(token, clearHide);
    
            if (r != null) {
                final Activity a = r.activity;
    
                if (localLOGV) Slog.v(
                    TAG, "Resume " + r + " started activity: " +
                    a.mStartedActivity + ", hideForNow: " + r.hideForNow
                    + ", finished: " + a.mFinished);
    
                final int forwardBit = isForward ?
                        WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
    
                // If the window hasn't yet been added to the window manager,
                // and this guy didn't finish itself or start another activity,
                // then go ahead and add the window.
                boolean willBeVisible = !a.mStartedActivity;
                if (!willBeVisible) {
                    try {
                        willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                                a.getActivityToken());
                    } catch (RemoteException e) {
                    }
                }
                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);
                    }
    
                // If the window has already been added, but during resume
                // we started another activity, then don't yet make the
                // window visible.
                } else if (!willBeVisible) {
                    if (localLOGV) Slog.v(
                        TAG, "Launch " + r + " mStartedActivity set");
                    r.hideForNow = true;
                }
    
                // Get rid of anything left hanging around.
                cleanUpPendingRemoveWindows(r);
    
                // The window is now visible if it has been added, we are not
                // simply finishing, and we are not starting another activity.
                if (!r.activity.mFinished && willBeVisible
                        && r.activity.mDecor != null && !r.hideForNow) {
                    if (r.newConfig != null) {
                        if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
                                + r.activityInfo.name + " with newConfig " + r.newConfig);
                        performConfigurationChanged(r.activity, r.newConfig);
                        freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig));
                        r.newConfig = null;
                    }
                    if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
                            + isForward);
                    WindowManager.LayoutParams l = r.window.getAttributes();
                    if ((l.softInputMode
                            & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                            != forwardBit) {
                        l.softInputMode = (l.softInputMode
                                & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                                | forwardBit;
                        if (r.activity.mVisibleFromClient) {
                            ViewManager wm = a.getWindowManager();
                            View decor = r.window.getDecorView();
                            wm.updateViewLayout(decor, l);
                        }
                    }
                    r.activity.mVisibleFromServer = true;
                    mNumVisibleActivities++;
                    if (r.activity.mVisibleFromClient) {
                        r.activity.makeVisible();//第一处
                    }
                }
    
                if (!r.onlyLocalRequest) {
                    r.nextIdle = mNewActivities;
                    mNewActivities = r;
                    if (localLOGV) Slog.v(
                        TAG, "Scheduling idle handler for " + r);
                    Looper.myQueue().addIdleHandler(new Idler());
                }
                r.onlyLocalRequest = false;
    
                // Tell the activity manager we have resumed.
                if (reallyResume) {
                    try {
                        ActivityManagerNative.getDefault().activityResumed(token);
                    } catch (RemoteException ex) {
                    }
                }
    
            } else {
                // If an exception was thrown when trying to resume, then
                // just end this activity.
                try {
                    ActivityManagerNative.getDefault()
                        .finishActivity(token, Activity.RESULT_CANCELED, null, false);
                } catch (RemoteException ex) {
                }
            }
        }
    

    其实这段代码中会调Activity的onResume方法,从方法名上也可以看出来。这段代码很长,我们只需要看到我标注为第一处的那段代码。也就是下面这句。
    r.activity.makeVisible();
    我们跟进去看看

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

    ViewManager中添加DecorView,那现在应该关注的就是ViewManager的addView方法了。而ViewManager是一个接口来的,我们应该找到ViewManager的实现类才行,而ViewManager的实现类是WindowManagerImpl。

    找到了WindowManagerImpl的addView方法,如下:

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

    然后第三行又调用了WindowManagerGlobal的addView方法,咱们继续跟进去看看.

     public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
           ……省略代码
            ViewRootImpl root;//第1处
            View panelParentView = null;
         ……省略代码
                root = new ViewRootImpl(view.getContext(), display);//第2处
    
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
     ……省略代码
        
        }
    

    看到第1,2处的代码木有,ViewRootImpl在这里创建了。也就是说ViewRootImpl实在Activity的onResume方法之后才调用的。onCreate方法时ViewRootImpl还没有创建,自然没办法检查线程,也就不会报错。

    再次尝试

    既然知道了结论,那么我们再试一下,这次我们在更新UI之前先sleep500毫秒,让它有时间执行到onResume,创建ViewRootImpl。代码如下:

    public class MainActivity extends AppCompatActivity {
        TextView tv;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
             tv = (TextView) findViewById(R.id.tv);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(500);
                        tv.setText("我是子线程,我更新UI了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                }
            }).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:7357)
    at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1099)
    

    相关文章

      网友评论

        本文标题:从源码的角度来分析子线程真的不能更新UI吗?

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