Android 启动优化
为什么做启动优化
APP启动值得是用户从桌面点击APP icon 到APP 页面第一帧数据渲染出来的时间,如果启动时间过长会让用户满意度下降,甚至放弃使用,因此我们要保证功能前提下,尽量降低启动时间
首先复习一下App启动
Android App启动过程
Android ContentProvider启动流程
- 首先是点击App图标,此时是运行在
Launcher
进程,通过ActivityManagerService
Binder IPC的形式向system_server
进程发起startActivity
的请求 system_server
进程接收到请求后,通过Process.start
方法向zygote
进程发送创建进程的请求zygote
进程fork
出新的子进程,即App
进程- 然后进入
ActivityThread.main
方法中,这时运行在App
进程中,通过ActivityManagerService
Binder IPC的形式向system_server
进程发起attachApplication
请求 system_server
接收到请求后,进行一些列准备工作后,再通过Binder IPC向App
进程发送scheduleLaunchActivity
请求App
进程binder线程(ApplicationThread)
收到请求后,通过Handler
向主线程发送LAUNCH_ACTIVITY
消息- 主线程收到Message后,通过反射机制创建目标
Activity
,并回调Activity
的onCreate
而我们的Appcation的启动
是在第四步的attachApplication
中请求的开始的,下面我们就具体看源码分析
ActivityManagerService.java
public final void attachApplication(IApplicationThread thread, long startSeq) {synchronized (this) {int callingPid = Binder.getCallingPid();final int callingUid = Binder.getCallingUid();final long origId = Binder.clearCallingIdentity();attachApplicationLocked(thread, callingPid, callingUid, startSeq);Binder.restoreCallingIdentity(origId);}}
private final boolean attachApplicationLocked(IApplicationThread thread,int pid) {....thread.bindApplication(processName, appInfo, providers,app.instr.mClass,profilerInfo, app.instr.mArguments,app.instr.mWatcher,app.instr.mUiAutomationConnection, testMode,mBinderTransactionTrackingEnabled, enableTrackAllocation,isRestrictedBackupMode || !normalMode, app.persistent,new Configuration(getGlobalConfiguration()), app.compat,getCommonServicesLocked(app.isolated),mCoreSettingsObserver.getCoreSettingsLocked(),buildSerial);
}
这里调用了thread
的bindApplication
方法,thread
的类型是IApplicationThread
,是一个binder
用于跨进程通信,实现类是ActivityThread
的内部类ApplicationThread
类
App 的 application 创建是在 ActivityThread 的 handleBindApplication 方法完成的。
private void handleBindApplication(AppBindData data) {...// If the app is Honeycomb MR1 or earlier, switch its AsyncTask// implementation to use the pool executor. Normally, we use the// serialized executor as the default. This has to happen in the// main thread so the main looper is set right.if (data.appInfo.targetSdkVersion <= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) {AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR);}...final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);...app = data.info.makeApplication(data.restrictedBackupMode, null);...mInitialApplication = app;...try {mInstrumentation.callApplicationOnCreate(app);...}
handleBindApplication 通过 LoadedApk 的 makeApplication 构造 application。
LoadedApk 的 makeApplication 方法如下:
public Application makeApplication(boolean forceDefaultAppClass,Instrumentation instrumentation) {...ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);appContext.setOuterContext(app);...return app;}
makeApplication 通过 ContextImpl.createAppContext 构造了 contextImpl,然后用 newApplication 构造了 application。
Instrumentation 的 newApplication 方法如下:
public Application newApplication(ClassLoader cl, String className, Context context)throws InstantiationException, IllegalAccessException, ClassNotFoundException {Application app = getFactory(context.getPackageName()).instantiateApplication(cl, className);app.attach(context);return app;}
可以看出 newApplication 先构造了 Application,然后再 attach context,这一点和 Activity 的构造类似,都是先创建,再 attach 一些内容。
Application 的 attach 方法如下:
/* package */ final void attach(Context context) {attachBaseContext(context);mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;}
到这里就调用了attachBaseContext
看了上面分析和文章,你就会发现,这个流程系统启动过程我们是无法干预的,但是当启动到了自己的App进程,就可以知己处理了
- Appcation 构造方法
- Appcation attachBaseContext
- ContentProvider启动
- Appcation onCreate
- Activity onCreate
- Activity onStart
- Activity onResume
- View onDraw
- Activity#onWindowFocusChanged()
这几个流程就是我们可以控制的流程,我们的优化也是在这几个流程中进行优化
启动时间检测
查看Logcat关键字 Displayed
在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。
ActivityTaskManager: Displayed com.example.wanandroid/.MainActivity: +331ms
这种方式最简单,适用于收集 App 与竞品 App 启动耗时对比分析。
adb shell 命令查看启动耗时
adb shell am start -W [packageName]/[启动activity的全路径]
比如
adb shell am start -W com.example.wanandroid/com.example.wanandroid.MainActivity
然后会有结果
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.wanandroid/.MainActivity }
Status: ok
LaunchState: WARM
Activity: com.example.wanandroid/.MainActivity
TotalTime: 348
WaitTime: 351
Complete
- TotalTime 表示所有activity 的启动耗时
- WaitTime 表示aws 启动activity的耗时
TotalTime 就是应用启动耗时 他包括进程启动+Application启动+Activity初始化到ui显示时间
这种适合线下使用,对比竞品的启动速度
前面提到的俩种方式,统计的是App启动到Activity首次调用onWindowFocusChanged的时间,如果我们想要统计,App启动到网络数据请求之后的总耗时,可以在终点调用, activity.reportFullyDrawn(),通知当前已经绘制完成,然后就可以在Locat中看到
Displayed com.example.wanandroid/com.example.wanandroid.MainActivity: +3s171ms
Fully drawn com.example.wanandroid/com.example.wanandroid.MainActivity: +4s459ms
自己打点计时
上面我们已经分析APP的启动流程
- Appcation 构造方法
- Appcation attachBaseContext
- ContentProvider启动
- Appcation onCreate
- Activity onCreate
- Activity onStart
- Activity onResume
- View onDraw
- Activity#onWindowFocusChanged()
我们自己打点可以在Appcation attachBaseContext 作为起始点,Activity#onWindowFocusChanged() 作为终点来计算真正的启动时间
终点的选择其实有俩种第一种就是Activity#onWindowFocusChanged() 这种的话可能不是首帧,可能是2-3帧,统计时间会多几帧的耗时,但是相对来说稳定
如果首帧的渲染需要通过网络请求之后才能渲染数据,那么这个就不是很准确,因为Activity#onWindowFocusChanged() 统计的是默认UI的显示(就是写死的ui)
这种情况可以选择列表的第一个ItemView的perDrawCallback() 回调来计算真正的时间,当列表的第一个Item 显示数据,表示已经网络加载完成
// itemView添加预绘制回调监听
itemView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {override fun onPreDraw(): Boolean {return false}
})
获取启动各个阶段的耗时
上面我们计算了App启动的整体耗时,这个只能得出总耗时的结论,我们还需要知道启动各个阶段的耗时,这样我们才能确定到底是哪里的代码导致的,获取各个阶段的耗时,一般有俩种方法
- 手动埋点
- 编译时AOP
手动埋点
手动埋点比较简单,在需要的地方加上统计代码即可
编译时AOP
编译时AOP表示在编译期间要对耗时的函数进行插桩,在他计时前后进行耗时统计,这个后序单独出一篇博客讲解
启动优化工具
除了获取启动时间,线下测试如果想要更进一步的获取启动耗时点,可以使用工具进行进一步分析,比如TraceView,CPU Profiler SysTrace Perfetto 这些之后也会单独开一篇文章
启动优化方案
视觉优化启动速度
在冷启动的时候
- 启动后立即显示应用程序空白的启动窗口。
- 创建应用程序进程。
也就是说点击app后会先显示一个空白的window,如果启动时间过长,这样启动就会显示白框很久,所以体验会很不好
这种的解决办法就是,设置闪屏主题
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"><item name="android:windowBackground">@drawable/lunch</item> //闪屏页图片<item name="android:windowFullscreen">true</item><item name="android:windowDrawsSystemBarBackgrounds">false</item><!--显示虚拟按键,并腾出空间--></style>
把这个style设置给闪屏activity,其他的不用换,这样就可以直接显示闪屏图片,解决白屏问题,但是这个是治标不治本
减少ContenProvider 初始化耗时
由于ContenProvider 有自动初始化的特性,在App启动时会自动调用onCreate() 并且是在主线程进行,很多框架都喜欢在ContenProvider#onCreate()
进行初始化SDK,这样的初始化代码多了以后,很容易出现耗时比较长的代码(比如数据库查询),直接影响启动速度
一种比较好的处理方式,提供一个ContenProvider
的封装类,和一个异步的onCreate
方法,然后再编译时将继承ContenProvider
的类,改为继承这个基类,同时把onCreate
改为 onCrateAsycn
这样就是实现了ContenProvider#onCreate()
的异步化,减少对启动的影响
或者使用jetpack
中的App Startup
也能起到优化作用,通过App Startup
的Initializer
,将多个ContenProvider完成的初始化工作,合并到同一个ContenProvider
,减少创建ContenProvider
的成本
Application 阶段的优化
在应用启动的时候,可能有很多组件需要进行初始化,这样的初始化多了以后就会拖累启动速度,对于这种我们有俩种决解决思路
- 异步化
- 延迟加载
异步化
异步化理解起来很简单,就是把任务放到子线程,但是各个组件的初始化可能有依赖关系,如果只是简单的子线程恐怕达不到目的,所以我们需要一种框架他需要支持一下几个特点
- 根据任务类型决定运行任务的线程
- 根据依赖关系,自动将前一个依赖任务执行
- 根据任务优先级顺序执行任务
比如阿里巴巴的开源框架 alpha
通过配置,生成有向无环图,俩保证依赖顺序,通过配置将任务分配到合理的线程
延迟优化
对于一些启动并优先级不高的的组件我们可以选择延迟优化,延迟优化,我们可以选择利用IdleHandler
,只有在主线程空闲时才执行任务
Activity 阶段优化
这个阶段主要分为俩步
- 布局优化
- 本地数据缓存
布局优化
布局越复杂,加载布局的时间就越长,所以注意下面几点
- 删除无用布局(再需求迭代中,有些布局不在使用)
- 使用Viewstub 对布局进行懒加载
- 降低布局层级
- 使用megra 减少布局层级
也可以对布局进行预加载
加载XML 有以下三个流程
- 将XML 文件加载到内存中, XmlResourceParser 的 IO 过程
- 根据不同的name 反射View对象
- 最终形成View树
这块我们可以直接用代码写布局代替xml,或者使用在 androidx 中已经有提供了 AsyncLayoutInflater 用于进行 xml 的异步加载
本地数据缓存
正常情况下,App启动都是需要请求网络,然后再进行数据渲染,我们可以把上一次的数据缓存下来,下一次启动的时候,直接加载缓存数据,来达到降低启动时间的目的
其他优化方式
绑定大核提升启动速度
CPU根据频率,cache(高速缓存)大小等,区分为大核和小核,一般大核执行频率更高,我们可以代码查看CPU的最高频率
Process proc = Runtime.getRuntime().exec("cat /sys/devices/system/cpu/cpu5/cpufreq/cpuinfo_max_freq");proc.waitFor();InputStream inputStream = proc.getInputStream();reader = new BufferedReader(new InputStreamReader(inputStream));resultBuilder = new StringBuilder();String line = "";while (null != (line = reader.readLine())) {resultBuilder.append(line);}String result = resultBuilder.toString();
然后绑定大核
- 通过CPU_ZERO初始化一个cpu_set_t。
- 通过CPU_SET设置进程运行在哪个CPU上,可以调用多次。
- 执行sched_setaffinity设置亲和度。
void set_affinity() {cpu_set_t set;CPU_ZERO(&set);CPU_SET(3, &set);CPU_CLR(2, &set);int ret = sched_se taffinity(0, sizeof(cpu_set_t), &set);LOG("set_affinity ret: %d", ret);struct timeval time{};time.tv_sec = 3;select(0, nullptr, nullptr, nullptr, &time);int cpu = sched_getcpu();LOG("after sleep, run in cpu%d", cpu);get_affinity();
}//测试代码入口函数
void affinity_test() {LOG(" ");LOG(" affinity_test >> max cpu_set size: %d", CPU_SETSIZE);int cpu = sched_getcpu();LOG("run in cpu%d", cpu);set_affinity();
}
区分机型 低 中 高端机 优化分类
- 任务延时时间,在低端机上适当延迟久一些
- 任务异步,在低端机要考虑线程数,低端机减少是线程数
- 通过ClassLoader记录启动的类及耗时,然后再启动时异步加载需要的类,从而减少加载耗时
参考
Android 性能优化与实践