所在项目组app的冷启动一直是个无人看护优化的状态,机缘巧合之下,领导弄了个专项让我去优化,陆续优化了几个月,效果还不错,分享一些通用的优化方案给大家。
在文章前面先把优化的思路都列出来,方便大家按需查看
在文章前面先把优化的思路都列出来,方便大家按需查看
1. 异步inflate布局
inflate这个过程就是把R.layout.xxx这一串布局数字,加载为对应的View类型。方法签名为
LayoutInflater.from(Context context).inflate(@LayoutRes int resource, ViewGroup root, boolean attachToRoot);
所谓的异步inflate布局,实际上就是提前在子线程加载出VIew,在setContentView或者onCreateView等用到View时直接取出,减少了主线程的等待。
官方已经提供了一个实现,AsyncLayoutInflater,原理很简单,代码也很少,但使用后发现有不少问题,比如View中使用looper会报错,无法提前或者root ViewGroup, LayoutParams设置失效等
基于此,我换了个思路,把inflate过程拆建并实现部分异步。
inflate可以分为两个步骤,
a) 加载R.layout.xxx对应的xml文件,加载到内存中,这是一个IO操作
b) 把xml文件解析并构建View
这两个步骤是可以分开执行的,android也提供了单独执行b步骤的接口
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
我们可以开子线程把a步骤做好了,等需要使用VIew时,在主线程把b做了,虽然不是全部异步,但效果也比较明显,且没有官方实现这么多的问题。
具体实现是因为有接口可以加载layoutid为XmlResourceParser
context.getResources().getLayout(layoutId)
2. 减少binder调用
binder是android提供的IPC的方式。android许多系统服务都是运行在system_server进程而非app进程,比如判断网络,获取电量,加密等,这导致会有很多的binder通信,而IPC是耗时操作,需要尽量减少调用。
要想知道启动时候执行了多少此binder通信,可以通过抓systrace时添加binder_driver参数查看。
而要减少binder调用,我的建议有以下两点
a) 缓存从serviceManager进程获取到的客户端binderProxy实例。android采取服务注册的方式提供服务,app需要获取系统服务,需要先跟serviceManager binder通信一次,获取到具体服务的binderProxy实例,然后再用binderProxy实例与具体服务做binder通信,这里需要做两次binder通信,通过缓存binderProxy实例可以减少一次binder
b) 轮询获取改为监听回调获取,最典型的就是网络状态的获取,可以用监听网络状态的接口来实现,把网络状态用volatile的变量缓存起来,接口直接返回变量,只在回调中赋值
3. 启动时ViewPager+Fragment加载的优化
这个比较长,用独立一篇文章介绍 ViewPager+Fragment的加载优化
4. SharedPreference替换为mmkv实现
这个就是简单粗暴了,因为SharePreference加载数据的过程是:new一个线程来加载全部数据为map,而调用线程则会一直等待直到加载完成。而mmkv在读写方面都有更好的性能,直接替换完事。
5. 确保系统已经完成dex2oat的优化
dex2oat是android提供的提升dex加载速度和运行速度的工具,在安装apk后就会开启进程独立运行。安装apk后进adb shell,ps查看进程,就会看到dex2oat进程。而优化是否成功,则会在logcat中会有体现,但具体我忘记了。
这个优化点是由一个bug诱发的,某个开发在apk中新加了一个支付相关的依赖,这个依赖跟android framework层用了同一个jar包,这导致apk安装后执行dex2oat时,程序判断有重复的代码,直接终止了oat优化,导致app启动慢了几百毫秒,这个也是耗费了很多时间才定位出来的问题,所以才有了这条建议:确保系统已经完成dex2oat的优化
6. 锁粒度的优化
因为java的synchronized封装得十分好,需要开发在需要加锁时,会直接使用synchronized,但因为synchronized是排他锁,其实可以根据业务场景使用读写锁等非排他锁。
7. 非页面展示的message往后移
activity的oncreate和onresume是由同一条message–ActivityThread中定义的159消息触发的,而在onResume后,理应跟着的是收到vsync后向主进程插入的doFrame消息。
如果通过打印looper消息发现onResume和doFrame之间有其他插入的消息,可以考虑把这些消息往后移,等待doFrame完成。
这个我目前只知道原理,具体技术还未验证。
8. 减功能和删代码
通过梳理业务和与产品沟通,把部分不再有用的功能给删掉,是最简单粗暴的优化方法。
删代码的方法主要就是复用基础工具类提供的能力,避免重复造轮子
9. bytex
bytex是字节提供的一套利用transform做字节码修改的框架,其中有个插件可以把R.java中的变量方法内联化,进而减少方法数和优化dex数目。项目上使用后,dex文件数目由两位数降到了个位数,apk体积有十分明显的优化。而且由于app启动时bindApplication需要去加载dex,所以对启动性能的优化也是有一定帮助。
另一个插件可以直接在字节码层面删除某些方法的调用,理论上也可以优化冷启动时间,比如日志打印,可以在release版本直接删掉。
冷启动时长作为一个app的门面,是很重要的性能和体验指标,网上冷启动优化的方案林林总总,各有千秋,如果大家有其他优化方案,可以留言一起研究。
做冷启动优化的过程中,我最大的感受就是,监控手段比优化手段更重要,优化总是在恶化一段时间后才能完成,而通过监控手段及时检测到冷启动的恶化,则更容易在火苗还小的时候就把火给灭了。防患于未然,总比积重难返要好。怎么监控冷启动时长,这个后续需要持续研究和探索。