还是先来一张图片,庆祝一下19-20赛季的湖人总冠军
吧!!
Android中的子线程能否操作UI
现在进入正题,想想从开始工作到现在,被无数次的告诫子线程不能更新UI,UI操作必须在主线程中完成
。然而作为辣鸡代码搬运工的我,怎么能轻易就听你们的呢。
public class MainActivity extends AppCompatActivity {
private TextView mTvTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvTest = findViewById(R.id.tv_test);
new Thread(new Runnable() {
@Override
public void run() {
mTvTest.setText("子线程修改UI成功");
}
}).start();
}
}
一番输出,run
起来,完全没有问题啊,Activity上的TextView
上面的的文字不也就更新了啊。
但是,在我们的实际开发中,当我们开启一个新的线程耗时操作之后,直接更新UI,就会出现问题。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.tv);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
tv.setText("子线程修改UI成功");
}
}).start();
}
瞬间就崩溃了
Process: cn.qiaowa.testapplication, PID: 21102
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9219)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1600)
抛出了一个Only the original thread that created a view hierarchy can touch its views.
的异常,告诉我们只能在主线程中操作UI。
从这两段代码,我们可以清楚的得出一个结论Android中子线程是可以操作UI的
,但是为什么第二段代码中,我们子线程中操作UI的时候,有会抛出异常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.
根据Log日志,我们可以知道这个异常是在 android.view.ViewRootImpl
这个类中抛出来的。定位一下源码
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
继续追踪一下ViewRootImpl#checkThread()
这个方法的调用地方
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
看到这里requestLayout ()
方法,就是View里面请求重新测量布局的方法。
再看看ViewPootImpl
的类继承关系
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
}
ViewPootImpl
实现了一个ViewParent
接口,而requestLayout ()
方法就是继承于这个地方。
public interface ViewParent {
// 去除了其他代码
/**
* Called when something has changed which has invalidated the layout of a
* child of this view parent. This will schedule a layout pass of the view
* tree.
*/
public void requestLayout();
}
到这里我们已经研究出这个异常的大致流程了,但是这个异常是从那个地方出发的呢?换句话说就requestLayout()
这个方法是怎么调用的呢?我们探究。
requestLayout()
这个方法是View里面请求重新测量布局的。那么我们就回到TextView.setText(...)
中看看。
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
//代码省略
if (mLayout != null) {
//关键代码所在
checkForRelayout();
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
} else {
notifyAutoFillManagerAfterTextChangedIfNeeded();
}
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
if (mEditor != null) mEditor.prepareCursorControllers();
}
再看看checkForRelayout ()
/**
* Check whether entirely new text requires a new view layout
* or merely a new text layout.
*/
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
//代码省略
// 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();
}
}
这里不管是走if
还是else
,都会走TextView#requestLayout()
方法,那我们再看看这个TextView#requestLayout()
public void requestLayout() {
//代码省略
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
最终它会调用mParent.requestLayout();
这个mParent
是一个ViewParent
接口类,而在开始我们分析ViewRootImpl
时候我们就知道了它就实现了ViewParent
接口,所以通过TextView.setText(...)
最终就会调用到ViewRootImpl #RequestLayout
方法。
再回到setText()
的源码中的
if (mLayout != null) {
checkForRelayout();
}
如果mLayout
不为空的时候才会进行线程的检测,这个时候如果再子线程操作UI的时候必然会抛出异常。想一下,如果这个mLayout
位空的话,不就可以绕开线程的校验了么(第一个事例子线程更新UI的事例就是这种情况),就不会抛出异常了。
走到这里我们可以得到一个更加准确的结论:Android中子线程是可以更新UI的,当mLayout还没有初始化完成的时候,子线程更新UI不会抛出异常。而在mLayout初始化完成之后,更新UI就会进行线程校验,如果是在当前线程是子线程就会抛出异常,告诉我们只能在主线程中更新UI
由此可见,子线程操作UI的时候,是否抛出异常的关键就是这个mLayout
的初始化情况了,那我们继续研究一下mLayout
的初始化情况。
mLayout 的初始化
从开始的两个事例中发现唯一不同的是,第二段代码让子线程sleep了3秒后才去更新UI的,结果就抛出了异常,也就是mLayout
已经初始化完成了。可以知道这个肯定是个生命周期有一定的关系。那么先看看ActivityThread
这个类。
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
//代码省略
// TODO Push resumeArgs into the activity for consideration
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
//代码省略
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
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 (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
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);
}
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
//代码省略
}
关键代码wm.addView(decor, l)
,这个就是将 decorView 添加到 window中。我们在看一下 WindowManager的实现类 WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
发现此时,addView操作交给了mGlobal这个代理类来处理,而这个代理类正是WindowManagerGlobal
,那我们继续看看
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类,然后调用setView方法,
view.assignParent(this);
在setView放里面调用了assignParent方法,将ViewRootImpl传入到View当中。
由此,可以看出在activity 的onResume生命周期中进行了 mLayout
的初始化,从初始化完成之后,操作UI就要进行线程的检测。