中國大學 MOOC Android 性能優化:冷啓動優化總結

首圖.gif

聯繫咱們:
有道技術團隊助手:ydtech01 / 郵箱ydtech@rd.netease.comandroid

本文的重點在於如何定量的排查冷啓動過程當中的耗時操做,並提供對應的優化思路和實踐方法總結。同時本文涉及到的冷啓動優化主要涵蓋兩個方面:Application 的性能優化和 Launcher Activity 的性能優化。shell

1、背景

中國大學 MOOC 是網易與高教社攜手推出的在線教育平臺,目前,通過長期的產品打磨和鑽研,在課程數量、質量以及影響力,中國大學 MOOC 已成爲全球領先的中文慕課平臺。同時通過這次優化,冷啓動速度總體提高27%。數據庫

在咱們平常開發中,隨着 app 總體迭代次數增多,因爲長久以來的迭代需求,android app 自己也集成了較多的第三方組件和 SDK,同時在平常迭代中,也是以業務迭代需求實現爲主要目的,致使如今 app 自己,或多或少存在一些性能可優化空間。因此有必要進行性能優化,提高用戶體驗安全

這次優化,主要側重於兩個方面:性能優化

  • Application 的性能優化
  • app 啓動頁性能優化

該文檔重點不在於代碼規範和業務代碼邏輯致使的性能問題,而是在假設代碼無明顯、嚴重性能漏洞,而且不改變原有業務邏輯,量化性能監測數據和問題,並針對其進行優化修改。網絡

2、冷啓動速度優化

2.1 相關知識點

2.1.1 冷啓動耗時統計

adb shell am start -S -W [packageName]/[activiytName]

上述 adb 命令中,幾個關鍵參數說明:app

  • -S:表示啓動該 app 前先完全關閉當前 app 進程
  • -W:啓動並輸出相關耗時數據
  • packageName:app 的 applicationID
  • activityName:app 啓動須要拉起的 Activity,若是用於統計冷啓動耗時,那麼該參數即爲應用的第一個啓動的 Activity(intent-filter 爲 LAUNCHER 的 Activity)

再執行上訴 adb 後,會成功喚起 APP,並在控制檯輸出三個比較關鍵的參數:異步

  • LaunchState:啓動模式,上訴啓動模式爲冷啓動
  • WaitTime:系統啓動應用耗時= TotalTime +系統資源啓動時間(單位 ms )
  • TotalTime:應用自身啓動耗時=該 Activity 啓動時間+應用 application 等資源啓動時間(單位 ms )

對於應用層面得冷啓動性能優化,咱們關注的時間 TotalTime,該時間大體能夠歸納爲:Application 構造方法→該 Activity 的 onWindowFocusChange 方法時間總和。而這個過程也能夠粗略認知爲,用戶點擊桌面圖標到 app 第一個 Activity 獲取焦點,業務代碼執行的總時間(針對業務代碼的優化,咱們暫時不關心 Zygote 進程、Launcher 進程、AMS 進程的交互)。async

2.1.2 冷啓動耗時堆棧觀察方法:

在 Android API>=26 的系統版本中,建議使用 CPU Profile 或者 Debug.startMethodTracing 進行監控並導出 trace 文件進行分析。無論哪一種方式,採集堆棧信息都有兩種模式:採樣模式和追蹤模式。追蹤模式會一直抓取數據,對設備性能要求較高。ide

(1)CPU Profile

(2)Debug.startMethodTracing

因爲冷啓動涉及到業務應用層面的時間是:該 Activity 啓動時間+應用 application 等資源啓動時間,因此咱們在 Application 構造方法中開始採集,在第一個 Activity 的 onWindowFocusChange 中中止採集,並輸出 trace文件。

/**
 * 在Application構造方法中開始採集
 */
public UcmoocApplication() {
    //保存Trace文件的目錄
    File file = new File(Environment.getExternalStorageDirectory(), "ucmooc.trace");

    //採集方式有如下兩種,根據需求選擇其一
    //第一種:經過採樣的方式,追蹤堆棧信息
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        //經過採樣方式追蹤堆棧信息,須要指定文件保存目錄、文件最大大小(單位M)、採樣間隔(單位us)
        Debug.startMethodTracingSampling(file.getAbsolutePath(), 8, 1000);
    }

    //第二種:經過追蹤的方式,全量採集堆棧信息
    Debug.startMethodTracing(file.getAbsolutePath());

    coreApplication = new CoreApplication();
}
/**
 * 在啓動後的第一個Activity的onWindowFocusChanged中中止監聽
 *
 * @param hasFocus
 */
@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    Debug.stopMethodTracing();
}

同時,因爲該操做涉及到文件讀寫權限,須要手動授予 APP 該權限

2.1.3 .trace 日誌文件閱讀:

在導出獲取到 .trace 文件後,把 .trace 拖動至 androidStudio 編輯區;或者直接瀏覽 CPU Profile 視圖,即可對程序運行的堆棧進行分析:

上圖就是 trace 文件打開後的效果,展現的是基於 CPU 使用和線程運行情況,針對啓動速度的優化,須要關注的上圖標註的幾個點:

(1)CPU 運行時間軸:橫向拖動能夠選擇查看的時間範圍

(4)當前設備 CPU 輪轉的線程:點擊能夠選擇須要查看的線程,咱們重點關注主線程

(2)當前選擇線程,跟隨時間軸,各個方法棧的調用狀況和其耗時情況。其不一樣顏色分別表明

  • 黃色:android 系統方法(FrameWork 層代碼,若是須要最終更底層的方法,須要最終 C/C++ 方法調用棧)
  • 藍色:Java JDK 方法
  • 綠色:屬於當前 app 進程執行的方法,包括一些類加載器和咱們的業務代碼(啓動速度優化主要針對這一部分)

(3)各個方法棧的調用順序和耗時狀況,能夠選擇不用的排序方式和視圖。

因此通常排查耗時方法時,建議先經過(2)視圖直觀檢測到耗時較爲嚴重的方法,鎖定後,在(3)視圖中查看具體的方法調用順序。

2.2 優化步驟

因爲在冷啓動過程當中,業務代碼耗時主要集中在 Application 和 launcher Activity 中,因此優化過程也是分別針對這兩塊進行優化。

2.2.1 優化成果

使用2.1.1的方式,在優化先後,分別作了10次冷啓動耗時統計,結果以下:

啓動速度總體提高 27%。

2.2.2 Application 優化

經過 trace 文件,能夠直觀的發現,在 application 中,耗時最長的方法是其生命週期中的 onCreate 方法,其中在 onCreate 方法中,耗時比較長的方法有:initMudleFactory、initURS、Unicorn.init、initUmeng。

在 Top Down 視圖中,能夠更加直觀的看出,這次採樣,也正是這四個方法耗時最多。

經過源碼排查,這是個方法,均是第三方 SDK 的初始化,同時在這幾個 SDK 內部,都含有較多的 IO 操做,而且內部實現了線程管理以保證線程安全,因此能夠將這幾個 SDK 的初始化,放在子線程中完成。這裏以友盟 SDK 爲例:

/**
* 友盟SDK中有涉及到線程不安全的地方,都本身維護了線程,保證線程安全
**/
try {
    var6 = getClass("com.umeng.umzid.ZIDManager");
    if (var6 == null) {
        Log.e("UMConfigure", "--->>> SDK 初始化失敗,請檢查是否集成umeng-asms-1.2.x.aar庫。<<<--- ");
        (new Thread() {
            public void run() {
                try {
                    Looper.prepare();
                    Toast.makeText(var5, "SDK 初始化失敗,請檢查是否集成umeng-asms-1.2.X.aar庫。", 1).show();
                    Looper.loop();
                } catch (Throwable var2) {
                }

            }
        }).start();
        return;
    }
} catch (Throwable var27) {
}
 
/**
* 在友盟SDK內部中有不少IO操做的地方,和加鎖操做,因此能夠將SDK初始化操做,放在子線程中
**/
if (!TextUtils.isEmpty(var1)) {
    sAppkey = var1;
    sChannel = var2;
    UMGlobalContext.getInstance(var3);
    k.a(var3);
    if (!needSendZcfgEnv(var3)) {
        FieldManager var4 = FieldManager.a();
        var4.a(var3);
    }

    synchronized(PreInitLock) {
        preInitComplete = true;
    }
}

最終,咱們能夠把上面提到的幾個 SDK 初始化工做放入在子線程中:

private void initSDKAsyn(){
    new Thread(() -> {
        if (Util.inMainProcess()){
            // 登陸
            initURS();

            if (BuildConfig.ENTERPRISE) {
                Unicorn.init(BaseApplication.this, "", QiyuOptionConfig.options(), new QiyuImageLoader());
                initModuleRegister();
            } else {
                Unicorn.init(BaseApplication.this, "", QiyuOptionConfig.options(), new QiyuImageLoader());
            }

            // 初始化下載服務
            try {
                initDownload();
            } catch (Exception e) {
                NTLog.f(TAG, e.toString());
            }
        }
        initModuleFactory();
        initUmeng();
    }).start();
}

對於一些必須在主線程中初始化完成的 SDK,能夠考慮使用 IdleHandler,在主線程空閒時,完成初始化(關於 IdleHandler 會在下面講到)。

2.2.3 Launcher Activity 優化

auncher Activity 是 WelcomeActivity,在對 Application 優化結束後,再對 WelcomeActivity 進行優化,仍是和上路的思路同樣,先經過 trace 文件追蹤:

能夠看到,在 WelcomeActivity 的 onCreate 方法中,耗時較多的三個地方,分別是:initActionBar、EventBus.register、setContentView,下面針對這三塊內容,分別進行對應的優化操做:

(1)initActionBar

在上圖中,能夠看到,initActionBar 中最耗時的操做是 getSupportActionBar,經過研究代碼發現,在 WelcomeActivity 中,並不須要操做 actionBar,因此直接複寫父類方法,去掉 super 調用便可。

(2)EventBus.register

EventBus 註冊時,性能較差,是由於在改過程當中涉及到大量的反射操做,因此對性能損耗較大。經過查看官方文檔,該問題在 EventBus3.0 中獲得了很好的處理,主要是經過 apt 技術增長索引,提高效率。(當前項目未升級版本,待後期優化)

(3)setContentView

setContentView 是 Activity 渲染布局時的必要方法,其耗時的點在於,解析 xml 佈局文件時,使用了反射,因此若是 xml 佈局文件很是複查的時候,可使用androidx.asynclayoutinflater:asynclayoutinflater進行異步加載 xml 文件,使用方式以下:

new AsyncLayoutInflater(this).inflate(R.layout.activity_welcome, null,
        (view, resid, parent) -> {
            setContentView(view);
        });

3、優化方法總結

上面針對冷啓動優化是基於當前項目自己作的步驟,這裏彙總一些冷啓動通用的優化思路

(1)合理的使用異步初始化、延遲初始化和懶加載機制:主要針對 Application 中各類 SDK 的初始化

(2)在主線程中應當避免很耗時的操做,好比 IO 操做、數據庫讀寫操做

(3)簡化 launcher Activity 的佈局結構,若是很是複雜的佈局,能夠有如下兩種方式進行優化:

  • 建議使用約束佈局(ConstraintLayout)來減小布局嵌套避免過分渲染。
  • 使用 androidx.asynclayoutinflater:asynclayoutinflater 進行異步加載 xml 文件。

(4)合理使用 IdleHandler 進行延遲初始化,使用方式以下:

/**
 * 須要在當前線程中處理耗時任務,而且並不須要立刻執行的話,可使用IdleHandler
 * 這樣該任務能夠消息隊列空閒時,被處理
 */
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override
    public boolean queueIdle() {
        //此處添加處理任務

        //返回值爲false:則執行完畢以後移除這條消息,
        //返回值爲true:則則執行完畢以後依然保留,等到下次空閒時會再次執行,
        return false;
    }
});

(5)開始嚴苛模式(StrictMode)

該模式並不能幫咱們自動優化性能,而是能夠幫助咱們檢測出咱們可能無心中或者一些第三方 SDK 中作的會阻塞 Main 線程的事情(好比磁盤操做、網絡操做),並將它們提醒出來,以便在開發階段進行修復。其檢測策略有線程檢測策略和虛擬機檢測策略,咱們能夠設置須要檢測的操做,當代碼操做違規時,能夠經過 Logcat 或者直接崩潰的形式提醒咱們,具體使用方式以下

/**
 * 開啓嚴苛模式,當代碼有違規操做時,能夠經過Logcat或崩潰的方式提醒咱們
 */
private void startStrictMode() {
    if (BuildConfig.DEBUG) { //必定要在Debug模式下使用,避免在生產環境中發生沒必要要的崩潰和日誌輸出

        //線程檢測策略
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()  //檢測主線程磁盤讀取操做
                .detectDiskWrites() //檢測主線程磁盤寫入操做
                .detectNetwork() //檢測主線程網絡請求操做
                .penaltyLog() //違規操做以log形式輸出
                .build());

        //虛擬機檢測策略
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects() //檢測SqlLite泄漏
                .detectLeakedClosableObjects() //檢測未關閉的closable對象泄漏
                .penaltyDeath() //發生違規操做時,直接崩潰
                .build());
    }
}
相關文章
相關標籤/搜索