首页 > 代码库 > 为什么我们可以在非UI线程中更新UI
为什么我们可以在非UI线程中更新UI
尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!
炮兵镇楼
看到这样的标题……估计N多人会说我是逗比…………因为很多盆友在学习Android(特别是从4.0之后开始入门的)的时候都会常看见或听到别人说我们更新UI呢要在UI线程(或者说主线程)中去更新UI,不要在子线程中更新UI,而Android官方呢也建议我们不要在非UI线程直接更新UI,为什么呢?借助Android官方的一句话来说就是:
“The Android UI toolkit is not thread-safe and the view must always be manipulated on the UI thread.”
因此,很多童鞋会有这么一个惯性思维:在非UI线程中不能更新UI!既然Android不建议我们这么做,那其必定会对我们在code时做一些限制,比如当我们尝试运行如下代码时:
/** * 主界面 * * @author Aige {@link http://blog.csdn.net/aigestudio} * @since 2014/11/17 */ public class MainActivity extends Activity { private TextView tvText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvText = (TextView) findViewById(R.id.main_tv); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } tvText.setText("OtherThread"); } }).start(); } }为了把情况说明,这里我也将xml布局文件代码贴出来:
<!-- http://blog.csdn.net/aigestudio --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:background="#ffffff" android:layout_height="match_parent" > <TextView android:id="@+id/main_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>当我们运行上述代码后,你便会在Logcat中得到如下error提示:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
这句话非常简单,而且……我相信每个做Android开发到一定时间的盆友都碰到过,Android通过检查我们当前的线程是否为UI线程从而抛出一个自定义的AndroidRuntimeException来提醒我们“Only the original thread that created a view hierarchy can touch its views”并强制终止程序运行,具体的实现在ViewRootImpl类的checkThread方法中:
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { // 省去海量代码………………………… void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } } // 省去巨量代码…………………… }这就是Android在4.0后对我们做出的一个限制。写这篇Blog的具体原因来自凯子哥的一篇博文:来来来,同学,咱们讨论一下“只能在UI主线程更新View”这件小事,鉴于凯子哥如此好学,我想想呢也许很多盆友也有类似疑问:究竟TM到底能不能在非UI线程中更新UI呢?同时也为了引出我对在非UI线程更新UI方法的一些总结,我决定在3/4之前先撸一篇Blog扫清障碍先。首先,我先回答几个问题包括凯子哥的:
- 究竟TM到底能不能在非UI线程中更新UI呢?答案:能、当然可以
- View的运行和Activity的生命周期有什么必然联系吗?答案:没有、或者隐晦地说没有必然联系
- 除了Handler外是否还有更简便的方式在非UI线程更新UI呢?答案:有、而且还不少
OK,这里我们再来看一下上面的一段代码,在线程中我调用了Thread.sleep(200);来让我们的匿名线程暂停了200ms,如果……假如……我们去掉它的话……………………会发生什么?来试试:
/** * 主界面 * * @author Aige {@link http://blog.csdn.net/aigestudio} * @since 2014/11/17 */ public class MainActivity extends Activity { private TextView tvText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvText = (TextView) findViewById(R.id.main_tv); new Thread(new Runnable() { @Override public void run() { tvText.setText("OtherThread"); } }).start(); } }这时你会发现我们的代码TM地正确执行了!而且我们的TextView正确显示出了“OtherThread”文本!看到屏幕上的这11个英文字母我相信大家又把刚放出来又吸进去的屁再一次地放了出来…………这就是凯子哥Blog中提到的问题,我们成功地在非UI线程中更新了UI。其实这里最最根本的原因是我们并没有checkThread我们的当前线程,而我在文章最开始的代码中通过Thread.sleep(200)暂停了一小段时间,这里为什么回暂停线程一段时间?在这段时间的背后Android背地里背着我们都干了什么?数百头母驴为何半夜惨叫?小卖部安全套为何屡遭黑手?女生宿舍内裤为何频频失窃?连环强奸母猪案,究竟是何人所为?老尼姑的门夜夜被敲,究竟是人是鬼?数百只小母狗意外身亡的背后又隐藏着什么?这一切的背后, 是人性的扭曲还是道德的沦丧?是性的爆发还是饥渴的无奈?抱歉……磕个药,上面我们讲到,我们能正确以上述代码的方式在非UI线程中更新UI而不报错,那么原因也许只有一个,那就是没有执行checkThread方法去检查我们的当前线程……但是,细看调用checkThread方法的调用方法们你就会发现,TM全是跟View创建生成相关:
也就是说一旦我们尝试去对我们的控件进行生成,这些方法其中一个必然会被调用,这时候很多朋友就会蛋疼了…………但是,请不要被checkThread方法的思维所束缚,这时候你该扩大你的思维范畴,既然checkThread方法属于ViewRootImpl的成员方法,那么会不会是此时我们的ViewRootImpl根本就没被创建呢?怀着这个出发点,我们再度审视ActivtyThread调度Activity生命周期的各个环节,首先看看performLaunchActivity方法中的处理:
public final class ActivityThread { // 省去海量代码………………………… private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ActivityInfo aInfo = r.activityInfo; // 省去对packageInfo的逻辑处理 // 省去对ComponentName的逻辑处理 Activity activity = null; try { java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); // 通过Instrumentation对象生成Activity类的实例 activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); // 省去三行代码………… } catch (Exception e) { // 省去对异常的捕获处理 } try { Application app = r.packageInfo.makeApplication(false, mInstrumentation); // 省去多行无关代码 if (activity != null) { Context appContext = createBaseContextForActivity(r, activity); CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); Configuration config = new Configuration(mCompatConfiguration); // 省去多行无关代码 if (customIntent != null) { activity.mIntent = customIntent; } r.lastNonConfigurationInstances = null; activity.mStartedActivity = false; int theme = r.activityInfo.getThemeResource(); if (theme != 0) { activity.setTheme(theme); } /* * 调用callActivityOnCreate方法处理Create逻辑 */ activity.mCalled = false; mInstrumentation.callActivityOnCreate(activity, r.state); if (!activity.mCalled) { // 省去多行无关代码 } r.activity = activity; r.stopped = true; /* * 调用performStart方法处理Start逻辑 */ if (!r.activity.mFinished) { activity.performStart(); r.stopped = false; } // 省去多行无关代码 } // 省去两行无关代码 } catch (SuperNotCalledException e) { // 省去对异常的捕获处理 } catch (Exception e) { // 省去对异常的捕获处理 } return activity; } // 省去巨量代码…………………… }performLaunchActivity方法中目测木有我我们想要的信息,其创建了Activity并调度了Create和Start的逻辑处理,那我们看看callActivityOnCreate方法呢:
public class Instrumentation { // 省去海量代码………………………… public void callActivityOnCreate(Activity activity, Bundle icicle) { // 省去某些逻辑…… activity.performCreate(icicle); // 省去某些逻辑…… } // 省去巨量代码…………………… }callActivityOnCreate中除了对MQ的一些调度外最重要的还是通过Activity的实例调用了performCreate方法:
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2 { // 省去海量代码………………………… final void performCreate(Bundle icicle) { onCreate(icicle); mVisibleFromClient = !mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, false); mFragments.dispatchActivityCreated(); } // 省去巨量代码…………………… }performCreate方法逻辑就更干脆了,最主要的还是调用了我们Activity的onCreate方法,我们没在这里找到我们想要的东西,那再来看performStart:
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2 { // 省去海量代码………………………… final void performStart() { mFragments.noteStateNotSaved(); mCalled = false; mFragments.execPendingActions(); mInstrumentation.callActivityOnStart(this); if (!mCalled) { throw new SuperNotCalledException( "Activity " + mComponent.toShortString() + " did not call through to super.onStart()"); } mFragments.dispatchStart(); if (mAllLoaderManagers != null) { final int N = mAllLoaderManagers.size(); LoaderManagerImpl loaders[] = new LoaderManagerImpl[N]; for (int i=N-1; i>=0; i--) { loaders[i] = mAllLoaderManagers.valueAt(i); } for (int i=0; i<N; i++) { LoaderManagerImpl lm = loaders[i]; lm.finishRetain(); lm.doReportStart(); } } } // 省去巨量代码…………………… }performStart相对于performCreate有更多的逻辑处理,但依然木有我们想要的结果,其最终还是同过Instrumentation对象调用callActivityOnStart:
public class Instrumentation { // 省去海量代码………………………… public void callActivityOnStart(Activity activity) { activity.onStart(); } // 省去巨量代码…………………… }callActivityOnStart仅仅是调用了Activity的onStart方法,同样……onStart方法中也没有我们想要的结果~~~~我们抱着即将从埃菲尔铁塔顶端做自由落体的心态继续看onResume方法的调度,其在ActivityThread中通过handleResumeActivity调度:
public final class ActivityThread { // 省去海量代码………………………… final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { unscheduleGcIdler(); ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; // 省去无关代码………… final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; 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); } } else if (!willBeVisible) { // 省去无关代码………… r.hideForNow = true; } cleanUpPendingRemoveWindows(r); if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { // 省去无关代码………… performConfigurationChanged(r.activity, r.newConfig); freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig)); r.newConfig = null; } // 省去无关代码………… 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; // 省去无关代码………… Looper.myQueue().addIdleHandler(new Idler()); } r.onlyLocalRequest = false; // 省去与ActivityManager的通信处理 } else { // 省略异常发生时对Activity的处理逻辑 } } // 省去巨量代码…………………… }handleResumeActivity方法逻辑相对要复杂一些,除了一啪啦对当前显示Window的逻辑判断以及没创建的初始化等等工作外其在最终会调用Activity的makeVisible方法:
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2 { // 省去海量代码………………………… void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); } // 省去巨量代码…………………… }在makeVisible方法中逻辑相当简单,获取一个窗口管理器对象并将我们曾在自定义控件其实很简单7/12中提到过的根视图DecorView添加到其中,addView的具体实现在WindowManagerGlobal中:
public final class WindowManagerGlobal { public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { // 省去很多代码 ViewRootImpl root; // 省去一行代码 synchronized (mLock) { // 省去无关代码 root = new ViewRootImpl(view.getContext(), display); // 省去一行代码 // 省去一行代码 mRoots.add(root); // 省去一行代码 } // 省去部分代码 } }在addView生成了一个ViewRootImpl对象并将其保存在了mRoots数组中,每当我们addView一次,就会生成一个ViewRootImpl对象,其实看到这里我们还可以扩展一下问题一个APP是否可以拥有多个根视图呢?答案是肯定的,因为只要我调用了addView方法,我们传入的View参数就可以被认为是一个根视图,但是!在framework的默认实现中有且仅有一个根视图,那就是我们上面makeVisible方法中addView进去的DecorView,所以为什么我们可以说一个APP虽然可以有多个Activity,但是每个Activity只会有一个Window一个DecorView一个ViewRootImpl,看到这里很多童鞋依然会问,也就是说在onResume方法被执行后我们的ViewRootImpl才会被生成对吧,但是为什么下面的代码依然可以正确运行呢:
/** * 主界面 * * @author Aige {@link http://blog.csdn.net/aigestudio} * @since 2014/11/17 */ public class MainActivity extends Activity { private TextView tvText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvText = (TextView) findViewById(R.id.main_tv); } @Override protected void onResume() { super.onResume(); new Thread(new Runnable() { @Override public void run() { tvText.setText("OtherThread"); } }).start(); } }没错,可以执行!首先我们这里的是个线程,其次这里要涉及framework对UI事件处理的方式,我们在Android翻页效果原理实现之引入折线中曾说过Android对UI事件的处理需要依赖于Message Queue,当一个Msg被压入MQ到处理这个过程并非立即的,它需要一段事件,我们在线程中通过Thread.sleep(200)在等,在等什么呢?在等ViewRootImpl的实例对象被创建,有关于GUI中Message Queue的处理如有机会我会浓缩在《深入理解 Android GUI 框架》系列中,这里就暂且先不说了,那么又有同学会问了!纳尼,既然ViewRootImpl还未被创建那么为什么会能绘制出文本?!!!如果你有这个疑问,我只能说你观察细致问得好,但是,这个问题我不打算解答,留给各位,上面我其实就在教大家如何去寻找原因了,渔已授之于你所以就不再多说了~~~~既然我们找到了原因所在,那么我们该如何摆脱“The Android UI toolkit is not thread-safe and the view must always be manipulated on the UI thread.”这个噩梦呢?
为什么我们可以在非UI线程中更新UI