前言

之前我们说了启动优化的一些常用方法,但是有的小伙伴就很不屑了:

“这些方法很久之前就知道了,不知道说点新东西?比如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方法就是用来启动并执行各个ContentProvideronCreate方法的,它会在ApplicationonCreate方法之前执行。

所以这些库只是把Application的三方库初始化工作提前放到ContentProvider中了,并不会减少启动耗时,反而会增加启动耗时。

怎么说呢?因为不同的库就定义了不同的ContentProvider类,多了这么多ContentProviderContentProvider作为四大组件之一,启动也是耗时的,自然也就增加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"标签就是用来合并所有申明了InitializationProviderContentProvider

等等,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官方做的统计,图片来源于郭神的博客:

探究 | App Startup真的能减少启动耗时吗-LMLPHP

可以看到这里统计的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)如果想解决多个库初始化任务太多导致的启动耗时问题:

参考

Google文档

App Startup-郭霖

Android启动时间—siyu8023

App Startup源码—叶志陈

拜拜

12-21 18:09