探究 | App Startup真的能減少啟動耗時嗎

jimuzz發表於2020-12-21

前言

之前我們說了啟動優化的一些常用方法,但是有的小夥伴就很不屑了:

“這些方法很久之前就知道了,不知道說點新東西?比如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官方做的統計,圖片來源於郭神的部落格:

可以看到這裡統計的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原始碼—葉志陳

拜拜

有一起學習的小夥伴可以關注下❤️ 我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。公眾號回覆111可獲得面試題《思考與解答》以往期刊。

相關文章