今日頭條啓動很快,你覺得可能是做了哪些優化?X2C 框架性能優化

https://juejin.im/post/5d95f4a4f265da5b8f10714b

X2C框架

    爲了即保留xml的優點,又解決它帶來的性能問題,我們開發了X2C方案。即在編譯生成APK期間,將需要翻譯的layout翻譯生成對應的java文件,這樣對於開發人員來說寫佈局還是寫原來的xml,但對於程序來說,運行時加載的是對應的java文件。     我們採用APT(Annotation Processor Tool)+ JavaPoet技術來完成編譯期間【註解】->【解註解】->【翻譯xml】->【生成java】整個流程的操作。

https://github.com/iReaderAndroid/X2C

0

前言

網上關於啓動優化的文章多不勝數,內容千篇一律,大都是列舉一些耗時操作,採用異步加載、懶加載等。

而在面試過程中,關於啓動優化的問題,如果只是很表面地回答耗時操作應該放在子線程,顯然太過於普通,無法跟競爭者拉開差距。如何讓面試官知道你的“內功深厚”,那肯定是要往原理層面去回答。

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

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

1

應用啓動流程

應用進程不存在的情況下,從點擊桌面應用圖標,到應用啓動(冷啓動),大概會經歷以下流程:

  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中初始化框架,不給你優化的機會。

2

啓動優化

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

  • 閃屏頁優化

  • 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 && !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 && !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爲新的數組,如下圖

 

 

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

 

2.2.4  ClassLoader 加載類原理

不管是 PathClassLoader還是DexClassLoader,都繼承自BaseDexClassLoader,加載類的代碼在 BaseDexClassLoader中

4.4 源碼

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

 

 

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

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

接着看DexPathList源碼/dalvik/src/main/java/dalvik/system/DexPathList.java

 

 

DexPathList裏面定義了一個dexElements 數組,findClass方法中用到,看下

 

 

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

 

 

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

 

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

 

答案就是反射DexPathList的dexElements字段,然後把我們的dex2添加進去,當然,dexElements裏面放的是Element對象,我們只有dex2的路徑,必須轉換成Element格式纔行,所以反射DexPathList裏面的makeDexElements 方法,將dex文件轉換成Element對象即可。

 

 

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開發高手課中看到張紹文也提到這一點

 

 

然後我在反編譯頭條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原理圖如下:

 

 

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的機器上,啓動閃屏頁,加載完準備進入主頁直接崩掉了。

 

 

報錯NoClassDefFoundError,一般都是該類沒有在主dex中,要在maindexlist.txt 將配置指定在主dex。**第三方庫中的ContentProvider必須指定在主dex中,否則也會找不到,爲什麼?**文章開頭說過應用的啓動流程,ContentProvider 初始化時機如下圖:

 

 

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

 

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

 

  1. MultiDex加載邏輯放在閃屏頁的話,閃屏頁中引用到的類都要配置在主dex。

  2. ContentProvider必須在主dex,一些第三方庫自帶ContentProvider,維護比較麻煩,要一個一個配置。

 

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

 

開始偷代碼...

 

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

 

今日頭條沒有加固,反編譯後很容易通過關鍵字搜索找到MultidexApplication這個類,

 

 

看註釋1的d.a(this);這個方法,代碼雖然混淆了,但是方法內部的代碼還是可以看出是幹嘛的,繼續跟這個方法,爲了不影響閱讀,我對混淆做了一些處理,改成正常的方法名。

 

 

每個方法開頭都有PatchProxy.isSupport這個if判斷,這個是美團Robust熱修復生成的代碼,今日頭條沒有自己的熱修復框架,沒有用Tinker,而是用美團的,想了解關於Robust細節可以參考文末鏈接。Robust直接跳過,看else代碼塊即可。

 

繼續看loadMultiDex方法

 

 

邏輯如下:


1. 創建臨時文件,作爲判斷MultiDex是否加載完的條件
2. 啓動LoadDexActivity去加載MultiDex(LoadDexActivity在單獨進程),加載完會刪除臨時文件
3. 開啓while循環,直到臨時文件不存在才跳出循環,進入Application的onCreate

 

創建臨時文件代碼

 

 

while循環代碼

 

 

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

 

 

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

 

 

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

 

  1. 打開桌面圖標

  2. 顯示默認背景

  3. 跳轉到加載dex的界面,展示一個loading的加載框幾秒鐘

  4. 跳轉到閃屏頁

 

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

 

 

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

 

 

開始擼代碼,最終實現效果如下

 

 

效果跟今日頭條是一致的,不再重複分析代碼了,源碼上傳到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

 

 

 

 

這段代碼是今日頭條裏面的,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腳本填充內容部分。

 

2.6 數據預加載

 

這種方式一般是在主頁空閒的時候,將其它頁面的數據加載好,保存到內存或數據庫,等到打開該頁面的時候,判斷已經預加載過,直接從內存或數據庫讀取數據並顯示。

 

2.7 線程優化

 

線程是程序運行的基本單位,線程的頻繁創建是耗性能的,所以大家應該都會用線程池。單個cpu情況下,即使是開多個線程,同時也只有一個線程可以工作,所以線程池的大小要根據cpu個數來確定。

 

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

 

3

啓動耗時分析方法

 

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

 

 

最後按下Enter停止捕獲trace信息,在目錄下生成報告test.log.html,直接可以用谷歌瀏覽器打開查看。

 

3.2 BlockCanary 也可以檢測

 

BlockCanary 可以監聽主線程耗時的方法,將閾值設置低一點,比如200毫秒,這樣的話如果一個方法執行時間超過200毫秒,獲取堆棧信息並通知開發者。

 

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

 

4

 總結

 

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

 

  1. 應用啓動流程

  2. 閃屏頁優化

  3. MultiDex 原理分析

  4. ClassLoader 加載一個類的流程分析

  5. 熱修復原理

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

  7. 快速啓動Activity的方式:預創建Activity,預加載數據。

  8. 啓動時間監控的方式:Systrace+插樁、BlockCanary。

 

面試問到啓動優化問題,不要簡單一兩句話回答,可以說說自己在實際項目中做了哪些優化,比如Multidex優化,把整個流程,原理說清楚。當然,前提是自己要去實踐,理解爲什麼要這樣做。

 

就這樣,有問題請留言,更多文章,敬請期待。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章