前面写了一篇文章专门的分析了View的measure、layout和draw三大流程,在那篇文章中,多次提到了requestLayout
、invalidate
和postInvalidate
方法,因为这三个方法会影响三大流程。今天我们来看看这三个方法到底做了什么,平时只管在用,却不知道它们得实现,难免感觉有些遗憾。本文也是为RecyclerView
源码分析打基础的第二篇文章。
本文参考资料:
注意,本文源码来自Api 27
1. 概述
我们在分析源码之前,首先从大概上的了解这三个方法到底是干嘛的,它们之间的区别又是什么。我们对此先有一个大概的了解。
首先来看看这三个方法的整个执行流程。
从图中,我们可以得到,
requestLayout
方法会重新走一遍三大流程。不过这里会涉及到一些问题,比如,我们在三大流程调用requestLayout
方法会不会导致死递归?不过我们在实际开发过程中确实没有导致死递归,那Google爸爸是怎么为我们解决这个问题的呢?同时,
invalidate
方法和postInvalidate
方法又有什么区别?我们知道,invalidate
方法是在UI线程调用的,postInvalidate
方法是用于非UI线程调用的。在非UI线程里面更新UI肯定使用到Handler
,但是我们知道它是怎么通过Handler
来实现的,实际上也是非常的简单😂。以上的几个问题都是本文重点关注的点。
1. requestLayout
首先我们来看第一个方法--requestLayout
方法。在正式分析requestLayout
方法之前,我们先对Android的View树型结构。
从上图中,我们可以得到两点。
- 整个View树结构是一个双向指针结构。其实这个不难理解,因为View的三大流程的分发、事件分发机制都是基于责任链模式,而责任链模式分为先上传递和向下传递,所以拥有
child
和parent
两个指针并不奇怪。- 我们知道获取一个
View
的Parent
得到的是一个ViewParent
对象,可能大家都有一个疑问为什么不是ViewGroup呢?因为View只会在一个ViewGroup啊。从这里,我们可以得到答案,DecorView
的Parent
并不是View
,更不是ViewGroup
,所以一个View
的Parent
不一定是ViewGroup
。
对整个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;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
整个requestLayout
方法比较简单,我将分为3步。
- 如果此时,View结构还处于laoyut阶段之前,直接调用
ViewRootImpl
的requestLayoutDuringLayout
方法,将当前View
对象传递进行,至于这里做什么,待会我们回去看ViewRootImpl
的performLayout
方法就知道了。从这里,我们就可以得到为什么requestLayout
方法不会导致死递归了。- 更新了
mPrivateFlags
变量。注意,mPrivateFlags
与PFLAG_FORCE_LAYOUT
、PFLAG_INVALIDATED
这两个变量。了解三大流程的同学,相信对PFLAG_FORCE_LAYOUT
变量不陌生🤓。- 调用
Parent
的requestLayout
,让整个责任链模式动起来。
关于requestLayout
方法,我打算从两点开始讲解。
- 从责任链的模式来看看,看看整个责任链是怎么传递,最后又是怎么走三大流程。
- 从三大流程角度上来看看
requestLayout
方法的调用流程。
(1).requestLayout的责任链
requestLayout
的责任链就是从调用Parent
的requestLayout
方法开始的,一层一层递归上去,最终会调用到ViewRootImpl
的requestLayout
方法里面。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
ViewRootImpl
的requestLayout
方法比较简单。首先是check了一下线程,如果当前线程不是主线程的话,那么就会抛出异常。
其次,就是调用scheduleTraversals
方法,这个scheduleTraversals
方法究竟做了什么呢?我们来看看。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
如果来详细的看scheduleTraversals
方法的话,肯定会非常的懵逼。这里我简单解释一下,这个方法做了什么吧。实际上,就是向Choreographer
的消息队列post一个CallBack
对象,用于下一次绘制。至于Choreographer
是什么,本文不做过多的解释,可以参考这篇文章:Choreographer 解析。这里,我们只需要知道,往Choreographer
的消息队列post一个CallBack
对象,表示下次绘制信号来的时候,会执行我们的任务。那要执行什么呢?当然是mTraversalRunnable
任务。
同时,这里还需要注意一点就是,我们通过调用如下代码先Handler
的MessageQueue
里面插入了一个同步屏障,来表示当前的任务不可被打断:
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
至于什么是同步屏障,相信熟悉JVM的同学并不陌生。在JVM里面,当一个变量被volatile
关键字修饰时,JVM会在这个变量初始化之前插入一个屏障,表示当前操作不可被打断。这里的打断有两个意思:1.不允许JVM进行命令重排序;2.保证当前的操作是原子性(感觉这两个意思是同一个意思😂)。这里Handler
的同步屏障表达的意思也差不多
哎呀,一不小心就跑了题,其实是我故意的🙄🙄。我们继续来看看代码吧。
表示当前任务不可被打断
中的任务是什么任务呢?当然是我们的mTraversalRunnable
任务呢。我们来看看mTraversalRunnable
任务究竟做了啥。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
感觉TraversalRunnable
非常的简单,只是调用了doTraversal
方法,我们来看看简单:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
看到doTraversal
方法时,你们开不开心?激不激动?哈哈,在doTraversal
方法里面,调用了performTraversals
方法。还记得performTraversals
方法是干嘛的吗?哈哈,它就是三大流程的开始。
(2).从requestLayout角度上来三大流程
在这之前,我们已经详细的分析了View的三大流程。在本篇文章中,我们再来简单看一下,这次,我们的重点放在View
的measure
方法。
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
这里将mPrivateFlags
与PFLAG_FORCE_LAYOUT
做了一个与运算,用来判断当前是否进行强制布局。从这里看出来可以知道是否进行强制布局呢?我们看最后一行代码:
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
这里,mPrivateFlags
跟PFLAG_LAYOUT_REQUIRED
做了一个异或操作。这一步有什么意义呢?我们可以从View
的layout
找到答案:
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// ······
}
如果mPrivateFlags
有PFLAG_LAYOUT_REQUIRED
的标记,那么肯定会调用onLayout
方法,这就是所谓的强制布局。
强制布局之后,肯定会强制绘制,也就是会调用draw
和onDraw
方法来进行绘制。这些都比较简单,这里就不过多的分析了。
(3). 为什么在ViewGroup的onLayout方法调用requestLayout方法不会导致死递归?
按照现在的思路来说,如果在我们onLayout里面调用了requestLayout
方法会重新走三大流程,从而导致又会回调到onLayou方法里面来,进而导致死递归。但是实际的效果并不是我们猜想的效果,我们来看看究竟是为什么。
首先,我先解释一个认知上的误区,在View树layout阶段中,调用requestLayout
方法申请重新三大流程,这个是真的会导致死递归。而Google爸爸是怎么解决的呢?我们来看看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;
}
// ······
}
如果当前View树处于layout
阶段,也就是说DecorView
的layout
方法还没有执行完毕,此时如果调用requestLayout
方法的话,不会立即进行三大流程,而是将这个任务放在ViewRootImpl
的一个任务队列里面,那这个任务队列在什么会执行呢?这个就得看ViewRootImpl
的performLayout
方法了。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
mHandlingLayoutInLayoutRequest = true;
int numValidRequests = validLayoutRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mHandlingLayoutInLayoutRequest = false;
}
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
看到没,再执行了DecorView
的layout
方法之后,就开始执行任务队列的任务了。但是这里又有一个问题了,我们发现在执行任务队列中的任务时,又调用了DecorView
的layout
方法,这会导致又会去执行我们的onLayout
方法,从而又会调用requestLayout
方法,进而导致任务队列的任务无穷无尽,这还是没有将上面的问题解释明白啊?
纳尼?又会调用我们的onLayout
方法 ? 既然View
的left
、top
、right
、bottom
都没有变,为什么还有回去调用onLayout
方法进行重新布局呢?是不是还嫌咱们Android卡的不够吗?😂😂
当然,说归说,我们还是必须找到依据来证明我们的猜想。答案在哪里呢?当然在layout方法里面:
public void layout(int l, int t, int r, int b) {
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// ······
}
}
看到changed
变量没?这个变量就是用来判断咱们四个属性是否变了。如果变了才会调用onLayout方法进行重新布局,反之则不会调用。
requestLayout
方法到此就分析完毕了。接下来,我们继续分析invalidate
和postInvalidate
方法。
2. invalidate
invalidate
方法分为全局刷新和局部刷新。顾名思义,全局刷新表示整个View树都会刷新一遍,局部刷新只会以当前View为根开始刷新。我们来看看invalidate
方法:
public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
其中invalidate
方法里面传入一个true,表示当前只做局部刷新。invalidate
方法最终调用到invalidateInternal
方法里面来。
invalidateInternal
方法显示判断当前是否跳过刷新操作,主要是通过skipInvalidate
方法来判断的。我们来看看skipInvalidate
方法究竟是怎么进行判断的:
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}
如果当前View不可见,并且自己的动画为空,同时父View也没在动画,那么就跳过刷新。这个判断逻辑我们非常容易的得到。
同时!(mParent instanceof ViewGroup)
又是什么鬼东西,前面我已经介绍了,一个View
的Parent
不一定是ViewGroup
,还有可能是ViewRootImpl
。如果一个View
的Parent
是什么一个情况呢?表示当前View
是DecorView
,而DecorView
不可见是什么一个情况呢?那就相当于整个View树都不可见,是不是很刺激???整个View树都不可见,那么刷新操作自然是多余的。
在invalidateInternal
方法里面,接下来的操作就是判断mPrivateFlags
是否符合刷新操作的要求,具体的细节就不去讨论,反正可能是调用了View其中一个方法导致mPrivateFlags
被更新。
最后就是调用Parent
的invalidateChild
方法。正常来说一个View
的Parent
应该是一个ViewGroup
,我们来看看ViewGroup
的invalidateChild
方法:
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
// ······
ViewParent parent = this;
if (attachInfo != null) {
// ······
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
// ······
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
} while (parent != null);
}
}
这个invalidateChild
方法比较长,这里将它简化了一下。其实简化的那些代码表达意思比较简单,就是Rect
矩阵的计算。然后通过递归遍历将整个操作传递上去。其中我们会发现,invalidateChild
方法里面通过invalidateChildInParent
方法来寻找一个Parent
,其实invalidateChildInParent
方法也没做什么,就是location的计算和mPrivateFlags
的更新,最后会返回当前View
的Parent
。
经过一层一层的递归,最终也会调用到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;
}
在invalidateChildInParent
方法里面,不管是调用invalidate
方法,还是最后调用的invalidateRectOnScreen
方法都会调用到scheduleTraversals
方法。调用到scheduleTraversals
方法表示着什么呢?表示又要开始三大流程了。咦,好像不对哦,前面的图不是说invalidate
方法只进行draw流程吗?怎么会进行三大流程呢?是我们的图画错了吗?不是的,实际上在走三大流程是,View
会通过mPrivateFlags
来判断是否进行measure
和layout
操作。而在调用invalidate
方法时,更新了mPrivateFlags
,所以最终只会走draw
流程。
如上,就是整个invalidate
方法的执行流程,还是比较简单的。接下来,我们将分析最后一个方法--postInvalidate
方法。
3. postInvalidate
postInvalidate
方法的执行流程跟invalidate
方法的差不多,所以本文重点关注它们两个方法的区别。
从两个方法的名字上,我们就可以看出来,invalidate
方法只能在UI线程里面里面调用,而postInvalidate
方法的存在是为了解决在非UI线程不能调用invalidate
方法刷新的问题。实际上,postInvalidate
方法在UI线程和非UI线程都可以调用,因为postInvalidate
方法实现刷新的原理是通过Handler
来实现的。
我们来看看具体的源码。通过调用postInvalidate
方法,最后会调用到postInvalidateDelayed
方法,我们来看看这个方法。
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
方法。这个方法像是掌握缩地成寸
的法术一样,直接一步跨到了最高层,不像其他的方法,还需要一层一层通过责任链方式传递,我只能给这个方法写一个大写的牛逼。
好吧,好像废话有点多。我们来看看ViewRootImpl
的dispatchInvalidateDelayed
方法:
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
意料之中,果不其然,postInvalidate
方法是通过Handler
实现在非UI线程里面更新UI的。我们来看看这个Handler
,这里我们只看一下这个Handler
的handleMessage
方法:
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
// ······
}
}
我们在Handler
的handleMessage
方法里面,直接调用了View
的invalidate
方法,这就回到上面讲解的invalidate
方法的原理。这里就不重复的讲解了。
哎,以为postInvalidateDelayed
方法掌握缩地成寸
的法术有多厉害呢,结果修的是残篇😂😂。
4. 总结
到此,我们将View
的三个方法分析完毕了,理解了这三个方法实现原理,同时也了解这三个方法是怎么跟三大流程配合工作的。这里,我做一个简单的总结。
requestLayout
方法会重新走三大流程,所以比较耗性能,所以尽可能的不要频繁的调用requestLayout
方法。- 在
layout
阶段调用requestLayout
方法不会立即执行,而是要等当前的layout阶段执行完毕才会执行。这就解决了requestLayout
死递归的问题。invalidate
方法和postInvalidate
方法的区别在于,invalidate
方法只能在UI线程里面调用,而postInvalidate
方法是无所谓线程。requestLayout
方法、invalidate
方法和postInvalidate
任务的驱动,最后依靠一个叫Choreographer
类实现的。我们只是往Choreographer
的任务队列post一个Callback
对象,就能保证下次屏幕刷新时能执行。
网友评论