1. 程式人生 > >Android啟動速度優化

Android啟動速度優化

最近做了一些Android App啟動速度的優化,有一些心得,整理整理

影響啟動速度的原因

高耗時任務

資料庫初始化、某些第三方框架初始化、大檔案讀取、MultiDex載入等,導致CPU阻塞

複雜的View層級

使用的巢狀Layout過多,層級加深,導致View在渲染過程中,遞迴加深,佔用CPU資源,影響Measure、Layout等方法的速度

類過於複雜

Java物件的建立也是需要一定時間的,如果一個類中結構特別複雜,new一個物件將消耗較高的資源,特別是一些單例的初始化,需要特別注意其中的結構

主題及Activity配置

有一些App是帶有Splash頁的,有的則直接進入主介面,由於主題切換,可能會導致白屏,或者點了Icon,過一會兒才出現主介面

一些典型的例子及優化方案

MultiDex

由於Android 5.0以下使用的Dalvik虛擬機器天生對MultiDex支援不好,導致在4.4(及以下)的系統上,如果使用了MultiDex做為分包方案,啟動速度可能會慢的多,實際數值跟dex檔案的大小、數量有關,估計會慢300~500ms

  • 解決方案:

    限制APP在5.0以上使用:目前大多數使用者已經在使用Android 5.0以上的版本了,當然,還有很多4.4使用者,很多APP也是隻支援4.4以上(比如:百度APP),為了使用者體驗,可以考慮捨棄一部分使用者

    優化方法數:儘量避免方法超過65535個,同時可以開啟Release配置的Minify選項,打包時刪掉沒有用的方法,不過如果框架引用的較多,基本沒效果

    少用一些不必要的框架:有些框架功能很強大,但不一定都能用得上,引進來會新增很多的方法,導致必須開啟MultiDex,可以自己造輪子,或者找輕量級的框架

    慎用Kotlin:由於Kotlin現在還沒有內建在Android系統中,所以APP如果使用了Kotlin,可能會導致引入很多的Kotlin方法,導致必須分割Dex,這個有待Google在Android P中解決

    Dex懶載入:在APP功能日益複雜的今天,MultiDex幾乎是已經無法避免了,為了啟動速度的優化,可以將啟動時必需的方法,放在主Dex中(即classes.dex),方法是在Gradle指令碼中配置multiDexKeepFile或者multiDexKeepProguard屬性(程式碼如下),詳見:

    官方文件,待App啟動完成後,再使用MultiDex.install來載入其他的Dex檔案。這種方法風險比較高,而且實現成本比較大,如果啟動依賴的庫比較多,還是無法實現

    android {
        buildTypes {
            release {
                multiDexKeepFile file('multidex-config.txt')        // multiDexKeepFile規則 
                multiDexKeepProguard file('multidex-config.pro')    // 類似ProGuard的規則
            }
        }
    }

    配置檔案示例:

    
    # 常規的multiDexKeepFile規則
    
    
    com/example/MyClass.class
    com/example/MyOtherClass.class
    
    
    # 類似ProGuard規則
    
    
    -keep class com.example.MyClass
    -keep class com.example.MyClassToo
    
    -keep class com.example.** { *; } // All classes in the com.example package

    外掛化或H5/React Native方案:即端只提供Native呼叫能力和容器,業務由外掛來做,本地只需要載入基礎的Native能力相關類即可,其他完全下發,或內建成資原始檔呼叫

Glide及其他圖片框架

Glide是一個很好用的圖片載入框架,除了常用的圖片載入、快取功能以外,Glide支援對網路層進行定製,比如換成OkHttp來支援HTTP 2.0。不過,如果在追求啟動速度的情況下,在Splash頁或主介面載入某一張圖片時,往往是第一次使用Glide,由於Glide沒有初始化,會導致這次圖片載入的時間比較長(不管本地還是網路),特別是在其他操作也在同時搶佔CPU資源的時候,慢的特別明顯!而後面再使用Glide載入圖片時,還是比較快的

Glide初始化耗時分析:Glide的初始化會載入所有配置的Module,然後初始化RequestManager(包括網路層、工作執行緒等,比較耗時),最後還要應用一些解碼的選項(Options)

解決方案:在Application的onCreate方法中,在工作執行緒呼叫一次GlideApp.get(this)

    override fun onCreate() {
        super.onCreate()
        // 使用Anko提供的非同步工作協程,或者自行建立一個併發執行緒池
        doAsync {
            GlideApp.get(this)   // 獲取一個Glide物件,Glide內部會進行初始化操作
        }
    }

greenDAO和其他資料庫框架

greenDAO實現了一種ORM框架,資料庫基於SQLite,使用起來很方便,不需要自己寫SQL語句、控制併發和事務等等,其他常見的資料庫框架如:Realm、DBFlow等等,使用起來也很方便,但他們的初始化,尤其是需要升級、遷移資料時,往往會帶來不小的CPU和I/O開銷,一旦資料量比較多(比如:很長時間的聊天記錄、瀏覽器瀏覽歷史記錄等),往往都需要專門一個介面來告知使用者:APP正在做資料處理工作。所以,如果為了提高APP啟動速度,避免在APP啟動時做資料庫的耗時任務,很有必要!

  • 解決方案:

    必要資料避免使用資料庫:如果首屏的展示內容需要根據配置來決定,那麼幹脆放棄資料庫儲存和讀取,直接放在檔案、SharedPreference裡面,特別是多組鍵值對的讀取,如果使用資料庫,在除過初始化佔用的時間以後,可能還需要30~50ms來完成(因為需要多次讀取),而如果存在SharedPreference中,即使是轉換成JSON並解析,可能也就在10ms之內

    資料庫預先非同步初始化:使用greenDAO時,預先初始化很有必要,可以保證在第一次讀取資料庫時,不佔用主執行緒資源,防止拖慢啟動速度,具體做法如下:

    // Application
    override fun onCreate() {
        super.onCreate()
        // 使用Anko提供的非同步工作協程,或者自行建立一個併發執行緒池
        doAsync {
            DbManager.daoSession   // 獲取一次greenDao的DaoSession例項化物件即可
        }
    }
    
    // DBManager(資料庫相關單例)
    object DbManager {
    
        // greenDAO的DaoMaster,用來初始化資料庫並建立連線
        private val daoMaster: DaoMaster by lazy {
            val openHelper = DaoMaster.OpenHelper(ContextUtils.getApplicationContext(), "Test.db")
            DaoMaster(openHelper.writableDatabase)
        }
    
        // 具體的資料庫會話
        val daoSession: DaoSession by lazy {
            daoMaster.newSession()
        }
    }

View和主題

View層級

主要在於首屏/Splash頁的Layout佈局層次過深,導致View在渲染時,遞迴加深,消耗過多的CPU和記憶體資源,阻塞主執行緒,所以最根本的思路就是解決層級問題,檢查一個App的View層級,可以使用Android Studio自帶的Layout Inspector工具,如圖:

Layout Inspector

在選擇了需要檢查的程序及Window(Dialog可能會建立新的Window,但顯示的Activity是同一個)以後,就可以看到Android Studio自動進行的Capture的內容了

Layout Inspector

根據左邊View層級顯示的內容,分析不必要的巢狀佈局,通過改造,即可對View層級進行優化

除了根據上面的方法分析層級以外,可以使用Google最新推出的ConstraintLayout,官網連結:ConstraintLayout

ConstraintLayout採用的約束佈局概念,類似於iOS的AutoLayout,但使用起來,遠比AutoLayout方便、強大,個人感覺吸取了RelativeLayout的方便、FrameLayout的靈活、LinearLayout的高效等特點,通過控制元件見相互的約束控制,可以構建出近乎平面的佈局,這樣就可以減少佈局層級,只用ConstraintLayout一層Layout實現複雜的UI佈局,非常值得學習和使用!

ConstraintLayout ChainStyle

如果分別使用RelativeLayout、FrameLayout、LinearLayout和ConstraintLayout構建一個複雜的佈局,或許,ConstraintLayout,就要比其他幾種佈局快50~100ms!

App主題

我們可以做個實驗,使用以下幾種主題,看看APP的啟動速度(以ActivityManager的Log為準):

@android:style/Theme.NoTitleBar.Fullscreen

@android:style/Theme.Black

預設(根據作業系統自動選擇)

其中,MainActivity的根佈局是一個空的LinearLayout,將App殺死冷啟動5,取平均時間

Black

Theme.Black

平均啟動時間:160ms

FullScreen

Theme.NoTitleBar.Fullscreen

平均啟動時間:126.8ms

預設:

預設

平均啟動時間:174.8ms

可以得出一個結論:使用一個沒有ActionBar的主題,比較快,而如果連StatusBar也去掉了,速度最快!

原因是這樣的,啟動一個Activity的時候,系統會建立包含一個DecorView的Window,而StatusBar也好,ActionBar也好,都是這個View中的子元素,多了一個View,當然多了一層佈局,肯定是耗時的

所以,如果想提高APP的啟動速度,尤其是使用Splash的App,務必將第一個Activity的主題設為FullScreen的,這樣能有效提高啟動速度

進一步優化

某些APP,如:微博,能夠做到點了圖示就立即做出響應,顯示出它的Splash頁,如下圖:

weibo

而像一些沒有Splash頁的APP就不行,要麼是點了桌面Icon以後沒反應,過一會兒出主介面,要麼是點了以後白屏一會兒才出主介面(在Android 4.4上由於MultiDex等問題特別明顯)

這是因為微博這樣有Splash頁的APP,其Splash頁在使用了FullScreen主題以後,又將主題的Background進行了處理,使得Splash在啟動時根本沒有載入實際的View,而僅僅是載入了主題,待Activity初始化完成以後,再渲染廣告位等View,這樣就避免了白屏和空屏的等待時常,讓使用者感覺到啟動速度快,我們來看一下微博在啟動過程中View的變化

weibo_layout

從上面的錄屏GIF中,可以看到:當微博啟動時,並沒有實際的View佈局,而是一整個Layer,過了一會兒,Slogan和Logo的ImageView佈局才以漸現動畫的方式逐漸加載出來,後面繼續載入廣告位的Layout

這麼做的理由很簡單,為了讓使用者感覺到快!

apktool一下微博的apk,可以發現微博對首頁的主題背景,使用了一個drawable來實現

    <!-- styles.xml -->
    <style name="NormalSplash" parent="@android:style/Theme">
        <item name="android:windowBackground">@drawable/welcome_layler_drawable</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:scrollbarThumbVertical">@drawable/global_scroll_thumb</item>
        <item name="android:windowAnimationStyle">@style/MyAnimationActivity</item>
    </style>
    <!-- welcome_layler_drawable.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:id="@id/welcome_background" android:drawable="@drawable/welcome_android" />
        <item android:bottom="@dimen/login_icon_padding_bottom">
            <bitmap android:gravity="bottom|center" android:src="@drawable/welcome_android_logo" />
        </item>
        <item android:top="@dimen/splash_slogan_margin_top">
            <bitmap android:gravity="center|top" android:src="@drawable/welcome_android_slogan" />
        </item>
        <item android:top="20.0dip" android:right="20.0dip">
            <bitmap android:gravity="center|right|top" android:src="@drawable/channel_logo" />
        </item>
    </layer-list>

由此可見,使用layer-list的形式,可以使一系列的Bitmap按照類似View佈局的形式來排布,通過將生成的drawable設定為background的形式,最終並不會生成任何View,極大程度減小View繪製佔用的時間,提升啟動速度!

通過實驗,發現市面上很多的APP(高德地圖、大眾點評、百度地圖、ofo小黃車等)都是採取了類似的方式,通過設定一個FullScreen主題的Activity,並設定background為和Splash佈局類似的形式,能夠做到點下圖示的即刻,展現介面

對於多執行緒的思考

在App啟動時,為了加快啟動速度,通常會使用多執行緒手段來並行執行任務,充分發揮多核CPU的優勢,提高運算效率。此方法固然能夠對啟動速度的優化,起到一定作用,但實際開發中,有以下幾點值得深思:

併發的執行緒數,多少合適?(效率高但不至於阻塞)

頻繁切換執行緒,是否帶來負面影響?(頻繁地從主執行緒扔進輔助執行緒操作再將結果拋回來會不會比直接執行更慢)

何時並行?何時序列?(有的任務能只能串,有的任務可以並行)

這個時候,拿Android經典的AsyncTask類來說事,再合適不過了!

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;

上面的程式碼是AsyncTask確定執行緒池數量的部分,其中,核心執行池保證最少2個執行緒,最多不超過CPU可用核數-1,最大執行緒池數量為CPU核數的2倍+1

這樣配置執行緒池的目的很簡單:防止併發過大,導致CPU阻塞,影響效率

而AsyncTask從Android 3.0開始,就改為序列執行了,實際上也是為了防止併發過大,導致任務搶奪CPU時間片,造成阻塞,或者錯誤

這樣做,也是為了讓有前後依賴關係的任務按照我們希望的順序執行,以便控制資料流程,防止造成不一致的情況,導致Crash或資料錯誤

雖然AsyncTask功能強大,但經常由於使用不當,造成記憶體洩露等問題,而且程式碼量比較多,所以在實際使用過程中,一般都使用自行封裝的任務佇列,更輕量,這樣便於在需要的時候,讓任務序列執行,以免造成過高的開銷,使得速度不升反降

這裡貼一段輕量序列佇列的實現程式碼,可以在需要的時候進行參考:

package com.xiyoumobile.kit.task

import android.os.Handler
import android.os.Looper
import android.os.Message
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue

/**
 * 加入到序列工作佇列中,並執行
 */
fun dispatchSerialWork(task: (() -> Unit)): Runnable {
    val runnable = Runnable {
        task()
    }
    addSerialTask(runnable)
    return runnable
}

/**
 * 加入到主執行緒佇列中,並執行
 */
fun dispatchMainLoopWork(task: (() -> Unit)): Runnable {
    val runnable = object : MainTask {
        override fun run() {
            task()
        }
    }
    val msg = Message()
    msg.obj = runnable
    msg.what = runnable.hashCode()
    MAIN_HANDLER.sendMessage(msg)
    return runnable
}

private val BACKGROUND_SERIAL_EXECUTOR = BackgroundSerialExecutor()

private fun addSerialTask(runnable: Runnable) {
    BACKGROUND_SERIAL_EXECUTOR.execute(runnable)
}

private class BackgroundSerialExecutor : Executor {
    private val tasks = LinkedBlockingQueue<Runnable>()
    private val executor = Executors.newSingleThreadExecutor()
    private var active: Runnable? = null

    @Synchronized
    override fun execute(r: Runnable) {
        tasks.offer(Runnable {
            try {
                r.run()
            } finally {
                scheduleNext()
            }
        })
        if (active == null) {
            scheduleNext()
        }
    }

    @Synchronized
    private fun scheduleNext() {
        active = tasks.poll()
        if (active != null) {
            executor.execute(active)
        }
    }
}

private val MAIN_HANDLER = MainLooperHandler()

private class MainLooperHandler : Handler(Looper.getMainLooper()) {

    override fun handleMessage(msg: Message?) {
        super.handleMessage(msg)
        if (msg?.obj is MainTask) {
            (msg.obj as MainTask).run()
        }
    }
}

private interface MainTask : Runnable

其次,可以配合Kotlin的協程doAsync進行後臺併發執行,其內部也使用了Executor,但基於更輕量的協程操作,開銷更小,適合做一些需要併發的操作,但不可隨意使用,防止阻塞

總結

在APP功能日益增加和使用者體驗不斷改良的今天,APP啟動速度,已然成為影響使用者體驗的第一道門檻。所謂快,其實是在使用者感官上的一種反應,如果能夠使用以上的手段對APP的啟動速度優化,雖然實際上啟動時的總操作量可能並沒有真正減少,但經過合理的先後順序安排,可以使得某些不必要的任務,延後再執行,起到在APP啟動時,更輕量、更靈敏的作用,這樣能夠比較快的響應使用者從Launcher點選Icon的操作,提升使用者體驗,讓使用者感覺到『快』。