1. 程式人生 > >淺談Android啟動優化

淺談Android啟動優化

一、前言
隨著我們的應用版本迭代,需要整合和增加的資源越來越多,尤其是在Application中,應用的效能也將出現很多需要優化的點。因此下面我們將從一個apk的啟動原理去分析和解決啟動時常常遇到的白屏、卡頓或者時間過長而帶來的體驗問題。

二、應用啟動方式
1、冷啟動
概念:是指啟動應用時系統程序中沒有該應用,這時系統會重新建立一個新的程序分配給該應用,這個啟動方式就是冷啟動。
特點:冷啟動因為系統會重新建立一個新的程序分配給它,所以會先建立和初始化Application類,再建立和初始化MainActivity類(包括一系列的測量、佈局、繪製),最後顯示在介面上。
2、熱啟動
概念:當啟動應用時,後臺已有該應用的程序(例:按back鍵、home鍵,應用雖然會退出,但是該應用的程序是依然會保留在後臺,可進入任務列表檢視),所以在已有程序的情況下,這種啟動會從已有的程序中來啟動應用,這個方式叫熱啟動。


特點:當啟動應用時,由於系統程序中已經有了該程序,所以熱啟動就不會走Application這步了,而是直接走MainActivity(包括一系列的測量、佈局、繪製),所以熱啟動的過程只需要建立和初始化一個MainActivity就行了,而不必建立和初始化Application,因為一個應用從新程序的建立到程序的銷燬,Application只會初始化一次。

以上兩種情況,我們可以在application中通過Log日誌去驗證。

三、啟動過程
解決問題之前我們有必要先了解一下Android應用的啟動過程,而啟動最慢、挑戰最大的就是冷啟動:系統和App本身都有更多的工作要從頭開始!因此我們重點總結一下冷啟動的過程:
點選桌面應用圖示,系統會從Zygote程序中fork創建出一個新的程序分配給該應用,之後依次建立Application類、Activity類,載入主題樣式Theme中的windowbackground等屬性給activity,最後通過inflate載入佈局,當oncreate/onstart/onresume等走完後最後進行contentView的measure/layout/draw顯示在介面上,到此為止,應用的首次啟動全部完成。

四、問題
我們通常在冷啟動過程中碰到的白黑屏以及delay過長的問題存在於從Application初始化到介面完成contentView的繪製之間:
在這裡插入圖片描述

1、Application

UI上:給系統Theme設定windowbackground屬性,用靜態啟動圖的展示去掩蓋資料的載入:

<application
        android:name=".function.application.AndroidApplication"
        android:theme="@style/AppThemeHxd">
</application>

.....

 <style name="AppThemeHxd" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="android:windowBackground">@mipmap/flash</item>
 </style>

邏輯上:UI傷的處理雖然看起來貌似啟動很快,但是實際問題並未從根本上得到解決(治標不治本)。因此我們還的需要看實際程式碼,查詢影響初始化的邏輯:
優化前:

class HxdApplication( ) {
    override fun onBaseContextAttached(base: Context?) {
        super.onBaseContextAttached(base)
        /**
         * You must install multiDex whatever tinker is installed!
         * 解決65535方法數超標的問題:採用 MultiDex 的 App 解壓後可以看到有classes.dex,classes2.dex等多個dex檔案,
         * 每個 dex 都可以最大承載 64k 個方法
         */
        MultiDex.install(base)
        //安裝tinker
        Beta.installTinker(this)
    }

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            // crash catch init
            val crashHandler = CrashHandlerUtil.getInstance()
            crashHandler.init(applicationContext)

            // logger init
            val formatStrategy = PrettyFormatStrategy.newBuilder()
                    .showThreadInfo(false)  //(可選)是否顯示執行緒資訊。 預設值為true
                    //.methodCount(2)         // (可選)要顯示的方法行數。 預設2
                    //.methodOffset(5)        // (可選)隱藏內部方法呼叫到偏移量。 預設5
                    //.logStrategy(customLog) //(可選)更改要列印的日誌策略。 預設LogCat
                    .tag("HXD")   //(可選)每個日誌的全域性標記。 預設PRETTY_LOGGER
                    .build()
            Logger.addLogAdapter(AndroidLogAdapter(formatStrategy))

            //init ARouter
            ARouter.openLog()
            ARouter.openDebug()
            ARouter.printStackTrace()
            //記憶體洩漏
            if (LeakCanary.isInAnalyzerProcess(instance)) {
                return
            }

            //嚴苛模式
            StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
                    .detectAll()//開啟所有的detectXX系列方法
                    //.penaltyDialog()//彈出違規提示框
                    .penaltyLog()//在Logcat中列印違規日誌
                    .build())
            StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
                    .detectActivityLeaks()//檢測Activity洩露
                    .penaltyLog()//在Logcat中列印違規日誌
                    .build())
        }

        initDeviceInfo()
        initInjector()
        //init ARouter
        ARouter.init(instance)
        // init user info
        initUserInfo()
        // 下拉重新整理
        SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, layout ->
            StoreHouseHeader(context).initWithString("loony")
        }
        // init 第三方庫
        ThirdInitUtil.thirdInitUtils(instance)
     }
    }

通過上面的程式碼我們可以看得出來Application的onBaseContextAttached()方法和onCreate()裡乾的事非常多:MultiDex分包、安裝tinker、初始化CrashHandler、初始化logger、初始化ARouter、三方庫StrictMode、初始化裝置資訊、初始化介面資源管理器Injector、初始化使用者資訊、初始化下拉重新整理控制元件以及其他三方庫的初始化!邏輯眾多,任務繁重,嚴重拖累了Application的後腿!通過整理我們改造後的邏輯是這樣的:
優化後:

Application

class HxdApplication( ) {
    override fun onBaseContextAttached(base: Context?) {
        super.onBaseContextAttached(base)
        MultiDex.install(base)
    }

    override fun onCreate() {
        super.onCreate()
        
		//TODO 為了不佔用該app程序在啟動時申請系統的空間和CPU運算資源,將以下三方資源的初始化放線上程裡,然後將該執行緒的優先順序降低,設定成後臺執行
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
        launch {
            //安裝tinker
            Beta.installTinker(this)
            //init ARouter 介面切換控制器
            ARouter.init(instance)
            // init 第三方庫
            ThirdInitUtil.thirdInitUtils(instance)

            if (BuildConfig.DEBUG) {
                //init ARouter
                ARouter.openLog()
                ARouter.openDebug()
                ARouter.printStackTrace()

                // crash catch init
                val crashHandler = CrashHandlerUtil.getInstance()
                crashHandler.init()

                // logger init
                val formatStrategy = PrettyFormatStrategy.newBuilder()
                        .showThreadInfo(false)  //(可選)是否顯示執行緒資訊。 預設值為true
                        .tag("HXD")   //(可選)每個日誌的全域性標記。 預設PRETTY_LOGGER
                        .build()
                Logger.addLogAdapter(AndroidLogAdapter(formatStrategy))

                //記憶體洩漏
                if (LeakCanary.isInAnalyzerProcess(instance)) {
                    [email protected]
                }

                //嚴苛模式
                StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
                        .detectAll()//開啟所有的detectXX系列方法
                        //.penaltyDialog()//彈出違規提示框
                        .penaltyLog()//在Logcat中列印違規日誌
                        .build())
                StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
                        .detectActivityLeaks()//檢測Activity洩露
                        .penaltyLog()//在Logcat中列印違規日誌
                        .build())
            }
        }

        //TODO 將原來和activity相關的配置以及全域性使用的變數初始化,延遲處理,因此放在了flash頁裡(WelcomeActivity)
    }

WelcomeActivity

class WelcomeActivity{
	 override fun initInjector() {
        //TODO 為了不阻礙該介面的開啟速度,非同步初始化全域性的基礎資料
        launch {
            // 下拉重新整理
            SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, _ ->
                StoreHouseHeader(context).initWithString("loony")
            }
            initDeviceInfo()
            // init user info
            initUserInfo()
        }
        //介面資源初始化管理器
        initInjectorApplication()
    }
}

通過上面的程式碼我們很明確的可以看得出來原來全部在Application中乾的事我們現在由Application和WelcomeActivity兩者來處理,同時結合非同步執行緒(launch表示協程)處理耗時的任務,達到了本質上的改善,總結一下有這幾點:

(1)、減壓。減少application中onBaseContextAttached的任務,結合這裡就是去除tinker的安裝操作,畢竟tinker的安裝操作屬於一個耗時的任務,並且在剛啟動時不是那麼緊急需要;
(2)、非同步。將一些必須要在application的oncreate中初始化的邏輯放線上程裡非同步處理,同時為了不佔用該app程序在啟動時申請系統的空間和CPU運算資源,將以下三方資源的初始化放線上程裡,然後將該執行緒的優先順序降低,設定成後臺執行,也就是在launch之前設定執行緒的屬性為後臺執行;
(3)、任務分解和延遲。將原來和activity相關的配置以及全域性使用的變數初始化,延遲處理,這裡是放在裡歡迎介面裡。注意這裡的歡迎介面不管冷啟動還是熱啟動還是免登入進入都會從這裡路過,所以放在這裡不會導致資源的遺漏。大家在具體優化自己的專案時根據自己專案的情況而決定這些配置資源放的位置。

2、Activity

說完了application的優化,我們下面簡單在總結一下Activity的啟動優化。這裡我們可以從這幾點來分析:

(1)、高效複用activity的四大啟動模式:

android:launchMode=“standard” standard模式作為activity啟動的預設模式,也是標準模式,它的原理是每次開啟一個activity都會在應用程序中建立一個新的物件,不會做任何判斷和檢查,在棧中就是依次增加:

在這裡插入圖片描述

android:launchMode=“singleTop” singleTop模式表示每次啟動Activity時ActivityManagerServer都會檢測棧頂是否是該Activity,如果是,則不會二次建立;如果不是,則建立一個新的並放在棧頂:
在這裡插入圖片描述
最終順序是Activity B、Activity A、Activity B和Activity C;

android:launchMode=“singleInstance” singleInstance模式表示將當前的Activity設定為單例模式存在於獨立的一個任務棧中,不受其他介面和任務棧的影響,此模式要慎用!

在這裡插入圖片描述

android:launchMode=“singleTask” singleTask模式表示每次建立Activity時先判斷該Activity在任務棧用是否存在,如果存在則將該Activity上面的其他Activity全部銷燬並將該Activity置頂;如果不存在則建立一個新的:

在這裡插入圖片描述
最終只剩下Activity B和Activity C。

通過上面對Activity啟動模式的理解,我們在每次建立Activity時要根據該Activity的作用設定它的啟動模式,以達到Activity的快速啟動和複用

(2)、減少佈局檔案中layout的層級
通常來說,我們一個介面的佈局如果超過四層就會有卡幀或者介面開啟延遲的問題,為了能夠快速完成xml的載入和測繪,減少xml的佈局層級既能提高介面測繪的速度,也能使得佈局程式碼更加簡潔明瞭(例如專案中的activity_idcardauth_default.xml、activity_commerce.xml等)。如果介面的模組較多,簡易利用分解佈局,例如include。

(3)、高效複用自定義樣式
在一些介面的layout中,多個Button或者EditText的樣式相似度很高,那麼我們可以將公共的屬性寫在style中,某個控制元件有特殊的設定,可以在它標籤裡單獨設定即可。

(4)、非同步載入資料
在Activity的oncreate中原則上我們製作一些View的初始化操作,資料的載入一定要通過子執行緒去處理,不管是本地資料還是伺服器資料,當然了一些耗時的I/O操作更要放在子執行緒裡處理。對於這類有資料載入或者耗時操作的Activityoncreate中首先要考慮創一個loading懸浮框來緩解介面這時候的黑白屏的尷尬

(5)、onCreate()與onStart()配合使用
在一些任務多的Activity裡,我們不必所有的任務都放在oncreate中去處理,onStart裡也可以去分擔一些任務,只不過在這裡處理資料時要考慮到介面二次回退重新整理時的邏輯,防止介面暫停時從別的介面回退回來重複處理。

(6)、減少Activity的父類繼承層級
我們在建立一個Activity時通常會繼承BaseActivity,而BaseActivity繼承自android.support.v7.app.AppCompatActivity或者android.support.v4.app.FragmentActivity,而這倆繼承自android.support.v4.app.SupportActivity或android.app.Activity,而它們繼承自ContextThemeWrapper以及ContextWrapper,最終來自Context,這期間開發者要是在定義一些額外的Activity的拓展類,會嚴重影響最外層Activity的過載速度和開啟時間。因此,我們能直接繼承android.app.Activity的話,不建議再去繼承其他的類。

(7)、給Activity的Theme設定轉場動畫
兩個Activity之間切換,應用預設會使用專案build tool對應的版本里動畫效果,在一些裝置中由於api的原因,可能會強制變成系統的動畫效果。因此,為了我們介面在切換時能夠統一轉場模式以及對轉場時間的把控,增加一個系統轉場動畫能夠有效解決問題,同時也能為下一個介面的開啟贏得資料載入的緩衝時間。

(8)、在ImageView中深入理解background、src倆屬性,佈局中和程式碼中儘量保持屬性使用的一致性,避免一個View中重複接收資源圖,造成佔用記憶體空間的浪費和介面顯示時的一些異常:

src的程式碼實現:
在這裡插入圖片描述
background的程式碼實現:
在這裡插入圖片描述
以上這倆都是給imageView繪畫,區別是src給內容繪畫,而background是設定背景,資源佔用的記憶體大小和效率都是一樣的,因此我們要避免倆屬性同時使用。

(9)、分頁處理和分包處理
分頁處理是說對於我們常見的列表型資料,在遇到條目內容比較豐富、載入資源多的情況下,我們不妨分頁載入,延遲展示多餘的內容,會大大提升使用者體驗和介面啟動的速度;
分包處理主要指的是遇到接收到服務端的資料流非常大的時候,我們可以採用斷點續傳、及時回收I/O、單執行緒處理等措施達到記憶體佔用的節省和CPU利用率的提升;在遇到查詢資料庫時,儘量然sql精簡和輕量化,不可使用過多關係網,壓縮資料來源或者分批使用資料包;最後我們在上報資料和採集資料時避免將過多的I/O放在記憶體裡,影響介面的啟動和操作流暢度。