Android Alpha动画隐形成本优化 前言 解决办法

前言

Android 开发指导中有这样一条建议:

谨慎使用 Alpha

当您使用 setAlpha()AlphaAnimationObjectAnimator 将视图设置为半透明时,该视图会在屏幕外缓冲区渲染,导致所需的填充率翻倍。在超大视图上应用 Alpha 时,请考虑将视图的层类型设置为 LAYER_TYPE_HARDWARE

fire_104.gif

在Android中,关于Alpha透明度的绘制是比较耗时的,一个主要的原因是Alpha图层绘制需要进行两次绘制:

  • 第一次是创建硬件缓冲区,然后绘制不透明的画面
  • 第二次是对alpha通道的颜色进行混合,丢弃硬件缓冲区的是不透明画面

为什么要这么做呢?主要原因是绘制时,多层View叠加渲染会导致非透明度叠加,进而导致靠近底层的绘制越来越模糊,为了解决此类问题,Android需要对视图进行混合,而不是简单的叠加。

如下图所示,左侧是Android混合优化的效果,右侧是叠加的效果:

fire_1003.gif

解决办法

其实在Android 官方的指导中,提供了一些方案,详细见《减少过度绘制》,同时对alpha绘制提出了优化建议,就是使用缓冲区,参考视频:《透明度绘制的隐形成本

减少alpha绘制

在屏幕上渲染透明像素,即所谓的透明度渲染,是导致过度绘制的重要因素。

在普通的过度绘制中,系统会在已绘制的现有像素上绘制不透明的像素,从而将其完全遮盖,与此不同的是,透明对象需要先绘制现有的像素,以便达到正确的混合效果。

诸如透明动画、淡出和阴影之类的视觉效果都会涉及某种透明度,因此有可能导致严重的过度绘制。您可以减少要渲染的透明对象的数量,以此来改善这些情况下的过度绘制。例如,如需获得灰色文本,您可以在 TextView中绘制黑色文本,再为其设置一个半透明的透明度值。但是,您可以通过用灰色绘制文本来获得同样的效果,而且能够提升性能。

总结一下: 你可以使用color#argb设置透明色值去绘制,而不是使用View#setAlpha,以此减少alpha绘制范围。

复写hasOverlappingRendering

对于明确不需要剔除叠加效果的View,即便是存在半透明效果的情况下,直接让其返回false,通知渲染器不需要叠渲染,因此也不会进行混合渲染。这种方式直接告诉渲染器不进行混合,因此不会创建硬件缓冲区。

1
2
3
4
5
6
7
java复制代码class MyView extends View {

@Override
public boolean hasOverlappingRendering() {
return false;
}
}

适用于两种情况:

  • View不存在叠加,只有一层View存在color或者drawable的情况
  • View存在叠加,但是顶部View是非透明的,底部就可以不叠加渲染,典型的就是CardView阴影部分

下面是绘制性能对比,当然你得取舍,如果没有View层级重叠问题,这个还是可以考虑的。

fire_1003.gif

使用缓冲

前面提到过,为了提出非透明度叠加效果,Android团队做了优化,但这个过程中,非透明的像素缓冲绘制完直接丢弃了,原因是因为基于有限的条件,无法知晓后续是不是需要此内存的数据,因此直接释放了内存,显然是明智的做法。但是,这种丢弃显然会引发内存抖动问题,因为一旦触发绘制,就需要重新申请内存。

官方提供的建议是使用下面方式优化

1
java复制代码   View#setLayerType(View.LAYER_TYPE_HARDWARE, null)

注意事项

这里我们补充一下关于View#setLayerType,此方法可以接受三种类型LAYER_TYPE_SOFTWARE、LAYER_TYPE_HARDWARE、LAYER_TYPE_NONE,对于前两者,都是创建相应的缓冲区,而LAYER_TYPE_NONE仅仅是销毁缓冲区。

  • 注意1: setLayerType可以开启硬件加速的是一个错误的理解,因为硬件加速在View绘制前的Activity中就已经标记好了。setLayerType(LAYER_TYPE_HARDWARE,null) 相当于在此基础上创建了个缓冲区 (FBO)。
  • 注意2:setLayerType(LAYER_TYPE_SOFTWARE,null) 关闭硬件缓冲区的理解是副作用引发的,其创建软件缓冲区(Bitmap)的时候,恰好规避了硬件绘制,因为LAYER_TYPE_SOFTWARE会让子View节点共享Canvas,从而达到图像合成。

官方参考代码

其实任何动画都可以优化,特别是对于alpha、rotation动画

1
2
3
4
5
6
7
8
9
java复制代码view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        view.setLayerType(View.LAYER_TYPE_NONE, null);
    }
});
animator.start();

同样,Android中也提供了绘制方法

1
java复制代码ViewPropertyAnimator.alpha(0.0f).withLayer();

但是通用性上来说还有些差,此外对于AnimatorSet可能产生大量重复方法,有没有更加方便的方法呢,我们的突破点在AnimatorListener这里,我们利用动画的生命周期方法进行设置会更加方便。

1
2
3
4
5
6
java复制代码public void addListener(AnimatorListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<AnimatorListener>();
}
mListeners.add(listener);
}

接下来我们对alpha动画进行优化

当然,这里有个重要的方法,就是查找是否包含alpha动画

1
2
3
4
5
6
7
8
9
java复制代码    static boolean shouldRunOnHWLayer(View v, Animator anim) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT || v == null || anim == null) {
return false;
}
boolean shouldRunOnHWLayer = v.getLayerType() == View.LAYER_TYPE_NONE
&& v.hasOverlappingRendering()
&& modifiesAlpha(anim);
return shouldRunOnHWLayer;
}

接着,我们在onAnimationStart和onAnimationEnd中调用此方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码       @Override
public void onAnimationStart(Animator animation) {
mShouldRunOnHWLayer = shouldRunOnHWLayer(mView, animation);
if (mShouldRunOnHWLayer) {
oldLayerType = mView.getLayerType();
MLog.d(TAG,"onAnimationStart post");
View targetView = mView;
targetView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
}

@Override
public void onAnimationEnd(Animator animation) {
if (mShouldRunOnHWLayer) {
MLog.d(TAG,"onAnimationEnd post");
View targetView = mView;
targetView.setLayerType(oldLayerType, null);
}
mView = null;
animation.removeListener(this);
}

使用方式方式也很简单

1
2
3
java复制代码AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(rotatioOpen,alphaHide,rotatioClose,alphaShow);
return AnimatorOptimizer.optimize(animatorSet,v);

这样就结束了,然而,这个优化还不够完美,在一些设备上,会造成libhwui中出现SEGV_MAPERR,而我们知道,SEGV_MAPERR一般类似java中的空指针,如何避免这个问题呢?我们先来看看问题。

libhwui Crash

很显然,这个优化是有副作用的,在上线之后陆陆续续收到一些libhwui中的crash问题的,经过一些测试发现是setLayerType引发的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)

#02 pc 00015d7d /system/lib/libhwui.so [armeabi-v7a]
#03 pc 00014517 /system/lib/libhwui.so [armeabi-v7a]
#04 pc 0001440b /system/lib/libhwui.so [armeabi-v7a]
#05 pc 0001d133 /system/lib/libhwui.so [armeabi-v7a]
#06 pc 000679c5 /system/lib/libandroid_runtime.so [armeabi-v7a]
#07 pc 0002054c /system/lib/libdvm.so (dvmPlatformInvoke +112) [armeabi-v7a]
#08 pc 0005132f /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*) +398) [armeabi-v7a]
#09 pc 000299e0 /system/lib/libdvm.so [armeabi-v7a]
#10 pc 00030f48 /system/lib/libdvm.so (dvmMterpStd(Thread*) +76) [armeabi-v7a]
#11 pc 0002e5e0 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*) +184) [armeabi-v7a]
#12 pc 00063af9 /system/lib/libdvm.so (dvmInvokeMethod(Object*, Method const*, ArrayObject*, ArrayObject*, ClassObject*, bool) +392) [armeabi-v7a]
#13 pc 0006ba1f /system/lib/libdvm.so [armeabi-v7a]
#14 pc 000299e0 /system/lib/libdvm.so [armeabi-v7a]
#15 pc 00030f48 /system/lib/libdvm.so (dvmMterpStd(Thread*) +76) [armeabi-v7a]
#16 pc 0002e5e0 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*) +184) [armeabi-v7a]
#17 pc 00063815 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list) +336) [armeabi-v7a]
#18 pc 0004cf17 /system/lib/libdvm.so [armeabi-v7a]
#19 pc 0004dfcf /system/lib/libandroid_runtime.so [armeabi-v7a]
#20 pc 0004ed27 /system/lib/libandroid_runtime.so (android::AndroidRuntime::start(char const*, char const*) +354) [armeabi-v7a]
#21 pc 0000109b /system/bin/app_process
#22 pc 0000e563 /system/lib/libc.so (__libc_init +50) [armeabi-v7a]
#23 pc 00000db0 /system/bin/app_process

java:
android.view.GLES20Canvas.nDrawDisplayList(Native Method)
android.view.GLES20Canvas.drawDisplayList(GLES20Canvas.java:420)
android.view.HardwareRenderer$GlRenderer.drawDisplayList(HardwareRenderer.java:1646)

具体原因很难分析出来,但是,在stackoverflow中有个解决的方法,就是保证在View attachedToWindow之后进行setLayerType操作,他是怎么做的呢?

同样还是在 onAnimationStart和onAnimatonEnd中进行优化的,就是在View#post方法中执行setLayerType,上线后这种问题下降到正常水平。

实际上ViewPropertyAnimator.alpha(0.0f).withLayer();中也做了类似的优化,但是,在onAnimationEnd中却没有这样做,为了保险起见,建议onAnimationStart和onAnimationEnd两者都做post逻辑。

至于为什么crash,目前没有定位到,主要是发生在线上版本,但是如果我们从Choreographer和View#post的角度观察,猜测和View RenderNode Layer 被detatchFromWindow后,底层的调用关闭方法时,发现RenderNode相关变量已经释放了,从而导致类似NullPointerException的Native Crash。

这里我们对比下两种调用的区别。

关于choreographer和view#post

我们知道,动画的执行都是通过choreographer实现的,同样包括补间动画,但是这也使得choreographer能够在脱离View生命周期的情况下正常执行,如ValueAnimator的使用。为了简单起见,我们换个对比方式,View#postAnimation和View#post

  • 前者和动画的更新原理一样,都是通过choreographer驱动,后者是通过Handler驱动
  • 前者脱离View生命周期依然可以执行,后者会停止执行,并缓冲仍未到TaskQueue中

很显然,View#postAnimation的执行可能在View被添加前和移除后都能执行,显然由此引发了不安全问题。而动画也是类似的情况。

完整代码

下面是本篇封装的完整代码,我们需要首先判断是否存在alpha动画,有的话加一个监听器,然后在动画开始时设置硬件缓冲,结束时关闭缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
java复制代码public class AnimatorOptimizer {

private static final String TAG = "AnimatorOptimizer";

public static <T extends Animator> T optimize(T animator, View view) {
if(shouldRunOnHWLayer(view,animator)) {
animator.addListener(new AnimatorOnHWLayerIfNeededListener(view));
}
return animator;
}

static class AnimatorOnHWLayerIfNeededListener extends AnimatorListenerAdapter {
private boolean mShouldRunOnHWLayer = false;
private View mView;
private int oldLayerType;

public AnimatorOnHWLayerIfNeededListener(final View v) {
if (v == null) {
return;
}
mView = v;
}

@Override
public void onAnimationStart(Animator animation) {
mShouldRunOnHWLayer = shouldRunOnHWLayer(mView, animation);
if (mShouldRunOnHWLayer) {
oldLayerType = mView.getLayerType();
MLog.d(TAG,"onAnimationStart post");
View targetView = mView;
mView.post(new Runnable() {
@Override
public void run() {
SLog.d(TAG,"onAnimationStart");
targetView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
});
}
}

@Override
public void onAnimationEnd(Animator animation) {
if (mShouldRunOnHWLayer) {
MLog.d(TAG,"onAnimationEnd post");
View targetView = mView;
mView.post(new Runnable() {
@Override
public void run() {
SLog.d(TAG,"onAnimationEnd");
targetView.setLayerType(oldLayerType, null);
}
});
}
mView = null;
animation.removeListener(this);
}

}

static boolean shouldRunOnHWLayer(View v, Animator anim) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT || v == null || anim == null) {
return false;
}
boolean shouldRunOnHWLayer = v.getLayerType() == View.LAYER_TYPE_NONE
&& v.hasOverlappingRendering()
&& modifiesAlpha(anim);
return shouldRunOnHWLayer;
}

static boolean modifiesAlpha(Animator anim) {
if (anim == null) {
return false;
}
if (anim instanceof ValueAnimator) {
ValueAnimator valueAnim = (ValueAnimator) anim;
PropertyValuesHolder[] values = valueAnim.getValues();
for (int i = 0; i < values.length; i++) {
if (("alpha").equals(values[i].getPropertyName())) {
return true;
}
}
} else if (anim instanceof AnimatorSet) {
List<Animator> animList = ((AnimatorSet) anim).getChildAnimations();
for (int i = 0; i < animList.size(); i++) {
if (modifiesAlpha(animList.get(i))) {
return true;
}
}
}
return false;
}

}

总结

本篇就到这里了,动画问题一直是个大问题,在低端设备上会放大的很明显,我们常用的手段主要如下:

  • 动画合并计算
  • 减少绘制区域
  • 减少过度绘制
  • 利用SurfaceView或者GLSurfaceView渲染
  • 异步解码
  • Bitmap 复用

本篇是对特定动画的优化,上一篇的《同频共帧》 是减少对称动画绘制区域最有效的方案,希望本篇对大家有所帮助。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%