最近在做一个App的性能优化,目标机型是OPPO Find X5 Pro。这款手机用的是骁龙8 Gen1处理器,性能很强,但我们的App在上面运行还是有卡顿。

经过两周的调优,我把核心页面的响应时间从500ms降到了100ms,降了80%。滑动帧率从45fps提升到了稳定的60fps。

今天分享这次性能调优的实战过程,包括怎么分析瓶颈、用了哪些优化手段、每一步的效果如何。涉及布局优化、内存优化、启动优化、渲染优化等方面,希望能给做Android开发的朋友一些参考。

一、问题背景

先说说项目背景。

我们的App是一个内容类应用,主要功能是信息流展示、详情页浏览、视频播放等。用户反馈在OPPO Find X5 Pro上滑动信息流的时候偶尔卡顿,进入详情页的响应时间也比较长。

虽然Find X5 Pro性能很强,但我们的App因为历史原因,代码比较臃肿,性能问题很多。之前一直在加功能,没怎么关注性能,现在问题积累得多了,必须优化。

优化前的性能数据:

  • 冷启动时间:1.8秒
  • 信息流滑动帧率:平均45fps,最低30fps
  • 进入详情页响应时间:500ms
  • 内存峰值:350MB
  • 偶尔出现ANR(应用无响应)

优化目标:

  • 冷启动时间降到1秒以内
  • 滑动帧率稳定60fps
  • 详情页响应时间降到150ms以内
  • 内存峰值控制在250MB以内
  • 消除ANR

二、第一步:性能分析

优化的第一步,不是上来就改代码,而是先分析瓶颈在哪里。

1. 使用Profiler分析

Android Studio自带的Profiler是最基础的性能分析工具。我用它分析了CPU、内存、网络的使用情况。

发现的问题:

  • CPU:信息流滑动的时候,主线程占用率高,有掉帧
  • 内存:有内存泄漏,Activity退出后没有释放
  • 网络:请求太多,有重复请求

2. 使用Systrace分析渲染

Systrace(现在叫Perfetto)是分析渲染性能的好工具。我抓取了滑动信息流时的trace,发现:

  • 每帧的渲染时间超过16ms(60fps的要求),主要是布局和绘制耗时
  • 有很多冗余的invalidate和requestLayout
  • 主线程有太多任务,导致UI线程阻塞

3. 使用LeakCanary检测内存泄漏

接入了LeakCanary,跑了一遍,发现了好几处内存泄漏:

  • 静态变量持有Activity引用
  • 监听器没有反注册
  • 单例持有Context引用
  • Handler没有移除消息

4. 使用Lint检查代码

Android Lint检查出了很多性能相关的警告:

  • 布局嵌套过深
  • 使用了wrap_content的ImageView,导致多次测量
  • 有冗余的布局层级
  • 资源没有释放

分析结论: 瓶颈主要在四个方面:

  1. 布局复杂,渲染耗时
  2. 内存泄漏,导致GC频繁
  3. 启动慢,初始化任务太多
  4. 主线程任务太多,阻塞UI

三、第二步:布局优化

布局优化是性价比最高的优化,改完效果明显。

1. 减少布局嵌套

我们的信息流item布局,嵌套了5层。用ConstraintLayout重构之后,降到了2层。

重构方法:

  • 用ConstraintLayout代替LinearLayout+RelativeLayout的组合
  • 用merge标签减少多余的层级
  • 用include复用公共布局

重构之后,item的测量和布局时间从8ms降到了3ms。

2. 使用ViewStub延迟加载

详情页有一些不常用的布局(比如分享面板、评论区),以前是写在布局里,用gone隐藏。这样虽然不显示,但还是会inflate,消耗时间。

改成ViewStub,只有在需要的时候才inflate。详情页的inflate时间从30ms降到了15ms。

3. 优化ImageView

以前的ImageView用的是wrap_content,导致每次加载图片都要重新测量和布局。改成固定尺寸,或者用adjustViewBounds,减少测量次数。

另外,用了图片库的占位图和渐入动画,避免图片加载时的闪烁和布局跳动。

4. 减少过度绘制

用手机的"调试GPU过度绘制"功能检查,发现我们的页面有很多过度绘制(红色区域)。

优化方法:

  • 去掉windowBackground,因为每个页面都有自己的背景
  • 去掉不必要的背景色
  • 用clipRect减少重叠区域的绘制

优化之后,过度绘制从4x降到了2x以内。

布局优化效果:

  • 信息流滑动帧率从45fps提升到55fps
  • 详情页响应时间从500ms降到350ms

四、第三步:内存优化

内存优化能减少GC,提升流畅度。

1. 修复内存泄漏

根据LeakCanary的报告,逐一修复了内存泄漏:

  • 静态变量改成弱引用,或者改用Application Context
  • 监听器在onDestroy里反注册
  • 单例用Application Context,不用Activity Context
  • Handler在onDestroy里removeCallbacksAndMessages

修复之后,内存泄漏基本消除了。

2. 优化图片内存

图片是内存大户。我们的App里有很多图片,以前没有很好地控制。

优化方法:

  • 根据ImageView的尺寸加载合适大小的图片,不要加载原图
  • 用RGB565代替ARGB8888(对图片质量要求不高的地方)
  • 图片缓存大小限制在合理范围,不要无限缓存
  • 大图用区域解码(BitmapRegionDecoder)

优化之后,图片内存占用减少了30%。

3. 避免对象频繁创建

在onDraw、onMeasure、getView等频繁调用的方法里,不要创建新对象。

比如:

  • 不要在onDraw里new Paint、new Path
  • 不要在getView里new OnClickListener
  • 用对象池复用对象(比如SparseArray、Pool)

优化之后,GC的频率明显降低了。

4. 优化数据结构

用更高效的数据结构:

  • 用SparseArray代替HashMap<Integer, Object>
  • 用ArrayMap代替HashMap(数据量小的时候)
  • 避免自动装箱(int vs Integer)

内存优化效果:

  • 内存峰值从350MB降到230MB
  • GC频率从每分钟5次降到每分钟1次
  • 滑动帧率稳定在58fps左右

五、第四步:启动优化

冷启动时间是用户的第一印象,很重要。

1. 分析启动耗时

用Debug.startMethodTracing抓取启动过程的trace,发现启动时间主要花在:

  • Application.onCreate:800ms
  • 首屏Activity inflate:300ms
  • 首屏数据加载:400ms
  • 其他:300ms

2. 异步初始化

Application.onCreate里有很多SDK初始化,以前都是同步执行,占了800ms。

优化方法:

  • 非必要的SDK,放到子线程异步初始化
  • 延迟初始化,用到的时候再初始化
  • 用启动器框架(比如Jetpack App Startup)管理初始化顺序

优化之后,Application.onCreate降到了200ms。

3. 首屏优化

  • 用SplashActivity做启动页,展示品牌logo,同时后台初始化
  • 首屏布局简化,用ViewStub延迟加载非必要部分
  • 首屏数据用缓存,先展示缓存数据,再加载新数据
  • 用占位图,避免空白

优化之后,首屏显示时间从1.2秒降到了500ms。

4. 类预加载

用ClassLoader提前加载常用的类,减少第一次使用时的加载时间。

启动优化效果:

  • 冷启动时间从1.8秒降到了900ms
  • 达到了1秒以内的目标

六、第五步:渲染优化

渲染优化是保证流畅度的关键。

1. 主线程减负

主线程只做UI相关的操作,其他操作都放到子线程:

  • 网络请求:本来就在子线程
  • 数据库读写:用Room或异步查询
  • 文件读写:放到子线程
  • 复杂计算:放到子线程,结果post回主线程

用了RxJava和Kotlin协程,简化了异步操作。

2. 减少invalidate和requestLayout

检查代码,发现有很多不必要的invalidate和requestLayout。比如:

  • 数据没变化也调用notifyDataSetChanged
  • 布局参数没变也调用requestLayout
  • 自定义View里频繁调用invalidate

优化方法:

  • 用DiffUtil,只更新变化的item
  • 只有布局参数变化时才调用requestLayout
  • 自定义View里,只在需要重绘时调用invalidate

优化之后,每帧的渲染时间稳定在12ms以内。

3. 硬件加速

确保所有页面都开启了硬件加速。自定义View里,用硬件加速的API(比如setLayerType)。

对复杂的自定义View,用setLayerType(LAYERTYPEHARDWARE, null),把View渲染到离屏缓冲,提升动画流畅度。

4. 优化RecyclerView

RecyclerView是我们用得最多的控件,优化点很多:

  • 用DiffUtil,不要全量notifyDataSetChanged
  • 设置setHasFixedSize(true),避免不必要的布局
  • 用setItemViewCacheSize,增加缓存数量
  • 预加载(setInitialPrefetchItemCount)
  • 优化ViewHolder,减少findViewById(用ViewBinding或DataBinding)
  • 图片加载时,不要在滑动时加载(用pauseRequests/resumeRequests)

渲染优化效果:

  • 滑动帧率稳定在60fps
  • 详情页响应时间降到100ms
  • 没有再出现ANR

七、第六步:网络优化

网络虽然不直接影响UI性能,但会影响数据加载速度,间接影响用户体验。

1. 减少请求数量

  • 合并接口,一个页面的多个请求合并成一个
  • 用缓存,不要重复请求
  • 预加载,进入页面之前就开始加载

2. 优化请求速度

  • 用HTTP/2,多路复用
  • 启用gzip压缩
  • 用CDN,减少网络延迟
  • DNS预解析

3. 数据缓存

  • 内存缓存+磁盘缓存
  • 先展示缓存数据,再加载新数据
  • 离线也能看缓存内容

八、优化效果总结

经过两周的调优,性能数据如下:

指标优化前优化后提升
冷启动时间1.8s0.9s50%
滑动帧率45fps60fps33%
详情页响应时间500ms100ms80%
内存峰值350MB230MB34%
ANR次数每周数次0100%

所有指标都达到了预期目标,用户反馈也明显好转。

九、性能优化的经验总结

这次优化,我总结了几条经验。

1. 先测量,再优化

不要凭感觉优化,先用工具分析,找到真正的瓶颈。很多时候,瓶颈不在你以为的地方。

2. 从性价比最高的开始

布局优化、内存泄漏修复,这些改动小、效果大,先做。复杂的优化(比如启动优化、渲染优化)放在后面。

3. 优化是持续的

性能优化不是一次就完了。新功能上线,可能会引入新的性能问题。要建立性能监控体系,持续关注。

4. 不要过度优化

优化到一定程度就够了,不要为了极致性能把代码搞复杂。60fps和59fps,用户感知不到差别,但实现复杂度差很多。

5. 建立性能基线

每次发版前,跑一遍性能测试,和基线对比。如果性能下降了,要找到原因,不要让性能问题积累。

十、写在最后

性能优化是Android开发的基本功,也是最能体现工程师价值的地方。

这次在OPPO Find X5 Pro上的性能调优,让我对Android性能优化有了更深的理解。很多优化方法,其实都是基础,只是平时容易忽略。

性能优化没有银弹,就是一点点分析,一点点改进。只要方法对了,坚持做下去,效果一定会出来。

2022年了,手机性能越来越强,但App也越来越臃肿。性能优化永远不会过时。

希望这篇文章能给做Android开发的朋友一些参考。如果你也有性能优化的经验或问题,欢迎在评论区交流。

祝大家的App都能又快又稳。