用户对一款应用的第一印象,往往来自它启动的速度和滑动的顺滑度。如果一个App启动要等好几秒,或者刷个列表都频频掉帧,用户很可能直接放弃。性能问题的根源,通常隐藏在启动任务的调度方式、视图渲染的复杂程度、网络请求的沟通策略以及内存资源的管理细节里。只要理清这几个环节,优化起来就能找准方向。
冷启动是应用留给用户的第一印象,但这个阶段如果处理不当,很容易变成“第一道坎”。很多应用习惯在启动时一股脑地同步初始化所有组件,像是加载各种SDK、解析本地配置文件、向服务器请求全量数据,结果主线程被这些杂事塞满,首屏迟迟画不出来。
优化的第一步,是拿出一张纸,把启动阶段要做的所有事情列出来,然后按“和首屏是否直接相关”来分个优先级。那些跟首屏没多大关系的操作,比如用户行为埋点的初始化、崩溃日志的上传、推送通道的建立,统统应该放到首帧渲染完成之后再去异步处理。而读取文件、查询数据库这类比较耗时的操作,也最好挪到子线程里去跑,千万别让它们堵在UI线程上。
这里有一个明确的边界:关系到用户能否正常进入应用的检查,比如验证登录态、获取首页的基础数据,这些是绝对不能推迟的。否则首屏画出来就是一个空壳。要判断冷启动是不是真的快了,建议拿一台中端配置的测试机来实测,通常冷启动时间在2秒以内算是一个比较合理的及格线。配合系统自带的性能剖析工具,盯一下启动这段时间的CPU占用曲线和磁盘读写峰值,很快就能找到让速度慢下来的元凶。
滑动页面时一卡一顿,通常是主线程太忙了。它既要做布局计算,又要响应触摸事件,还得抽空解析业务数据,绘制任务只能排在最后面。要解决这个问题,核心思路很简单——让主线程专职干一件事,就是做视图的布局和绘制。
打开布局检查工具,仔细看看你的页面嵌套结构。很多时候,你会发现很多容器视图其实可有可无,还有一些重叠的半透明遮罩层纯粹是为了视觉效果却拖慢了渲染。视图层数越深,GPU在合成每一帧时要做的工作就越多。把这些复杂的嵌套结构适当地压平,或者把功能相近的视图合并一下,就能实打实地缩短每一帧的渲染耗时。
长列表如果不做视图复用,滚动起来就会非常卡。不要在滚动过程中不断创建新的视图实例,这是一条铁律。图片的解码、数据的格式转换这类重活,也一定要放到后台线程去完成,处理好了再切回主线程做一次简单的刷新。举个常见的反面例子:在列表项的加载方法里同步去解码一张几MB的本地大图,那手指一滑,界面就会瞬间“冻住”。
更合理的办法是,根据列表项控件实际显示的尺寸,预先加载并缓存一张大小合适的缩略图,而不是把原图塞进去。同时,还可以根据用户当前的滚动速度,提前预取下一屏的数据。用帧率监控工具实跑一下,稳定在55FPS以上,算是达到了流畅的合格标准。另外,如果页面正在播放复杂的转场动画,可以临时把后台线程的任务优先级调低一点,让出资源给渲染。
用户对应用“快不快”的感知,很大程度来自网络响应速度。后端的性能我们暂时管不着,但客户端在请求策略上能做文章的地方不少。比如启用HTTP/2协议,它支持多路复用,可以大幅度降低多个并发请求带来的连接开销。
对于那些不太会变的数据,比如图文配置、用户偏好设置,一定要利用好本地缓存。给它们设置一个5到15分钟的有效期,在这段时间内直接用缓存数据,就不用每次重新请求了。当数据只是部分更新时,尽量用增量接口去同步那些有变化的字段,别一上来就全量拉取,白白浪费用户流量和时间。还有那些高频轮询的请求,得克制一点。比方说那种每30秒就发起一次的无差别轮询,既耗电又占带宽。如果业务场景对实时性要求高,不如改用长连接或者服务端推送,体验会好很多。
怎么衡量网络优化做得好不好?可以在弱网环境下,对比一下优化前后的平均请求耗时和失败率。如果失败率偏高,就该考虑加超时重试机制了。但重试也要讲究策略,最好是采用指数退避的方式,比如第一次等1秒,第二次等2秒,第三次等4秒……这样既能保证恢复,又不会因为重试请求太密集而把服务器给冲垮。
内存占用如果只增不减,轻则触发系统回收导致卡顿,重则直接闪退。内存泄漏的原因往往很隐蔽,最常见的就是这几种:注册了事件监听器却忘了注销;匿名内部类隐式持有了外部类的引用;还有那些在页面销毁之后还继续运行的定时任务——它们都会让本来该被回收的对象一直赖在内存里不走。
要防住这些问题,一方面得养成好习惯,在页面销毁的生命周期回调里,把该注销的监听器和该取消的定时任务都清干净。另一方面,也要靠工具来排查。Android上可以用LeakCanary,iOS则能用Xcode内置的Instruments里面的Leaks工具,它们能比较精准地帮你定位到内存泄漏发生的位置。处理图片这类大对象时,还要注意及时释放对Bitmap等资源的引用,避免因为占着大块内存而被系统判定为高危进程。
在优化内存时,一个实用的方法是定期检查应用的内存增长趋势,而不是只看某一个瞬间的数值。如果发现内存曲线呈现持续上升的“锯齿”,那就要重点排查是不是有对象没能被回收。
这个问题没有一个放之四海而皆准的数字,因为它受设备配置、应用复杂程度影响很大。但有一个比较通用的衡量标准:在主流中端配置的测试机上,冷启动完成首帧渲染的时间应控制在2秒以内。如果你的应用是一款游戏或者大型工具类应用,启动时间可以适当放宽,但至少要保证用户能快速看到加载进度或者过渡画面,而不是干瞪眼等一个白屏。
帧率只是平均值指标,偶尔的卡顿往往被其它帧的平均值掩盖了。遇到这种情况,建议关注帧绘制时间的分布,也就是看看有没有个别帧的耗时特别长。通常这种“偶发卡顿”跟网络回调不及时触发UI刷新、或者某张图片在滚动时才突然发起解码有关。你可以试着把图片解码和网络请求都提前做好缓存,尽量避免在滚动过程中触发这些耗时操作。
LeakCanary擅长找Activity泄漏,但有些泄漏发生在单例对象、全局缓存或者不直接关联生命周期的地方。这时候可以换个思路,用Android Studio自带的Memory Profiler抓取一份堆转储文件(Heap Dump),并打开“最受对象”分析面板,看看哪些类的实例数量异常偏多。这往往比依靠单一工具走得更远,也更贴近实际情况。
性能优化不是为了追求一个漂亮的测试分数,而是为了改善真实用户的每一丝使用感受。你可以从今天就开始动手:第一,抛开代码,先用文档梳理一下冷启动流程里有哪些事是“等首屏出来再做也不迟”的;第二,用设备跑一遍核心页面的打开与滚动,记录下卡顿出现的操作路径;第三步,再针对性地去精简布局或调整网络策略。优化的收益不是一次性的,养成每次提交代码前都关注一下性能和内存的习惯,长期积累下来,应用的质量会有一个明显的跃升。