配色: 字号:
Android中子线程真的不能更新UI吗?
2016-10-20 | 阅:  转:  |  分享 
  
Android中子线程真的不能更新UI吗?

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



但是有没有极端的情况?使得我们在子线程中访问UI也可以使程序跑起来呢?接下来我们用一个例子去证实一下。



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






android:layout_width="match_parent"

android:layout_height="match_parent"

>




android:id="@+id/main_tv"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textSize="18sp"

android:layout_centerInParent="true"

/>







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



MainActivity代码如下所示:



publicclassMainActivityextendsAppCompatActivity{



privateTextViewmain_tv;



@Override

protectedvoidonCreate(BundlesavedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);



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



newThread(newRunnable(){



@Override

publicvoidrun(){

main_tv.setText("子线程中访问");

}

}).start();



}



}



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



点击运行。你会发现即使在子线程中访问UI,程序一样能跑起来。结果如下所示:



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



先不急,这是一个极端的情况,修改MainActivity如下:



publicclassMainActivityextendsAppCompatActivity{



privateTextViewmain_tv;



@Override

protectedvoidonCreate(BundlesavedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);



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



newThread(newRunnable(){



@Override

publicvoidrun(){

try{

Thread.sleep(200);

}catch(InterruptedExceptione){

e.printStackTrace();

}

main_tv.setText("子线程中访问");

}

}).start();



}



}



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



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



android.view.ViewRootImpl$CalledFromWrongThreadException:Onlytheoriginalthreadthatcreatedaviewhierarchycantouchitsviews.

atandroid.view.ViewRootImpl.checkThread(ViewRootImpl.Java:6581)

atandroid.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)



……



作为一名开发者,我们应该认真阅读一下这些异常信息,是可以根据这些异常信息来找到为什么一开始的那种情况可以访问UI的。那我们分析一下异常信息:



首先,从以下异常信息可以知道



atandroid.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)



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



那现在跟进ViewRootImpl的checkThread方法瞧瞧,源码如下:



voidcheckThread(){

if(mThread!=Thread.currentThread()){

thrownewCalledFromWrongThreadException(

"Onlytheoriginalthreadthatcreatedaviewhierarchycantouchitsviews.");

}

}



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



由此我们可以得出结论:

在访问UI的时候,ViewRootImpl会去检查当前是哪个线程访问的UI,如果不是主线程,那就会抛出如下异常:



Onlytheoriginalthreadthatcreatedaviewhierarchycantouchitsviews



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



atandroid.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)



那现在就看看requestLayout方法,



@Override

publicvoidrequestLayout(){

if(!mHandlingLayoutInLayoutRequest){

checkThread();

mLayoutRequested=true;

scheduleTraversals();

}

}



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



voidscheduleTraversals(){

if(!mTraversalScheduled){

mTraversalScheduled=true;

mTraversalBarrier=mHandler.getLooper().getQueue().postSyncBarrier();

mChoreographer.postCallback(

Choreographer.CALLBACK_TRAVERSAL,mTraversalRunnable,null);

if(!mUnbufferedInputDispatch){

scheduleConsumeBatchedInput();

}

notifyRendererOfFramePending();

pokeDrawLockIfNeeded();

}

}



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



finalclassTraversalRunnableimplementsRunnable{

@Override

publicvoidrun(){

doTraversal();

}

}



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



voiddoTraversal(){

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方法,如下:



finalvoidhandleResumeActivity(IBindertoken,

booleanclearHide,booleanisForward,booleanreallyResume){

//Ifwearegettingreadytogcaftergoingtothebackground,well

//wearebackactivesoskipit.

unscheduleGcIdler();

mSomeActivitiesChanged=true;



//TODOPushresumeArgsintotheactivityforconsideration

ActivityClientRecordr=performResumeActivity(token,clearHide);



if(r!=null){

finalActivitya=r.activity;



//代码省略



r.activity.mVisibleFromServer=true;

mNumVisibleActivities++;

if(r.activity.mVisibleFromClient){

r.activity.makeVisible();

}

}



//代码省略

}



可以看到内部调用了performResumeActivity方法,这个方法看名字肯定是回调onResume方法的入口的,那么我们还是跟进去瞧瞧。



publicfinalActivityClientRecordperformResumeActivity(IBindertoken,

booleanclewww.shanxiwang.netarHide){

ActivityClientRecordr=mActivities.get(token);

if(localLOGV)Slog.v(TAG,"Performingresumeof"+r

+"finished="+r.activity.mFinished);

if(r!=null&&!r.activity.mFinished){

//代码省略

r.activity.performResume();



//代码省略



returnr;

}



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



finalvoidperformResume(){

performRestart();



mFragments.execPendingActions();



mLastNonConfigurationInstances=null;



mCalled=false;

//mResumedissetbytheinstrumentation

mInstrumentation.callActivityOnResume(this);



//代码省略



}



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



publicvoidcallActivityOnResume(Activityactivity){

activity.mResumed=true;

activity.onResume();



if(mActivityMonitors!=null){

synchronized(mSync){

finalintN=mActivityMonitors.size();

for(inti=0;i
finalActivityMonitoram=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方法,这应该是让什么显示的吧,跟进去探探。



voidmakeVisible(){

if(!mWindowAdded){

ViewManagerwm=getWindowManager();

wm.addView(mDecor,getWindow().getAttributes());

mWindowAdded=true;

}

mDecor.setVisibility(View.VISIBLE);

}



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



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



@Override

publicvoidaddView(@NonNullViewview,@NonNullViewGroup.LayoutParamsparams){

applyDefaultToken(params);

mGlobal.addView(view,params,mDisplay,mParentWindow);

}



里面调用了WindowManagerGlobal的addView方法,那现在就锁定

WindowManagerGlobal的addView方法:



publicvoidaddView(Viewview,ViewGroup.LayoutParamsparams,

Displaydisplay,WindowparentWindow){



//代码省略





ViewRootImplroot;

ViewpanelParentView=null;



//代码省略



root=newViewRootImpl(view.getContext(),display);



view.setLayoutParams(wparams);



mViews.add(view);

mRoots.add(root);

mParams.add(wparams);

}



//dothislastbecauseitfiresoffmessagestostartdoingthings

try{

root.setView(view,wparams,panelParentView);

}catch(RuntimeExceptione){

//BadTokenExceptionorInvalidDisplayException,cleanup.

synchronized(mLock){

finalintindex=findViewLocked(view,false);

if(index>=0){

removeViewLocked(index,true);

}

}

throwe;

}

}



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



回顾前面的分析,总结一下:

ViewRootImpl的创建在onResume方法回调之后,而我们一开篇是在onCreate方法中创建了子线程并访问UI,在那个时刻,ViewRootImpl是没有创建的,无法检测当前线程是否是UI线程,所以程序没有崩溃一样能跑起来,而之后修改了程序,让线程休眠了200毫秒后,程序就崩了。很明显200毫秒后ViewRootImpl已经创建了,可以执行checkThread方法检查当前线程。



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



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

献花(0)
+1
(本文系网络学习天...首藏)