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

前言

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

“这些方法很久之前就知道了,不知道说点新东西?比如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来对此情况进行优化了~

官网简介

The App Startup library provides a straightforward, performant way to initialize components at application startup. Both library developers and app developers can use App Startup to streamline startup sequences and explicitly set the order of initialization.Instead of defining separate content providers for each component you need to initialize, App Startup allows you to define component initializers that share a single content provider. This can significantly improve app startup time.

主要说了两点特性:

  • 可以共享单个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官方做的统计,图片来源于郭神的博客:

image

可以看到这里统计的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源码—叶志陈

拜拜

感谢大家的阅读,有一起学习的小伙伴可以关注下我的公众号——码上积木❤️❤️
每日三问知识点/面试题,积少成多。
这里有一群很好的Android小伙伴,欢迎大家加入~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容