或许我们都有过这样的体验,如果打开一个app需要加载很长时间才到首页,有些时候可能因为网络环境问题根本打不开,那么用户就会直接退出不再使用这个app,长此以往用户量就会大规模流失,所以快速启动是app的生存之道,尤其是现在用户对于app的体验要求很高,因为启动慢、白屏受到很多用户的吐槽。
但是启动优化也仅仅是优化,有些时间是必须要耗费在启动上,所以我们能做的就是把不必要的时间消耗给去除,把时间花在刀刃上。
1 App的黑白屏优化
一个app的启动主要分为以下几个流程:
(1)点击app icon按钮,加载并启动app;
(2)启动后,立即为该app显示一个空白的启动窗口(不一定是白色,决定权在windowBackground的颜色);
(3)创建app的进程,在Binder驱动成分配一块内存;
(4)创建主线程,创建主Activity;
(5)执行主Activity的生命周期,到onResume时,显示布局用户可交互。
这是冷启动的主要流程,因为只有在主Activity页面执行onResume时,白屏才会消失,所以中间这个过程时间越长,那么白屏的时间就越长。
1.1 白屏的处理
所以对于白屏的处理,我们先看下目前的效果
我们可以看到,中间有很长时间的一个白屏现象,所以我们想能不能去掉这个白屏窗口,用户打开app后直接看到首页。
<style name="StartupTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowDisablePreview">true</item>
</style>
当然是可以的,但是用户的直观感受为卡顿,看下效果
在点下图标之后,并没有启动而是像卡主一下,然后启动到了app首页,这种用户体验不好。
因此我们可以换一个方案,我们不要白屏,而是使用一个闪屏页将白屏替代
<style name="StartupTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
</style>
加上闪屏页之后,我们发现首页的主题也跟着改变了,其实在加闪屏页的时候,可以让设计师设计一个跟主题相关的闪屏页作为背景,这样就不需要重新设置页面的背景。
1.2 网易云启动闪屏方案
既然我们不想影响到主Activity,那么我们可以单独设置一个SplashActivity作为闪屏的承接方,从而闪屏完成之后,进入到首页。
首先,我们先自定义一个drawable布局,作为闪屏页的背景
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/red" />
<item android:top="30dp">
<bitmap
android:gravity="top"
android:src="@drawable/ic_launcher_round" />
</item>
</layer-list>
我们看到当闪屏页启动之后,出现一行字,有没有伙伴们觉得这个是一个动画,其实并不是,而且因为前面我们对于windowBackground属性的使用
<style name="StartupTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowBackground">@drawable/splash</item>
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
当我们把一个页面的windowBackground设置为一张图之后,那么默认的背景也会是这张图会被首先加载进来,然后App完全启动之后,SplashActivity页面才会加载进来,显示文字
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SplashActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="音乐的力量"
android:fontFamily="sans-serif-medium"
android:textSize="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="30dp"
android:textColor="#FFFFFF"/>
</androidx.constraintlayout.widget.ConstraintLayout>
当SplashActivity完全加载之后,意味着App已经完全启动了,便可以跳转到首页
override fun onResume() {
super.onResume()
startActivity(Intent(this,WebActivity::class.java))
}
2 App启动时间优化
经过前面对于黑白屏的优化,其实仅仅是从用户体验方面来完成的,并没有从启动时间方面做出优化,如果app的启动时间很长,即便是加了闪屏页,也会导致在闪屏页的停留时间过长,用户耐心消耗完成也不再进入app,所以app启动时间优化才是重中之重。
2.1 App启动时间测量
App启动时间测量的方式有多种,其中包括:
(1)系统日志
通过在Logcat窗口中搜索关键字ActivityTaskManager,可以看到启动闪屏页用时1.8s
2022-10-23 15:30:30.780 2008-2046/system_process I/ActivityTaskManager:Displayed com.lay.mvi/.SplashActivity: +1s861ms
(2)adb命令
adb shell am start -W com.lay.mvi/.SplashActivity
通过adb命令,我们可以看到更详细的启动耗时,它的LaunchState(启动状态)是冷启动,总共耗时1.5s左右。
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.lay.mvi/.SplashActivity }
Status: ok
LaunchState: COLD
Activity: com.lay.mvi/.SplashActivity
TotalTime: 1556
WaitTime: 1557
Complete
2.2 方法耗时统计
因为在App启动的时候,像系统层的耗时我们其实很难处理,真正需要优化的就是应用层,也就是说从Application调用onCreate方法开始,所以对于Application中方法调用的耗时,我们可以做一次统计并分析。
override fun onCreate() {
super.onCreate()
Log.e("TAG", "Application onCreate")
Debug.startMethodTracing(”Launcher“)
threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
threadPool!!.execute {
//....
}
Thread.sleep(200)
Debug.stopMethodTracing()
}
程序运行之后,会生成一个trace文件,记得要开启读写权限
从上图中,我们可以看到在main方法中耗时最严重,那么这个是拿某个线上项目来看的,首先耗时最严重的,在初始化某个SDK的时候,耗时占比达到了34%;另外一个是初始化神策埋点的时候,耗时占用了25%,还有一个就是读写存储内容的时候,耗时占用了16%。
其实我们大概能看到耗时点主要分为两种:一种是初始化SDK,还有一种就是读写存储数据。
2.3 启动优化中的取舍
我们的SDK一定要放在主线程中初始化吗?这个答案只能说不一定,假设我们有3个SDK
耗时情况如下:SDK1耗时0.8s,SDK耗时1.5s,SDK3耗时1.1s,如果按照顺序执行,总共耗时为3.4s,那么如果我们有以下几个场景:
(1)3个SDK都不会立刻用到,那么全部可以懒加载放在子线程,耗时0s;
(2)SDK1和SDK2必须要用到,但是SDK3不会,SDK3放在子线程懒加载,总耗时2.7s;
(3)假设SDK3依赖SDK1和SDK2的结果,只能等到SDK3初始化完成才能进入首页,那么总耗时最大就是3.4s,其实我们可以将SDK1和SDK2都放在子线程,最多耗时1.5s返回全部的结果给SDK3,所以总耗时就是2.6s。
那么这是3个SDK,如果有5个、10ge甚至更多,而且相互之间存在依赖关系,在# Android性能优化 -- 图论在启动优化中的应用这篇文章中做了详细的介绍。
其实对于启动优化,我们能做的事情就是在应用层尽可能多地去减少耗时,有时候10ms、20ms的优化都是质的提升,所以在耗时的处理上如果遇到瓶颈,也别忘记在视觉上给用户好的体验。