Android alpha动画隐形成本优化

前言

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

谨慎使用 Alpha

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

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

  • 第一次是绘制不透明的画面
  • 第二次是对alpha通道的颜色进行混合

为什么要这么做呢?主要原因是绘制时,多层渲染逻辑会非透明度会叠加,导致一些重要的元素看不清。

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

解决办法

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

减少alpha绘制

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

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

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

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

复写hasOverlappingRendering

对于明确不需要剔除叠加效果的View,直接让其返回不支持叠加渲染,当然,一般的非ViewGroup子类是优化目标(因为ViewGroup默认不绘制),在确认不需要叠加剔除时,直接告诉渲染器不进行混合。

java 复制代码
class MyView extends View {

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

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

使用缓冲

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

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

sql 复制代码
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)的时候,恰好规避了硬件绘制。

我们知道software绘制共用一个Canvas,那么意味着使用软件绘制的效果是差🆚混合后的效果。

官方参考代码

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

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中也提供了绘制方法

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

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

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

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

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

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中调用此方法

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);
        }

使用方式方式也很简单

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

libhwui Crash

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

java 复制代码
#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)
android.view.HardwareRen

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

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

关于choreographer和view#post

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

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

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

总结

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

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

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

相关推荐
姜 萌@cnblogs7 分钟前
【实战】深入浅出 Rust 并发:RwLock 与 Mutex 在 Tauri 项目中的实践
前端·ai·rust·tauri
蓝天白云下遛狗14 分钟前
google-Chrome常用插件
前端·chrome
limingade1 小时前
手机打电话时如何将通话对方的声音在手机上识别成文字
android·智能手机·语音识别·funasr·蓝牙电话·ai电话机器人·funasr安卓移植和部署
多多*1 小时前
Spring之Bean的初始化 Bean的生命周期 全站式解析
java·开发语言·前端·数据库·后端·spring·servlet
linweidong1 小时前
在企业级应用中,你如何构建一个全面的前端测试策略,包括单元测试、集成测试、端到端测试
前端·selenium·单元测试·集成测试·前端面试·mocha·前端面经
努力学习的小廉1 小时前
深入了解linux系统—— 基础IO(上)
android·linux·运维
满怀10151 小时前
【HTML 全栈进阶】从语义化到现代 Web 开发实战
前端·html
东锋1.31 小时前
前端动画库 Anime.js 的V4 版本,兼容 Vue、React
前端·javascript·vue.js
tmacfrank2 小时前
Android 性能优化入门(一)—— 数据结构优化
android·数据结构·性能优化
满怀10152 小时前
【Flask全栈开发指南】从零构建企业级Web应用
前端·python·flask·后端开发·全栈开发