1.view.post()
在开发过程中用到过几次view.post(),但是对它的原理不甚了解,今天就从源码看看它是怎么实现的吧。
其实view.post()的内部也是调用了Handler,它主要用于更新UI操作、获取view的实际宽高。简单来说,view.post()对任务的运行时机做了调整。
举个例子:在Activity中,view绘制流程的开始时机是在ActivityThread的handleResumeActivity方法中,该方法首先完成Activity生命周期onResume方法回调,然后开始view绘制任务。也就是说,view的绘制流程要在onResume方法之后,但是我们绝大多数业务是在onCreate方法,比如要获取某个view的实际宽高,由于view的绘制任务还未开始,所以就无法正确获取。此时大家肯定使用过view.post()来解决问题(当然也可以使用ViewTreeObserver或更长延迟的postDelayed()方法)。注意view绘制流程也是向Handler添加任务,如果在onCreate方法直接使用Handler.post(),则该任务一定在view绘制任务之前(同一个线程队列机制)。
现在带着3个问题去看源码:
①为什么View.post()可以对UI进行操作呢,即使在子线程中调用View.post()?
②View.post()执行时,View的宽高已经计算完毕,所以经常看见在Activity的onCreate()里调用View.post()来解决获取View宽高为0的问题,为什么可以这样做呢?
③用 View.postDelay() 会导致内存泄漏吗?
2.源码
View.java:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if(attachInfo != null) {
//attachInfo不为空,直接调用其内部Handler的post方法
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action); //attachInfo为空,则加入当前view的等待队列
return true;
}
AttachInfo是View的静态内部类,每个View都会持有一个AttachInfo,它默认为null。
从源码中可以看到,pos方法里分了两种情况:
①mAttachInfo != null 时,post方法最终是由attachInfo中的mHandler调用post来处理,从而保证在UI线程中执行,所以从根本上来说之后的整个流程就是Handler的处理机制流程,那么mAttachInfo又是什么时候赋值的呢?搜索源码看到:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
……
//Transfer all pending runnable.
if(mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
……
}
mAttachInfo是在dispatchAttachedToWindow方法中调用的,而dispatchAttachedToWindow是在 ViewRootImpl类的performTraversals调用的,而这个方法在view初始化的时候会被调用。
②mAttachInfo == null时,调用getRunQueue().post(action),来看下这个getRunQueue()的源码:
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
getRunQueue()返回的是HandlerActionQueue,也就是调用了HandlerActionQueue的post()方法:
HandlerActionQueue.java:
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) { //保存HandlerAction的数组
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append( mActions, mCount, handlerAction); //要执行的任务HandlerAction保存在mActions数组中
mCount++; //mActions的数组下标加1
}
}
HandlerAction表示一个待执行的任务,内部持有要执行的Runnable和延迟时间。类声明如下:
private static class HandlerAction {
final Runnable action; // post的任务
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()方法将传入的action用HandlerAction包装了一下,然后保存到成员变量mActions数组里。这个数组默认长度为4,用于保存post()添加的任务。GrowingArrayUtils.append()是个工具类,如果数组不够用就扩充。跟踪到这,大家是否有这样的疑惑: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;
mHandler = handler; // 当前渲染线程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;
// ... …
// 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方法如下:
HandlerActionQueue.java:
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.postDelayed(handlerAction.action, handlerAction.delay);//发送到Handler中,等待执行
}
mActions = null; //此时不再需要,因为mAttachInfo已经不为空,后续的post将被添加到AttachInfo中
mCount = 0;
}
}
遍历所有已保存的任务,发送到Handler中排队执行,所以刚刚缓存起来的runnable最终还是通过handler来执行的。最后将保存任务的mActions置为null,因为后续View.post()直接添加到AttachInfo内部的Handler 。
注意,调用executeActions执行runnable的地方正是在dispatchAttachedToWindow方法里。
现在清楚了:打开一个activity时,如果调用post方法时还没有开始执行dispatchAttachedToWindow就先调用getRunQueue().post(action)方法将runnable先缓存起来,当执行到dispatchAttachedToWindow时就通过mAttachInfo.mHandler来执行这些被缓存起来的Runnable操作。从这以后到view被detachedFromWindow这段期间,如果再次调用view.post(Runnable)的话,这些Runnable就不用再缓存了,而是直接交给mAttachInfo.mHandler来执行。
继续向下分析:
同一个View Hierachy树结构中所有View共用一个 AttachInfo,AttachInfo的创建是在ViewRootImpl 的构造方法中:
ViewRootImpl.java:
public ViewRootImpl(Context context, Display display, IWindowSession session, boolean useSfChoreographer) {
……
mAttachInfo = new View.AttachInfo( mWindowSession, mWindow, display, this, mHandler, this, context);
……
}
一般Activity包含多个View形成View Hierachy的树形结构,只有最顶层的DecorView才是对 WindowManagerService “可见的”。
view的dispatchAttachedToWindow()的调用时机是在View绘制流程的开始阶段。在ViewRootImpl 的performTraversals方法,在该方法将会依次完成View绘制流程的三大阶段:测量、布局和绘制。
// View绘制流程开始在ViewRootImpl
private void performTraversals() {
final View host = mView; //mView是DecorView
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);
performMeasure(); // View绘制流程的测量阶段
performLayout(); // View绘制流程的布局阶段
performDraw(); // View绘制流程的绘制阶段
...
}
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;
final int count = mChildrenCount;//子View数量
final View[] children = mChildren;
for (int i = 0; i < count; i++) { //遍历所有子View
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内)绘制流程结束之后才被执行。
这里会有一点疑问:
看ViewRootImpl.performTraversals() 的分析:遍历 View 树进行测量、布局、绘制操作的代码显然是在调用了 dispatchAttachedToWindow() 之后才执行,那这样一来是如何保证 View.post(Runnable) 的 Runnable 操作可以获取到 View 的宽高呢?明明测量的代码 performMeasure() 是在 dispatchAttachedToWindow() 后面才执行。
大概来讲,就是我们的 app 都是基于消息驱动机制来运行的,主线程的 Looper 会无限的循环,不断的从 MessageQueue 里取出 Message 来执行,当一个 Message 执行完后才会去取下一个 Message 来执行。而 Handler 则是用于将 Message 发送到 MessageQueue 里,等轮到 Message 执行时,又通过 Handler 发送到 Target 去执行,等执行完再取下一个 Message,如此循环下去。
清楚了这点后,我们再回过头来看看:
performTraversals() 会先执行 dispatchAttachedToWindow(),这时候所有子 View 通过 View.post(Runnable) 缓存起来的 Runnable 操作就都会通过 mAttachInfo.mHandler 的 post() 方法将这些 Runnable 封装到 Message 里发送到 MessageQueue 里。mHandler上面也分析过了,绑定的是主线程的 Looper,所以这些 Runnable 其实都是发送到主线程的 MessageQueue 里排队,等待执行。然后 performTraversals() 继续往下工作,相继执行 performMeasure(),performLayout() 等操作。等全部执行完后,表示这个 Message 已经处理完毕,所以 Looper 才会去取下一个 Message,这时候,才有可能轮到这些 Runnable 执行。所以,这些 Runnable 操作也就肯定会在 performMeasure() 操作之后才执行,宽高也就可以获取到了。画张图,帮助理解一下:
分析了半天,最后我们来稍微小结一下:
View.post(Runnable) 内部会自动分两种情况处理,当 View 还没 attachedToWindow 时,会先将这些 Runnable 操作缓存下来;否则就直接通过 mAttachInfo.mHandler 将这些 Runnable 操作 post 到主线程的 MessageQueue 中等待执行。如果 View.post(Runnable) 的 Runnable 操作被缓存下来了,那么这些操作将会在 dispatchAttachedToWindow() 被回调时,通过 mAttachInfo.mHandler.post() 发送到主线程的 MessageQueue 中等待执行。
mAttachInfo 是 ViewRootImpl 的成员变量,在构造函数中初始化,Activity View 树里所有的子 View 中的 mAttachInfo 都是 ViewRootImpl.mAttachInfo 的引用。
mAttachInfo.mHandler 也是 ViewRootImpl 中的成员变量,在声明时就初始化了,所以这个 mHandler 绑定的是主线程的 Looper,所以 View.post() 的操作都会发送到主线程中执行,那么也就支持 UI 操作了。
dispatchAttachedToWindow() 被调用的时机是在 ViewRootImol 的 performTraversals() 中,该方法会进行 View 树的测量、布局、绘制三大流程的操作。
Handler 消息机制通常情况下是一个 Message 执行完后才去取下一个 Message 来执行(异步 Message 还没接触),所以 View.post(Runnable) 中的 Runnable 操作肯定会在 performMeaure() 之后才执行,所以此时可以获取到 View 的宽高。
碎片化问题来了,如果只是创建一个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.java:
void dispatchDetachedFromWindow() {
// ... …
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) { //遍历所有子view
//通知childView的 dispatchDetachedFromWindow
children[i].dispatchDetachedFromWindow();
}
// ... …
super.dispatchDetachedFromWindow();
}
ViewGroup的dispatchDetachedFromWindow 方法会遍历所有childView。
View.java:
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();
if (mView != null) { // mView是DecorView
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则负责管理这些窗口。因此直接找到管理应用进程窗口的 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.java:
@Override
public void removeViewImmediate(View view) {
//调用WindowManagerGlobal的removeView方法
mGlobal.removeView(view, true);
}
即AttachInfo的释放操作是在Activity生命周期onDestory方法之后,在整个Activity的生命周期内都可以正常使用View.post()任务。
3.总结
①关于View.post()要注意在 API Level 24 前后的版本差异,不过该问题也不用过于担心,试想,会有哪些业务场景需要创建一个 View 却不把它添加到窗口视图呢?
②View.post()任务能够保证在所有View绘制流程结束之后被调用,故如果需要依赖View绘制任务,此时可以优先考虑使用该机制。
③使用 View.post(),还是有可能会造成内存泄漏的,Handler 会造成内存泄漏的原因是由于内部类持有外部的引用,如果任务是延迟的,就会造成外部类无法被回收。而根据分析,mAttachInfo.mHandler 只是 ViewRootImpl 一个内部类的实例,所以使用不当还是有可能会造成内存泄漏的。