一、复杂场景中尽量使用ConstraintLayout布局
主要需要注意两点
1、一个是在复杂布局场景,如果在较为简单的能够通过FrameLayout或者LinearLayout实现的,则无需使用ConstraintLayout,比如使用不同布局的嵌套层数是相等的。
2、 一个是尽量使用ConstraintLayout
ConstraintLayout被推荐使用的主要原因有两点
- 其布局灵活性比较高,可以最大程度减少View的嵌套,并且ConstraintLayout借助com.android.support:transition可以很方便的实现动画。
- RelativeLayout虽然也比较灵活,但是需要对子View纵向和横向进行两次测量,效率相对较低。并且RelativeLayout的大多数场景均可被替代,代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// ...
// 进行第一个横向的测量
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
int[] rules = params.getRules(layoutDirection);
applyHorizontalSizeRules(params, myWidth, rules);
measureChildHorizontal(child, params, myWidth, myHeight);
// ...
}
}
// ..
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE) {
final LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight, child.getBaseline());
// 第二次测量
measureChild(child, params, myWidth, myHeight);
// ...
}
}
// .......
}
使用ConstraintLayout时,目的是为了减少嵌套,不然灵活性的优势不能体现,与其他的ViewGroup书写性能差异就不大了,甚至是弱于部分布局,比如Framelayout。
二、去除无用View嵌套
措施主要有两个:
1、借助ConstraintLayout减少过多的子ViewGroup的嵌套,尽量减少层级,能不多写ViewGroup的嵌套即不写。
2、对于部分场景,巧用Merge元素
- 自定义View使用了layout.xml的定义,如下:
class PopCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
init {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.pop_card_view, this)
}
}
那么在pop_card_view.xml的布局里,根布局不用设置为任何其他的ViewGroup,而是写成Merge:
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
</merge>
这里通过tools:parentTag,可以标识当前的父布局,仅可方便子元素的编写,并不会运行时生效。
注:merge标识的布局,如果需要动态加载,也可以执行:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
其中root不可为空,attachToRoot必须为true,因为merge说到底不是一个View,他必须依赖父View存在。
- Include 方式复用布局时,可以使用
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/include_inner_layout"/>
include_inner_layout的根布局,也可以不是一个特定的ViewGroup,而是一个Merge节点,例如:
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<View
android:layout_width="match_parent"
android:layout_height="0.5dp" />
</merge>
相当于直接把View加入到父布局中,并且merge不是View,他设置的属性均不生效。
注: Merge必须是xml布局的根节点
三、异步布局加载
Android提供了异步Layout加载的辅助类AsyncLayoutInflater。对于一些View不需要最快速度展现,可以通过AsyncLayoutInflater加载来降低对主线程的占用。AsyncLayoutInflater的实现原理很简单,可以看一下主要代码:
public AsyncLayoutInflater(@NonNull Context context) {
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
@NonNull OnInflateFinishedListener callback) {
if (callback == null) {
throw new NullPointerException("callback argument may not be null!");
}
InflateRequest request = mInflateThread.obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
mInflateThread.enqueue(request);
}
AsyncLayoutInflater会借助一个单例的常驻线程(InflateThread)实现,InflateThread又维护着一个阻塞队列,用于存储inflate的请求:
private static class InflateThread extends Thread {
// 阻塞队列,维护inflate的请求
private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
public void runInner() {
InflateRequest request;
// 从队列中取出inflate请求
try {
request = mQueue.take();
} catch (InterruptedException ex) {
}
// 执行inflate
try {
request.view = request.inflater.mInflater.inflate(
request.resid, request.parent, false);
} catch (RuntimeException ex) {
}
// 通过handler抛回主线程,回调告知加载完成
Message.obtain(request.inflater.mHandler, 0, request)
.sendToTarget();
}
@Override
public void run() {
while (true) {
runInner();
}
}
// inflate请求入队
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
}
}
}
这其实就是一个典型的生产者消费者模型,需要inflate View的请求生产出来,InflateThread不断地去消费请求,并且通过handler抛回主线程回调告知inflate完成。
AsyncLayoutInflater用于异步加载的时候,需要按照队列的先后顺序进行inflate,所以过多的异步加载,会降低加载速度,异步加载的线程为默认优先级,最终与主线程在竞争时,不能够尽快处理,所以一般速度会慢于主线程直接加载。
所以AsyncLayoutInflater仅仅只能用于,较为复杂的布局,并且不需要即时加载的场景。
四、View的延迟加载
很多时候,我们布局有部分View不需要在开始时加载,仅仅在后续某种特定场景下才进行加载。这就需要利用动态加载来减少对View的集中加载,有两个主要措施:
- View动态化
相当于所有的View都是动态add的,在需要的时候,才调用addView使用,这种方式成本相对较高。 - ViewStub
ViewStub 是android官方提供的动态方案,ViewStub本身相当于一个占位符,当真正需要的时候,调用ViewStub.inflate方法才会真正加载布局,不妨看一下源码:
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view = inflateViewNoAdd(parent);
replaceSelfWithView(view, parent);
mInflatedViewRef = new WeakReference<>(view);
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
return view;
}
}
}
主要实现在inflateViewNoAdd和replaceSelfWithView中:
private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
// 通过LayoutInflater加载布局
final View view = factory.inflate(mLayoutResource, parent, false);
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
return view;
}
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
parent.removeViewInLayout(this);
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 将inflate的View 加入父布局,并且会使用ViewStub的布局参数
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
相当于android帮我们做了动态addView的工作,他会在需要的时候,加载那部分的layout.xml。所以ViewStub也不是View,他是一个缓存着部分layout.xml和布局属性的占位对象,当需要的时候,就会动态的去添加。
五、减少onMeasure和onLayout
我们知道,我们去新增View会调用addView,而addView一定会触发requestLayout函数:
public void requestLayout() {
// 清空测量缓存值
if (mMeasureCache != null) mMeasureCache.clear();
// ...
// 给当前View打PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED标
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 判断当前父View是否已经在执行layout的过程中,isLayoutRequested
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
//..
}
相当于requestLayout,除了给自己打标PFLAG_FORCE_LAYOUT,还会给自己的父View打标,一直循环下去,直到到Activity的根View——DecorView,而DecorView的ViewParent则是ViewRootImpl,ViewRootImpl不是View,他只是处理View的回调和点击等事件的,所以最终调用的是ViewRootImpl的requestLayout方法:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
scheduleTraversals会提交绘制任务,等待VSYNC信号回来,即触发绘制流程,绘制流程就会触发我们熟悉的三大绘制流程:
performMeasure
performLayout
performDraw
继而会遍历调用所有子View的绘制流程。
首先看一下measure的过程:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ....
// 判断是否打标PFLAG_FORCE_LAYOUT,requestLayout会打此标
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// 判断布局参数是否变更
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
// 测量方式是否是MeasureSpec.EXACTLY,也就是指定大小的测量方式
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
// 测量值是不是与MeasureSpec.getSize()中直接取到的尺寸参数一样
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
// sAlwaysRemeasureExactly 是一个标识,M版本之前,为True,也就是说onMeasure的优化在M版本之后生效
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// 如果没有缓存值,则重新调用onMeasure的过程,缓存值在requestLayout时被清空
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 打标PFLAG_LAYOUT_REQUIRED,后续执行onLayout的布局过程
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
// 缓存测量值
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL);
}
主要看一下forceLayout || needsLayout这个条件,涉及两个值,一个是forceLayout,也就是View被打标PFLAG_FORCE_LAYOUT,这个标我们看到requestLayout会打,所以说requestLayout一定会触发onMeasure。
另一个是needsLayout,此变量依赖sAlwaysRemeasureExactly、isSpecExactly和matchesSpecSize:
- sAlwaysRemeasureExactly
M版本限制,M版本之后才有的onMeasure优化 - isSpecExactly
长宽是否是MeasureSpec.EXACTLY测量方式,也就是View的不是设置的固定长宽,或者match_parent - matchesSpecSize
当前估算的mMeasuredWidth和mMeasuredHeight是不是与MeasureSpec.getSize()直接取得的相同。这个条件主要原因是View默认的onMeasure方法如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// ...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}
也就是说MeasureSpec.EXACTLY和MeasureSpec.AT_MOST测量方式时,默认取得长宽均是通过MeasureSpec.getSize()获取。
从之前的代码可以看到,需要保证sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize时,才会不进行onMeasure的过程,意味着需要满足三个条件 1、不小于Android M版本 2、布局长宽定长或者match_parent 3、之前估算过的长宽与本次默认的估算值一致。
这里可能有读者要问,如果是自己实现的onMeasure方法的方式,并且满足上述的条件,会不会重新触发onMeasure的过程呢?答案是不会的,从代码中有个重要的mMeasureCache成员变量,其会缓存onMeasure计算的测量宽高,此时就不会走进onMeasure,具体见代码段:
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED
还需要注意的是,两种情况,都会给View打标PFLAG_LAYOUT_REQUIRED。
故而对于我们书写而言,即尽量使用match_parent或者固定长度的宽高,这样在父布局的测量宽高没有发生变更时,一般是不会触发子布局的onMeasure过程的。
上面我们提到的PFLAG_LAYOUT_REQUIRED标很重要,其是触发onLayout的罪魁祸首,不妨看一下源码中主要的布局(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) {
// 大多数子View进行布局需要Override的实现
onLayout(changed, l, t, r, b);
// ....
// 清空PFLAG_LAYOUT_REQUIRED标
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
// .....
}
// ...
}
也就是说,满足changed 或 (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED的任意一种条件,即重新执行layout中的布局过程,changed的条件看一下setFrame函数,该函数主要用来计算当前View的大小和位置:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
//....
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// 执行invalidate操作
invalidate(sizeChanged)
}
return changed;
}
mLeft、mRight、mTop、mBottom都是相对于父布局的坐标位置,一旦发生了变化,则认为是变更了,才需要重新计算。而另外一个条件(mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED 判断当前View是否打标PFLAG_LAYOUT_REQUIRED,却可以一定程度避免,即尽量的去减少调用View的requestLayout方法,除此以外,通过之前分析measure的过程可知,尽量使用match_parent或者固定长度的宽高,在父布局刷新布局时,也能够减少触发onLayout。
六、invalidate与draw如何使用
从上节layout的过程中,我们发现只要坐标位置发生变更,会触发invalidate函数,invalidate函数与View的重绘息息相关,不妨看一下invalidate的实现:
public void invalidate() {
invalidate(true);
}
// 内部方法,invalidateCache表示是否重绘时使用缓存,大多数情况invalidateCache为true
@UnsupportedAppUsage
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
继续追一下invalidateInternal函数:
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
// ....
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)) {
// 清空PFLAG_DRAWN,也就是设置为未绘制状态
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
// ....
mPrivateFlags |= PFLAG_DIRTY;
// invalidateCache 为true时,需要打标PFLAG_INVALIDATED,并且清空PFLAG_DRAWING_CACHE_VALID
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
// ...
}
}
这里打标PFLAG_INVALIDATED即表示后续需要重绘,PFLAG_DRAWING_CACHE_VALID则会清空缓存,缓存主要用于LAYER_TYPE_SOFTWARE绘制方式时会缓存View的bitmap数据,实际上也是标识后续需要重绘。
除了打标,上述函数主要执行父View的invalidateChild:
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
// 如果支持硬件加速
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
ViewParent parent = this;
if (attachInfo != null) {
// .....
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
// .....
if (view != null) {
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
}
}
parent = parent.invalidateChildInParent(location, dirty);
// ...
} while (parent != null);
}
}
首先看一下硬件加速时调用函数onDescendantInvalidated:
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);
if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {
mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;.
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// 如果绘制方式是LAYER_TYPE_SOFTWARE,也就是绘制为bitmap
if (mLayerType == LAYER_TYPE_SOFTWARE) {
mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
target = this;
}
// 循环向上调用
if (mParent != null) {
mParent.onDescendantInvalidated(this, target);
}
}
如果不支持硬件加速,则主要循环调用invalidateChildInParent,invalidateChildInParent实际上是确认当前View需要显示的位置和大小信息,并且返回其父View:
@Override
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
// PFLAG_DRAWN标记已绘制过,PFLAG_DRAWING_CACHE_VALID标识有绘制缓存,也就是满足其一,则需要打标以完成后续重绘
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
// 清空当前View的PFLAG_DRAWING_CACHE_VALID,也就是绘制缓存不生效了
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
// 如果当前绘制方式不是默认的LAYER_TYPE_NONE,则需要打标PFLAG_INVALIDATED
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
return mParent;
}
return null;
}
可以看到子view执行invalidate之后,也会清空ViewTree上所有父View的绘制缓存,即PFLAG_DRAWING_CACHE_VALID标识,如果绘制方式不是LAYER_TYPE_NONE,则会给父View打标PFLAG_INVALIDATED,mLayerType总共有三种取值:
- LAYER_TYPE_NONE:view按一般方式绘制,不使用离屏缓冲.这是默认的行为.
- LAYER_TYPE_HARDWARE:如果应用被硬加速了,view会被绘制到一个硬件纹理中.如果应用没被硬加速,此类型的layer的行为同于LAYER_TYPE_SOFTWARE.
- LAYER_TYPE_SOFTWARE:view被绘制到一个bitmap中.
因为invalidateChild中循环获取parent调用,最终同样会调用到ViewRootImpl的对应方法:
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
// ....
invalidateRectOnScreen(dirty);
return null;
}
追一下invalidateRectOnScreen函数:
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
// 确定重绘区域在View的位置
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
final float appScale = mAttachInfo.mApplicationScale;
// 获取最终的绘制区域,区域不为空,则返回true
final boolean intersected = localDirty.intersect(0, 0,
(int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
if (!intersected) {
localDirty.setEmpty();
}
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
// 执行一次测量、布局、绘制流程
scheduleTraversals();
}
}
可以看到invalidate会触发重新测量、布局、绘制的流程,但是实际上测量、布局不会去真的走,在ViewRootImpl中会通过mLayoutRequested标记为标识,这个标记 requestLayout()中才会标记为True,故而invalidate最终能触发的只有绘制流程。而绘制的流程也不一定会触发,需要满足条件!mWillDrawSoon && (intersected || mIsAnimating),其中mWillDrawSoon标识当前已经要进行draw的流程了,此标识可以达到去重作用。除此以外,intersected则表示有重绘区域,mIsAnimating则表示正在进行动画,这两种情况满足其一既可以触发重绘。
performDraw主要绘制流程实现在函数draw中:
private boolean draw(boolean fullRedrawNeeded) {
// ...
// 需要进行重绘
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty || mNextDrawUseBlastSync) {
if (isHardwareEnabled()) {
// ....
// 进行硬件重绘
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
// ....
// 进行软件重绘
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}
// ...
return useAsyncReport;
}
首先是硬件重绘ThreadedRenderer.draw -> ThreadedRenderer.updateRootDisplayList ->
ThreadedRenderer.updateViewTreeDisplayList :
private void updateViewTreeDisplayList(View view) {
view.mPrivateFlags |= View.PFLAG_DRAWN;
view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
== View.PFLAG_INVALIDATED;
view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
view.updateDisplayListIfDirty();
view.mRecreateDisplayList = false;
}
这里mRecreateDisplayList,完全依赖是否达标PFLAG_INVALIDATED,而这个打标过程我们从之前分析发现,只要mLayerType 不是 LAYER_TYPE_NONE,view的整个ViewTree都会打上此标,继续看View绘制流程updateDisplayListIfDirty:
public RenderNode updateDisplayListIfDirty() {
// ...
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
|| !renderNode.hasDisplayList()
|| (mRecreateDisplayList)) {
// 如果本身不需要重新创建display list,即没有打标过PFLAG_INVALIDATED
if (renderNode.hasDisplayList()
&& !mRecreateDisplayList) {
// 直接打标绘制完成
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
// 遍历所有的子View,判断是否需要重新创建display list
dispatchGetDisplayList();
return renderNode;
}
// ...
// 一旦需要重建display list
try {
// 如果设置绘制方式是LAYER_TYPE_SOFTWARE,则需要将View绘制为bitmap,并放在cache中
if (layerType == LAYER_TYPE_SOFTWARE) {
buildDrawingCache(true);
Bitmap cache = getDrawingCache(true);
if (cache != null) {
canvas.drawBitmap(cache, 0, 0, mLayerPaint);
}
} else {
// ...
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
// 当前View被打标PFLAG_SKIP_DRAW,也就是不需要绘制,则直接遍历子View
dispatchDraw(canvas);
// ...
} else {
// 执行当前View的draw方法,也会遍历子View
draw(canvas);
}
}
}
} else {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
return renderNode;
}
以上就是硬件draw方法,软件draw方法drawSoftware()的主体实现则是直接调用draw(canvas)并打标PFLAG_DRAWN,不妨看一下draw的实现:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
int saveCount;
// 绘制背景
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
// verticalEdges 和 horizontalEdges 表示需要绘制边界渐变的View,默认情况下不包含
if (!verticalEdges && !horizontalEdges) {
// 执行onDraw回调,也就是自定义View的主要实现位置
onDraw(canvas);
// 分配子View进行draw操作
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 绘制前景图片、scroll bar这些
onDrawForeground(canvas);
// 绘制焦点高亮
drawDefaultFocusHighlight(canvas);
return;
}
// ..
}
可以看到,如果软件重绘(不开启硬件加速),会直接执行View的draw(),这就会造成子View整个ViewTree的重绘,并不仅仅是当前View的父子View。反之,开启硬件加速时,并且绘制方式不是LAYER_TYPE_SOFTWARE,则只会给当前View打标PFLAG_INVALIDATED,不需要对整个ViewTree进行重绘,因为LAYER_TYPE_SOFTWARE的方式,需要把View绘制成bitmap并cache下来,需要重绘整个ViewTree,不过由于bitmap前一次绘制会cache下来,其他的View就不用重复重新创建bitmap。
所以对于开启硬件加速的设备,则可以通过最小化invalidate()的范围,这样只会重绘当前View及其子View,并不会造成全ViewTree的重绘。
对于不开启硬件加速的设备,则在任意View里执行一次invalidate(),即可完成整个ViewTree的重绘。
invalidate与onDraw具有直接的关联性。
requestLayout如果在布局之后没有发生宽高值的变化,不会触发View的重绘,所以为了重绘,需要主动调用invalidate。
invalidate(int l, int t, int r, int b)和invalidate(Rect dirty)可以框定刷新范围,这种方式可以锁住画布,最终保证画布只绘制框定范围的View,但是在不开启硬件加速时,仍然会遍历执行整个ViewTree的draw方法,本身在绘制时有效率提升,只是不如开启硬件加速时刷新效率。
七、动画优化
举个例子:单View多动画交叉播放,如果创建多个ObjectAnimator,实现一个View的放大动画,并且伴随透明度从0到1。
ObjectAnimator animator1 = ObjectAnimator.ofFloat(view , "scaleX", 0f, 1.3f);
animator1.setDuration(100L);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(view , "scaleX", 1.3f, 1.0f);
animator2.setDuration(200L);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(view , "scaleY", 0f, 1.3f);
animator3.setDuration(100L);
ObjectAnimator animator4 = ObjectAnimator.ofFloat(view , "scaleY", 1.3f, 1.0f);
animator4.setDuration(200L);
ObjectAnimator animator5 = ObjectAnimator.ofFloat(view , "alpha", 0.0f, 1.0f);
animator5.setDuration(400L);
AnimatorSet scaleX = new AnimatorSet();
scaleX.playSequentially(animator1, animator2);
AnimatorSet scaleY = new AnimatorSet();
scaleY.playSequentially(animator3, animator4);
AnimatorSet total = new AnimatorSet();
total.playTogether(scaleX, scaleY, animator5);
total.start();
创建过多的ObjectAnimator,本身对内存是有损耗的,并且这样的播放动画,ObjectAnimator需要各自独立执行动画的播放逻辑,对性能也存在损耗,ObjectAnimator本身提供了很好的聚合方法,PropertyValuesHolder和KeyFrame。PropertyValuesHolder可以聚合多种动画,KeyFrame对于单个动画也可以聚合,并且可以设定时间,于是代码可以写成:
Animator total = ObjectAnimator.ofPropertyValuesHolder(
view,
PropertyValuesHolder.ofFloat("alpha", 0f, 1f),
PropertyValuesHolder.ofKeyframe("scaleX", Keyframe.ofFloat(0.25f,0.0f,1.3f), Keyframe.ofFloat(0.75f,1.3f,1.0f)),
PropertyValuesHolder.ofKeyframe("scaleY", Keyframe.ofFloat(0.25f,0.0f,1.3f), Keyframe.ofFloat(0.75f,1.3f,1.0f))
);
total.setDuration(400L)
total.start();
Keyframe.ofFloat的第一个参数,表示整个过程的time,0.25f就是相当于400 * 0.25 = 100ms,可以用于分段控制duration。
针对PropertyValuesHolder,其设置动画的第一个参数有两种设置方法,这两种方法也有性能差距:
public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
return new FloatPropertyValuesHolder(propertyName, values);
}
public static PropertyValuesHolder ofFloat(Property<?, Float> property, float... values) {
return new FloatPropertyValuesHolder(property, values);
}
主要区别从初始化动画开始:
void initAnimation() {
if (!mInitialized) {
// target即View
final Object target = getTarget();
if (target != null) {
// mValues即所有属性动画的集合PropertyValuesHolder[],这边做遍历执行
final int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].setupSetterAndGetter(target);
}
}
super.initAnimation();
}
}
看一下setupSetterAndGetter方法:
void setupSetterAndGetter(Object target) {
// 逻辑1
if (mProperty != null) {
// 检查KeyFrame 是否与PropertyValuesHolder的匹配
// ....
}
// 逻辑2
if (mProperty == null) {
Class targetClass = target.getClass();
if (mSetter == null) {
setupSetter(targetClass);
}
// 检查KeyFrame 是否与PropertyValuesHolder的匹配
// ...
}
}
这里如果调用的是PropertyValuesHolder ofFloat(Property<?, Float> property, float... values)方法,已经初始化了mProperty,这里就不需要走逻辑2,PropertyValuesHolder ofFloat(String propertyName, float... values)就需要走,逻辑2的主要实现见setupSetter:
void setupSetter(Class targetClass) {
Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType();
mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType);
}
private Method setupSetterOrGetter(Class targetClass,
HashMap<Class, HashMap<String, Method>> propertyMapMap,
String prefix, Class valueType) {
Method setterOrGetter = null;
synchronized(propertyMapMap) {
HashMap<String, Method> propertyMap = propertyMapMap.get(targetClass);
boolean wasInMap = false;
if (propertyMap != null) {
wasInMap = propertyMap.containsKey(mPropertyName);
if (wasInMap) {
setterOrGetter = propertyMap.get(mPropertyName);
}
}
if (!wasInMap) {
// 反射获取targetClass对应的属性变更方法
setterOrGetter = getPropertyFunction(targetClass, prefix, valueType);
if (propertyMap == null) {
propertyMap = new HashMap<String, Method>();
propertyMapMap.put(targetClass, propertyMap);
}
propertyMap.put(mPropertyName, setterOrGetter);
}
}
return setterOrGetter;
}
可以看到,如果使用的PropertyValuesHolder ofFloat(String propertyName, float... values)方法,需要利用反射来获取对应的方法,比如propertyName为"rotation",就需要反射target的内部方法:
setRotation(float rotation)
getRotation()
如果直接配置Property,就可以省去反射的过程,大多数的情况下我们使用的target都是继承与View,则提供了对应的方法,减去反射的耗时,以rotation为例:
public static final Property<View, Float> ROTATION = new FloatProperty<View>("rotation") {
@Override
public void setValue(View object, float value) {
object.setRotation(value);
}
@Override
public Float get(View object) {
return object.getRotation();
}
};
从上述代码分析,我们也可以看到ObjectAnimator不仅仅是为了View动画做的,他可以用于任何Object的属性动画的实现,只是需要Object有对应方法的实现。
为什么少用反射:https://www.skoumal.com/en/is-android-reflection-really-slow/
八、xml无需初始化的属性不要设置
很多时候layout.xml会定义ImageView:
<ImageView
android:id="@+id/XXXXXXX"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/XXXX" />
而在代码中,也需要动态初始化ImageView的初始值:
if(isNew){
imageView.setImageResource(R.drawable.XXXX)
} else {
imageView.setImageResource(R.drawable.XXXX2) // 如果执行此逻辑,那设置@drawable/XXXX就是多余的
}
此时就不需要在xml里写android:src,对图片的载入都有无用损耗(需要注意的是,如果重复设置同一个drawable,android本身对drawable有缓存操作,可以进行重入)。如果为了方便预览可以写成:
<ImageView
android:id="@+id/XXXXXXX"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:src="@drawable/XXXXX" />
这里仅仅是举例ImageView的场景,很多其他的属性同样也有此问题
注:多次调用setImageResource,不会立即造成ImageView的重绘,他还是需要等待VSYNC信号回来,执行onDraw才会重绘的,只是setImageResource(@DrawableRes int resId)设置drawable时,如果每次设置的drawable不同,会造成多次drawable的创建。
注:ImageView使用时,建议定长高,尽量不设置为wrap_content,因为一旦图片长宽变更,就会调用requestLayout,反之则只会调用invalidate。