Android中为什么在子线程不能更新UI

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版本及主题有关),上面是标题栏,下面是内容栏。

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)
@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 就可以在其他线程更新,具体的大家可以去网上了解一下相关资料。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容