背景
在一款新的机型上,测试同学报告了一个问题,开机的时候,开机动画会很明显地掉帧,使得开机画面看上去卡卡的,极不流畅。在后面分析完这个问题后,发现它是一个在高版本Android上新增的功能触发的问题,和此前碰到的开机动画卡顿问题都不一样,特此记录分享。
初步分析
开机动画如果出了问题,我们很容易想到,也应该首要想到,就是看看最近有没有改过开机动画、开机动画会不会太大、分辨率会不会太高等等。再有就是Kernel启动过程中打了太多的日志、驱动初始化的时候delay了太多,以及屏驱动对屏被设置了不当的参数等等。另外,还可以通过对比新旧版本、AOSP默认开机动画等方法来大概定位到是什么样的变更导致的卡顿问题。
缩小问题范围:卡顿掉帧只发生在zygote重启时
Android的开机动画,是贯穿了Kernel启动、zygote启动、system_server启动三大主要阶段的。这三个阶段分别都做了很多工作,都有影响开机动画流畅性的可能。比如Kernel的驱动、zygote对IO和CPU资源的竞争、system_server的代码逻辑等等。因此做了如下几个实验,分割一下问题范围。
- 在设备开好机、进入到Launcher的状态下,主动启动BootAnimation来在当前界面上面叠加播放开机动画(可以通过命令setprop service.bootanim.exit 0; setprop ctl.start bootanim来实现。这种启动方式完全和AOSP标准、原生的开机动画启动方式一致)。这个实验做完,如果动画掉帧的话,那么问题就是BootAnimation内部自身的问题。否则,它就是被别的什么东西影响、拖累的。——实验结果:流畅不掉帧,说明问题由其他模块导致,排除BootAnimation自身问题。
- 仅重启system_server(可以通过命令am restart或killall system_server来实现。其中,killall system_server的时候要主动启动一下BootAnimation)。system_server重启过程中,观察开机动画,如果发生掉帧,结合第一步做的实验,则说明卡顿和system_server的启动是强相关的。——实验结果:流畅不掉帧,说明卡顿问题不是system_server启动过程导致的。
- 重启zygote(先执行stop将zygote杀死,再执行start启动zygote)。如果掉帧说明卡顿和zygote的启动过程强相关——实验结果:发生卡顿掉帧,说明和zygote启动流程强相关。
到目前能够把问题的范围缩小到zygote启动流程上了。但是zygote本身做的工作很多,还需要进一步的实验把范围缩得更小。
排查硬件性能瓶颈:卡顿不是来源于CPU/IO瓶颈
在进一步分析zygote的启动流程为什么影响了BootAnimation的帧率之前,先来调整一下BootAnimation的硬件资源分配策略。这么做是为了了解清楚卡顿的原因和硬件性能有多大的相关性。调整的主要对象是BootAnimation和Zygote两者,调整一下它们的工作量和CPU、RAM、IO等资源,检验一下掉帧问题和硬件性能关系大不大。
-
降低BootAnimation的工作量,提高BootAnimation在运行的时候的优先级
-
将BootAniamtion的帧率降低到24帧
-
调整init配置bootanim.rc中对BootAnimation的进程优先级配置,提高进程优先级(bootanim.rc增加priority -20)
-
调整init配置bootanim.rc中对BootAnimation的cgroup资源优先级配置,将bootanimation进程放在性能最高最强的cgroup节点下(bootanim.rc增加task_profiles ProcessCapacityMax MaxPerformance)
-
调整init配置bootanim.rc中对BootAnimation的io优先级配置,使其处于最高io优先级(bootanim.rc增加iopriority 0)
-
降低zygote自动带来的资源消耗,降低优先级
-
zygote重启过程中,会通过rc文件去重启media、camera、net等系列服务。将这些服务的重启声明移除掉,避免重启这些附带服务,减少它们对硬件性能的使用程度
-
调整init关于zygote的rc文件,移除其高优先级的taskprofile等
-
可以考虑减少zygote preloaded-class的数量,Java class的预加载的过程是一个消耗CPU和IO资源的重量级操作
-
将DDR锁定在最高频率
-
在卡顿过程中实时记录CPU、IO使用率
这一系列的修改,可以一定程度地减少Zygote启动过程中的性能压力,并将性能分配尽可能向BootAnimation倾斜。把这些修改导入到设备后,重启zygote,发现掉帧卡顿的现象依然存在。此外,CPU和IO使用率也不高。说明卡顿并不是由于常规硬件性能瓶颈导致的。
分解BootAnimation动画流程:定位到耗时操作和GPU强相关
我们虽然将问题范围缩小到了「裁剪后」的Zygote启动流程,但是能不能像火焰图一样将问题范围缩小到函数级别呢?来看BootAnimation的显示流程,我们可以采用一个土办法——加日志算函数运行时间。
BootAnimation的核心流程非常简单,就是拿到并解析开机动画文件,然后将动画提供的图片序列进行解码(decode)然后借助OpenGLES进行显示。这个流程是比较简单的,代码量也小。因此我们在关键的代码调用前后加上日志,通过日志来记录函数的运行耗时。
我们主要在收到屏幕刷新事件(processDisplayEvents)、完成一帧开机动画的buffer分配和解码(initTexture和glClear)、上传至GPU并触发绘制(calling draw、done draw)这几个核心流程打印日志。并且在一帧数据绘制完成后把该帧总耗时和相对于不卡顿情况下耗时慢了多少毫秒打印出来(past和delay)。日志结果如下。
可以看到,掉帧的时候相对于合理的帧耗时delay高达100~400ms,initTexture耗时不小但是稳定(50ms左右),draw极其耗时且不稳定(100-300ms)。
到这里,由于99%+的耗时操作都和OpenGL操作有关,因此可以断言耗时操作是和GPU的性能和使用率强相关的了,这个维度的性能数据也是上一步中我们没有覆盖到的。
再次缩小问题范围:卡顿发生在zygote重启流程的子集SurfaceFlinger重启流程中
前面几步我们有两个很关键的信息:问题和GPU有关、问题和Zygote有关。在Android中,SurfaceFlinger + RenderEngine + HWC负责管理GPU,也是为数不多的可能在开机过程中与GPU交互的组件。而且,SurfaceFlinger是在Zygote的启动流程中启动的(init rc配置如果zygote重启,那么SurfaceFlinger也会重启)。因此有理由怀疑SurfaceFlinger可能是卡顿问题的强相关。前面我们在对zygote作「精简」测试时,没有把SurfaceFlinger的重启给屏蔽掉,进一步减少Zygote启动流程中的变量,是因为如果我们干掉SurfaceFlinger不让它重启,设备显示不了画面,就看不到开机动画有没有卡顿掉帧了。
通过下面的命令做个实验,验证一下不启动Zygote,只启动SurfaceFlinger,看看我们的开机动画是否会卡顿。
stop # 关闭Zygote
start surfaceflinger # 启动surfaceflinger
实验的结果是,开机动画卡顿掉帧。现在可以断言,问题范围缩小至SurfaceFlinger启动流程中。
定位到问题根因:SkiaRenderEngine正在和BootAnimation竞争GPU
SurfaceFlinger对GPU的交互,实际上通过RenderEngine来实现的。高版本的Android用SkiaRenderEngine取代了低版本的GLESRenderEngine。因此,可以说绝大多数的GPU操作都应该在RenderEngine中找到蛛丝马迹。
通过找代码,还是看日志,以及对SurfaceFlinger、RenderEngine启动流程的经验,很快就发现RenderEngine在启动时对Shader的缓存操作耗时移除,接近5秒。
此外,在Shader缓存完成后,BootAnimation的帧率上升了,说明GPU瓶颈正在减退。
这个Shader缓存是做什么的文章的后面简要介绍一下。先简单讲讲它如何消耗了GPU资源。SkiaRenderEngine在启动过程中,通过若干个长得像下图的多级for循环嵌套绘图来产生GPU Shader缓存。
给这个Shader缓存流程加点「土办法」日志,可以看到确实非常耗时。
由于这些缓存操作只是为了「优化一下性能」,因此如果我们把它注释掉,是不会有功能问题的。我们做了一下这个实验,拿掉所有cache调用,发现开机动画不掉帧不卡顿了。
到这里,可以实锤了卡顿的原因是GPU性能短缺,元凶是这一段Shader cache性能优化代码。
怎么优化:关闭 Prime Shader cache
这个Shader cache,是在SurfaceFlinger开机过程中,模拟一下应用常见的绘图操作,绘图到一个空的buffer中,以此来让GPU产生OpenGLES资源的缓存。这些缓存能让首次绘制的时候减少资源的生成,提定程度上提升性能。
在代码中这个过程称为Prime Shader Cache:注入/填充Shader缓存。既然是为了优化性能,我们直接拿掉这段优化动作也是OK的。
实际上代码中提供了属性「service.sf.prime_shader_cache」来开关这个功能。当该属性设置为0的时候,SurfaceFlinger就不会调用SkiaRenderEngine来进行缓存操作。
这个Cache如果没有,那么在应用启动期间,如果有机会,还是会生成Cache的。Cache的信息可以从SurfaceFlinger中dump出来。
因此,最后解决这个卡顿问题的修改代码也比较简单,就是将属性「service.sf.prime_shader_cache」设置一下,关闭掉prime shader cache即可。