Android有条铁则:子线程不能修改UI 至于为什么 ,就是修改了会报错呗
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
newThread(newRunnable() {
@Override
public 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-2857
Process: com.example.exile.exiledemo, PID: 24916
android.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版本及主题有关),上面是标题栏,下面是内容栏。
仔细看这个方法:
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)
@Override
public voidrequestLayout() {
if(!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested=true;
scheduleTraversals();
}
}
at android.view.View.requestLayout(View.java:20170)
是因为view调用了requestLayout的方法导致了
@CallSuper
public 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 hierarchy
ViewRootImpl 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方法
@Override
public 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 {
@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时,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 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
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方法,这应该是让什么显示的吧,跟进去探探。
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方法,如下:
@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方法检查当前线程。
4策略和考虑
首先UI线程(mainThread)并不是线程安全的,这样如果子线程修改UI容易数据错乱,如果做到线程安全的话,这样做是很低效的。
其次谷歌推荐如果子线程需要修改UI可以使用handler,这样的队列设也是考虑到并发,效率的体现。
为什么 android 会设计成只有创建 ViewRootImpl 的原始线程才能更改 ui 呢?这就要说到 Android 的单线程模型了,因为如果支持多线程修改 View 的话,由此产生的线程同步和线程安全问题将是非常繁琐的,所以 Android 直接就定死了,View 的操作必须在创建它的 UI 线程,从而简化了系统设计。
有没有可以在其他非原始线程更新 ui 的情况呢?有,SurfaceView 就可以在其他线程更新,具体的大家可以去网上了解一下相关资料。