Android中子线程真的不能更新UI吗?

Android的UI访问是没有加锁的,这样在多个线程访问UI是不安全的。因此Android中规定只能在UI线程中访问UI。java

可是有没有极端的状况?使得咱们在子线程中访问UI也可使程序跑起来呢?接下来咱们用一个例子去证明一下。android

新建一个工程,activity_main.xml布局以下所示:web

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" >

    <TextView  android:id="@+id/main_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="18sp" android:layout_centerInParent="true" />

</RelativeLayout>

很简单,只是添加了一个居中的TextView面试

MainActivity代码以下所示:安全

public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                main_tv.setText("子线程中访问");
            }
        }).start();

    }

}

也是很简单的几行,在onCreate方法中建立了一个子线程,并进行UI访问操做。app

点击运行。你会发现即便在子线程中访问UI,程序同样能跑起来。结果以下所示:
这里写图片描述ide

咦,那为嘛之前在子线程中更新UI会报错呢?难道真的能够在子线程中访问UI?svg

先不急,这是一个极端的状况,修改MainActivity以下:oop

public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                main_tv.setText("子线程中访问");
            }
        }).start();

    }

}

让子线程睡眠200毫秒,醒来后再进行UI访问。布局

结果你会发现,程序崩了。这才是正常的现象嘛。抛出了以下很熟悉的异常:

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

……

做为一名开发者,咱们应该认真阅读一下这些异常信息,是能够根据这些异常信息来找到为何一开始的那种状况能够访问UI的。那咱们分析一下异常信息:

首先,从如下异常信息能够知道

at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)

这个异常是从android.view.ViewRootImpl的checkThread方法抛出的。

那如今跟进ViewRootImpl的checkThread方法瞧瞧,源码以下:

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

只有那么几行代码而已的,而mThread是主线程,在应用程序启动的时候,就已经被初始化了。

由此咱们能够得出结论:
在访问UI的时候,ViewRootImpl会去检查当前是哪一个线程访问的UI,若是不是主线程,那就会抛出以下异常:

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

这好像并不能解释什么?继续看到异常信息

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

那如今就看看requestLayout方法,

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

这里也是调用了checkThread()方法来检查当前线程,咦?除了检查线程好像没有什么信息。那再点进scheduleTraversals()方法看看

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

注意到postCallback方法的的第二个参数传入了很像是一个后台任务。那再点进去

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

找到了,那么继续跟进doTraversal()方法。

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

能够看到里面调用了一个performTraversals()方法,View的绘制过程就是从这个performTraversals方法开始的。PerformTraversals方法的代码有点长就不贴出来了,若是继续跟进去就是学习View的绘制了。而咱们如今知道了,每一次访问了UI,Android都会从新绘制View。这个是很好理解的。

分析到了这里,其实异常信息对咱们帮助也不大了,它只告诉了咱们子线程中访问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;

        //代码省略

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

      //代码省略 
}

能够看到内部调用了performResumeActivity方法,这个方法看名字确定是回调onResume方法的入口的,那么咱们仍是跟进去瞧瞧。

public final ActivityClientRecord performResumeActivity(IBinder token,
        boolean clearHide) {
    ActivityClientRecord r = mActivities.get(token);
    if (localLOGV) Slog.v(TAG, "Performing resume of " + r
            + " finished=" + r.activity.mFinished);
    if (r != null && !r.activity.mFinished) {
    //代码省略
            r.activity.performResume();

    //代码省略

    return r;
}

能够看到r.activity.performResume()这行代码,跟进 performResume方法,以下:

final void performResume() {
    performRestart();

    mFragments.execPendingActions();

    mLastNonConfigurationInstances = null;

    mCalled = false;
    // mResumed is set by the instrumentation
    mInstrumentation.callActivityOnResume(this);

    //代码省略

}

Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码以下:

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()。这也证明了,performResumeActivity方法确实是回调onResume方法的入口。

那么如今咱们看回来handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后,
会来到这一块代码:

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

activity调用了makeVisible方法,这应该是让什么显示的吧,跟进去探探。

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

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

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

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

里面调用了WindowManagerGlobal的addView方法,那如今就锁定
WindowManagerGlobal的addView方法:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

    //代码省略 


    ViewRootImpl root;
    View panelParentView = null;

    //代码省略

        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.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

终于击破,ViewRootImpl是在WindowManagerGlobal的addView方法中建立的。

回顾前面的分析,总结一下:
ViewRootImpl的建立在onResume方法回调以后,而咱们一开篇是在onCreate方法中建立了子线程并访问UI,在那个时刻,ViewRootImpl是没有建立的,没法检测当前线程是不是UI线程,因此程序没有崩溃同样能跑起来,而以后修改了程序,让线程休眠了200毫秒后,程序就崩了。很明显200毫秒后ViewRootImpl已经建立了,能够执行checkThread方法检查当前线程。

这篇博客的分析如题目同样,Android中子线程真的不能更新UI吗?在onCreate方法中建立的子线程访问UI是一种极端的状况,这个不仔细分析源码是不知道的。我是最近看了一个面试题,才发现这个。

从中我也学习到了从异常信息中跟进源码寻找答案,你呢?