我们都知道在平常开发的过程中,如果一个view关于布局的属性改变了,想要在界面上看到效果,需要我们手动去调用view#requestLayout或者view#invalidate方法。可是有些时候又不需要我们直接调用,比如ImageView.setImageDrawable()。那这到底这是为什么呢?背后到底发生了什么?下面开始我们的解密之旅。
View的间接调用
我们先来看看上面举的ImageView.setImageDrawable的例子。
ImageView#setImageDrawable
public void setImageDrawable(@Nullable Drawable drawable) {
if (mDrawable != drawable) {
mResource = 0;
mUri = null;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
updateDrawable(drawable);
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
//注意这行代码
invalidate();
}
}
通过代码我们发现,虽然我们没有显示的去调用2种方式中的一种,但是方法内部却间接的调用了invalidate()。当然还存在另一种情况,就是当我们的控件的布局(位置或者大小)发生改变的时候。举个例子:当我们往往会通过view的内部类LayoutParams改变view的大小或者布局,然后再通过View#setLayoutParams最终来改变view的布局,下面让我们来看下代码。
View#setLayoutParams
public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params == null) {
throw new NullPointerException("Layout parameters cannot be null");
}
mLayoutParams = params;
resolveLayoutParams();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).onSetLayoutParams(this, params);
}
//注意这行代码
requestLayout();
}
很明显setLayoutParams的内部调用了requestLayout。
我们上面只举了比较常用的2个例子,当然还有其他的,大家可以自己去查找一下,只要是跟view的布局属性相关的改变,都会间接的调用到那两个方法。可是,这2个方法有什么区别呢,在什么情况下该调用invalidate或者requestLayout?下面让进一步深入这2个方法。
View#invalidate的真相
我们首先来看下invalidate的注释。
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if (mGhostView != null) {
mGhostView.invalidate(true);
return;
}
//这里判断该子View是否可见或者是否处于动画中
if (skipInvalidate()) {
return;
}
//根据View的标记位来判断该子View是否需要重绘,假如View没有任何变化,那么就不需要重绘,这里很重要一定要注意,通过标志位来决定是否需要重新绘制
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
//当父容器不为null把需要重绘的区域传递给父容器
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
//注意这行代码
p.invalidateChild(this, damage);
}
// Damage the entire projection receiver, if necessary.
if (mBackground != null && mBackground.isProjected()) {
final View receiver = getProjectionReceiver();
if (receiver != null) {
receiver.damageInParent();
}
}
}
}
意思就是当整个view可见的时候,会使整个view无效,会间接调用到onDraw方法,并且只能在UI线程中调用,如果要在非UI线程中调用推荐使用postInvalidate。
首先来看下 mParent.invalidateChild方法。在view中invalidateChild是个抽象方法,那我们进入ViewGroup来看下他的invalidateChild。
ViewGroup#invalidateChild
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
……
ViewParent parent = this;
do {
View view = null;
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
//注意这行代码 //调用ViewGrup的invalidateChildInParent,如果已经达到最顶层view,则调用ViewRootImpl
//的invalidateChildInParent。
parent = parent.invalidateChildInParent(location, dirty);
……
} while (parent != null);
}
}
ViewGroup#invalidateChildInParent
@Override
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
// either DRAWN, or DRAWING_CACHE_VALID
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
!= FLAG_OPTIMIZE_INVALIDATE) {
//将dirty中的坐标转化为父容器中的坐标,考虑mScrollX和mScrollY的影响
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
//求并集,结果是把子视图的dirty区域转化为父容器的dirty区域
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
final int left = mLeft;
final int top = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
} else {
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
} else {
// in case the dirty rect extends outside the bounds of this container
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
location[CHILD_LEFT_INDEX] = mLeft;
location[CHILD_TOP_INDEX] = mTop;
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
//返回当前视图的父容器
return mParent;
}
return null;
}
从上文中我们知道parent.invalidateChildInParent会一致向上调用到ViewRootImpl#invalidateChildInParent方法,其实这就是责任链模式,在android的view绘制体系中大量存在。根据我以前分析过得文章中可知View#mParent是在ViewGroup#addView中绑定的。
ViewRootImpl#invalidateChildInParent
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
if (mCurScrollY != 0 || mTranslator != null) {
mTempRect.set(dirty);
dirty = mTempRect;
if (mCurScrollY != 0) {
dirty.offset(0, -mCurScrollY);
}
if (mTranslator != null) {
mTranslator.translateRectInAppWindowToScreen(dirty);
}
if (mAttachInfo.mScalingRequired) {
dirty.inset(-1, -1);
}
}
invalidateRectOnScreen(dirty);
return null;
}
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//这里会往UI线程发送一个runnable请求就是mTraversalRunnable
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
//进入遍历绘制操作
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
try {
//执行遍历绘制操作 performTraversals代码太长,我们就不分析,反正会根据标志位进行meaure、layout、draw操作
performTraversals();
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
好了invalidate到这就结束,我们再回过头看下postInvalidate方法。
View#postInvalidate
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
ViewRootImpl#dispatchInvalidateDelayed
MSG_INVALIDATE = MSG_INVALIDATE;
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
#Handler#handleMessage
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
}
我们可以看到其实就是进入ViewRootImpl进行了一次线程切换然后又回到了View#invalidate流程。
好了,关于invalidate流程到这就结束了,我们来简单的总结下
- invalidate:首先会进行本身的标志位设置,看看是否需要进行mease、layout、draw操作,需要需要会进入ViewRootImpl#invalidateChildInParent,最终进入到performTraversals执行遍历绘制操作。
- postInvalidate:postInvalidate比invalidate多了一步在ViewRootImpl的线程切换,然后下面的流程跟invalidate一摸一样。
View#requestLayout的真相
View#requestLayout
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
//注意 只有设置了PFLAG_FORCE_LAYOUT标致位才会进行mease操作,文末会附上mesure和layout的代码
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
//进入ViewGroup的requestLayout操作
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
根据上面的分析我们应该能想到最终也会进入到ViewRootImpl的requestLayout,大家可以自己去跟一遍代码。
ViewRootImpl#requestLayout
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
首先进行了UI线程检查,然后同样进入到了scheduleTraversals()操作,跟invalidate一样,我们这里就不解释了。
总结
其实invalidate相比requestLayout就是在进入ViewRootImpl之前进行了一系列的标致位设置。防止view进行一系列不必要的测量和布局,当然draw流程是两者都需要进行的。然后进入ViewRootImpl的函数调用流程是这样的:
scheduleTraversals-》doTraversal-》performTraversals,其中scheduleTraversals到doTraversal是通过Handler机制进行调用的。所以,当我们在编码时,当明确的知道没有进行控件的大小和布局改变的时候调用invalidate可以减少不必要的测量和布局,提升性能。
参考
View#measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
}
首先是判断一下标记位,如果当前View的标记位为PFLAG_FORCE_LAYOUT,那么就会进行测量流程,调用onMeasure,对该View进行测量,接着最后为标记位设置为PFLAG_LAYOUT_REQUIRED,这个标记位的作用就是在View的layout流程中,如果当前View设置了该标记位,则会进行布局流程
View#layout
public void layout(int l, int t, int r, int b) {
...
//判断标记位是否为PFLAG_LAYOUT_REQUIRED,如果有,则对该View进行布局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
//onLayout方法完成后,清除PFLAG_LAYOUT_REQUIRED标记位
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
//最后清除PFLAG_FORCE_LAYOUT标记位
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
其中Draw流程是2这都要进行的,这里就不贴代码了,而且view的onDraw方法是空实现,具体的实现流程都交给了子view。
网友评论