今年7月,三星发布了最新的折叠屏手机Galaxy Z Fold 6。我们团队的App需要适配这款新设备,但适配完成后,用户反馈说在Z Fold 6上应用特别卡,尤其是展开大屏的时候,切换页面要等好几秒。
我负责这个性能优化项目。经过一周的排查和调优,我们把应用的平均响应时间从1200毫秒降到了240毫秒,下降了80%。这篇文章,就来分享一下这次性能调优的完整过程,包括问题分析、瓶颈定位、具体优化手段,以及最终的效果。
背景:折叠屏带来的新挑战
先说说背景。我们的App是一个内容类应用,主要功能是图文浏览和视频播放。在普通的直板手机上,运行得很流畅,平均响应时间在300毫秒左右。
但在三星Z Fold 6上,问题就来了。Z Fold 6是一款折叠屏手机,展开后屏幕尺寸是7.6英寸,分辨率是2160x1856,比普通手机大很多。我们的App在适配折叠屏的时候,做了一些特殊处理,比如大屏模式下显示更多内容、分栏布局、更复杂的动画效果。
结果,这些适配导致了严重的性能问题。用户反馈说:
- 展开屏幕的时候,界面要等两三秒才能刷新
- 切换页面的时候,动画卡顿明显
- 滚动长列表的时候,会掉帧
- 打开大图的时候,加载特别慢
我们的测试团队也复现了这些问题,数据显示,在Z Fold 6上,应用的平均响应时间是1200毫秒,而在普通手机上只有300毫秒。差距太大了。
老板给了我一周时间,要求把响应时间降到300毫秒以内。我接下了这个任务,开始了性能调优之旅。
第一步:分析问题,定位瓶颈
性能调优的第一步,不是急着改代码,而是分析问题,定位瓶颈。
我用了Android Studio的Profiler工具,对应用在Z Fold 6上的运行情况做了详细的分析。主要看了这几个方面:
- CPU使用率:哪些操作占用了大量CPU
- 内存使用:有没有内存泄漏,内存占用是否过高
- 渲染性能:每帧的渲染时间,有没有掉帧
- 网络请求:请求的数量和耗时
- 方法耗时:哪些方法执行时间最长
经过两天的分析,我找到了几个主要的性能瓶颈。
第一个瓶颈,是布局过于复杂。在大屏模式下,我们为了充分利用大屏幕,设计了非常复杂的布局。一个页面里嵌套了五层以上的LinearLayout和RelativeLayout,还有十几个自定义View。每次渲染的时候,系统都要花费大量时间来测量和布局这些View。
第二个瓶颈,是过度绘制。大屏模式下,我们用了很多层叠的UI元素,比如背景图、渐变遮罩、半透明浮层。这些层叠的元素导致了严重的过度绘制,有些区域甚至被绘制了四五次。GPU的负担很重,渲染一帧需要很长时间。
第三个瓶颈,是图片加载慢。大屏模式下,我们为了让图片更清晰,加载了更高分辨率的图片。但Z Fold 6的屏幕分辨率很高,大图加载需要更多的时间和内存。而且,我们没有对图片做很好的缓存,每次打开页面都要重新加载。
第四个瓶颈,是动画不流畅。为了让大屏模式更炫酷,我们加了很多复杂的动画,比如页面切换的3D翻转动画、元素的淡入淡出、列表的滚动效果。这些动画在普通手机上还能流畅运行,但在Z Fold 6的高分辨率屏幕上,就显得力不从心了。
第五个瓶颈,是主线程阻塞。我们有一些耗时操作,比如数据库查询、数据解析、文件读写,都放在了主线程执行。在普通手机上,这些操作耗时较短,影响不大。但在Z Fold 6上,由于数据量更大(大屏显示更多内容),这些操作的耗时明显增加,导致主线程被阻塞,界面无法响应。
找到了这五个瓶颈之后,我开始逐个击破。
优化一:简化布局,用ConstraintLayout
第一个优化,是简化布局。
我们原来的布局,用了大量的LinearLayout和RelativeLayout嵌套。比如,一个页面的根布局是LinearLayout,里面套了RelativeLayout,RelativeLayout里又套了LinearLayout,最深的地方有六层嵌套。
这种嵌套布局,测量和布局的时间复杂度是指数级的。每多一层嵌套,测量时间就会增加很多。在高分辨率的屏幕上,这个问题被放大了。
我的优化方案是:用ConstraintLayout替代嵌套的LinearLayout和RelativeLayout。ConstraintLayout是Android官方推荐的布局方式,它可以用一层布局实现复杂的界面,大大减少嵌套层级。
我花了一天时间,把五个主要页面的布局都重写了,用ConstraintLayout替代了原来的嵌套布局。重写之后,布局的层级从六层降到了两到三层,View的数量也减少了30%左右。
效果很明显:布局测量和布局的时间,从平均80毫秒降到了20毫秒,减少了75%。页面切换的响应速度,立刻就有了提升。
除了用ConstraintLayout,我还做了这些布局优化:
- 用merge标签减少不必要的View层级
- 用ViewStub延迟加载不常用的View
- 去掉了一些不必要的装饰性View
- 用include标签复用公共布局,减少重复代码
优化二:减少过度绘制
第二个优化,是减少过度绘制。
过度绘制(Overdraw),是指同一个像素在一帧内被绘制了多次。比如,先绘制了背景,然后绘制了一个不透明的按钮,按钮覆盖的区域,背景其实是白画了。过度绘制会浪费GPU的算力,导致渲染变慢。
我用Android手机的"调试GPU过度绘制"功能,检查了我们应用的过度绘制情况。结果触目惊心:很多区域是红色的(过度绘制4次以上),尤其是有背景图和渐变遮罩的页面。
我的优化方案是:
- 去掉不必要的背景。很多View设置了背景色,但其实被父布局的背景覆盖了,根本看不到。我把这些不必要的背景都去掉了。
- 用clipRect减少绘制区域。对于被遮挡的部分,用canvas.clipRect()限制绘制区域,避免绘制不可见的部分。
- 减少半透明效果。半透明的View会导致过度绘制,因为下面的层也要绘制。我把一些不必要的半透明效果改成了不透明,或者用图片替代。
- 优化图层混合。对于必须的半透明效果,用更高效的混合模式,减少GPU的计算量。
优化之后,过度绘制的情况大大改善。大部分区域变成了蓝色(过度绘制1次)或者绿色(过度绘制2次),红色区域基本消失了。每帧的渲染时间,从平均40毫秒降到了15毫秒,减少了62.5%。
优化三:图片加载优化
第三个优化,是图片加载优化。
大屏模式下,我们加载的图片分辨率很高,单张图片可能有几MB。加载这些图片需要大量的时间和内存,而且如果没有做好缓存,每次都要重新加载,体验很差。
我们用的图片加载库是Glide,本身已经做了很多优化,但我们的使用方式有问题。
我做了这些优化:
第一,根据屏幕尺寸加载合适分辨率的图片。我们原来不管屏幕大小,都加载最高分辨率的图片。现在,我们根据设备的屏幕分辨率,动态选择图片的尺寸。在Z Fold 6的大屏上,加载适合大屏分辨率的图片;在小屏手机上,加载小一点的图片。这样既保证了清晰度,又减少了加载时间。
第二,加强内存缓存和磁盘缓存。我们原来的缓存配置比较保守,内存缓存只有20MB,磁盘缓存只有100MB。我把内存缓存调到了应用可用内存的1/4,磁盘缓存调到了500MB。这样,常用的图片可以缓存在内存里,第二次打开的时候几乎是秒开。
第三,用缩略图预加载。在加载高清图之前,先加载一个低分辨率的缩略图,让用户先看到一个模糊的预览,然后再慢慢加载高清图。这样虽然总加载时间没变,但用户感知到的等待时间变短了。
第四,图片压缩和格式优化。我们把图片从JPG换成了WebP格式,同样的质量,WebP的体积比JPG小30%左右。同时,我们对图片做了压缩,去掉了不必要的元数据。
第五,懒加载。在长列表中,只加载当前可见区域的图片,滚动到哪里加载到哪里。避免一次性加载大量图片,导致内存不足和卡顿。
优化之后,图片的平均加载时间从500毫秒降到了100毫秒,减少了80%。内存占用也下降了40%左右,不再出现因为图片加载导致的OOM(内存溢出)问题。
优化四:动画优化
第四个优化,是动画优化。
我们为大屏模式设计了很多炫酷的动画,但这些动画在高分辨率屏幕上运行不流畅。主要原因是,动画的计算量太大,GPU渲染不过来。
我做了这些优化:
第一,简化动画。把一些过于复杂的动画简化了。比如,原来的页面切换是3D翻转动画,需要计算大量的3D变换,渲染很慢。我改成了简单的淡入淡出加位移动画,虽然没那么炫酷了,但流畅了很多。
第二,用硬件加速。对于必须的复杂动画,开启硬件加速,让GPU来处理动画的渲染,而不是CPU。Android的View动画默认是硬件加速的,但一些自定义View的动画需要手动开启。
第三,用属性动画替代传统动画。属性动画(ObjectAnimator)比传统的补间动画(Tween Animation)更高效,因为它只修改View的属性,不需要每次都重新测量和布局。
第四,减少动画的同时运行数量。原来我们有很多动画同时运行,比如页面切换的时候,背景、标题、内容、按钮都在做动画。我把动画做了串行处理,先播背景动画,再播内容动画,减少了同时渲染的压力。
第五,用更低的帧率。对于一些不那么重要的动画,把帧率从60fps降到了30fps。人眼其实很难区分30fps和60fps的动画,但GPU的渲染负担减少了一半。
优化之后,动画的流畅度大大提升。原来切换页面的时候,动画会卡顿两三帧,现在基本能保持60fps的流畅度。
优化五:异步处理耗时操作
第五个优化,是把耗时操作从主线程移到后台线程。
我们原来有一些耗时操作放在了主线程,比如:
- 数据库查询:每次打开页面,都要查询数据库,获取要显示的数据
- 数据解析:从网络获取的数据,要解析成对象,这个过程在主线程做
- 文件读写:读取本地的配置文件和缓存文件
- 图片处理:对图片做裁剪、压缩、滤镜等处理
这些操作在普通手机上耗时较短(几十毫秒),用户感知不到。但在Z Fold 6上,由于大屏显示更多内容,数据量更大,这些操作的耗时增加到了几百毫秒,导致主线程被阻塞,界面无法响应。
我的优化方案是:
- 用Kotlin协程(Coroutines)处理异步操作。协程是Android推荐的异步处理方式,比传统的AsyncTask更轻量、更高效。
- 数据库查询用Room库的异步查询。Room支持返回LiveData或者Flow,自动在后台线程执行查询,主线程只负责观察数据变化。
- 网络请求用Retrofit+协程,自动在后台线程执行,返回结果后切回主线程更新UI。
- 图片处理用Glide的异步API,不要在主线程做图片处理。
- 用WorkManager处理后台任务,比如预加载数据、清理缓存等。
优化之后,主线程基本不再有耗时操作,界面的响应速度大大提升。点击按钮到界面响应的时间,从平均500毫秒降到了80毫秒,减少了84%。
优化六:折叠屏适配优化
除了上面这些通用的性能优化,我还针对折叠屏的特点做了一些专门的优化。
第一,折叠状态切换优化。Z Fold 6可以在折叠和展开之间切换,每次切换都会触发Activity的重建。我们原来的实现,每次切换都要重新加载所有数据,重新创建所有View,非常慢。
我的优化是:用ViewModel保存数据,避免配置变化时重新加载。同时,用onConfigurationChanged()监听折叠状态变化,自己处理布局切换,而不是让系统重建Activity。这样,折叠切换的时间从2秒降到了300毫秒。
第二,大屏模式的资源加载优化。在大屏模式下,我们会加载一些大屏专属的资源(比如更大的图片、更复杂的布局)。我把这些资源做了懒加载,只有在真正展开大屏的时候才加载,折叠状态下不加载,节省了启动时间和内存。
第三,分屏模式优化。Z Fold 6支持分屏模式,可以同时运行两个应用。我们的App在分屏模式下,原来会出现布局错乱和性能问题。我针对分屏模式做了专门的适配,优化了布局和资源加载,确保在分屏模式下也能流畅运行。
第四,多窗口优化。Android支持多窗口模式,我们的App在多窗口模式下,原来会重新加载数据。我做了优化,在多窗口切换的时候,保持数据和状态,避免重新加载。
最终效果
经过一周的调优,最终的效果如下:
- 平均响应时间:从1200毫秒降到了240毫秒,下降了80%
- 页面切换时间:从800毫秒降到了150毫秒,下降了81%
- 图片加载时间:从500毫秒降到了100毫秒,下降了80%
- 帧率:从平均40fps提升到了58fps,接近满帧
- 内存占用:从400MB降到了250MB,下降了37.5%
- 折叠切换时间:从2000毫秒降到了300毫秒,下降了85%
老板对这个结果很满意,用户的反馈也从"特别卡"变成了"很流畅"。这个性能调优项目,算是圆满完成了。
经验总结
这次性能调优,给了我很多经验和启发。
第一,性能调优要先分析再动手。不要一上来就改代码,要用工具分析,找到真正的瓶颈。盲目的优化,往往事倍功半。
第二,折叠屏带来了新的性能挑战。更大的屏幕、更高的分辨率、更复杂的布局,都会增加系统的负担。在适配折叠屏的时候,一定要做性能测试,不能只看功能是否正常。
第三,布局优化是基础。很多性能问题,根源都是布局太复杂。用ConstraintLayout减少嵌套,减少View数量,是最有效的优化手段之一。
第四,图片加载是大户。在内容类应用中,图片加载往往是最大的性能瓶颈。做好图片的尺寸适配、缓存、压缩、懒加载,能显著提升性能。
第五,主线程一定要保持空闲。任何耗时操作都不应该放在主线程。主线程只负责UI更新,其他的事情都交给后台线程。
第六,动画要适可而止。炫酷的动画能提升用户体验,但过多的动画会导致卡顿。在性能有限的设备上,要适当简化动画,保证流畅度优先。
第七,测试要覆盖各种设备。我们的App在普通手机上很流畅,但在折叠屏上就出了问题。这说明,测试不能只在主流设备上做,要覆盖各种尺寸和形态的设备。
写在最后
折叠屏手机是未来的一个趋势,越来越多的用户开始使用折叠屏。作为开发者,我们需要认真对待折叠屏的适配和优化,不能只是简单地把界面拉伸一下就完事了。
这次在三星Z Fold 6上的性能调优,让我对折叠屏的性能特点有了更深入的理解。折叠屏不是简单的"大屏幕手机",它有自己的特点和挑战,需要我们专门去适配和优化。
如果你也在做折叠屏的适配,希望这篇文章能给你一些参考。性能优化是一个持续的过程,没有最好,只有更好。
愿每个应用,都能在各种设备上流畅运行。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录