作为Android开发者,曾经谨记一条铁律:不能在非UI线程中更新UI! 但是,当有一天写了下面一段代码,并成功执行后,就对曾经的铁律产生了质疑:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
...
new Thread(new Runnable() {
@Override
public void run() {
textView.setText("hello word!");
}
}).start();
}
}
复制代码
如果将上述代码复制到OnResume()
中执行,也可以正确的执行。子线程不能更新ui好像也不是正确的结论,想要弄清楚到底怎么回事,那就需要了解Android系统中View的更新原理。
总览
Android 中 Activity 是作为应用程序的载体存在,代表着一个完整的用户界面,提供了一个窗口来绘制各种视图,想要知道View的更新原理,首先需要对Activity视图的组成有一个宏观的了解。
上图是 View 和 Activity 之间的关系。先解释图中一些类的作用以及相关关系:
- Activity : 对于每一个 activity 都会有拥有一个 PhoneWindow。
- PhoneWindow :该类继承于 Window 类,是 Window 类的具体实现,即我们可以通过该类具体去绘制窗口。并且,该类内部包含了一个 DecorView 对象,该 DectorView 对象是所有应用窗口的根 View。
- DecorView 是一个应用窗口的根容器,它本质上是一个 FrameLayout。DecorView 有唯一一个子 View,它是一个垂直 LinearLayout,包含两个子元素,一个是 TitleView( ActionBar 的容器),另一个是 ContentView(窗口内容的容器)。
- ContentView :是一个 FrameLayout(android.R.id.content),我们平常用的 setContentView 就是设置它的子 View 。
- WindowManager : 是一个接口,里面常用的方法有:添加View,更新View和删除View。主要是用来管理 Window 的。WindowManager 具体的实现类是WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。
- ViewRootImpl: View事件处理的起点,WindowManagerGlobal将所有View的更新等事件全部交由它处理。
- WindowManagerService (WMS) : 负责管理各 app 窗口的创建,更新,删除, 显示顺序。运行在 system_server 进程。
起因
已知View的绘制流程发起点为ViewRootImpl#RequestLayout(),而之所以子线程中不能更新UI的原因,是由于在ViewRootImpl#RequestLayout的时候进行了UI线程检查,因此就有了子线程不能更新UI的说法。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread(); //线程检查
mLayoutRequested = true;
scheduleTraversals();
}
}
复制代码
那么,接下来的问题就是排查ViewRootImpl#RequestLayout()最早被调用的时机。要想知道ViewRootImpl#RequestLayout()最早的调用时机,先要知道ViewRootImpl在什么被创建出来,深入Activity的启动流程,可以知道,View的创建肯定在Window创建之后完成,先来看看Window是什么时候创建的。
Window的创建
当一个Activity启动的时候,必须创建出一个Window来展示视图,当StartActivity的时候,会将启动activity的信息传递给AMS,而AMS会进行一系列的处理逻辑后,通过ActivityThread的performLaunchActivity()
来调用Activity的attach()
方法,而window就是在执行activity的attach()
的时候创建。
#android.app.Activity#attach
final void attach(/**一堆参数*/) {
//...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
//...
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
//...
}
复制代码
可以看到,在Activity创建PhoneWindow的时候同时也创建了WindowManagerService并绑定到PhoneWindow。
在Activity完成了PhoneWindow的创建和绑定了WMS后,ActivityThread就调用了Activity的OnCreate()
那么也就意味着其实在OnCreate()
执行之前,并没有关于View的处理。
DecorView的创建
现在回头来看Activity#OnCreate(),最先执行的就是super.onCreate和setContentView,而setContentView()
以后就可以处理View的逻辑了,那么DecorView的创建和绑定肯定就是在setContentView()中完成的,具体流程如下:
在PhoneWindow的generateDecor()
中创建了DecorView并将PhoneWindow设置给了DecorView。
到目前为止,创建了PhoneWindow并绑定了WMS,并且PhoneWindow中也持有了DecorView。但是,并没有发现还是没有发现ViewRootImpl的创建,那么ViewRootImpl应该是在Activity#onResume的时候创建的,那就去看看Activity#OnResume的调用时机。
ViewRootImpl的创建
Activity的生命周期入口都需要去ActivityThread中寻找,当担OnResume()方法的调用也不例外,它是在ActivityThread#handleResumeActivity中调用的,来看看这个方法:
#android.app.ActivityThread#handleResumeActivity
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
//...
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
//...
if (r.window == null && !a.mFinished && willBeVisible) {
//...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
//...
a.onWindowAttributesChanged(l);
}
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
//...
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward);
ViewRootImpl impl = r.window.getDecorView().getViewRootImpl();
WindowManager.LayoutParams l = impl != null
? impl.mWindowAttributes : r.window.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
!= forwardBit) {
l.softInputMode = (l.softInputMode
& (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
| forwardBit;
if (r.activity.mVisibleFromClient) {
ViewManager wm = a.getWindowManager();
View decor = r.window.getDecorView();
wm.updateViewLayout(decor, l);
}
}
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
//...
}
复制代码
在上述代码中,可以看到performResumeActivity()
函数会被调用,我们也知道这个函数最终会调用到Activity#onResume中,当它执行完成后,下面有一段很重要的code:
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
复制代码
这里有一个decor被添加到了wm中,wm其实就是WindowManagerImpl,而它的是WindowManagerGlobal的包装类,WindowManagerGlobal完成了WindowManagerImpl的所有的方法调用。比如addView、updateViewLayout。
#android.view.WindowManagerGlobal
public void addView(/**一堆参数*/) {
//...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
//...
}
复制代码
可以看到,ViewRootImpl被创建出来了。而且在ActivityThread#handleResumeActivity中可以看到,在wm.addView()
之后,就调用了wm.updateViewLayout()
。后面调用如下图:
graph TD
WindowManagerImpl.updateViewLayout --> WindowManagerGolbal.updateViewLayout --> ViewRootImpl.setLayoutParams --> ViewRootImpl.requestLayout
找到了RequestLayout也就意味着View的绘制流程就要开始了,那么也就会有线程检查的逻辑,下面来梳理一下:
- Activity#onCreate执行时,window已经被创建了,也成功绑定了WindowManagerService,此时还未创建DecorView
- Activity#setContentView执行后,decorView被创建,可以处理View的逻辑了并且没有线程检查。
- Activity#OnResume执行时,ViewRootImpl也没有创建,也就没有线程检查,那么在子线程中处理View的逻辑也没有问题
- Activity#OnResume执行后,ViewRootImpl已经被创建了,所以此时如果在子线程中更新View就会有线程检查,必然报错
现在应该已经明白了为什么子线程可以更新View,为什么不能更新View的原理了,借助这个启动优化是不是可以安排了呢?
OnResume中Measure View宽高不准确原理
在了解了子线程中能更新Ui的原理后,想着按照理解,onResume()
中应该是不能获取View的宽高等数据的。那为什么下面代码是可以正确获取View的宽高呢?
@Override
protected void onResume() {
super.onResume();
// 不能
int h1 = textView.getHeight();
// 不能
new Handler().post(new Runnable() {
@Override
public void run() {
int h2 = textView.getHeight();
}
});
// 能
textView.post(new Runnable() {
@Override
public void run() {
int h3 = textView.getHeight();
}
});
}
复制代码
有了上面子线程更行UI的理论知识后,可以很轻松的明白h1为什么不能获取到View的高度了,这里在啰嗦一点,在ViewRootImpl#RequestLayout的时候,就会很快执行View的三大方法:
h2的获取是通过Handler的方式来获取,那么也就意味着post了一个meassage给UI线程中的Looper去处理了。在Android中,Handler消息都是以meassage为处理单元,并且Activity/Fragment等组件的生命周期也是通过meassage的方式发送给UI线程来处理的。
那么问题来了,既然OnResume是某个meassage执行中的一部分逻辑,那么在OnResume的时候post一个meassage就需要当前meassage执行完成,才能执行被post的meassage,按照子线程更新UI的原理来看的话,在OnResume所在的这个meassage中会进行View的第一次RequestLayout,那么在后面的meassage中获取View的高度应该没有问题,但是,结果确不对,难道是这两个meassage的执行顺序发生了变化?确实如此!
# android.view.ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 同步消息屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
复制代码
通过上述代码,可以看到,View的真正执行时通过mChoreographer发送了一个Runable来执行的,Choreographer的原理可以看:android 图形系统之Choreographer,也就是说此meassage已经处理完成了,等到Choreographer有Sync信号过来之后再执行requestLayout的真正逻辑,那么也就意味着刚才post的那个meassage在requestLayout之前被执行了。虽然在等待Choreographer有Sync信号前发送了一个Handler的同步消息屏障的消息,但是奈何消息屏障的meassage是在post的消息之后执行。
下面看view.post为什么可以获取到正确的高度:
textView.post(new Runnable() {
@Override
public void run() {
int h3 = textView.getHeight();
}
});
复制代码
简单来说:这种方式能获取view的高度是因为:View以Handler为基础,View.post() 将传入任务的执行时机调整到View 绘制完成之后,下面来看代码:
public boolean post(Runnable action) {
// ...
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
复制代码
通过上述代码,可以看到,View将post分为了两个支线:
- 当AttachInfo不为null时,直接调用其内部Handler的post;
- 当AttachInfo为null时,则将任务加入当前View的等待队列中。
AttachInfo不为null dispatchAttachedToWindow()调用时机是在 View 绘制流程的开始阶段,即 ViewRootImpl.performTraversals(),暂时不做讨论。 AttachInfo为null onResume中view.post主要是AttachInfo为null的情况:
# HandlerActionQueue.post()
public void post(Runnable action) {
// ...
postDelayed(action, 0);
}
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
private static class HandlerAction {
final Runnable action;
final long delay;
public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}
// ...
}
复制代码
可以看到,View.post会将Runnable封装成一个HandlerAction对象,然后创建一个默认长度为4的HandlerAction数组,用于保存post过来的任务。当View被真正attach到window上以后,就会执行post过来的Runnable,那么在onResume中view.post可以获取View的正确高度也就明白了。
上述一些查看源码后梳理,欢迎大家指正讨论!!!