Android View优化策略与分析

一、复杂场景中尽量使用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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值