Android APP啓動優化方案

2

應用啓動流程

 

1 、應用啓動的類型

 

冷啓動

 

從點擊應用圖標到UI界面完全顯示且用戶可操作的全部過程。

 

特點

 

耗時最多,衡量標準

 

啓動流程

 

Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl

 

熱啓動

 

因爲會從已有的應用進程啓動,所以不會再創建和初始化Application,只會重新創建並初始化Activity。

 

特點

 

耗時較少

 

啓動流程

 

LifeCycle -> ViewRootImpl

 

ViewRootImpl

 

ViewRoot是GUI管理系統與GUI呈現系統之間的橋樑。每一個ViewRootImpl關聯一個Window,


ViewRootImpl最終會通過它的setView方法綁定Window所對應的View,並通過其performTraversals方法對View進行佈局、測量和繪製。

 

3

啓動耗時檢測

 

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、代碼打點(函數插樁)

 

可以寫一個統計耗時的工具類來記錄整個過程的耗時情況。

 

其中需要注意的有:

 

  • 在上傳數據到服務器時建議根據用戶ID的尾號來抽樣上報。

  • 在項目中核心基類的關鍵回調函數和核心方法中加入打點。

 

代碼如下:

 


 
/**
* 耗時監視器對象,記錄整個過程的耗時情況,可以用在很多需要統計的地方,比如Activity的啓動耗時和Fragment的啓動耗時。
*/
public class TimeMonitor {

    private final String TAG = TimeMonitor.class.getSimpleName();
    private int mMonitord = -1;

    // 保存一個耗時統計模塊的各種耗時,tag對應某一個階段的時間
    private HashMap<String, Long> mTimeTag = new HashMap<>();
    private long mStartTime = 0;

    public TimeMonitor(int mMonitorId) {
        Log.d(TAG, "init TimeMonitor id: " + mMonitorId);
        this.mMonitorId = mMonitorId;
    }

    public int getMonitorId() {
        return mMonitorId;
    }

    public void startMonitor() {
        // 每次重新啓動都把前面的數據清除,避免統計錯誤的數據
        if (mTimeTag.size() > 0) {
        mTimeTag.clear();
        }
        mStartTime = System.currentTimeMillis();
    }

    /**
    * 每打一次點,記錄某個tag的耗時
    */
    public void recordingTimeTag(String tag) {
        // 若保存過相同的tag,先清除
        if (mTimeTag.get(tag) != null) {
            mTimeTag.remove(tag);
        }
        long time = System.currentTimeMillis() - mStartTime;
        Log.d(TAG, tag + ": " + time);
        mTimeTag.put(tag, time);
    }

    public void end(String tag, boolean writeLog) {
        recordingTimeTag(tag);
        end(writeLog);
    }

    public void end(boolean writeLog) {
        if (writeLog) {
            //寫入到本地文件
        }
    }

    public HashMap<String, Long> getTimeTags() {
        return mTimeTag;
    }
}

 

爲了使代碼更好管理,定義一個打點配置類:

 


 
/**
* 打點配置類,用於統計各階段的耗時,便於代碼的維護和管理。
*/
public final class TimeMonitorConfig {

    // 應用啓動耗時
    public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
}

 

因爲,耗時統計可能會在多個模塊和類中需要打點,所以需要一個單例類來管理各個耗時統計的數據:


 

 

/**
* 採用單例管理各個耗時統計的數據。
*/
public class TimeMonitorManager {

    private static TimeMonitorManager mTimeMonitorManager = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorMap = null;

    public synchronized static TimeMonitorManager getInstance() {
        if (mTimeMonitorManager == null) {
            mTimeMonitorManager = new TimeMonitorManager();
        }
        return mTimeMonitorManager;
    }

    public TimeMonitorManager() {
        this.mTimeMonitorMap = new HashMap<Integer, TimeMonitor>();
    }

    /**
     * 初始化打點模塊
    */
    public void resetTimeMonitor(int id) {
        if (mTimeMonitorMap.get(id) != null) {
            mTimeMonitorMap.remove(id);
        }
        getTimeMonitor(id);
    }

    /**
    * 獲取打點器
    */
    public TimeMonitor getTimeMonitor(int id) {
        TimeMonitor monitor = mTimeMonitorMap.get(id);
        if (monitor == null) {
            monitor = new TimeMonitor(id);
            mTimeMonitorMap.put(id, monitor);
        }
        return monitor;
    }
}

 

主要在以下幾個方面需要打點:

 

  • 應用程序的生命週期節點。

  • 啓動時需要初始化的重要方法,如數據庫初始化,讀取本地的一些數據。

  • 其他耗時的一些算法。

 

例如,啓動時在Application和第一個Activity加入打點統計:

 

Application

 

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);
}

@Override
public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("Application-onCreate");
}

 

第一個Activity:

 


 
@Override
protected void onCreate(Bundle savedInstanceState) {
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate");
    super.onCreate(savedInstanceState);

    initData();

    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate-Over");
}

@Override
protected void onStart() {
    super.onStart();
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("SplashActivity-onStart", false);
}

 

特點

 

精確,可帶到線上,推薦使用。

 

注意

 

  • 在上傳數據到服務器時建議根據用戶ID的尾號來抽樣上報。

  • onWindowFocusChanged只是首幀時間,App啓動完成的結束點應該是真實數據展示出來的時候,如列表第一條數據展示,記得使用getViewTreeObserver().addOnPreDrawListener(),它會把任務延遲到列表顯示後再執行。

 

 

AOP(Aspect Oriented Programming)打點

 

面向切面編程,通過預編譯和運行期動態代理實現程序功能統一維護的一種技術。

 

作用

 

利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合性降低,提高程序的可重用性,同時大大提高了開發效率。

 

AOP核心概念

 

1、橫切關注點

 

對哪些方法進行攔截,攔截後怎麼處理。

 

2、切面(Aspect)

 

類是對物體特徵的抽象,切面就是對橫切關注點的抽象。

 

3、連接點(JoinPoint)

 

被攔截到的點(方法、字段、構造器)。

 

4、切入點(PointCut)

 

對JoinPoint進行攔截的定義。

 

5、通知(Advice)

 

攔截到JoinPoint後要執行的代碼,分爲前置、後置、環繞三種類型。

 

準備

 

首先,爲了在Android使用AOP埋點需要引入AspectJ,在項目根目錄的build.gradle下加入:

 


 
classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'

 

然後,在app目錄下的build.gradle下加入:

 


 
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'

 

AOP埋點實戰

 

JoinPoint一般定位在如下位置

 

  • 1、函數調用

  • 2、獲取、設置變量

  • 3、類初始化

 

使用PointCut對我們指定的連接點進行攔截,通過Advice,就可以攔截到JoinPoint後要執行的代碼。Advice通常有以下幾種類型:

 

  • 1、Before:PointCut之前執行

  • 2、After:PointCut之後執行

  • 3、Around:PointCut之前、之後分別執行

 

首先,我們舉一個小栗子:

 


 
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}

 

在execution中的是一個匹配規則,第一個*代表匹配任意的方法返回值,後面的語法代碼匹配所有Activity中on開頭的方法。

 

處理Join Point的類型:

 

  • 1、call:插入在函數體裏面

  • 2、execution:插入在函數體外面

 

如何統計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爲Before、After時,方法入參爲JoinPoint。
當Action爲Around時,方法入參爲ProceedingPoint。

 

Around和Before、After的最大區別:

 

ProceedingPoint不同於JoinPoint,其提供了proceed方法執行目標方法。

 

總結AOP特性:

 

  • 1、無侵入性

  • 2、修改方便

 

4

啓動速度分析工具 — TraceView

 

使用方式

 

1、代碼中添加:Debug.startMethodTracing()、檢測方法、Debug.stopMethodTracing()。(需要使用adb pull將生成的**.trace文件導出到電腦,然後使用Android Studio的Profiler加載)

 

2、打開Profiler -> CPU -> 點擊 Record -> 點擊 Stop -> 查看Profiler下方Top Down/Bottom Up 區域找出耗時的熱點方法。

 

Profile CPU

 

1、Trace types

 

Trace Java Methods

 

會記錄每個方法的時間、CPU信息。對運行時性能影響較大。

 

Sample Java Methods

 

相比於Trace Java Methods會記錄每個方法的時間、CPU信息,它會在應用的Java代碼執行期間頻繁捕獲應用的調用堆棧,對運行時性能的影響比較小,能夠記錄更大的數據區域。

 

Sample C/C++ Functions

 

需部署到Android 8.0及以上設備,內部使用simpleperf跟蹤應用的native代碼,也可以命令行使用simpleperf。

 

Trace System Calls

 

  • 檢查應用與系統資源的交互情況。

  • 查看所有核心的CPU瓶。

  • 內部採用systrace,也可以使用systrace命令。

 

2、Event timeline

 

顯示應用程序中在其生命週期中轉換不同狀態的活動,如用戶交互、屏幕旋轉事件等。

 

3、CPU timeline

 

顯示應用程序實時CPU使用率、其它進程實時CPU使用率、應用程序使用的線程總數。

 

4、Thread activity timeline

 

列出應用程序進程中的每個線程,並使用了不同的顏色在其時間軸上指示其活動。

 

  • 綠色:線程處於活動狀態或準備好使用CPU。

  • 黃色:線程正等待IO操作。(重要)

  • 灰色:線程正在睡眠,不消耗CPU時間。

 

Profile提供的檢查跟蹤數據窗口有四種

 

1、Call Chart

 

提供函數跟蹤數據的圖形表示形式。

 

  • 水平軸:表示調用的時間段和時間。

  • 垂直軸:顯示被調用方。

  • 橙色:系統API。

  • 綠色:應用自有方法

  • 藍色:第三方API(包括Java API)

 

提示:右鍵點擊Jump to source跳轉至指定函數。

 

2、Flame Chart

 

將具有相同調用方順序的完全相同的方法收集起來。

 

  • 水平軸:執行每個方法的相對時間量。

  • 垂直軸:顯示被調用方。

 

注意:看頂層的哪個函數佔據的寬度最大(平頂),可能存在性能問題。

 

3、Top Down

 

  • 遞歸調用列表,提供self、children、total時間和比率來表示被調用的函數信息。

  • Flame Chart是Top Down列表數據的圖形化。

 

4、Bottom Up

 

  • 展開函數會顯示其調用方。

  • 按照消耗CPU時間由多到少的順序對函數排序。

 

注意點:

 

  • Wall Clock Time:程序執行時間。

  • Thread Time:CPU執行的時間。

 

TraceView小結

 

特點

 

1、圖形的形式展示執行時間、調用棧等。

2、信息全面,包含所有線程。

3、運行時開銷嚴重,整體都會變慢,得出的結果並不真實。

 

作用

 

主要做熱點分析,得到兩種數據:

 

  • 單次執行最耗時的方法。

  • 執行次數最多的方法。

 

 

5

啓動速度分析工具 — Systrace

 

使用方式:

 

代碼插樁

 

定義Trace靜態工廠類,將Trace.begainSection(),Trace.endSection()封裝成i、o方法,然後再在想要分析的方法前後進行插樁即可。

 

在命令行下執行systrace.py腳本:

 


 
python /Users/quchao/Library/Android/sdk/platform-tools/systrace/systrace.py -t 20 sched gfx view wm am app webview -a "com.wanandroid.json.chao" -o ~/Documents/open-project/systrace_data/wanandroid_start_1.html

 

具體參數含義如下:

 

  • -t:指定統計時間爲20s。

  • shced:cpu調度信息。

  • gfx:圖形信息。

  • view:視圖。

  • wm:窗口管理。

  • am:活動管理。

  • app:應用信息。

  • webview:webview信息。

  • -a:指定目標應用程序的包名。

  • -o:生成的systrace.html文件。

 

如何查看數據?

 

在UIThread一欄可以看到核心的系統方法時間區域和我們自己使用代碼插樁捕獲的方法時間區域。

 

Systrace原理

 

  • 在系統的一些關鍵鏈路(如SystemServcie、虛擬機、Binder驅動)插入一些信息(Label);

  • 通過Label的開始和結束來確定某個核心過程的執行時間;
    把這些Label信息收集起來得到系統關鍵路徑的運行時間信息,最後得到整個系統的運行性能信息;

  • Android Framework裏面一些重要的模塊都插入了label信息,用戶App中可以添加自定義的Lable。

 

Systrace小結

 

特性

 

  • 結合Android內核的數據,生成Html報告。

  • 系統版本越高,Android Framework中添加的系統可用Label就越多,能夠支持和分析的系統模塊也就越多。

  • 必須手動縮小範圍,會幫助你加速收斂問題的分析過程,進而快速地定位和解決問題。

 

作用

 

  • 主要用於分析繪製性能方面的問題。

  • 分析系統關鍵方法和應用方法耗時。

 

6

啓動監控

 

1、實驗室監控:視頻錄製

 

  • 80%繪製

  • 圖像識別

 

注意

 

覆蓋高中低端機型不同的場景。

 

2、線上監控

 

需要準確地統計啓動耗時。

 

1、啓動結束的統計時機

 

是否是使用界面顯示且用戶真正可以操作的時間作爲啓動結束時間。

 

2、啓動時間扣除邏輯

 

閃屏、廣告和新手引導這些時間都應該從啓動時間裏扣除。

 

3、啓動排除邏輯

 

Broadcast、Server拉起,啓動過程進入後臺都需要排除統計。

 

4、使用什麼指標來衡量啓動速度的快慢?

 

平均啓動時間的問題

一些體驗很差的用戶很可能被平均了。

 

建議的指標

 

1、快開慢開比

 

如2s快開比,5s慢開比,可以看到有多少比例的用戶體驗好,多少比例的用戶比較糟糕。

 

2、90%用戶的啓動時間

 

如果90%用戶的啓動時間都小於5s,那麼90%區間的啓動耗時就是5s。

 

5、啓動的類型有哪幾種?

 

  • 首次安裝啓動

  • 覆蓋安裝啓動

  • 冷啓動(指標)

  • 熱啓動(反映程序的活躍或保活能力)

 

借鑑Facebook的profilo工具原理,對啓動整個流程的耗時監控,在後臺對不同的版本做自動化對比,監控新版本是否有新增耗時的函數。

 

7

啓動優化常規方案

 

啓動過程中的常見問題

 

  • 點擊圖標很久都不響應:預覽窗口被禁用或設置爲透明。

  • 首頁顯示太慢:初始化任務太多。

  • 首頁顯示後無法進行操作:太多延遲初始化任務佔用主線程CPU時間片。

 

優化區域

 

Application、Activity創建以及回調等過程。

 

常規方案,省略了一些常規方案細節,感興趣可以查看原文。

 

1、主題切換

 

2、第三方庫懶加載

 

 

3、異步初始化

 

4、延遲初始化

 

5、Multidex預加載優化

 

6、預加載SharedPreferences

 

7、類預加載優化

 

在Application中提前異步加載初始化耗時較長的類。

 

如何找到耗時較長的類?

 

替換系統的ClassLoader,打印類加載的時間,按需選取需要異步加載的類。

 

注意:

 

  • Class.forName()只加載類本身及其靜態變量的引用類。

  • new 類實例 可以額外加載類成員變量的引用類。

 

8、WebView啓動優化

 

  • 1、WebView首次創建比較耗時,需要預先創建WebView提前將其內核初始化。

  • 2、使用WebView緩存池,用到WebView的時候都從緩存池中拿,注意內存泄漏問題。

  • 3、本地離線包,即預置靜態頁面資源。

 

9、頁面數據預加載

 

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

 

10、啓動階段不啓動子進程

 

子進程會共享CPU資源,導致主進程CPU緊張。此外,在多進程情況下一定要可以在onCreate中去區分進程做一些初始化工作。

 

注意啓動順序:

 

App onCreate之前是ContentProvider初始化。

 

11、閃屏頁與主頁的繪製優化

 

  • 1、佈局優化。

  • 2、過渡繪製優化。

 

關於繪製優化可以參考Android性能優化之繪製優化。

 

8

啓動優化黑科技

 

1、啓動階段抑制GC

 

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

 

前提條件

 

  • 1、設備廠商沒有加密內存中的Dalvik庫文件。

  • 2、設備廠商沒有改動Google的Dalvik源碼。

 

實現原理

 

  • 在源碼級別找到抑制GC的修改方法,例如改變跳轉分支。

  • 在二進制代碼裏找到 A 分支條件跳轉的”指令指紋”,以及用於改變分支的二進制代碼,假設爲 override_A。

  • 應用啓動後掃描內存中的 libdvm.so,根據”指令指紋”定位到修改位置,然後用 override_A 覆蓋。

 

缺點

 

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

 

2、CPU鎖頻

 

在Android系統中,CPU相關的信息存儲在/sys/devices/system/cpu目錄的文件中,通過對該目錄下的特定文件進行寫值,實現對CPU頻率等狀態信息的更改。

 

缺點

 

暴力拉伸CPU頻率,導致耗電量增加。

 

3、數據重排

 

Dex文件用到的類和APK裏面各種資源文件都比較小,讀取頻繁,且磁盤地址分佈範圍比較廣。我們可以利用Linux文件IO流程中的page cache機制將它們按照讀取順序重新排列在一起,以減少真實的磁盤IO次數。

 

1、類重排

 

使用Facebook的ReDex的Interdex調整類在Dex中的排列順序。

 

2、資源文件重排

 

  • 最佳方案是修改內核源碼,實現統計、度量、自動化。

  • 其次可以使用Hook框架進行統計得出資源加載順序列表。

  • 最後,調整apk文件列表需要修改7zip源碼以支持傳入文件列表順序。

 

技術視野:

 

  • 所謂的創新,不一定是要創造前所未有的東西,也可以將已有的方案移植到新的平臺,並結合該平臺的特性落地,就是一個很大的創新。

  • 當我們足夠熟悉底層的知識時,可以利用系統的特性去做更加深層次的優化。

 

4、類加載優化(Dalvik)

 

1、類預加載原理

 

對象第一次創建的時候,JVM首先檢查對應的Class對象是否已經加載。如果沒有加載,JVM會根據類名查找.class文件,將其Class對象載入。同一個類第二次new的時候就不需要加載類對象,而是直接實例化,創建時間就縮短了。

 

2、類加載優化過程

 

  • 在Dalvik VM加載類的時候會有一個類校驗過程,它需要校驗方法的每一個指令。

  • 通過Hook去掉verify步驟 -> 幾十ms的優化

  • 最大優化場景在於首次安裝和覆蓋安裝時,在Dalvik平臺上,一個2MB的Dex正常需要350ms,將classVerifyMode設爲VERIFY_MODE_NONE後,只需150ms,節省超過50%時間。

 

ART比較複雜,Hook需要兼容幾個版本。而且在安裝時,大部分Dex已經優化好了,去掉ART平臺的verify只會對動態加載的Dex帶來一些好處。所以暫時不建議在ART平臺使用。

 

9

總結

 

1、優化總方針

 

  • 異步、延遲、懶加載

  • 技術、業務相結合

 

2、注意事項

 

1、cpu time和wall time

 

  • wall time(代碼執行時間)與cpu time(代碼消耗CPU時間),鎖衝突會造成兩者時間差距過大。

  • cpu time纔是優化方向,應盡力按照systrace的cpu time和wall time跑滿cpu。

 

2、監控的完善

 

  • 線上監控多階段時間(App、Activity、生命週期間隔時間)。

  • 處理聚合看趨勢。

  • 收斂啓動代碼修改權限。

  • 結合CI修改啓動代碼需要Review通知。

 

3、常見問題

 

1、啓動優化是怎麼做的?

 

  • 分析現狀、確認問題

  • 針對性優化(先概括,引導其深入)

  • 長期保持優化效果

 

2、是怎麼異步的,異步遇到問題沒有?

 

  • 體現演進過程

  • 詳細介紹啓動器

 

3、啓動優化有哪些容易忽略的注意點?

 

  • cpu time與wall time

  • 注意延遲初始化的優化

  • 介紹下黑科技

4、版本迭代導致的啓動變慢有好的解決方式嗎?

 

  • 啓動器

  • 結合CI

  • 監控完善

 

至此,探索Android啓動速度優化的旅途也應該告一段落了,如果你耐心讀到最後的話,會發現要想極致地提升App的性能,需要有一定的廣度,如我們引入了始於後端的AOP編程來實現無侵入式的函數插樁,也需要有一定的深度,從前面的探索之旅來看,我們先後涉及了Framework層、Native層、Dalvik虛擬機、甚至是Linux IO和文件系統相關的原理。

 

因此,我想說,Android開發並不簡單,即使是App層面的性能優化這一知識體系,也是需要我們不斷地加深自身知識的深度和廣度。

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