UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识、如何优化 UI 渲染两部分内容。
UI 优化系列专题
- UI 渲染背景知识
《View 绘制流程之 setContentView() 到底做了什么?》
《View 绘制流程之 DecorView 添加至窗口的过程》
《深入 Activity 三部曲(3)View 绘制流程》
《Android 之 LayoutInflater 全面解析》
《关于渲染,你需要了解什么?》
《Android 之 Choreographer 详细分析》
- 如何优化 UI 渲染
《Android 之如何优化 UI 渲染(上)》
《Android 之如何优化 UI 渲染(下)》
关于 View.post() 相信每个 Android 开发人员都不会感到陌生,它最常见的场景主要有两种。
更新 UI 操作
获取 View 的实际宽高
view.post() 的内部也是调用了 Handler,这可能是绝大多数开发人员所了解的,从本质来说这样理解并没有错,不过它并能解释上面提出的第 2 个场景。
在 Activity 中,View 绘制流程的开始时机是在 ActivityThread 的 handleResumeActivity 方法,在该方法首先完成 Activity 生命周期 onResume 方法回调,然后开始 View 绘制任务。也就是说 View 绘制流程要在 onResume 方法之后,但是我们绝大部分业务是在 onCreate 方法,比如要获取某个 View 的实际宽高,由于 View 的绘制任务还未开始,所以就无法正确获取。具体可以参考《View 绘制流程之 setContentView() 到底做了什么 ?》
此时大家肯定使用过 View.post() 来解决该问题,注意 View 绘制流程也是向 Handler 添加任务,如果在 onCreate 方法直接使用 Handler.post(),则该任务一定在 View 绘制任务之前(同一个线程队列机制)。
- 注意这里不考虑使用 ViewTreeObserver 或更长延迟的 postDelayed()。
那 View.post() 内部也是使用 Handler,它是如何实现的呢?简单来说,View.post() 对任务的运行时机做了调整。
View.post()
翻开 View 源码,找到 View 的 post 方法如下:
public boolean post(Runnable action) {
// 首先判断AttachInfo是否为null
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// 如果不为null,直接调用其内部Handler的post
return attachInfo.mHandler.post(action);
}
// 否则加入当前View的等待队列
getRunQueue().post(action);
return true;
}
注意 AttachInfo 是 View 的静态内部类,每个 View 都会持有一个 AttachInfo,它默认为 null;需要先来看下 getRunQueue().post():
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
getRunQueue() 返回的是 HandlerActionQueue,也就是调用了 HandlerActionQueue 的 post 方法:
public void post(Runnable action) {
// 调用到postDelayed方法,这有点类似于Handler发送消息
postDelayed(action, 0);
}
// 实际调用postDelayed
public void postDelayed(Runnable action, long delayMillis) {
// HandlerAction表示要执行的任务
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
// 创建一个保存HandlerAction的数组
mActions = new HandlerAction[4];
}
// 表示要执行的任务HandlerAction 保存在 mActions 数组中
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
// mActions数组下标位置累加1
mCount++;
}
}
HandlerAction 表示一个待执行的任务,内部持有要执行的 Runnable 和延迟时间;类声明如下:
private static class HandlerAction {
// post的任务
final Runnable action;
// 延迟时间
final long delay;
public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}
// 比较是否是同一个任务
// 用于匹配某个 Runnable 和对应的HandlerAction
public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}
注意 postDelayed() 创建一个默认长度为 4 的 HandlerAction 数组,用于保存 post() 添加的任务;跟踪到这,大家是否有这样的疑惑:View.post() 添加的任务没有被执行?
实际上,此时我们要回过头来,重新看下 AttachInfo 的创建过程,先看下它的构造方法:
AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
mSession = session;
mWindow = window;
mWindowToken = window.asBinder();
mDisplay = display;
// 持有当前ViewRootImpl
mViewRootImpl = viewRootImpl;
// 当前渲染线程Handler
mHandler = handler;
mRootCallbacks = effectPlayer;
// 为其创建一个ViewTreeObserver
mTreeObserver = new ViewTreeObserver(context);
}
注意 AttachInfo 中持有当前线程的 Handler。翻阅 View 源码,发现仅有两处对 mAttachInfor 赋值操作,一处是为其赋值,另一处是将其置为 null。
- mAttachInfo 赋值过程:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
// 给当前View赋值AttachInfo,此时所有的View共用同一个AttachInfo(同一个ViewRootImpl内)
mAttachInfo = info;
// View浮层,是在Android 4.3添加的
if (mOverlay != null) {
// 任何一个View都有一个ViewOverlay
// ViewGroup的是ViewGroupOverlay
// 它区别于直接在类似RelativeLaout/FrameLayout添加View,通过ViewOverlay添加的元素没有任何事件
// 此时主要分发给这些View浮层
mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
}
mWindowAttachCount++;
// ... 省略
if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER) != 0) {
mAttachInfo.mScrollContainers.add(this);
mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
}
// mRunQueue,就是在前面的 getRunQueue().post()
// 实际类型是 HandlerActionQueue,内部保存了当前View.post的任务
if (mRunQueue != null) {
// 执行使用View.post的任务
// 注意这里是post到渲染线程的Handler中
mRunQueue.executeActions(info.mHandler);
// 保存延迟任务的队列被置为null,因为此时所有的View共用AttachInfo
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
// 回调View的onAttachedToWindow方法
// 在Activity的onResume方法中调用,但是在View绘制流程之前
onAttachedToWindow();
ListenerInfo li = mListenerInfo;
final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
li != null ? li.mOnAttachStateChangeListeners : null;
if (listeners != null && listeners.size() > 0) {
for (OnAttachStateChangeListener listener : listeners) {
// 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
// 但此时View还没有开始绘制,不能正确获取测量大小或View实际大小
listener.onViewAttachedToWindow(this);
}
}
// ... 省略
// 回调View的onVisibilityChanged
// 注意这时候View绘制流程还未真正开始
onVisibilityChanged(this, visibility);
// ... 省略
}
方法最开始为当前 View 赋值 AttachInfo。注意 mRunQueue 就是保存了 View.post() 任务的 HandlerActionQueue;此时调用它的 executeActions 方法如下:
public void executeActions(Handler handler) {
synchronized (this) {
// 任务队列
final HandlerAction[] actions = mActions;
// 遍历所有任务
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
//发送到Handler中,等待执行
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
//此时不在需要,后续的post,将被添加到AttachInfo中
mActions = null;
mCount = 0;
}
}
遍历所有已保存的任务,发送到 Handler 中排队执行;将保存任务的 mActions 置为 null,因为后续 View.post() 直接添加到 AttachInfo 内部的 Handler 。所以不得不去跟踪 dispatchAttachedToWindow() 的调用时机。
ViewRootImpl
同一个 View Hierachy 树结构中所有 View 共用一个 AttachInfo,AttachInfo 的创建是在 ViewRootImpl 的构造方法中:
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
- 一般 Activity 包含多个 View 形成 View Hierachy 的树形结构,只有最顶层的 DecorView 才是对 WindowManagerService “可见的”。
dispatchAttachedToWindow() 的调用时机是在 View 绘制流程的开始阶段。在 ViewRootImpl 的 performTraversals 方法,在该方法将会依次完成 View 绘制流程的三大阶段:测量、布局和绘制,不过这部分不是今天要分析的重点。
// View 绘制流程开始在 ViewRootImpl
private void performTraversals() {
// mView是DecorView
final View host = mView;
if (mFirst) {
.....
// host为DecorView
// 调用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 给子view
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
.....
}
mFirst=false
...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
// View 绘制流程的测量阶段
performMeasure();
// View 绘制流程的布局阶段
performLayout();
// View 绘制流程的绘制阶段
performDraw();
...
}
host 的实际类型是 DecorView,DecorView 继承自 FrameLayout。
- 每个 Activity 都有一个关联的 Window 对象,用来描述应用程序窗口,每个窗口内部又包含一个 DecorView 对象,DecorView 对象用来描述窗口的视图 — xml 布局。通过 setContentView() 设置的 View 布局最终添加到 DecorView 的 content 容器中。
跟踪 DecorView 的 dispatchAttachedToWindow 方法的执行过程,DecorView 并没有重写该方法,而是在其父类 ViewGroup 中:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
super.dispatchAttachedToWindow(info, visibility);
mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
// 子View的数量
final int count = mChildrenCount;
final View[] children = mChildren;
// 遍历所有子View
for (int i = 0; i < count; i++) {
final View child = children[i];
// 遍历调用所有子View的dispatchAttachedToWindow
// 为每个子View关联AttachInfo
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
// ...
}
for 循环遍历当前 ViewGroup 的所有 childView,为其关联 AttachInfo。子 View 的 dispatchAttachedToWindow 方法在前面我们已经分析过了:首先为当前 View 关联 AttachInfo,然后将之前 View.post() 保存的任务添加到 AttachInfo 内部的 Handler。
注意回到 ViewRootImpl 的 performTraversals 方法,咋一看,这个过程好像没有太多新奇的地方。不过你是否注意到这一过程是在 View 的绘制任务中。
通过 View.post() 添加的任务,是在 View 绘制流程的开始阶段,将所有任务重新发送到消息队列的尾部,此时相关任务的执行已经在 View 绘制任务之后,即 View 绘制流程已经结束,此时便可以正确获取到 View 的宽高了。
View.post() 添加的任务能够保证在所有 View(同一个 View Hierachy 内) 绘制流程结束之后才被执行。
碎片化问题来了,如果我们只是创建一个 View,调用它的 post 方法,它会不会被执行呢?代码如下:
final ImageView view = new ImageView(this);
view.post(new Runnable() {
@Override
public void run() {
// do something
}
});
答案是否定的,因为它没有添加到窗口视图,不会走绘制流程,自然也就不会被执行。此时只需要添加如下代码即可:
// 将View添加到窗口
// 此时重新发起绘制流程,post任务会被执行
contentView.addView(view);
不过该问题在 API Level 24 之前不会发生,看下之前的代码实现:
// API Level 24之前的post实现
public boolean post(Runnable action) {
// 这里的逻辑与API Level 24及以后一致
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// 主要是这里,此时管理待执行的任务直接交给了 ViewRootImpl 中。
// 而在API Level 24及以后,每个View自行维护待执行任务队列,
// 故,如果View不添加到Window视图,dispatchAttachedToWindow 不会被调用,
// View中的post任务将永远得不到执行
ViewRootImpl.getRunQueue().post(action);
return true;
}
在 API Level 24 之前,通过 View.post() 任务被直接添加到 ViewRootImpl 中,在 24 及以后,每个 View 自行维护待执行的 post() 任务,它们要依赖于 dispatchAttachedToWindow 方法,如果 View 未添加到窗口视图,post() 添加的任务将永远得不到执行。
这样的碎片化问题在 Android 中可能数不胜数,这也告诫我们如果对某项功能点了解的不够充分,最后可能导致程序未按照意愿执行。
至此,View.post() 的原理我们就算搞清楚了,不过还是有必要跟踪下 AttachInfo 的释放过程。
- mAttachInfo 置 null 的过程:
先看下表示 DecorView 的 dispatchDetachedFromWindow 方法,实际是调用其父类 ViewGroup 中:
// ViewGroup 的 dispatchDetachedFromWindow
void dispatchDetachedFromWindow() {
// ... 省略
final int count = mChildrenCount;
final View[] children = mChildren;
// 遍历所有childView
for (int i = 0; i < count; i++) {
// 通知childView dispatchDetachedFromWindow
children[i].dispatchDetachedFromWindow();
}
// ... 省略
super.dispatchDetachedFromWindow();
}
不出所料 ViewGroup 的 dispatchDetachedFromWindow 方法会遍历所有 childView。
void dispatchDetachedFromWindow() {
AttachInfo info = mAttachInfo;
if (info != null) {
int vis = info.mWindowVisibility;
if (vis != GONE) {
// 通知 Window显示状态发生变化
onWindowVisibilityChanged(GONE);
if (isShown()) {
onVisibilityAggregated(false);
}
}
}
// 回调View的onDetachedFromWindow
onDetachedFromWindow();
onDetachedFromWindowInternal();
// ... 省略
ListenerInfo li = mListenerInfo;
final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
li != null ? li.mOnAttachStateChangeListeners : null;
if (listeners != null && listeners.size() > 0) {
// 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
for (OnAttachStateChangeListener listener : listeners) {
// 通知回调 onViewDetachedFromWindow
listener.onViewDetachedFromWindow(this);
}
}
// ... 省略
// 将AttachInfo置为null
mAttachInfo = null;
if (mOverlay != null) {
// 通知浮层View
mOverlay.getOverlayView().dispatchDetachedFromWindow();
}
notifyEnterOrExitForAutoFillIfNeeded(false);
}
可以看到在 dispatchDetachedFromWindow 方法,首先回调 View 的 onDetachedFromWindow(),然后通知所有监听者 onViewDetachedFromWindow(),最后将 mAttachInfo 置为 null。
由于 dispatchAttachedToWindow 方法是在 ViewRootImpl 中完成,此时很容易想到它的释放过程肯定也在 ViewRootImpl,跟踪发现如下调用过程:
void doDie() {
// 检查执行线程
checkThread();
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
// 回调View的dispatchDetachedFromWindow
dispatchDetachedFromWindow();
}
if (mAdded && !mFirst) {
destroyHardwareRenderer();
// mView是DecorView
if (mView != null) {
int viewVisibility = mView.getVisibility();
// 窗口状态是否发生变化
boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
if (mWindowAttributesChanged || viewVisibilityChanged) {
try {
if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
& WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
mWindowSession.finishDrawing(mWindow);
}
} catch (RemoteException e) {
}
}
// 释放画布
mSurface.release();
}
}
mAdded = false;
}
// 将其从WindowManagerGlobal中移除
// 移除DecorView
// 移除DecorView对应的ViewRootImpl
// 移除DecorView
WindowManagerGlobal.getInstance().doRemoveView(this);
}
可以看到 dispatchDetachedFromWindow 方法被调用,注意方法最后将 ViewRootImpl 从 WindowManager 中移除。
经过前面的分析我们已经知道 AttachInfo 的赋值操作是在 View 绘制任务的开始阶段,而它的调用者是 ActivityThread 的 handleResumeActivity 方法,即 Activity 生命周期 onResume 方法之后。
那它是在 Activity 的哪个生命周期阶段被释放的呢?在 Android 中, Window 是 View 的容器,而 WindowManager 则负责管理这些窗口,具体可以参考《View 绘制流程之 DecorView 添加至窗口的过程》。
我们直接找到管理应用进程窗口的 WindowManagerGlobal,查看 DecorView 的移除工作:
/**
* 将DecorView从WindowManager中移除
*/
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
// 找到保存该DecorView的下标,true表示找不到要抛出异常
int index = findViewLocked(view, true);
// 找到对应的ViewRootImpl,内部的DecorView
View curView = mRoots.get(index).getView();
// 从WindowManager中移除该DecorView
// immediate 表示是否立即移除
removeViewLocked(index, immediate);
if (curView == view) {
// 判断要移除的与WindowManager中保存的是否为同一个
return;
}
// 如果不是同一个View(DecorView),抛异常
throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}
根据要移除的 DecorView 找到在 WindowManager 中保存的 ViewRootImpl,真正移除是在 removeViewLocked 方法:
private void removeViewLocked(int index, boolean immediate) {
// 找到对应的ViewRootImpl
ViewRootImpl root = mRoots.get(index);
// 该View是DecorView
View view = root.getView();
// ... 省略
// 调用ViewRootImpl的die
// 并且将当前ViewRootImpl在WindowManagerGlobal中移除
boolean deferred = root.die(immediate);
if (view != null) {
// 断开DecorView与ViewRootImpl的关联
view.assignParent(null);
if (deferred) {
// 返回 true 表示延迟移除,加入待死亡队列
mDyingViews.add(view);
}
}
}
可以看到调用了 ViewRootImpl 的 die 方法,回到 ViewRootImpl 中:
boolean die(boolean immediate) {
// immediate 表示立即执行
// mIsInTraversal 表示是否正在执行绘制任务
if (immediate && !mIsInTraversal) {
// 内部调用了View的dispatchDetachedFromWindow
doDie();
// return false 表示已经执行完成
return false;
}
if (!mIsDrawing) {
// 释放硬件加速绘制
destroyHardwareRenderer();
}
// 如果正在执行遍历绘制任务,此时需要等待遍历任务完成
// 故发送消息到尾部
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
注意 doDie 方法(源码在前面已经贴出),它最终会调用 dispatchDetachedFromWindow 方法。
最后,移除 Window 窗口任务是通过 ActivityThread 完成的,具体调用在 handleDestoryActivity 方法完成:
private void handleDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance) {
// 回调 Activity 的 onDestory 方法
ActivityClientRecord r = performDestroyActivity(token, finishing,
configChanges, getNonConfigInstance);
if (r != null) {
cleanUpPendingRemoveWindows(r, finishing);
// 获取当前Window的WindowManager, 实际是WindowManagerImpl
WindowManager wm = r.activity.getWindowManager();
// 当前Window的DecorView
View v = r.activity.mDecor;
if (v != null) {
if (r.activity.mVisibleFromServer) {
mNumVisibleActivities--;
}
IBinder wtoken = v.getWindowToken();
// Window 是否添加过,到WindowManager
if (r.activity.mWindowAdded) {
if (r.mPreserveWindow) {
r.mPendingRemoveWindow = r.window;
r.mPendingRemoveWindowManager = wm;
r.window.clearContentView();
} else {
// 通知 WindowManager,移除当前 Window窗口
wm.removeViewImmediate(v);
}
}
}
注意 performDestoryActivity() 将完成 Activity 生命周期 onDestory 方法回调。然后调用 WindowManager 的 removeViewImmediate():
/**
* WindowManagerImpl
*/
@Override
public void removeViewImmediate(View view) {
// 调用WindowManagerGlobal的removeView方法
mGlobal.removeView(view, true);
}
即 AttachInfo 的释放操作是在 Activity 生命周期 onDestory 方法之后,在整个 Activity 的生命周期内都可以正常使用 View.post() 任务。
总结
关于 View.post() 要注意在 API Level 24 前后的版本差异,不过该问题也不用过于担心,试想,会有哪些业务场景需要创建一个 View 却不把它添加到窗口视图呢?
View.post() 任务能够保证在所有 View 绘制流程结束之后被调用,故如果需要依赖 View 绘制任务,此时可以优先考虑使用该机制。
最后,如果需要更好的理解 View.post() 执行原理,可能还需要进一步理解 AttachInfo 的创建过程,关于这部分的详细分析,你可以参考《Android 之 ViewTreeObserver 全面解析》。
文中如有不妥或有更好的分析结果,欢迎您的分享留言或指正。
文章如果对你有帮助,请留个赞吧!
扩展阅读
- View 绘制流程之 setContentView() 到底做了什么 ?
- View 绘制流程之 DecorView 添加至窗口的过程
- 深入 Activity 三部曲(3)之 View 绘制流程
- 关于 UI 渲染,你需要了解什么?
- Android 之 Choreographer 详细分析
其他专题