前言
之前我们说了启动优化的一些常用方法,但是有的小伙伴就很不屑了:
“这些方法很久之前就知道了,不知道说点新东西?比如App Startup?能对启动优化有帮助吗?”
ok,既然你诚心诚意的发问了,那我就大发慈悲的告诉你:俺也不知道😢
。
走吧,一起瞅瞅这个App Startup
吧,是不是真的能给我们的启动带来优化呢?
(想看结果的可以直接跳到最后的实践
和总结
阶段)
Contentprovider中初始化
想必大家都了解,很多三方库都需要在Application
中进行初始化,并顺便获取到Application
的上下文。
但是也有的库不需要我们自己去初始化,它偷偷摸摸就给初始化了,用到的方法就是使用ContentProvider
进行初始化,定义一个ContentProvider
,然后在onCreate拿到上下文,就可以进行三方库自己的初始化工作了。而在APP的启动流程中,有一步就是要执行到程序中所有注册过的ContentProvider
的onCreate方法,所以这个库的初始化就默默完成了。
这种做法确实给集成库的开发者们带来了很大的便利,现在很多库都用到了这种方法,比如Facebook,Firebase
,这里拿Facebook
举例看看他的ContentProvider:
<provider
android:name="com.facebook.internal.FacebookInitProvider"
android:authorities="${applicationId}.FacebookInitProvider"
android:exported="false" />
public final class FacebookInitProvider extends ContentProvider {
private static final String TAG = FacebookInitProvider.class.getSimpleName();
@Override
@SuppressWarnings("deprecation")
public boolean onCreate() {
try {
FacebookSdk.sdkInitialize(getContext());
} catch (Exception ex) {
Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex);
}
return false;
}
//...
}
可以看到,在Fackbook的sdk中,定义了一个FacebookInitProvider
,并且在onCreate
中进行了初始化。所以我们才无需单独对Facebook的sdk进行初始化。
虽然更方便了,但是这种做法有给启动优化带来什么好处吗?我们一起再回顾下之前的启动流程研究下,截取一部分:
- ...
- attachBaseContext
- Application attach
- installContentProviders
- Application onCreate
- Looper.loop
- Activity onCreate,onResume
这其中installContentProviders
方法就是用来启动并执行各个ContentProvider
的onCreate
方法的,它会在Application
的onCreate
方法之前执行。
所以这些库只是把Application
的三方库初始化工作提前放到ContentProvider
中了,并不会减少启动耗时,反而会增加启动耗时。
怎么说呢?因为不同的库就定义了不同的ContentProvider
类,多了这么多ContentProvider
,ContentProvider
作为四大组件之一,启动也是耗时的,自然也就增加App启动消耗的时间了。
这时候就需要App Startup
来对此情况进行优化了~
官网简介
主要说了两点特性:
- 可以共享单个Contentprovider。
- 可以明确地设置初始化顺序。
可以共享单个Contentprovider
这一点功能就能解决刚才的问题了,不同的库不再需要去启动多个Contentprovider
了,而是共享同一个Contentprovider
。
这样就至少不会增加启动耗时了。
怎么操作呢?假如我们是FacebookSDK
设计者,我们就来改一下刚才的FacebookSDK
,集成App Startup
:
//导入库
implementation "androidx.startup:startup-runtime:1.0.0"
// Initializes facebooksdk.
class FacebookSDKInitializer : Initializer<Unit> {
private val TAG = "FacebookSDKInitializer"
override fun create(context: Context): Unit {
try {
FacebookSdk.sdkInitialize(context)
} catch (ex: Exception) {
Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex)
}
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
//AndroidManifest.xml中定义
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.FacebookSDKInitializer"
android:value="androidx.startup" />
</provider>
实现了Initializer
接口,然后在onCreate方法中进行初始化即可,只要所有的库都按照这个标准来初始化,而不是自己单独自定义ContentProvider
,那么确实可以减少启动耗时。
其中,tools:node="merge"
标签就是用来合并所有申明了InitializationProvider
的ContentProvider
。
等等,Initializer
接口还有一个方法dependencies,这又是干啥的呢?
可以明确地设置初始化顺序
这也就是App Startup的第二个特性了,可以设置初始化顺序。
可以想象,按照上述做法,所有库都这样设定了,那么都会在同一个ContentProvider
也就是androidx.startup.InitializationProvider
中初始化,但是如果我需要设定不同库的初始化顺序怎么办呢?
比如上述的facebook
初始化,我需要设定在另一个库WorkManager
之后运行,那么我们就可以重写dependencies
方法:
class FacebookSDKInitializer : Initializer<Unit> {
private val TAG = "FacebookSDKInitializer"
override fun create(context: Context): Unit {
try {
FacebookSdk.sdkInitialize(context)
} catch (ex: Exception) {
Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex)
}
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return listOf(WorkManagerInitializer::class.java)
}
}
不错吧,这样设定之后,三方库的初始化顺序就变成了:
WorkManager初始化 -> FacebookSDK初始化。
实践出真理
说了这么多,从理论上来说,确实App Startup
减少了耗时,毕竟将多个ContentProvider
融合成了一个,那么我们秉着“实践才是检验真理的唯一标准”,就来实践看看耗时减少了多少。
该怎么统计这个启动时间呢?一般有以下几个方案:
如果是Application和Activity的时间可以通过
TraceView、systrace
等 的方式进行时间统计,但是ContentProvider
的初始化在Application之前,不适用我们这次实践。Android官方提供了一个可以统计线上应用启动时间的工具——
Android Vitals
,它可以在GooglePlay管理中心显示应用启动过长情况的启动时间,很显然这个也不适用于我们,这个必须上线到Googleplay
。视频录制。如果是线下的app,我们可以采用
视频录制
的方法准确测量启动时间,也就是通过判定视频的每一帧截图来知晓什么时候app启动了,然后统计这个启动时间。具体做法就是使用adb shell screenrecord
命令进行屏幕录制然后分析视频,有兴趣的小伙伴可以网上找找资料,这里就不细说了。最后,就是用系统自带的统计时间
TotalTime
。
这个时间是Android源码中帮我们计算的,可统计到Activity的启动时间,如果我们在Home页执行命令,也就能得到一个冷启动的时间。虽然这个时间不是很准确,但是我只需要比较App StartUp使用的的前后时间大小,所以也够用了,开干。
1)测试2个ContentProvider
第一次,我们测试2个ContentProvider
的情况。
<provider
android:name=".appstartup.LibraryAContentProvider"
android:authorities="${applicationId}.LibraryAContentProvider"
android:exported="false" />
<provider
android:name=".appstartup.LibraryBContentProvider"
android:authorities="${applicationId}.LibraryBContentProvider"
android:exported="false" />
安装到手机后,打开应用,Terminal
中输入命令:
adb shell am start -W -n packagename/packageName.MainActivity
由于每次启动时间不一,所以我们运行五次,取平均值:
TotalTime: 927
TotalTime: 938
TotalTime: 948
TotalTime: 934
TotalTime: 937
平均值:936.8
然后注释刚才的ContentProvider
注册代码,添加App startup代码,并注册:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.studynote.appstartup.LibraryAInitializer"
android:value="androidx.startup" />
<meta-data android:name="com.example.studynote.appstartup.LibraryBInitializer"
android:value="androidx.startup" />
</provider>
运行App,并执行命令,得出启动时间:
TotalTime: 931
TotalTime: 947
TotalTime: 937
TotalTime: 940
TotalTime: 932
平均值:937.4
咦??我手机坏了吗?怎么跟预想的不一样啊,结果耗时还增加了?
按道理来说原来有两个ContentProvider
,用了App startup
,集成为一个,耗时不应该减少么。
其实这就涉及到ContentProvider
的实际耗时了,我在网上找到一张图,关于ContentProvider
耗时,是Google
官方做的统计,图片来源于郭神的博客:
可以看到这里统计的1个ContentProvider
耗时2ms
左右,10ContentProvider
耗时6ms左右。
所以我们只减少了一个ContentProvider的耗时,几乎可以忽略不计。再加上我们用到的App Startup库中InitializationProvider
的一些任务也会产生耗时,比如:
- 会去遍历所有
metadata
标签的组件 - 会通过反射获取每个组件的
Initializer
接口,并获取相应的依赖项,并进行排序
。
这些操作也是耗时的,也就是集成App Startup
库之后增加的耗时时间。所以就有可能会发生上面的情况了,集成App Startup库之后启动耗时反而增多。
那难道这个库就没用了吗?肯定不是的,当ContentProvider的数量变多,它的作用就体现出来了,再试下10个ContentProvider
的情况。
2)10个ContentProvider
首先写好10个ContentProvider
,并在AndroidManifest.xml中注册:
<provider
android:name=".appstartup.LibraryAContentProvider"
android:authorities="${applicationId}.LibraryAContentProvider"
android:exported="false" />
<!-- 省略剩下9个provider注册代码 -->
运行五次,取平均值:
TotalTime: 1758
TotalTime: 1759
TotalTime: 1733
TotalTime: 1737
TotalTime: 1747
平均值:1746.8
然后注释刚才的ContentProvider
注册代码,添加App startup代码,并注册:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.studynote.appstartup.LibraryAInitializer"
android:value="androidx.startup" />
<!--省略剩下9个meta-data注册代码-->
</provider>
运行App,并执行命令,得出启动时间:
TotalTime: 1741
TotalTime: 1755
TotalTime: 1722
TotalTime: 1739
TotalTime: 1730
平均值:1737.4
可以看到,这里App Startup的作用就体现了出来,在使用App Startup
之前的启动耗时是1746.8ms
,使用之后启动耗时是1737.4ms
,减少了9.4ms
。
所以得出结论,当集成的库使用的ContentProvider
达到一定个数之后,确实能减少耗时,但是减少的不多,比如这里我们是10个ContentProvider
集成App Startup
后能减少的耗时在10ms
左右,再结合上图官方的统计时间来看,一般一个项目集成了十几个使用ContentProvider的库,耗时减少应该能在20ms之内。
所以我们的App Startup
解决的就是这个耗时时间,虽然不多,但是也确实有减少耗时的功能。
思考
虽然这个库能解决一定的三方库初始化
耗时问题,但是我觉得还是有很大的局限性
,比如这些问题:
本身依赖的库就不多
。如果我们的项目本身依赖就不多,那么有没有必要去集成这个呢?极端情况下,只依赖了一个库,那么还要专门提供一个InitializationProvider,是不是又变相的增加了耗时呢?延时初始化
。上次我们说过,有些库并不需要一开始就初始化,那么我们最好将其延迟初始化,进行懒加载。异步初始化
。同样,有些库不需要在主线程进行初始化,那么我们可以对其进行异步初始化,从而减少启动耗时。多个异步任务依赖关系
。如果有些任务需要异步执行的同时还有互相的依赖关系,该怎么办呢。
如果我们在使用App Startup
的时候,有以上需求,那么有没有解决办法呢?
- 没有,也可以说有,就是关闭
App Startup
的初始化动作,然后自己进行初始化任务管理。
这可不是开玩笑,App Startup
的目的只是解决一个问题,就是多个ContentProvider
创建的问题,通过一个统一的ContentProvider
来形成规范,减少耗时。所以它的用法应该是针对各个三方库的设计者,当你设计一个库的时候,如果想静默初始化,就可以接入App Startup。当尽量多的库遵循这个要求,都接入App Startup
的时候,开发者的启动耗时自然就降低了。
但是如果我们有其他的需求,比如上述说到的延迟初始化,异步初始化等问题,我们就要关闭部分库或者所有库的App Startup
的功能,然后自己单独对任务进行初始化工作,比如通过启动器
来处理各个初始化任务的关系。
如果一个库已经集成了App Startup
功能,我们该怎么关闭呢?这就用到tools:node="remove"
标签了。
<!-- 禁用所有InitializationProvider组件初始化 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<!-- 禁用单个InitializationProvider组件初始化 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.FacebookSDKInitializer"
android:value="androidx.startup"
tools:node="remove"/>
</provider>
这样FacebookSDK
就不会自动进行初始化了,需要我们手动调用初始化方法。
总结
1)App Startup
的设计是为了解决一个问题:
- 即不同的库使用不同的ContentProvider进行初始化,导致ContentProvider太多,管理杂乱,影响耗时的问题。
2)App Startup
具体能减少多少耗时时间:
- 上面也实践过了,如果二三十个三方库都集成了App Startup,减少的耗时大概在20ms以内。
3)App Startup
的使用场景应该是:
- 针对三方库的设计者或者组件化的场景。当你设计一个库或者一个组件的时候,就可以接入App Startup。当尽量多的库遵循这个标准,都接入App Startup的时候,就能形成一种规范,App的启动耗时自然就降低了。
4)如果想解决多个库初始化任务太多导致的启动耗时
问题:
- 请左转前往各种启动器,比如alibaba/alpha