联系咱们:
有道技术团队助手:ydtech01 / 邮箱:ydtech@rd.netease.comandroid
本文的重点在于如何定量的排查冷启动过程当中的耗时操做,并提供对应的优化思路和实践方法总结。同时本文涉及到的冷启动优化主要涵盖两个方面:Application 的性能优化和 Launcher Activity 的性能优化。shell
中国大学 MOOC 是网易与高教社携手推出的在线教育平台,目前,通过长期的产品打磨和钻研,在课程数量、质量以及影响力,中国大学 MOOC 已成为全球领先的中文慕课平台。同时通过这次优化,冷启动速度总体提高27%。数据库
在咱们平常开发中,随着 app 总体迭代次数增多,因为长久以来的迭代需求,android app 自己也集成了较多的第三方组件和 SDK,同时在平常迭代中,也是以业务迭代需求实现为主要目的,致使如今 app 自己,或多或少存在一些性能可优化空间。因此有必要进行性能优化,提高用户体验安全
这次优化,主要侧重于两个方面:性能优化
该文档重点不在于代码规范和业务代码逻辑致使的性能问题,而是在假设代码无明显、严重性能漏洞,而且不改变原有业务逻辑,量化性能监测数据和问题,并针对其进行优化修改。网络
adb shell am start -S -W [packageName]/[activiytName]
上述 adb 命令中,几个关键参数说明:app
再执行上诉 adb 后,会成功唤起 APP,并在控制台输出三个比较关键的参数:异步
对于应用层面得冷启动性能优化,咱们关注的时间 TotalTime,该时间大体能够归纳为:Application 构造方法→该 Activity 的 onWindowFocusChange 方法时间总和。而这个过程也能够粗略认知为,用户点击桌面图标到 app 第一个 Activity 获取焦点,业务代码执行的总时间(针对业务代码的优化,咱们暂时不关心 Zygote 进程、Launcher 进程、AMS 进程的交互)。async
在 Android API>=26 的系统版本中,建议使用 CPU Profile 或者 Debug.startMethodTracing 进行监控并导出 trace 文件进行分析。无论哪一种方式,采集堆栈信息都有两种模式:采样模式和追踪模式。追踪模式会一直抓取数据,对设备性能要求较高。ide
(1)CPU Profile
(2)Debug.startMethodTracing
因为冷启动涉及到业务应用层面的时间是:该 Activity 启动时间+应用 application 等资源启动时间,因此咱们在 Application 构造方法中开始采集,在第一个 Activity 的 onWindowFocusChange 中中止采集,并输出 trace文件。
/** * 在Application构造方法中开始采集 */ public UcmoocApplication() { //保存Trace文件的目录 File file = new File(Environment.getExternalStorageDirectory(), "ucmooc.trace"); //采集方式有如下两种,根据需求选择其一 //第一种:经过采样的方式,追踪堆栈信息 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //经过采样方式追踪堆栈信息,须要指定文件保存目录、文件最大大小(单位M)、采样间隔(单位us) Debug.startMethodTracingSampling(file.getAbsolutePath(), 8, 1000); } //第二种:经过追踪的方式,全量采集堆栈信息 Debug.startMethodTracing(file.getAbsolutePath()); coreApplication = new CoreApplication(); }
/** * 在启动后的第一个Activity的onWindowFocusChanged中中止监听 * * @param hasFocus */ @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); Debug.stopMethodTracing(); }
同时,因为该操做涉及到文件读写权限,须要手动授予 APP 该权限
在导出获取到 .trace 文件后,把 .trace 拖动至 androidStudio 编辑区;或者直接浏览 CPU Profile 视图,即可对程序运行的堆栈进行分析:
上图就是 trace 文件打开后的效果,展现的是基于 CPU 使用和线程运行情况,针对启动速度的优化,须要关注的上图标注的几个点:
(1)CPU 运行时间轴:横向拖动能够选择查看的时间范围
(4)当前设备 CPU 轮转的线程:点击能够选择须要查看的线程,咱们重点关注主线程
(2)当前选择线程,跟随时间轴,各个方法栈的调用状况和其耗时情况。其不一样颜色分别表明
- 黄色:android 系统方法(FrameWork 层代码,若是须要最终更底层的方法,须要最终 C/C++ 方法调用栈)
- 蓝色:Java JDK 方法
- 绿色:属于当前 app 进程执行的方法,包括一些类加载器和咱们的业务代码(启动速度优化主要针对这一部分)
(3)各个方法栈的调用顺序和耗时状况,能够选择不用的排序方式和视图。
因此通常排查耗时方法时,建议先经过(2)视图直观检测到耗时较为严重的方法,锁定后,在(3)视图中查看具体的方法调用顺序。
因为在冷启动过程当中,业务代码耗时主要集中在 Application 和 launcher Activity 中,因此优化过程也是分别针对这两块进行优化。
使用2.1.1的方式,在优化先后,分别作了10次冷启动耗时统计,结果以下:
启动速度总体提高 27%。
经过 trace 文件,能够直观的发现,在 application 中,耗时最长的方法是其生命周期中的 onCreate 方法,其中在 onCreate 方法中,耗时比较长的方法有:initMudleFactory、initURS、Unicorn.init、initUmeng。
在 Top Down 视图中,能够更加直观的看出,这次采样,也正是这四个方法耗时最多。
经过源码排查,这是个方法,均是第三方 SDK 的初始化,同时在这几个 SDK 内部,都含有较多的 IO 操做,而且内部实现了线程管理以保证线程安全,因此能够将这几个 SDK 的初始化,放在子线程中完成。这里以友盟 SDK 为例:
/** * 友盟SDK中有涉及到线程不安全的地方,都本身维护了线程,保证线程安全 **/ try { var6 = getClass("com.umeng.umzid.ZIDManager"); if (var6 == null) { Log.e("UMConfigure", "--->>> SDK 初始化失败,请检查是否集成umeng-asms-1.2.x.aar库。<<<--- "); (new Thread() { public void run() { try { Looper.prepare(); Toast.makeText(var5, "SDK 初始化失败,请检查是否集成umeng-asms-1.2.X.aar库。", 1).show(); Looper.loop(); } catch (Throwable var2) { } } }).start(); return; } } catch (Throwable var27) { } /** * 在友盟SDK内部中有不少IO操做的地方,和加锁操做,因此能够将SDK初始化操做,放在子线程中 **/ if (!TextUtils.isEmpty(var1)) { sAppkey = var1; sChannel = var2; UMGlobalContext.getInstance(var3); k.a(var3); if (!needSendZcfgEnv(var3)) { FieldManager var4 = FieldManager.a(); var4.a(var3); } synchronized(PreInitLock) { preInitComplete = true; } }
最终,咱们能够把上面提到的几个 SDK 初始化工做放入在子线程中:
private void initSDKAsyn(){ new Thread(() -> { if (Util.inMainProcess()){ // 登陆 initURS(); if (BuildConfig.ENTERPRISE) { Unicorn.init(BaseApplication.this, "", QiyuOptionConfig.options(), new QiyuImageLoader()); initModuleRegister(); } else { Unicorn.init(BaseApplication.this, "", QiyuOptionConfig.options(), new QiyuImageLoader()); } // 初始化下载服务 try { initDownload(); } catch (Exception e) { NTLog.f(TAG, e.toString()); } } initModuleFactory(); initUmeng(); }).start(); }
对于一些必须在主线程中初始化完成的 SDK,能够考虑使用 IdleHandler,在主线程空闲时,完成初始化(关于 IdleHandler 会在下面讲到)。
auncher Activity 是 WelcomeActivity,在对 Application 优化结束后,再对 WelcomeActivity 进行优化,仍是和上路的思路同样,先经过 trace 文件追踪:
能够看到,在 WelcomeActivity 的 onCreate 方法中,耗时较多的三个地方,分别是:initActionBar、EventBus.register、setContentView,下面针对这三块内容,分别进行对应的优化操做:
(1)initActionBar
在上图中,能够看到,initActionBar 中最耗时的操做是 getSupportActionBar,经过研究代码发现,在 WelcomeActivity 中,并不须要操做 actionBar,因此直接复写父类方法,去掉 super 调用便可。
(2)EventBus.register
EventBus 注册时,性能较差,是由于在改过程当中涉及到大量的反射操做,因此对性能损耗较大。经过查看官方文档,该问题在 EventBus3.0 中获得了很好的处理,主要是经过 apt 技术增长索引,提高效率。(当前项目未升级版本,待后期优化)
(3)setContentView
setContentView 是 Activity 渲染布局时的必要方法,其耗时的点在于,解析 xml 布局文件时,使用了反射,因此若是 xml 布局文件很是复查的时候,可使用androidx.asynclayoutinflater:asynclayoutinflater进行异步加载 xml 文件,使用方式以下:
new AsyncLayoutInflater(this).inflate(R.layout.activity_welcome, null, (view, resid, parent) -> { setContentView(view); });
上面针对冷启动优化是基于当前项目自己作的步骤,这里汇总一些冷启动通用的优化思路:
(1)合理的使用异步初始化、延迟初始化和懒加载机制:主要针对 Application 中各类 SDK 的初始化
(2)在主线程中应当避免很耗时的操做,好比 IO 操做、数据库读写操做
(3)简化 launcher Activity 的布局结构,若是很是复杂的布局,能够有如下两种方式进行优化:
(4)合理使用 IdleHandler 进行延迟初始化,使用方式以下:
/** * 须要在当前线程中处理耗时任务,而且并不须要立刻执行的话,可使用IdleHandler * 这样该任务能够消息队列空闲时,被处理 */ Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { //此处添加处理任务 //返回值为false:则执行完毕以后移除这条消息, //返回值为true:则则执行完毕以后依然保留,等到下次空闲时会再次执行, return false; } });
(5)开始严苛模式(StrictMode)
该模式并不能帮咱们自动优化性能,而是能够帮助咱们检测出咱们可能无心中或者一些第三方 SDK 中作的会阻塞 Main 线程的事情(好比磁盘操做、网络操做),并将它们提醒出来,以便在开发阶段进行修复。其检测策略有线程检测策略和虚拟机检测策略,咱们能够设置须要检测的操做,当代码操做违规时,能够经过 Logcat 或者直接崩溃的形式提醒咱们,具体使用方式以下:
/** * 开启严苛模式,当代码有违规操做时,能够经过Logcat或崩溃的方式提醒咱们 */ private void startStrictMode() { if (BuildConfig.DEBUG) { //必定要在Debug模式下使用,避免在生产环境中发生没必要要的崩溃和日志输出 //线程检测策略 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() //检测主线程磁盘读取操做 .detectDiskWrites() //检测主线程磁盘写入操做 .detectNetwork() //检测主线程网络请求操做 .penaltyLog() //违规操做以log形式输出 .build()); //虚拟机检测策略 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() //检测SqlLite泄漏 .detectLeakedClosableObjects() //检测未关闭的closable对象泄漏 .penaltyDeath() //发生违规操做时,直接崩溃 .build()); } }