前言:
文本主要会介绍三大块:
1.简略介绍APP启动的完整流程,对整个流程有所了解,才知道在哪里可以进行优化。
2.一些常用的APP启动优化的方案,主要分为三大块优化方向。
3.一些不常见的APP启动优化的方案,甚至包含一些FW层的代码改动,有的可能是对应用开发者无效的,但是对于车载开发是有用的。
一安卓APP启动完整流程分析(冷启动)
图1:
主要分为三个阶段:
1.1 桌面点击APP图标,通知到AMS去完成应用进程的创建的流程
1.安卓系统启动后,拉起的第一个应用是桌面应用。它由SystemServer负责创建,并持有AMS的binder引用。
2.点击桌面图标后,Launcher会通过binder通知AMS启动该APP。
3.AMS会根据传递过来的信息查询APP应用进程在后台是否存在。如果在后台,则属于热启动,如果不在后台,则属于冷启动。优化的重点一般都是冷启动。
4.如果APP进程不存在。AMS主要会做两件事:
一:AMS首先会读取对应APP的Manifest信息(此配置信息是存在于AMS中,手机启动或者应用安装时读取到内存中的),然后根据MainActivity的主题设置,读取其背景图并展示到屏幕上。
二:通过socket的方式通知Zygote去fork产生APP进程。
如图2所示:
1.2 应用进程创建后的流程
5.APP进程创建后,会通过执行ActivityThread中的main方法。此方法主要做了两件事,
第一会进行Looper的初始化;
第二会通过attach方法通知AMS,进行进程的绑定,此时也会把APP创建的binder传递给AMS。
6.AMS中完成注册绑定后,会通过binder通知APP进行application的绑定。APP端binder的接收者是ApplicationThread(下同)。ApplicationThread会被调用bindApplication方法,然后通过handler通知ActivityThread去调用handleBindApplication方法。
7.handleBindApplication方法负责应用初始化的所有流程。主要流程图如下图所示:
图3:
首先,在方法中,通过ContextImpl.createAppContext(this, data.info)去加载DEX文件以及资源。
具体加载DEX的方法在LoadedApk的createOrUpdateClassLoaderLocked方法中:
private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
...
//生成Classloader
mDefaultClassLoader = ApplicationLoaders.getDefault().getClassLoaderWithSharedLibraries(
zip, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
libraryPermittedPath, mBaseClassLoader,
mApplicationInfo.classLoaderName, sharedLibraries, nativeSharedLibraries);
mAppComponentFactory = createAppFactory(mApplicationInfo, mDefaultClassLoader);
...
//把上面生成的mDefaultClassLoader赋值给mClassLoader
if (mClassLoader == null) {
mClassLoader = mAppComponentFactory.instantiateClassLoader(mDefaultClassLoader,
new ApplicationInfo(mApplicationInfo));
}
}
第二,去执行Application的的attachBaseContext方法。
第三,执行installProvider方法,加载App中的各个ContentProvider
第四,执行Application的onCreate()方法
1.3 Activity的首屏展示流程
8.AMS通知APP创建Application后,还会通知APP进程去启动activity。这时候ApplicationThread接收到之后,会通知ActivityThread完成Activity的启动流程。这个根据android版本不同有区别,android12之后传递的是ClientTransaction。该对象包含一系列事务,对应的会通知构建activity的各个流程。
9.ActivityThread中会分别执行handleLaunchActivity,handleStartActivity,handleResumeActivity等方法,对应的会执行Activity的onCreate,onStart,onResume方法。
10.在handleResumeActivity方法中,执行完resume方法后,判断如何未关联到window上,则会把DecorView加到到ViewManager上。
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
...
}
11.ViewManager的最终实现类是WindowManagerGlobal,在addView(DecorView)时,会创建ViewRootImpl负责后续绘制的完整流程。
12.添加完成后。ViewRootImpl会生成渲染任务TraversalRunnable,在收到垂直信号量之后执行。任务执行完成后,则首屏就显示在屏幕上了。
二.如何排查启动卡顿问题
2.1 使用SystemTrace的方式进行分析
2.1.1 如何得到冷启动总耗时的准确数据
首先的确保杀死应用,adb shell pkill beanlab (包名匹配到beanlab就杀掉)
然后使用 adb shell am start -W [包名]/[包名.Activity] 。
启动APP,查询App的启动时间
查询结果中,对应的时间参数详细解析如下:
ThisTime:对应activity启动耗时;
TotalTime:应用自身启动耗时 = ThisTime + 应用application等资源启动时间
WaitTime:系统启动应用耗时 = TotalTime + 系统资源启动时间
2.1.2 如何观察冷启动耗时的各个时间段的准确数据
通过抓取systrace可以从图上观察到 bindApplication ->activityStart->activityResume->Choreographer#doFrame
也可以添加自己的方法抓取,注意开始和结束必须成对出现
Trace.beginSection(“你的方法”);
Trace.endSection();
2.1.3 如何得到冷启动结束的回调
首先是display time:从Android KitKat版本开始,Logcat中会输出从程序
启动到某个Activity显示到画面上所花费的时间。这个方法比较适合测量程序的启动
时间。
筛选AcctivityManager: Displayed
ActivityManager: Displayed com.beantechs.beanlab/.ui.HomeActivity: +842ms 这个时间 和 adb启动分析的 TotalTime 一致
2.2 使用matrix框架
三.如何进行启动优化
第一章时,我们了解了一个APP启动的完整流程。对这个流程分析一下,我们可以住要分成以下三块:
1.从用户点击图标,到通知系统去创建APP进程。
2.APP进程创建后,通知AMS并且进行绑定并走Application的所有流程。
3.启动Activity的流程。
所以如何进行启动优化,也主要按照这三块分类去讲解。
3.1 优化APP进程创建之前的卡顿问题
这一块由于主要运行在系统层面,所以我们可优化的点不多。虽然我们不能彻底解决,但是还是可以一定程度上优化用户的体验。
3.1.1 通过预制图进行体验感觉上的优化。
第一章的时候我们讲过,AMS会根据传递过来的信息,会在启动APP进程之前,加载一张APP的背景图。所以我们可以通过提前加载预制图,让用户感官上知道我们APP已经启动。目前市场上大的APP都有设置预制图,比如支付宝的预制图就是以下这张:
配置方法如下:
在Manifest中,给Main的Activity设置theme即可。
<activity
android:name=".SplashActivity"
android:theme="@style/LoadingAppTheme"
tools:replace="android:label">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<style name="LoadingAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:listDivider">@drawable/divider_line</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@drawable/launcher_previewwindow</item>
</style>
android12及以上的版本,支持黑白屏动画形式的展示,具体的使用方式这里就不展开了,原理是一样的。
将现有的启动画面实现迁移到 Android 12 及更高版本
3.2 优化应用初始化耗时问题
应用初始化的优化,主要其实就是就是对bindApplication这个方法的优化,通过打点日志分析,我们发现其实主要有以下几个耗时点,也是对应的我们的优化点。
3.2.1 优化APK加载
第一章时我们知道,加载DEX是在ContextImpl.createAppContext()的时候,自然也是启动主流程上。加载DEX的流程是从APK包中解压,然后ODEX优化,最后加载到内存当中。自然的,如果DEX越少,那么解压的就越少,ODEX优化的越少,速度也就越快。
所以我们优化的主要方法是拆分APP,也是通过组件化,插件化的方式去进行加载。
系统需要在启动的时候读取哪些内容呢?主要有两个部分,dex文件和资源文件。所以我们可以把一个很大的APK,按照业务拆分成多个小的APK。其中主APK中的Dex文件和资源弄的很小,其余业务APK放到asset文件夹中。等到APP启动后,再去解压加载业务APK。因为主APK很小,所以启动速度自然就会快得多。而等到应用启动后,再去通过懒加载的方式,逐渐加载其它模块的业务APK,因为在后台加载,所以也不会影响用户正常的操作,这就是我们组件化启动优化的方案。
结合实际项目,大多数APP其实都不大,所以并不太需要通过这种模块化的方案进行优化,所以就不详细讲如何优化了。有兴趣的可以看一下
Shadow
,
RePlugin
等框架。
3.2.2 ContentProvider优化
通过第一章的图3我们可以知道,在Application的启动流程中,会依次执行attachBaseContext,ContentProvider的onCreate,Application的onCreate方法。
所以ContentProvider的onCreate方法中,一定不能有耗时操作,否则会拖慢运行速度。
举例:
public class XXXProvider{
...
@Override
public boolean onCreate() {
//context init
...
//db init
dbUtil = DatabaseUtil.getInstance();
dbUtil.open();
return true;
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return dbUtil.fetchAll();
}
...
}
我们可以知道,加载数据库是耗时操作,则我们应该挪到其他步骤当中。
解决问题:
ContentProvider的onCreate()是启动流程当中的,所以我们可以把加载数据库的耗时操作放到query中,或者等到启动完成后延时加载。
public class XXXProvider{
...
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
if(dbUtil==null){
dbUtil = DatabaseUtil.getInstance();
dbUtil.open();
}
return dbUtil.fetchAll();
}
...
}
3.2.3 Application中onCreate优化
进行onCreate优化,核心是要梳理onCreate中我们到底做了什么。通过梳理可以发现,onCreate方法中,我们经常要初始化各种框架,比如Bugly.init(),LeakCanary.init()等等,这些框架虽然每个耗时并不多,但是因为是串行执行,累加起来,总耗时反而不少。
举例:
这里以某款APP为例,我们先查看其systrace文件(该文件在5.2当中)
其中bindApplication方法中有一部分如下图所示:
图中显示有加载多个不同SDK类的操作,而且是串行的结构,所以我们可以推断,应用在bindApplication的时候做了太多的初始化操作并且并行执行,导致耗时较多,而且bindApplication对应的是我们项目中Application的onCreate方法。
解决问题:
所以我们可以进行以下几点的优化:
1.部分任务不要主线程执行的,可以挪到子线程执行。
2.有些任务在子线程执行,但是依赖主线程某个任务执行完才可以。这种我们可以等到对应主线任务执行完再把任务加入到子线程池中。
当然,如果相互依赖的逻辑复杂,上面的方式就不太合适了,我们可以使用一个已经封装好的任务拓扑依赖框架来解决这问题:
android-startup
使用简介:
首先build.gradle中添加依赖:
implementation 'io.github.idisfkj:android-startup:1.1.0'
创建待执行的启动任务类:
class SampleFirstStartup : AndroidStartup<String>() {
//是否主线程执行
override fun callCreateOnMainThread(): Boolean = true
//是否依赖主线程任务
override fun waitOnMainThread(): Boolean = false
//执行的初始化操作
override fun create(context: Context): String? {
// todo something
return this.javaClass.simpleName
}
//依赖哪些其他任务
override fun dependenciesByName(): List<String>? {
return null
}
}
然后我们在启动的时候,把这些StartUp组装一下就可以了。
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
StartupManager.Builder()
.addStartup(SampleFirstStartup())
.addStartup(SampleSecondStartup())
.addStartup(SampleThirdStartup())
.addStartup(SampleFourthStartup())
.build(this)
.start()
.await()
}
}
或者在manifest中注册也可以
<provider
android:name="com.rousetime.android_startup.provider.StartupProvider"
android:authorities="${applicationId}.android_startup"
android:exported="false">
<meta-data
android:name="com.rousetime.sample.startup.SampleFourthStartup"
android:value="android.startup" />
</provider>
3.2.4 其它初始化时的耗时操作
1.SharedPreferences涉及到IO操作,所以如果SP存储数据较大的话,阻塞时间会较长
SharedPreferences sp = getSharedPreferences("1", 0);
sp.getString("1", "1");
3.3 解决Activity加载问题
3.3.1:主线程不执行耗时操作
通过第一章的流程讲解,我们可以知道在第一帧绘制之前,主线程会执行onCreate,onStart,onResume三个方法,所以这三个方法中,一定不能有耗时的方法。
如果一定需要主线程执行的,可以使用IdelHandler的方式解决(IdelHandler会在主线程不忙时执行)。
比如我们在加载数据的时候,就可以通过IdelHandler的方式来代替:
实例如下:
原代码:
@Override
protected void onResume() {
super.onResume();
...
loadData();
...
}
private void loadData() {
...
组装请求..
发送请求...
解析数据...
}
改成:
@Override
protected void onResume() {
super.onResume();
...
MessageQueue.IdleHandler idleHandler = () -> {
loadData();
return false;
};
getMainLooper().getQueue().addIdleHandler(idleHandler);
...
}
private void loadData() {
...
组装请求...
发送请求...
解析数据...
}
3.3.2:预加载页面
我们通过第二章的工具可以发现,Activity的几个生命周期中,onCreate方法一般是最为耗时的,而onCreate方法中,最为耗时的一般是setContentView(int)方法。
通过深入阅读源码可以可以发现,这其中最为耗时的部分就是把复杂的xml转化为ViewGroup对象。
这种问题,我们可以通过下面的方案来解决:
1. 使用AsyncLayoutInflater。
2.我可以做一个这样的操作,在Application的onCreate方法的任务中,添加一个这样的任务,子线程中把xml转化为ViewGroup。这样执行到Activity的onCreate方法中时,我们可以直接使用ViewGroup对象,从而节省了XML解析的时间。(如果布局文件中含有fragment不能采用此方案)
3.转Compose。Compose的话没有解析XML的时间,也不受到过多布局层级的影响。
3.3.3:局部加载优先显示框架或者占位图
还是通过第二章的工具,我们发现在项目中,measure/layout/draw也是很耗时的(其中一半measure是最耗时的)。这是因为我们布局太复杂了,导致界面渲染的时候需要反复计算,从而耗费时间。
所以针对这种情况,我们可以主要有4个方法来解决:
1.非主框架的部分使用ViewStub加载,等到主框架加载并显示出来后,再去加载内容的部分。我们经过测算,在第四帧之后,框架是可以完全显示出来的,所以在第四帧之后进行ViewStub内容的加载,是最为合适的。
2.降低布局层次嵌套和复杂度。复杂布局使用约束布局,尽量少使用weight属性等等。
3.解决过度绘制问题
4.优先加载占位图,等到内容显示好之后,在把占位图隐藏掉。
3.3.4:预加载数据
页面加载好了之后,自然就是请求网络加载数据了。
通过网络加载数据,大多数都是通过OKHttp进行请求。如果想更快的展示出来并且不在乎有效性,那么可以开启使用缓存,并且设置缓存有效时间。
但是这样也有个问题,如果设置了有效期,那么有效期内该请求全部都使用缓存。此时就无法获取最新的数据了,哪怕非首次请求也不行,因为服务并不会为你单独开辟一个新的接口。
我们可以实现这样的一个小需求:“首次请求使用缓存,非首次请求不使用缓存,并且还能把收到的响应更新到首次请求的缓存中”,这样就能比较好的解决上面所说的问题了。
具体例子可以参考我的另外一篇文章中的6.2。
OKHttp原理讲解之责任链模式及扩展_失落夏天的博客-CSDN博客_android okhttp责任链
3.4 其它优化
除了针对我们自身代码的优化,还有什么别的优化空间吗?当然有
3.4.1 Baseline Profile
这是google2022年开发者大会新提出的方案。
其核心原理是安卓7.0以后Android支持JIT,AOT并存的混合编译模式。
两者各有优势,JIT即时编译,虽然运行速度慢,但不需要编译时间。而AOT需要编译,后续运行速度快。
一般情况下,首次启动的时候会使用JIT编译,因为AOT需要转换,会导致首次启动耗时。后续使用的时候,安卓系统会根据使用频率计算出那些高频使用的代码,转换为AOT的方式进行编译加载,保证后续这块代码的运行速度。
而Baseline Profile就是需要我们自己把这些高频使用代码,提前打包到APK中,这样首次启动的时候,安卓会通过AOT转换为ODEX代码,以后使用这些可执行文件,速度上就会更快。
3.4.2 Hardcoder
这是腾讯开源的一个框架,核心原理是在需要手机性能时,主动通知系统去提升CPU频率,从而提升手机性能。而不需要性能时,则通知系统降频,避免手机电量的浪费。
https://github.com/Tencent/Hardcoder
3.4.3 Embryo方案
这是一加手机的一个方案,简单解释下就是在后台预创建一个进程,提前加载好资源。这样等到这个APP真的启动的时候,就可以直接使用,而不是重新创建了。
这里其实我有一个更简单有效的方案,我们知道,APP冷启动的时候,是System_server通知Zygote去fork应用进程的,应用进程创建后,再回掉通知System_server进程。至少在这段时间内,是没有绑定任何应用层信息的,也就是下图红框中的部分。
所以,就像APP中的预加载一样,我们为什么不能在系统层预加载一个APP进程呢?等到真的有APP应用创建需求的时候,直接去使用这个APP进程,而不是走创建流程创建一个。因为走的是socket通信,以及fork进程需要时间,所以整个流程有可能可以节省多达100ms的流程。这个方案还在调研探索中,目前理论上是可行的。
3.4.4 Redex方案
这个是facebook提出的一个方案,其实核心本质和proguard有一些类似,混淆字节码,让加载的DEX文件变的更小,则加载变得就更快。redex还有一个突出的亮点就是除了混淆之外,还能做一定的字节码层面的优化,比如A调用B,B调用C的场景,直接改成A调用C,这样减少了一层方法栈,执行速度肯定是更快的。
当然,这个的目标就不单纯的只是解决启动速度了,而是让APP运行的更快乐。
但是这个方法已经已经推出了很久了,到目前为止并不是很流行,其原因也是这样的操作容易引起各种各样的问题。
四.声明和备注
4.1 声明
文章中的原理和资料来自于网上搜索的资料,以及针对安卓源码的调试所得。基于的安卓版本是12。如有描述不准确的地方,或者好的建议,欢迎指出来。