Android有条铁则:子线程不能修改UI 至于为什么 ,就是修改了会报错呗

@Overrideprotected voidonCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);newThread(newRunnable() {@Overridepublic voidrun() {try{Thread.sleep(1000);}catch(InterruptedException e) {e.printStackTrace();}((TextView) findViewById(R.id.tv_hello)).setText("longlong");findViewById(R.id.tv_hello).setOnClickListener(MainActivity.this);}}).start();}

具体错误如下

3-23 14:24:59.871 24916-24991/com.example.exile.exiledemo E/AndroidRuntime: FATAL EXCEPTION: Thread-2857Process: com.example.exile.exiledemo, PID: 24916android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8358)at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1327)at android.view.View.requestLayout(View.java:20170)···at java.lang.Thread.run(Thread.java:818)

但是令人称奇的是:如果我把sleep去掉 竟然是可以更新的并不会报错,这就勾起了我的好奇心,我想进一步深入源码的认识下一到底为什么这时候可以更新,为甚延时之后又不能更新了,

为什么修改UI会报错:(在哪里检查的报错)

为什么没有延时的时候在子线程又可以修改了:(什么时候开始执行了检查方法)

为什么延时后修改又报错了:(在什么时候生效了)

Android这样不允许在子线程修改UI有什么考虑,和好处:

1:

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

报错在这里:

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

这里顺便铺垫一个知识点:

ViewRootImpl,ViewRoot和View

ViewRoot对应ViewViewRootImpl类,它是连接WindowManager和DecorView的纽带,view的三大流程(measure,layout,draw)均是通过ViewRoot来完成的,ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象并和DecorView建立联系。DecorView作为顶级的View一般情况下它内部会包含一个竖向的LinearLayout,这个LinearLayout里面有上下两个部分(具体情况和Android版本及主题有关),上面是标题栏,下面是内容栏。

Android中为什么在子线程不能更新UI_第1张图片 Paste_Image.png

仔细看这个方法:

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

这句异常写在这里 但是值得注意的是 这里只是把当前的线程和ViewRootImpl创建的线程进行了对比,意思很明显ViewRootImpl创建在哪个线程中之后的更新UI操作就要在哪个线程。只是通常情况下,它是在UI线程中被创建。

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1327)
@Overridepublic voidrequestLayout() {if(!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested=true;scheduleTraversals();}}
at android.view.View.requestLayout(View.java:20170)

是因为view调用了requestLayout的方法导致了

@CallSuperpublic voidrequestLayout() {if(mMeasureCache!=null)mMeasureCache.clear();if(mAttachInfo!=null&&mAttachInfo.mViewRequestingLayout==null) {// Only trigger request-during-layout logic if this is the view requesting it,// not the views in its parent hierarchyViewRootImpl viewRoot = getViewRootImpl();if(viewRoot !=null&& viewRoot.isInLayout()) {if(!viewRoot.requestLayoutDuringLayout(this)) {return;}}mAttachInfo.mViewRequestingLayout=this;}mPrivateFlags|=PFLAG_FORCE_LAYOUT;mPrivateFlags|=PFLAG_INVALIDATED;if(mParent!=null&& !mParent.isLayoutRequested()) {mParent.requestLayout();}if(mAttachInfo!=null&&mAttachInfo.mViewRequestingLayout==this) {mAttachInfo.mViewRequestingLayout=null;}}

这里是子view调用了parentView的requestLayout方法

重点来了 既然知道 viewrootimpl是viewroot的实现类,这就说明viewrootimpl其实是最顶层view的实现类(但是ViewRootImpl并不是View而是DecorView的Parent引用) 并且 viewrootimpl实现了ViewParent接口

/*** The top of a view hierarchy(view的顶层), implementing the needed protocol between View* and the WindowManager.  This is for the most part an internal implementation* detail of {@linkWindowManagerGlobal}.** {@hide}*/@SuppressWarnings({"EmptyCatchBlock","PointlessBooleanExpression"})public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks,ThreadedRenderer.HardwareDrawCallbacks {

这就说明最终会被调用的其实就是

ViewRootImpl的requestLayout方法,当然啊错误日志也给出了这样的关系,

现在让我们仔细看ViewRootImpl的requestLayout方法

@Overridepublic voidrequestLayout() {if(!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested=true;scheduleTraversals();}}

每次requestlayout的时候 都会调用checkThread方法,来检查线程

先看下requestLayout都干了什么吧,1 检查线程,2 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 {@Overridepublic 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时,ViewRoot会调用checkThread方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在MainActivity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢??

唯一的解释就是执行onCreate方法的那个时候ViewRootImpl还没创建,无法去检查当前线程。

那么就可以这样深入进去。寻找ViewRootImpl是在哪里,是什么时候创建的。好,继续前进

这里省略Avtivity的创建过程(在每个生命周期都做了什么,生命周期怎么被调用的,Activity是怎么被创建的)

在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 considerationActivityClientRecord 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 instrumentationmInstrumentation.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; ifinal 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方法,这应该是让什么显示的吧,跟进去探探。

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

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

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

@Overridepublic 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 thingstry {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方法检查当前线程。

4策略和考虑

首先UI线程(mainThread)并不是线程安全的,这样如果子线程修改UI容易数据错乱,如果做到线程安全的话,这样做是很低效的。

其次谷歌推荐如果子线程需要修改UI可以使用handler,这样的队列设也是考虑到并发,效率的体现。

为什么 android 会设计成只有创建 ViewRootImpl 的原始线程才能更改 ui 呢?这就要说到 Android 的单线程模型了,因为如果支持多线程修改 View 的话,由此产生的线程同步和线程安全问题将是非常繁琐的,所以 Android 直接就定死了,View 的操作必须在创建它的 UI 线程,从而简化了系统设计。

有没有可以在其他非原始线程更新 ui 的情况呢?有,SurfaceView 就可以在其他线程更新,具体的大家可以去网上了解一下相关资料。

参考资料:http://blog.csdn.net/xyh269/article/details/52728861s

更多相关文章

  1. 通过ddmlib杀死某个android进程的方法
  2. Android一些常用知识和代码(不断更新)
  3. Android之 inflate() 方法总结
  4. Android读取服务器图片的三种方法
  5. android播放本地工程里的音乐和视频的方法
  6. ActionBar的一些使用方法
  7. H5调用原生APP的js方法

随机推荐

  1. android 单元测试
  2. Android(安卓)GPS Location学习一
  3. android 调用系统相册并得到图片地址
  4. android 颜色叠加
  5. Android(安卓)Http 与断点续传
  6. 只有安卓才会跳
  7. android 生成验证码图片
  8. android.net.wifi.p2p package API
  9. Android(安卓)动态设置控件高度
  10. android 图表引擎