果真起名才是码农最大的考验,躲过了撸码的变量起名,却绕不开博客名重复率高。言归正传,关于子线程更新UI的文章,网上资料已经不少了,但仍是总结了一下。受篇幅所限,也许你们根本没有耐心看完,这里就先说结论吧java
不可免俗的要举个子线程更新UI的例子,经过在在onCreate方法中建立了一个子线程,并进行UI访问操做android
public class MainActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.main_tv); new Thread(new Runnable() { @Override public void run() { //SystemClock.sleep(3000); textView.setText("子线程中访问"); } }).start(); } }
此时成功的在子线程更新了UI,并不会报异常。可是加上注释的代码,让子线程休眠一段时间,再去进行UI访问操做,结果会报以下异常:web
Only the original thread that created a view hierarchy can touch its views 简单的直译就是只有建立这个view的线程,才能触摸(访问)这个View。(并无说必定要在主线程才能更新UI哦)app
... android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7286) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1155) ...
根据异常日志信息,咱们大概能够问题出在ViewRootImpl的checkThread()方法:经过检查Thread类型的mThread变量是否等于当前线程,若是不等于就会报异常。也就是说mThread 和 Thread.currentThread()是同一个线程就不会报错。(仍是没有说必定要在主线程才能更新UI哦)ide
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
再看看前面异常日志中有requestLayout方法,再次走进ViewRootImpl类svg
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
前面子线程没有sleep的时候,checkThread()并无抛异常,那就接着点进scheduleTraversals()oop
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
postCallback方法第二个参数mTraversalRunnable是Runnable类型post
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
进入doTraversal()方法this
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } //关键代码 performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
一路追踪到performTraversal()方法,而View的绘制绘制就是从ViewRootImpl的performTraversal()开始的,如今咱们知道了,每一次访问了UI,Android都会从新绘制View。spa
为了更好的进行后面的分析,咱们须要知道关于View绘制的一些知识,View的绘制是由ViewRoot其实是ViewRootImpl来负责的。每一个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。那么decorView与ViewRoot的关联关系是在何时创建的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中创建了它们二者的关联关系。
首先,咱们要简单了解下Activity的建立过程(不太清楚的自行百度):
在ActivityThread#handleLaunchActivity中启动Activity,在这里面会调用到Activity#onCreate方法,里边会有SetContentView()过程,从而完成上面所述的DecorView建立动做,结合前面的例子,子线程没有休眠的时候,在子线程更新UI并不会报错,也就是说没有调用ViewRootImpl的checkThread()方法,说明此时ViewRootImpl尚未实例化。那就继续日后分析
当onCreate()方法执行完毕,在handleLaunchActivity方法会继续调用到ActivityThread#handleResumeActivity方法
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { ... //须要关注的代码 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; }
进入performanceResume()方法:
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(); ... }
看到activity.onResume()。这也证明了performResumeActivity方法确实是回调onResume方法的入口。
那么如今咱们看回来handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { ... //注释1 ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; ... r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) { //注释2 r.activity.makeVisible(); } } ... }
执行注释2处r.activity.makeVisible(); 顾名思义还像是显示Activity的意思,进一步跟进:
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; ... //注释1 root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); } ... }
终于找到了ViewRootImpl实例化的地方,在看看ViewRootImpl的构造方法
public ViewRootImpl(Context context, Display display) { ... mThread = Thread.currentThread(); ... }
一切谜团都解开了,ViewRootImpl是在WindowManagerGlobal的addView方法中建立的,ViewRootImpl的建立在onResume方法回调以后,而咱们一开篇是在onCreate方法中建立了子线程并访问UI,在那个时刻,ViewRootImpl是没有建立的,没法检测当前线程是不是UI线程,因此程序没有崩溃同样能跑起来,而以后修改了程序,让线程休眠了一段时间后,程序就崩了。很明显休眠后ViewRootImpl已经建立了,能够执行checkThread方法进行线程检查。
可是可是可是,重要的事情说三遍,从头至尾也没有发现哪里规定必定要在主线程更新UI,若是ViewRootImpl尚未实例化,是能够在子线程更新UI,这种情形分析意义不大,重要的是若是ViewRootImpl已经实例化了,还能不能在子线程更新UI呢?答案是能的,由于线程检查合规的条件只是,进行UI的线程和ViewRootImpl实例化的线程是同一个线程就好了,也就是说,若是ViewRootImpl是在子线程实例化的,那么咱们彻底能够在子线程进行UI操做。下面就进行代码验证
仍是前面的例子,在子线程休眠一段时间后,确保已经经过checkThread()方法进行线程检查了,看此次在子线程更新UI会不会抛异常,而后再点击button,在主线程更新UI,又会是什么结果,代码以下
public class MainActivity extends AppCompatActivity { private Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button= (Button) findViewById(R.id.main_btn); //真正意义上的在子线程更新UI new Thread(new Runnable() { @Override public void run() { //休眠一段时间,确保经过checkThread()进行线程检查 SystemClock.sleep(3000); Looper.prepare(); TextView tx = new TextView(MainActivity.this); tx.setText("子线程更新UI"); tx.setTextColor(Color.RED); WindowManager windowManager = MainActivity.this.getWindowManager(); WindowManager.LayoutParams params = new WindowManager.LayoutParams( 500, 500, 50, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW, WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE); windowManager.addView(tx, params); Looper.loop(); } }).start(); } /** * button的点击事件,在主线程更新UI */ public void testUpdateUi(View view) { tx.setText("主线程更新UI"); } }
效果以下:
异常信息以下,相信本身的眼睛,在主线程更新UI的确也会报一样的异常。
... Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. ...
显然,windowManager.addView(xxx)方法在子线程调用,也就是说ViewRootImpl在子线程实例化,此时在子线程更新UI是彻底没有问题的。反而点击button,在主线程更新UI抛异常了。若是你耐心看到这里,就应该明白了,主线程和UI线程是两个概念,对于button而言,UI线程是主线程,对于TextView tv 而言,UI线程是子线程。源码告诉咱们,ViewRootImpl建立的线程和操做UI在同一个线程就没有问题,因此UI线程就是ViewRootImpl实例化时所在的线程。