1.view.post()
view.post()主要有两个作用:更新UI、获取view的实际宽高。
其实view.post()内部调用了Handler,它之所以能实现这两个功能,是因为它对任务的运行时机做了调整。
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); //把action封装成HandlerAction对象
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的创建过程,先看下它的构造方法:
View.java:
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赋值过程:
View.java:
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;
}
// 回调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已经onAttachedToWindow的客户端,即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里:
ViewRootImpl.java:
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
...
//每次调用performTraversals方法时,都要执行getRunQueue()里排队的action,防止有的view已经detach了还在排队中
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中。
ViewGroup.java:
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来执行,所以View.post(Runnable)中的Runnable操作肯定会在performMeaure()之后才执行,所以此时可以获取到View的宽高。
碎片化问题来了,如果只是创建一个View,调用它的post方法,它会不会被执行呢?比如:
final ImageView view = new ImageView(this);
view.post(new Runnable() {
@Override
public void run() {
// do something
}
});
答案是否定的,因为它没有添加到窗口视图,不会走绘制流程,自然也就不会被执行。此时只需要添加如下代码即可:
contentView.addView(view);// 将View添加到窗口,此时重新发起绘制流程,post任务会被执行
不过该问题在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一个内部类的实例,所以使用不当还是有可能会造成内存泄漏的。