聯繫咱們:
有道技術團隊助手:ydtech01 / 郵箱:ydtech@rd.netease.comandroid
本文的重點在於如何定量的排查冷啓動過程當中的耗時操做,並提供對應的優化思路和實踐方法總結。同時本文涉及到的冷啓動優化主要涵蓋兩個方面:Application 的性能優化和 Launcher Activity 的性能優化。shell
中國大學 MOOC 是網易與高教社攜手推出的在線教育平臺,目前,通過長期的產品打磨和鑽研,在課程數量、質量以及影響力,中國大學 MOOC 已成爲全球領先的中文慕課平臺。同時通過這次優化,冷啓動速度總體提高27%。數據庫
在咱們平常開發中,隨着 app 總體迭代次數增多,因爲長久以來的迭代需求,android app 自己也集成了較多的第三方組件和 SDK,同時在平常迭代中,也是以業務迭代需求實現爲主要目的,致使如今 app 自己,或多或少存在一些性能可優化空間。因此有必要進行性能優化,提高用戶體驗安全
這次優化,主要側重於兩個方面:性能優化
該文檔重點不在於代碼規範和業務代碼邏輯致使的性能問題,而是在假設代碼無明顯、嚴重性能漏洞,而且不改變原有業務邏輯,量化性能監測數據和問題,並針對其進行優化修改。網絡
adb shell am start -S -W [packageName]/[activiytName]
上述 adb 命令中,幾個關鍵參數說明:app
再執行上訴 adb 後,會成功喚起 APP,並在控制檯輸出三個比較關鍵的參數:異步
對於應用層面得冷啓動性能優化,咱們關注的時間 TotalTime,該時間大體能夠歸納爲:Application 構造方法→該 Activity 的 onWindowFocusChange 方法時間總和。而這個過程也能夠粗略認知爲,用戶點擊桌面圖標到 app 第一個 Activity 獲取焦點,業務代碼執行的總時間(針對業務代碼的優化,咱們暫時不關心 Zygote 進程、Launcher 進程、AMS 進程的交互)。async
在 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 該權限
在導出獲取到 .trace 文件後,把 .trace 拖動至 androidStudio 編輯區;或者直接瀏覽 CPU Profile 視圖,即可對程序運行的堆棧進行分析:
上圖就是 trace 文件打開後的效果,展現的是基於 CPU 使用和線程運行情況,針對啓動速度的優化,須要關注的上圖標註的幾個點:
(1)CPU 運行時間軸:橫向拖動能夠選擇查看的時間範圍
(4)當前設備 CPU 輪轉的線程:點擊能夠選擇須要查看的線程,咱們重點關注主線程
(2)當前選擇線程,跟隨時間軸,各個方法棧的調用狀況和其耗時情況。其不一樣顏色分別表明
- 黃色:android 系統方法(FrameWork 層代碼,若是須要最終更底層的方法,須要最終 C/C++ 方法調用棧)
- 藍色:Java JDK 方法
- 綠色:屬於當前 app 進程執行的方法,包括一些類加載器和咱們的業務代碼(啓動速度優化主要針對這一部分)
(3)各個方法棧的調用順序和耗時狀況,能夠選擇不用的排序方式和視圖。
因此通常排查耗時方法時,建議先經過(2)視圖直觀檢測到耗時較爲嚴重的方法,鎖定後,在(3)視圖中查看具體的方法調用順序。
因爲在冷啓動過程當中,業務代碼耗時主要集中在 Application 和 launcher Activity 中,因此優化過程也是分別針對這兩塊進行優化。
使用2.1.1的方式,在優化先後,分別作了10次冷啓動耗時統計,結果以下:
啓動速度總體提高 27%。
經過 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 會在下面講到)。
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); });
上面針對冷啓動優化是基於當前項目自己作的步驟,這裏彙總一些冷啓動通用的優化思路:
(1)合理的使用異步初始化、延遲初始化和懶加載機制:主要針對 Application 中各類 SDK 的初始化
(2)在主線程中應當避免很耗時的操做,好比 IO 操做、數據庫讀寫操做
(3)簡化 launcher Activity 的佈局結構,若是很是複雜的佈局,能夠有如下兩種方式進行優化:
(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()); } }