天天看點

今日頭條啟動優化

本文重點還是關注原理,冷啟動優化這個問題能延伸到很多原理層面的知識點,本文比較有意思的地方是通過反編譯今日頭條App,研究大廠的啟動優化方案。

講啟動優化之前,先看下應用的啟動流程

一、應用啟動流程

應用程序不存在的情況下,從點選桌面應用圖示,到應用啟動(冷啟動),大概會經曆以下流程:

  1. Launcher startActivity
  2. AMS startActivity
  3. Zygote fork 程序
  4. ActivityThread main()

    4.1. ActivityThread attach

    4.2. handleBindApplication

    4.3 attachBaseContext

    4.4. installContentProviders

    4.5. Application onCreate

  5. ActivityThread 進入loop循環
  6. Activity生命周期回調,onCreate、onStart、onResume…

整個啟動流程我們能幹預的主要是 4.3、4.5 和6,應用啟動優化主要從這三個地方入手。理想狀況下,這三個地方如果不做任何耗時操作,那麼應用啟動速度就是最快的,但是現實很骨感,很多開源庫接入第一步一般都是在Application onCreate方法初始化,有的甚至直接内置ContentProvider,直接在ContentProvider中初始化架構,不給你優化的機會。

二、啟動優化

直奔主題,常見的啟動優化方式大概有這些:

  • 閃屏頁優化
  • MultipDex優化(本文重點)
  • 第三方庫懶加載
  • WebView優化
  • 線程優化
  • 系統調用優化

2.1 閃屏頁優化

消除啟動時的白屏/黑屏,市面上大部分App都采用了這種方法,非常簡單,是一個障眼法,不會縮短實際冷啟動時間,簡單貼下實作方式吧。

<application
    android:name=".MainApplication"
    ...
    android:theme="@style/AppThemeWelcome>
           

styles.xml 增加一個主題叫AppThemeWelcome

<style name="AppThemeWelcome" parent="Theme.AppCompat.NoActionBar">
    ...
    <item name="android:windowBackground">@drawable/logo</item>  <!-- 預設背景-->
</style>
           

閃屏頁設定這個主題,或者全局給Application設定

<activity android:name=".ui.activity.DemoSplashActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:theme="@style/AppThemeWelcome"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
           

這樣的話啟動Activity之後背景會一直在,是以在Activity的onCreate方法中切換成正常主題

protected void onCreate(@Nullable Bundle savedInstanceState) {
    setTheme(R.style.AppTheme); //切換正常主題
    super.onCreate(savedInstanceState);
           

這樣打開桌面圖示會馬上顯示logo,不會出現黑/白屏,直到Activity啟動完成,替換主題,logo消失,但是總的啟動時間并沒有改變。

2.2 MultiDex 優化(本文重點)

MultiDex

之前,先梳理下apk編譯流程

2.2.1 apk編譯流程

Android Studio 按下編譯按鈕後發生了什麼?

  1. 打包資源檔案,生成R.java檔案(使用工具AAPT)
  2. 處理AIDL檔案,生成java代碼(沒有AIDL則忽略)
  3. 編譯 java 檔案,生成對應.class檔案(java compiler)
  4. .class 檔案轉換成dex檔案(dex)
  5. 打包成沒有簽名的apk(使用工具apkbuilder)
  6. 使用簽名工具給apk簽名(使用工具Jarsigner)
  7. 對簽名後的.apk檔案進行對齊處理,不進行對齊處理不能釋出到Google Market(使用工具zipalign)

在第4步,将class檔案轉換成dex檔案,預設隻會生成一個dex檔案,單個dex檔案中的方法數不能超過65536,不然編譯會報錯:

Unable to execute dex: method ID not in [0, 0xffff]: 65536

App內建一堆庫之後,方法數一般都是超過65536的,解決辦法就是:一個dex裝不下,用多個dex來裝,gradle增加一行配置即可。

multiDexEnabled true

這樣解決了編譯問題,在5.0以上手機運作正常,但是5.0以下手機運作直接crash,報錯 Class NotFound xxx。

Android 5.0以下,ClassLoader加載類的時候隻會從class.dex(主dex)裡加載,ClassLoader不認識其它的class2.dex、class3.dex、…,當通路到不在主dex中的類的時候,就會報錯:Class NotFound xxx,是以谷歌給出相容方案,

MultiDex

2.2.2 MultiDex 原來這麼耗時

在Android 4.4的機器列印

MultiDex.install(context)

耗時如下:

MultiDex.install 耗時:1320
           

平均耗時1秒以上,目前大部分應用應該還是會相容5.0以下手機,那麼MultiDex優化是冷啟動優化的大頭。

為什麼

MultiDex

會這麼耗時?老規矩,分析一下MultiDex原理~

2.2.3 MultiDex 原理

下面看下

MultiDex

的install 方法做了什麼事

public static void install(Context context) {
        Log.i("MultiDex", "Installing application");
        if (IS_VM_MULTIDEX_CAPABLE) { //5.0 以上VM基本支援多dex,啥事都不用幹
            Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
        } else if (VERSION.SDK_INT < 4) { // 
            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
        } else {
            ...
            doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
            ...
            Log.i("MultiDex", "install done");
        }
    }
           

從入口的判斷來看,如果虛拟機本身就支援加載多個dex檔案,那就啥都不用做;如果是不支援加載多個dex(5.0以下是不支援的),則走到

doInstallation

方法。

private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
...
                    //擷取非主dex檔案
                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                    IOException closeException = null;

                    try {

                        // 1. 這個load方法,第一次沒有緩存,會非常耗時
                        List files = extractor.load(mainContext, prefsKeyPrefix, false);

                        try {
                            //2. 安裝dex
                            installSecondaryDexes(loader, dexDir, files);
                        } 
                        ...

                }
            }
        }
    }
           

先看注釋1,

MultiDexExtractor#load

List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
        if (!this.cacheLock.isValid()) {
            throw new IllegalStateException("MultiDexExtractor was closed");
        } else {
            List files;
            if (!forceReload &amp;&amp; !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
                try {
                    //讀緩存的dex
                    files = this.loadExistingExtractions(context, prefsKeyPrefix);
                } catch (IOException var6) {
                    Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
                    //讀取緩存的dex失敗,可能是損壞了,那就重新去解壓apk讀取,跟else代碼塊一樣
                    files = this.performExtractions();
                    //儲存标志位到sp,下次進來就走if了,不走else
                    putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
                }
            } else {
                //沒有緩存,解壓apk讀取
                files = this.performExtractions();
                //儲存dex資訊到sp,下次進來就走if了,不走else
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }

            Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
            return files;
        }
    }
           

查找dex檔案,有兩個邏輯,有緩存就調用

loadExistingExtractions

方法,沒有緩存或者緩存讀取失敗就調用

performExtractions

方法,然後再緩存起來。使用到緩存,那麼

performExtractions

方法想必應該是很耗時的,分析一下代碼:

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
        //先确定命名格式
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        this.clearDexDir();
        List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
        ZipFile apk = new ZipFile(this.sourceApk); // apk轉為zip格式

        try {
            int secondaryNumber = 2;
            //apk已經是改為zip格式了,解壓周遊zip檔案,裡面是dex檔案,
            //名字有規律,如classes1.dex,class2.dex
            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
                //檔案名:xxx.classes1.zip
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                //建立這個classes1.zip檔案
                MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
                //classes1.zip檔案添加到list
                files.add(extractedFile);
                Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;

                while(numAttempts < 3 &amp;&amp; !isExtractionSuccessful) {
                    ++numAttempts;
                    //這個方法是将classes1.dex檔案寫到壓縮檔案classes1.zip裡去,最多重試三次
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);

                 ...
                }
        //傳回dex的壓縮檔案清單
        return files;
    }
           

這裡的邏輯就是解壓apk,周遊出裡面的dex檔案,例如class1.dex,class2.dex,然後又壓縮成class1.zip,class2.zip…,然後傳回zip檔案清單。

思考為什麼這裡要壓縮呢? 後面涉及到ClassLoader加載類原理的時候會分析ClassLoader支援的檔案格式。

第一次加載才會執行解壓和壓縮過程,第二次進來讀取sp中儲存的dex資訊,直接傳回file list,是以第一次啟動的時候比較耗時。

dex檔案清單找到了,回到上面

MultiDex#doInstallation

方法的注釋2,找到的dex檔案清單,然後調用

installSecondaryDexes

方法進行安裝,怎麼安裝呢?方法點進去看SDK 19 以上的實作

private static final class V19 {
        private V19() {
        }

        static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            Field pathListField = MultiDex.findField(loader, "pathList");//1 反射ClassLoader 的 pathList 字段
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList();
            // 2 擴充數組
            MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
           ...
        }

        private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
            return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
        }
    }
           
  1. 反射ClassLoader 的 pathList 字段
  2. 找到pathList 字段對應的類的

    makeDexElements

    方法
  3. 通過

    MultiDex.expandFieldArray

    這個方法擴充

    dexElements

    數組,怎麼擴充?看下代碼:
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field jlrField = findField(instance, fieldName);
        Object[] original = (Object[])((Object[])jlrField.get(instance)); //取出原來的dexElements 數組
        Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length)); //新的數組
        System.arraycopy(original, 0, combined, 0, original.length); //原來數組内容拷貝到新的數組
        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); //dex2、dex3...拷貝到新的數組
        jlrField.set(instance, combined); //将dexElements 重新指派為新的數組
    }
           

就是建立一個新的數組,把原來數組内容(主dex)和要增加的内容(dex2、dex3…)拷貝進去,反射替換原來的

dexElements

為新的數組,如下圖

今日頭條啟動優化

image

看起來有點眼熟, Tinker熱修複 的原理也是通過反射将修複後的dex添加到這個dex數組去,不同的是熱修複是添加到數組最前面,而MultiDex是添加到數組後面。這樣講可能還不是很好了解?來看看ClassLoader怎麼加載一個類的就明白了~

2.2.4 ClassLoader 加載類原理

不管是

PathClassLoader

還是

DexClassLoader

,都繼承自

BaseDexClassLoader

,加載類的代碼在

BaseDexClassLoader

4.4 源碼

/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

今日頭條啟動優化

4.4 BaseDexClassLoader

  1. 構造方法通過傳入dex路徑,建立了

    DexPathList

  2. ClassLoader的findClass方法最終是調用DexPathList 的findClass方法

接着看

DexPathList

源碼

/dalvik/src/main/java/dalvik/system/DexPathList.java

今日頭條啟動優化

DexPathList-dexElements

DexPathList

裡面定義了一個

dexElements

數組,

findClass

方法中用到,看下

今日頭條啟動優化

DexPathList#findClass

findClass方法邏輯很簡單,就是 周遊dexElements 數組,拿到裡面的DexFile對象,通過DexFile的loadClassBinaryName方法加載一個類。

今日頭條啟動優化

DexFile#loadClassBinaryName

最終建立Class是通過native方法,就不追下去了,大家有興趣可以看下native層是怎麼建立Class對象的。DexFile.cpp

那麼問題來了,5.0以下這個dexElements 裡面隻有主dex(可以認為是一個bug),沒有dex2、dex3…,MultiDex是怎麼把dex2添加進去呢?

答案就是反射

DexPathList

dexElements

字段,然後把我們的dex2添加進去,當然,dexElements裡面放的是Element對象,我們隻有dex2的路徑,必須轉換成Element格式才行,是以 反射DexPathList裡面的makeDexElements 方法 ,将dex檔案轉換成Element對象即可。

今日頭條啟動優化

image

dex2、dex3…通過

makeDexElements

方法轉換成要新增的Element數組, 最後一步就是反射DexPathList的dexElements字段,将原來的Element數組和新增的Element數組合并,然後反射指派給dexElements變量,最後DexPathList的dexElements變量就包含我們新加的dex在裡面了。

makeDexElements

方法會判斷file類型,上面講dex提取的時候解壓apk得到dex,然後又将dex壓縮成zip,壓縮成zip,就會走到第二個判斷裡去。仔細想想,其實dex不壓縮成zip,走第一個判斷也沒啥問題吧,那谷歌的MultiDex為什麼要将dex壓縮成zip呢?在Android開發高手課中看到張紹文也提到這一點

今日頭條啟動優化

image

然後我在反編譯頭條App的時候,發現頭條參考谷歌的MultiDex,自己寫了一套,猜想可能是優化這個多餘的壓縮過程,頭條的方案下面會介紹。

2.2.5 原理小結

ClassLoader 加載類原理:

ClassLoader.loadClass -> DexPathList.loadClass -> 周遊dexElements數組 ->DexFile.loadClassBinaryName

通俗點說就是:ClassLoader加載類的時候是通過周遊dex數組,從dex檔案裡面去加載一個類,加載成功就傳回,加載失敗則抛出Class Not Found 異常。

MultiDex原理:

在明白ClassLoader加載類原理之後,我們可以通過反射dexElements數組,将新增的dex添加到數組後面,這樣就保證ClassLoader加載類的時候可以從新增的dex中加載到目标類,經過分析後最終MultipDex原理圖如下:
今日頭條啟動優化
MultiDex原理

2.2.6 MultiDex 優化(兩種方案)

知道了MultiDex原理之後,可以了解install過程為什麼耗時,因為涉及到解壓apk取出dex、壓縮dex、将dex檔案通過反射轉換成DexFile對象、反射替換數組。

那麼MultiDex到底應該怎麼優化呢,放子線程可行嗎?

方案1:子線程install(不推薦)

這個方法大家很容易就能想到,在閃屏頁開一個子線程去執行

MultiDex.install

,然後加載完才跳轉到首頁。需要注意的是閃屏頁的Activity,包括閃屏頁中引用到的其它類必須在主dex中,不然在

MultiDex.install

之前加載這些不在主dex中的類會報錯Class Not Found。這個可以通過gradle配置,如下:

defaultConfig {
        //分包,指定某個類在main dex
        multiDexEnabled true
        multiDexKeepProguard file('multiDexKeep.pro') // 打包到main dex的這些類的混淆規制,沒特殊需求就給個空檔案
        multiDexKeepFile file('maindexlist.txt') // 指定哪些類要放到main dex
    }
           

maindexlist.txt 檔案指定哪些類要打包到主dex中,内容格式如下

com/lanshifu/launchtest/SplashActivity.class
           

在已有項目中用這種方式,一頓操作猛如虎之後,編譯運作在4.4的機器上,啟動閃屏頁,加載完準備進入首頁直接崩掉了。

今日頭條啟動優化

image

報錯

NoClassDefFoundError

,一般都是該類沒有在主dex中,要在maindexlist.txt 将配置指定在主dex。

第三方庫中的ContentProvider必須指定在主dex中,否則也會找不到,為什麼? 文章開頭說過應用的啟動流程, ContentProvider 初始化時機 如下圖:

今日頭條啟動優化

image

ContentProvider初始化太早了,如果不在主dex中,還沒啟動閃屏頁就已經crash了。

是以這種方案的缺點很明顯:

  1. MultiDex加載邏輯放在閃屏頁的話,閃屏頁中引用到的類都要配置在主dex。
  1. ContentProvider必須在主dex,一些第三方庫自帶ContentProvider,維護比較麻煩,要一個一個配置。

這時候就思考一下,有沒有其它更好的方案呢?大廠是怎麼做的?今日頭條肯定要對MultiDex進行優化吧,反編譯瞧瞧?

今日頭條啟動優化

image

點了一根煙之後,開始偷代碼…

MultiDex優化方案2:今日頭條方案

今日頭條沒有加強,反編譯後很容易通過關鍵字搜尋找到

MultidexApplication

這個類,

今日頭條啟動優化

image

看注釋1的

d.a(this);

這個方法,代碼雖然混淆了,但是方法内部的代碼還是可以看出是幹嘛的,繼續跟這個方法,為了不影響閱讀,我對混淆做了一些處理,改成正常的方法名。

今日頭條啟動優化

image

每個方法開頭都有

PatchProxy.isSupport

這個if判斷,這個是美團Robust熱修複生成的代碼,今日頭條沒有自己的熱修複架構,沒有用Tinker,而是用美團的,想了解關于Robust細節可以參考文末連結。Robust直接跳過,看else代碼塊即可。

繼續看

loadMultiDex

方法

今日頭條啟動優化

image

邏輯如下:

1. 建立臨時檔案,作為判斷MultiDex是否加載完的條件

2. 啟動LoadDexActivity去加載MultiDex(LoadDexActivity在單獨程序),加載完會删除臨時檔案

3. 開啟while循環,直到臨時檔案不存在才跳出循環,進入Application的onCreate

建立臨時檔案代碼

今日頭條啟動優化

image

while循環代碼

今日頭條啟動優化

image

LoadDexActivity 隻有一個加載框,加載完再跳轉到閃屏頁

今日頭條啟動優化

image

dex加載完應該要finish掉目前Activity

今日頭條啟動優化

image

按照上面代碼分析,今日頭條在5.0以下手機首次啟動應該是這樣:

  1. 打開桌面圖示
  2. 顯示預設背景
  3. 跳轉到加載dex的界面,展示一個loading的加載框幾秒鐘
  4. 跳轉到閃屏頁

實際上是不是這樣呢,用4.4機器試下?

今日頭條啟動優化

今日頭條啟動

看起來完全跟猜想的一緻,撸幾行代碼驗證應該不難吧?

今日頭條啟動優化

image

點了一根煙之後,開始撸代碼,最終實作效果如下

今日頭條啟動優化

仿頭條啟動.gif

效果跟今日頭條是一緻的,不再重複分析代碼了,源碼上傳到github,感興趣的同學可以參考參考,頭條的方案,值得嘗試~

https://github.com/lanshifu/MultiDexTest/

再次梳理一下這種方式:

  1. 在主程序Application 的 attachBaseContext 方法中判斷如果需要使用MultiDex,則建立一個臨時檔案,然後開一個程序(LoadDexActivity),顯示Loading,異步執行MultiDex.install 邏輯,執行完就删除臨時檔案并finish自己。
  2. 主程序Application 的 attachBaseContext 進入while代碼塊,定時輪循臨時檔案是否被删除,如果被删除,說明MultiDex已經執行完,則跳出循環,繼續正常的應用啟動流程。
  3. 注意LoadDexActivity 必須要配置在main dex中。

有些同學可能會問,啟動還是很久啊,冷啟動時間有變化嗎?

冷啟動時間是指點選桌面圖示到第一個Activity顯示這段時間。

MultiDex優化總結

方案1:直接在閃屏頁開個子線程去執行MultiDex邏輯,MultiDex不影響冷啟動速度,但是難維護。

方案2:今日頭條的MultiDex優化方案:

  1. 在Application 的attachBaseContext 方法裡,啟動另一個程序的LoadDexActivity去異步執行MultiDex邏輯,顯示Loading。
  2. 然後主程序Application進入while循環,不斷檢測MultiDex操作是否完成
  3. MultiDex執行完之後主程序Application繼續走,ContentProvider初始化和Application onCreate方法,也就是執行主程序正常的邏輯。

其實應該還有方案3,因為我發現頭條并沒有直接使用Google的MultiDex,而是參考谷歌的MultiDex,自己寫了一套,耗時應該會少一些,大家有興趣可以去研究一下。

2.3 預建立Activity

今日頭條啟動優化

image

這段代碼是今日頭條裡面的,Activity對象預先new出來,

對象第一次建立的時候,java虛拟機首先檢查類對應的Class

對象是否已經加載。如果沒有加載,jvm會根據類名查找.class檔案,将其Class對象載入。同一個類第二次new的時候就不需要加載類對象,而是直接執行個體化,建立時間就縮短了。

頭條真是把啟動優化做到極緻。

2.4 第三方庫懶加載

很多第三方開源庫都說在Application中進行初始化,十幾個開源庫都放在Application中,肯定對冷啟動會有影響,是以可以考慮按需初始化,例如Glide,可以放在自己封裝的圖檔加載類中,調用到再初始化,其它庫也是同理,讓Application變得更輕。

2.5 WebView啟動優化。

WebView啟動優化文章也比較多,這裡隻說一下大概優化思路。

  1. WebView第一次建立比較耗時,可以預先建立WebView,提前将其核心初始化。
  2. 使用WebView緩存池,用到WebView的地方都從緩存池取,緩存池中沒有緩存再建立,注意記憶體洩漏問題。
  3. 本地預置html和css,WebView建立的時候先預加載本地html,之後通過js腳本填充内容部分。

這一部分可以參考:

https://mp.weixin.qq.com/s/KwvWURD5WKgLKCetwsH0EQ,

2.6 資料預加載

這種方式一般是在首頁空閑的時候,将其它頁面的資料加載好,儲存到記憶體或資料庫,等到打開該頁面的時候,判斷已經預加載過,直接從記憶體或資料庫讀取資料并顯示。

2.7 線程優化

線程是程式運作的基本機關,線程的頻繁建立是耗性能的,是以大家應該都會用線程池。單個cpu情況下,即使是開多個線程,同時也隻有一個線程可以工作,是以線程池的大小要根據cpu個數來确定。

啟動優化方式就先介紹到這裡,常見的就是這些,其它的可以作為補充。

三、啟動耗時分析方法

TraceView

性能損耗太大,得到的結果不真實。

Systrace

可以友善追蹤關鍵系統調用的耗時情況,如 Choreographer,但是不支援應用程式代碼的耗時分析。

3.1 Systrace + 函數插樁

結合

Systrace

函數插樁

,就是将如下代碼插入到每個方法的入口和出口

class Trace{
    public static void i(String tag){
        android.os.Trace.beginSection(tag);
    }

    public static void o(){
        android.os.Trace.endSection();
    }

}
           

插樁後的代碼如下

void test(){
    Trace.i("test");
    System.out.println("doSomething");
    Trace.o();
}
           

插樁工具參考:

https://github.com/AndroidAdvanceWithGeektime/Chapter07

mac下systrace路徑在

/Users/{xxx}/Library/Android/sdk/platform-tools/systrace/

編譯運作app,執行指令

python2 /Users/lanshifu/Library/Android/sdk/platform-tools/systrace/systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a com.sample.systrace -o test.log.html
今日頭條啟動優化

image

最後按下Enter停止捕獲trace資訊,在目錄下生成報告test.log.html,直接可以用谷歌浏覽器打開檢視。

3.2 BlockCanary 也可以檢測

BlockCanary 可以監聽主線程耗時的方法,将門檻值設定低一點,比如200毫秒,這樣的話如果一個方法執行時間超過200毫秒,擷取堆棧資訊并通知開發者。

BlockCanary 原理在之前那篇卡頓優化的文章裡面講過一些,這裡就不再重複。

總結

文章有點長,看到這裡,是不是忘記開頭講什麼了?總結一下這篇文章主要涉及到哪些内容:

  1. 應用啟動流程
  2. 閃屏頁優化
  3. MultiDex 原理分析
  4. ClassLoader 加載一個類的流程分析
  5. 熱修複原理
  6. MultiDex優化:

    介紹了兩種方式,一種是直接在閃屏頁開個子線程去加載dex,難維護,不推薦;一種是今日頭條的方案,在單獨一個程序加載dex,加載完主程序再繼續。

  7. 快速啟動Activity的方式:預建立Activity,預加載資料。
  8. 啟動時間監控的方式:Systrace+插樁、BlockCanary。

作者:藍師傅_Android

連結:https://www.jianshu.com/p/d0fe74f4e9c4

來源:簡書

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。