Android为什么不能直接升级,Android 为什么不能再子线程更新UI

作为android开发人员,总是被要求着不能再子线程去更新UI,必须得再主线程更新UI,由于好奇,也由于看这些源码也可以提升自己,就去查了相关资料来学习(本文是自我学习记录的文章,欢迎讨论,若有不对还麻烦指正出来)

先来看看下面的代码

class PracticeActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_practice)

Thread(Runnable {

tv_text.text = "报错"

}).start()

}

}

在onCreate里面,实例化了一个Thread,并在里面进行了更新TextView的操作,按照常理来说,不能再子线程更新UI,那么会不会报错呢?

运行一下

e21883a1d6ed

onCreate里子线程更新UI

并没有报错..这不符合常理啊?再试试在onResume里运行

class PracticeActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_practice)

}

override fun onResume() {

super.onResume()

Thread(Runnable {

tv_text.text = "报错吗"

}).start()

}

}

e21883a1d6ed

onResume里子线程更新UI

啊这..依旧完美运行,难道我们以前报错都是假的吗?我学那么久的android都是白学的?

不信邪,onPause里再试试!

override fun onPause() {

super.onPause()

Thread(Runnable {

tv_text.text = "还不报错?"

}).start()

}

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

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

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)

舒服了,这熟悉的异常,还是那个味道。

那么这是为什么呢?

先说结论

在activity的onResume(包括)之前里的子线程是可以在子线程更新UI的,但是不能用耗时操作去更新,在onResume以后则无法在子线程更新UI(20.11.13 更正:因为activity的页面上的view,是在handleResumeactivity的方法里创建的,抛出 "Only the original thread that created a view hierarchy can touch its views."的原因是当前的Thread和创建view(ViewRootImpl)的Thread不是同一个,所以在activity的onResume之前,view还没创建,所以可以随意修改;因此,若在子线程创建一个视图,然后在主线程修改显示也是会报错的,比如在子线程创建一个dialog,然后在主线程show就会报错,比如在子线程创建一个Toast然后子线程show,也是可以显示且不会保存,不过在子线程创建的时候需要加Looper.prepare()和Looper.loop())。

原因:不能再子线程更新UI的具体表现为,会抛出一个

CalledFromWrongThreadException ("Only the original thread that created a view hierarchy can touch its views.")

这个异常的抛出点在ViewRootImpl 里的(此处是在监测当前的所在的线程是否为创建此view的线程)

void checkThread() {

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

throw new CalledFromWrongThreadException(

"Only the original thread that created a view hierarchy can touch its views.");

}

}

这个ViewRootImpl是在哪里调用这个checkThread()方法的呢?

public void requestLayout() {

if (!mHandlingLayoutInLayoutRequest) {

checkThread();//检查线程

mLayoutRequested = true;

scheduleTraversals();

}

}

public void invalidateChild(View child, Rect dirty) {

invalidateChildInParent(null, dirty);

}

@Override

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

checkThread();//检查线程

···

}

那么这些方法又是在哪里调用了呢?

这得从更新View说起,就拿TextView.setTextView()开始

在TextView.java里的setText方法里

@UnsupportedAppUsage

private void setText(CharSequence text, BufferType type,

boolean notifyBefore, int oldlen) {

···//省略设置文本的方法

if (mLayout != null) { checkForRelayout(); }//接着看这个方法

}

private void checkForRelayout() {

···

if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {

// In a fixed-height view, so use our new text layout.

if (mLayoutParams.height != LayoutParams.WRAP_CONTENT

&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {

autoSizeText();

invalidate();

return;

}

// Dynamic height, but height has stayed the same,

// so use our new text layout.

if (mLayout.getHeight() == oldht

&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {

autoSizeText();

invalidate();

return;

}

}

// We lose: the height has changed and we have a dynamic height.

// Request a new view layout using our new text layout.

requestLayout();

invalidate();

} else {

// Dynamic width, so we have no choice but to request a new

// view layout with a new text layout.

nullLayouts();

requestLayout();

invalidate();

}

}

在checkForRelayout()里无论走哪里的判断,最后都会走invalidate()方法,所以我们先来看看这个方法

public void invalidate(boolean invalidateCache) {

invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);

}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,

boolean fullInvalidate) {

···

// Propagate the damage rectangle to the parent view.

final AttachInfo ai = mAttachInfo;

final ViewParent p = mParent;//ViewParent是一个接口

if (p != null && ai != null && l < r && t < b) {

final Rect damage = ai.mTmpInvalRect;

damage.set(l, t, r, b);

p.invalidateChild(this, damage);//着重看这个方法

}

···

}

这里的p也就是ViewParent,是一个接口类

/**

* Defines the responsibilities for a class that will be a parent of a View.

* This is the API that a view sees when it wants to interact with its parent.

*

*/

public interface ViewParent {

public void requestLayout();

···

public void invalidateChild(View child, Rect r);

}

这个接口主要就是为了当前view和父view进行交互的;那么接着看,这个mParent,到底是谁去实现这个接口的呢?

在View.java里我们找到mParent复制的相关方法。

void assignParent(ViewParent parent) {

if (mParent == null) {

mParent = parent;

} else if (parent == null) {

mParent = null;

} else {

throw new RuntimeException("view " + this + " being added, but"

+ " it already has a parent");

}

}

这里是通过assignParent进行赋值的,那么又是什么时候、谁去赋值的呢?

直接给出答案,ViewRootImpl;那么这个ViewRootImpl又是什么?

/**

* The top of a view hierarchy, implementing the needed protocol between View

* and the WindowManager. This is for the most part an internal implementation

* detail of {@link WindowManagerGlobal}.

*

* {@hide}

*/

@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})

public final class ViewRootImpl implements ViewParent,

View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks

根据头部信息翻译(机翻)一下:

视图层次结构的顶部,实现视图之间所需的协议

和WindowManager。这在很大程度上是一个内部实现

{@link WindowManagerGlobal}的详细信息。

简单的说就是ViewRootImpl实现了View和WindowManager之间的通讯协议(小声BB:这个也是绘制View三大流程的幕后黑手)。

那么这个ViewRootImpl是在哪里初始化呢?

在ActivityThread里的handleResumeActivity()里,简单的说,就是在Activity的onResume的时候。

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,

String reason) {

···

//此处的方法里面调用了Activity.onResume()

final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);

···

if (r.window == null && !a.mFinished && willBeVisible) {

r.window = r.activity.getWindow();

//获取DecorView并添加到PhoneWindow上

View decor = r.window.getDecorView();

decor.setVisibility(View.INVISIBLE);

ViewManager wm = a.getWindowManager();

WindowManager.LayoutParams l = r.window.getAttributes();

a.mDecor = decor;

l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;

l.softInputMode |= forwardBit;

···

if (a.mVisibleFromClient) {

if (!a.mWindowAdded) {

a.mWindowAdded = true;

//添加到WindowManager里

wm.addView(decor, l);

} else {

// The activity will get a callback for this {@link LayoutParams} change

// earlier. However, at that time the decor will not be set (this is set

// in this method), so no action will be taken. This call ensures the

// callback occurs with the decor set.

a.onWindowAttributesChanged(l);

}

}

}

在activity的setContentView时,DecorView 还没有被 WindowManager 正式添加到 Window 中,接着会调用到 ActivityThread 类的 handleResumeActivity 方法将顶层视图 DecorView 添加到 PhoneWindow 窗口,activity 的视图才能被用户看到。(补充知识:在activity.setContentView的时候创建了DecorView,但此时还未将DecorView 于WindowManager关联起来,是在这个流程里进行关联的)

接着看wm,addView()

ViewManager wm = a.getWindowManager();

···

public WindowManager getWindowManager() {

return mWindowManager;

}

mWindowManager = mWindow.getWindowManager();

···

mWindow = new PhoneWindow(this, window, activityConfigCallback);

然后进入PhoneWindow并没有getWindowManager()方法,所以进去父类Window.java查找

public WindowManager getWindowManager() {

return mWindowManager;

}

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,

boolean hardwareAccelerated) {

···

mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);

}

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {

return new WindowManagerImpl(mContext, parentWindow);

}

所以此处也就是最终拿到的WindowManagerImpl,进去WindowManagerImpl看addView方法

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {

applyDefaultToken(params);

mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);

}

最终调用的是WindowManagerGlobal 的addView方法

public void addView(View view, ViewGroup.LayoutParams params,

Display display, Window parentWindow) {

···

ViewRootImpl root;

···

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.

if (index >= 0) {

removeViewLocked(index, true);

}

throw e;

}

}

}

终于!!在此方法里创建了ViewRootImpl,并把相应的View设置到ViewRootImpl 里面去。

然后进入ViewRootImpl的setView方法里

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

···

// Schedule the first layout -before- adding to the window

// manager, to make sure we do the relayout before receiving

// any other events from the system.

requestLayout();

···

view.assignParent(this);//将对应的view关联上相应的ViewRootImpl

···

}

public void requestLayout() {

if (!mHandlingLayoutInLayoutRequest) {

checkThread();

mLayoutRequested = true;

scheduleTraversals();//此方法里会执行view的三大绘制流程:测量、布局、绘制,不过不在本文讨论范围

}

}

View.java

@UnsupportedAppUsage

void assignParent(ViewParent parent) {

if (mParent == null) {

mParent = parent;

} else if (parent == null) {

mParent = null;

} else {

throw new RuntimeException("view " + this + " being added, but"

+ " it already has a parent");

}

}

看到这里,是不是就和前面对应上了。

总结:

1.当View更新重绘时,也就是调用invalidate()的时候回去调用ViewParent的invalidateChild()方法。

2.而这个ViewParent就是在Activity的OnResume的时候通过WindowManager(WindowManagerGlobal )创建的ViewRootImpl。

3.所以在onCreate或者onStrat的时候,通过子线程去更新View是可以的,但是不能做耗时操作(比如sleep了2s,然后在setText,同样会报错,因为ViewParent为null的时候,就不会去调用invalidateChild()方法。

4.在OnResume后,绑定了DecorView,并且为每个view都关联了 相应的ViewRootImpl后,invalidateChild()时就会判断是否在主线程。

5.总的来说,为什么能在onCreate、onStart、onResume里面的子线程里直接进行UI更新,是因为此时还未创建ViewRootImpl,DecorView 还未与WindowManager绑定,所以无法进行ViewRootImpl的checkThread()操作。

6.这些绑定创建流程不都是在resume里发送的吗?为毛onResume也可以在子线程更新?

因为handleResumeActivity里的performResumeActivity()方法先与WindowManager.addView(decor, l)方法...也就是说onResume过后再进行创建ViewRootImpl。

20.11.13 更正:现在发现写文章的时候说法有误,特此更正

因为activity的页面上的view,是在handleResumeactivity的方法里创建的,抛出 "Only the original thread that created a view hierarchy can touch its views."的原因是当前的Thread和创建view(ViewRootImpl)的Thread不是同一个,所以在activity的onResume之前,view还没创建,所以可以随意修改;因此,若在子线程创建一个视图,然后在主线程修改显示也是会报错的,比如在子线程创建一个dialog,然后在主线程show就会报错,比如在子线程创建一个Toast然后子线程show,也是可以显示且不会保存,不过在子线程创建的时候需要加Looper.prepare()和Looper.loop()

//此方式可行,且不会报错

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

Looper.prepare();

Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT).show();

Looper.loop();

}

});

thread.start();

//子线程中调用

public void showDialog(){

new Thread(new Runnable() {

@Override

public void run() {

//创建Looper,MessageQueue

Looper.prepare();

new Handler().post(new Runnable() {

@Override

public void run() {

builder = new AlertDialog.Builder(HandlerActivity.this);

builder.setTitle("子线程");

alertDialog = builder.create();

alertDialog.show();

alertDialog.hide();

}

});

//开始处理消息

Looper.loop();

}

}).start();

}

在子线程中调用showDialog方法,先调用alertDialog.show()方法,再调用alertDialog.hide()方法,hide方法只是将Dialog隐藏,并没有做其他任何操作(没有移除Window),然后再在主线程调用alertDialog.show();便会抛出Only the original thread that created a view hierarchy can touch its views异常了

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

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

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)

本文是自我学习记录的文章,欢迎讨论,若有不对还麻烦指正出来,谢谢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值