都2020年了Andoid還能如何性能優化(1)—— 啓動速度優化

一.啓動類型

冷啓動

指進程死亡的情況下,從點擊應用圖標到UI界面完全顯示且用戶可操作的全部過程。

大致流程:
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl

用戶點擊桌面圖標,這個點擊事件它會觸發一個IPC的操作,之後便會執行到Process的start方法中,這個方法是用於進程創建的,接着,便會執行到ActivityThread的main方法,這個方法可以看做是我們單個App進程的入口,相當於Java進程的main方法,在其中會執行消息循環的創建與主線程Handler的創建,創建完成之後,就會執行到 bindApplication 方法,在這裏使用了反射去創建 Application以及調用了 Application相關的生命週期,Application結束之後,便會執行Activity的生命週期,在Activity生命週期結束之後,最後,就會執行到 ViewRootImpl,這時纔會進行真正的一個頁面的繪製。

熱啓動

即進程存活情況下,點擊桌面圖標,應用從後臺切換到前臺

二.如何檢測啓動耗時

1.查看Logcat

在Android Studio Logcat中過濾關鍵字“Displayed”,可以看到對應的冷啓動耗時日誌。

2.adb shell

使用adb shell獲取應用的啓動時間

// 其中的AppstartActivity全路徑可以省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路徑]

執行後會得到三個時間:ThisTime、TotalTime和WaitTime,詳情如下:
ThisTime
表示最後一個Activity啓動耗時。
TotalTime
表示所有Activity啓動耗時。
WaitTime
表示AMS啓動Activity的總耗時。
一般來說,只需查看得到的TotalTime,即應用的啓動時間,其包括 創建進程 + Application初始化 + Activity初始化到界面顯示 的過程。
特點:

1、線下使用方便,不能帶到線上。
2、非嚴謹、精確時間。

3.AOP(Aspect Oriented Programming) 打點

具體AOP可以自行上網查找文章
下面以統計統計Application中的所有方法耗時爲例子

@Aspect
public class ApplicationAop {

    @Around("call (* com.json.chao.application.BaseApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint) {
    Signature signature = joinPoint.getSignature();
    String name = signature.toShortString();
    long time = System.currentTimeMillis();
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    Log.i(TAG, name + " cost" +     (System.currentTimeMillis() - time));
    }
}

在上述代碼中,我們需要注意 不同的Action類型其對應的方法入參是不同的,具體的差異如下所示:

當Action爲Before、After時,方法入參爲JoinPoint。
當Action爲Around時,方法入參爲ProceedingPoint。

Around和Before、After的最大區別:
ProceedingPoint不同於JoinPoint,其提供了proceed方法執行目標方法。

4.使用TraceView
這個的使用參考我以前寫的文章 《Android性能優化系列之App啓動優化

三.啓動優化進階方法

啓動優化一些常用的方法參考《Android性能優化系列之App啓動優化》,這裏不再贅述,這裏講一些進階的方法

1.定製一套APP啓動框架

常見的啓動優化,我們會將一些sdk或者模塊的初始化進行併發的進行,但這些工作之間可能存在前後依賴的關係,所以我們又需要想辦法保證他們執行順序的正確性,所以需要通過啓動框架,爲各個任務建立依賴關係,最終構成一個有向無環圖。對於可以併發的任務,會通過線程池最大程度提升啓動速度。

目前開源的啓動框架有:
阿里的alpha:https://github.com/alibaba/alpha
美團的AppInit: https://github.com/laohong/AppInit
具體原理,感興趣的可以check源碼來看

2.I/O 優化

SharedPreference 在初始化的時候還是要全部數據一起解析。如果它的數據量超過
1000 條,啓動過程解析時間可能就超過 100 毫秒。如果只解析啓動過程用到的數據項則會很大程度減少解析時間,啓動過程適合使用隨機讀寫的數據結構。

解決方式:可以將 ArrayMap 改造成支持隨機讀寫、延時解析的數據存儲方式。具體實現後續將出文章講解。

3.數據重排

Linux 文件 I/O 流程
在這裏插入圖片描述
Linux 文件系統從磁盤讀文件的時候,會以 block 爲單位去磁盤讀取,一般 block 大小是4KB。也就是說一次磁盤讀寫大小至少是 4KB,然後會把 4KB 數據放到頁緩存 Page Cache 中。如果下次讀取文件數據已經在頁緩存中,那就不會發生真實的磁盤 I/O,而是直接從頁緩存中讀取,大大提升了讀的速度。所以上面的例子,我們雖然讀了 1000 次,但事實上只會發生一次磁盤 I/O,其他的數據都會在頁緩存中得到。
Dex 文件用的到的類和安裝包 APK 裏面各種資源文件一般都比較小,但是讀取非常頻繁。我們可以利用系統這個機制將它們按照讀取順序重新排列,減少真實的磁盤 I/O 次數。

類重排

啓動過程類加載順序可以通過複寫 ClassLoader 得到。

class GetClassLoader extends PathClassLoader 
{ 
public Class<?> findClass(String name) { // 將 name 記錄到文件 writeToFile(name,"coldstart_classes.txt");
 return super.findClass(name); 
 }
  }

具體實現可以參考 ReDex 的Interdex,調整類在 Dex 中的排列順序,可以利用 010 Editor 查看修改後的效果。

資源文件重排

修改 Kernel 源碼,單獨編譯一個特殊的 ROM。這樣做的目的
有三個:
1)統計。統計應用啓動過程加載了安裝包中哪些資源文件,比如 assets、drawable、layout 等。跟類重排一樣,我們可以得到一個資源加載的順序列表。
2)度量。在完成資源順序重排後,我們需要確定是否真正生效。比如有哪些資源文件加載了,它是發生真實的磁盤 I/O,還是命中了 Page Cache。
3)自動化。任何代碼提交都有可能改變啓動過程中類和資源的加載順序,如果完全依靠人工手動處理,這個事情很難持續下去。通過定製 ROM 的一些埋點和配合的工具,我們可以將它們放到自動化流程當中。

事實上如果僅僅爲了統計,我們也可以使用 Hook 的方式。下面是利用 Frida 實現獲得Android 資源加載順序的方法

resourceImpl.loadXmlResourceParser.implementation=function(a,b,c,d){ 
	send('file:'+a)
 	return this.loadXmlResourceParser(a,b,c,d) 
 }
 resourceImpl.loadDrawableForCookie.implementation=function(a,b,c,d,e){ 
 	send("file:"+a)
 	return this.loadDrawableForCookie(a,b,c,d,e) 
 }

調整安裝包文件排列需要修改 7zip 源碼實現支持傳入文件列表順序,同樣最後可以利用010 Editor 查看修改後的效果。

類的加載

在加載類的過程有一個 verify class 的步驟,它需要校驗方法的每一個指令,是一個比較耗時的操作。
在這裏插入圖片描述
我們可以通過 Hook 來去掉 verify 這個步驟,這對啓動速度有幾十毫秒的優化。其實最大的優化場景在於首次和覆蓋安裝時。以 Dalvik 平臺爲例,一個 2MB 的 Dex
正常需要 350 毫秒,將 classVerifyMode 設爲 VERIFY_MODE_NONE 後,只需要 150毫秒,節省超過 50% 的時間。

但是 ART 平臺要複雜很多,Hook 需要兼容幾個版本。而且在安裝時大部分 Dex 已經優化好了,去掉 ART 平臺的 verify 只會對動態加載的 Dex 帶來一些好處。Atlas 中的dalvik_hack可以通過下面的方法去掉 verify,但是當前沒有支持 ART 平臺。

這個黑科技可以大大降低首次啓動的速度,代價是對後續運行會產生輕微的影響。同時也要考慮兼容性問題,暫時不建議在 ART 平臺使用。

最後附上redex地址:https://github.com/facebook/redex

啓動階段抑制GC

啓動時CG抑制,允許堆一直增長,直到手動或OOM停止GC抑制。(空間換時間)
前提條件

1、設備廠商沒有加密內存中的Dalvik庫文件。
2、設備廠商沒有改動Google的Dalvik源碼。

實現原理
1、首先,在源碼級別找到抑制GC的修改方法,例如改變跳轉分支。
2、然後,在二進制代碼裏找到 A 分支條件跳轉的"指令指紋",以及用於改變分支的二進制代碼,假設爲 override_A。
3、最後,應用啓動後掃描內存中的 libdvm.so,根據"指令指紋"定位到修改位置,並使用 override_A 覆蓋。

缺點
需要白名單覆蓋所有設備,但維護成本高。

5.0 以下Multidex預加載優化

安裝或者升級後首次 MultiDex 花費的時間過於漫長,我們需要進行Multidex的預加載優化。
優化步驟
1、啓動時單獨開一個進程去異步進行Multidex的第一次加載,即Dex提取和Dexopt操作。
2、此時,主進程Application進入while循環,不斷檢測Multidex操作是否完成。
3、執行到Multidex時,則已經發現提取並優化好了Dex,直接執行。MultiDex執行完之後主進程Application繼續執行ContentProvider初始化和Application的onCreate方法。

注意
5.0以上默認使用ART,在安裝時已將Class.dex轉換爲oat文件了,無需優化,所以應判斷只有在主進程及SDK 5.0以下才進行Multidex的預加載。

參考:
抖音BoostMultiDex優化實踐:Android低版本上APP首次啓動時間減少80%

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