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,那么会不会报错呢?
运行一下
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()
}
}
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)
本文是自我学习记录的文章,欢迎讨论,若有不对还麻烦指正出来,谢谢~