前言
如果你已經有 2 - 3 年以上開發經驗還不懂的怎么去優化自己的項目,那就有點說不過去了,下面是我自己總結的一套通用級別的 Android 性能優化。如果圖片不清晰文末可以下載原始 xmind 圖。
如果你正在找工作, 那么你需要一份 Android 高級開發面試寶典
1、 你對 APP 的啟動有過研究嗎? 有做過相關的啟動優化嗎?
程序員:
之前做熱修復的時候研究過 Application 的啟動原理。項目中也做過一些啟動優化。
面試官:
哦,你之前研究過熱修復? (這個時候有可能就會深入的問問熱修復的原理,這里咱們就不討論熱修復原理) 那你說說對啟動方面都做了哪些優化?
程序員:
-
我發現程序在冷啟動的時候,會有 1s 左右的白屏閃現,低版本是黑屏的現象,在這期間我通過翻閱系統主題源碼,發現了系統 AppTheme 設置了一個
windowBackground
,由此推斷就是這個屬性搗的鬼,開始我是通過設置windowIsTranslucent
透明屬性,發現雖然沒有了白屏,但是中間還是有一小段不可見,這個用戶體驗還是不好的。最后我觀察了市面上大部分的 Android 軟件在冷啟動的時候都會有一個Splash
的廣告頁,同時在增加一個倒數的計時器,最后才進入到登錄頁面或者主頁面。我最后也是這樣做的,原因是這樣做的好處可以讓用戶先基於廣告對本 APP 有一個基本認識,而且在倒數的時候也預留給咱們一些對插件和一些必須或者耗時的初始化做一些准備。Ps:這里會讓面試官感覺你是一個注重用戶體驗的
-
通過翻閱 Application 啟動的源碼,當我們點擊桌面圖標進入我們軟件應用的時候,會由 AMS 通過 Socket 給 Zygote 發送一個 fork 子進程的消息,當 Zygote fork 子進程完成之后會通過反射啟動 ActivityThread##main 函數,最后又由 AMS 通過 aidl 告訴 ActivityThread##H 來反射啟動創建Application 實例,並且依次執行
attachBaseContext
、onCreate
生命周期,由此可見我們不能在這 2 個生命周期里做主線程耗時操作。Ps: 這里會讓面試官感覺你對 App 應用的啟動流程研究的比較深,有過真實的翻閱底層源碼,而並不是背誦答案。
-
知道了
attachBaseContext
、onCreate
在應用中最先啟動,那么我們就可以通過 TreceView 等性能檢測工具,來檢測具體函數耗時時間,然后來對其做具體的優化。- 項目不及時需要的代碼通過異步加載。
- 將對一些使用率不高的初始化,做懶加載。
- 將對一些耗時任務通過開啟一個 IntentService來處理。
- 還通過 redex 重排列 class 文件,將啟動階段需要用到的文件在 APK 文件中排布在一起,盡可能的利用 Linux 文件系統的 pagecache 機制,用最少的磁盤 IO 次數,讀取盡可能多的啟動階段需要的文件,減少 IO 開銷,從而達到提升啟動性能的目的。
- 通過抖音發布的文章知曉在 5.0 低版本可以做
MultiDex
優化,在第一次啟動的時候,直接加載沒有經過 OPT 優化的原始 DEX,先使得 APP 能夠正常啟動。然后在后台啟動一個單獨進程,慢慢地做完 DEX 的 OPT 工作,盡可能避免影響到前台 APP 的正常使用。
Ps:1. 面試官這里會覺得你對啟動優化確實了解的不錯,有一定的啟動優化經驗。
- 在第五點面試官會覺得你比較關注該圈子的動態,發現好的解決方案,並能用在自己項目上。這一點是加分項!
-
Application 啟動完之后,AMS 會找出前台棧頂待啟動的 Activity , 最后也是通過 AIDL 通知 ActivityThread#H 來進行對 Activity 的實例化並依次執行生命周期
onCreate
、onStart
、onRemuse
函數,那么這里由於 onCreate 生命周期中如果調用了setContentView
函數,底層就會通過將 XML2View 那么這個過程肯定是耗時的。所以要精簡 XML 布局代碼,盡可能的使用ViewStub
、include
、merge
標簽來優化布局。接着在 onResume 聲明周期中會請求 JNI 接收 Vsync (垂直同步刷新的信號) 請求,16ms 之后如果接收到了刷新的消息,那么就會對 DecorView 進行onMeasure->onLayout->onDraw
繪制。最后才是將 Activity 的根布局 DecorView 添加到 Window 並交於 SurfaceFlinger 顯示。所以這一步除了要精簡 XML 布局,還有對自定義 View 的測量,布局,繪制等函數不能有耗時和導致 GC 的操作。最后也可以通過
TreaceView
工具來檢測這三個聲明周期耗時時間,從而進一步優化,達到極限。這一步給面試官的感覺你對整個 Activity 的啟動和 View 的繪制還有刷新機制都有深入的研究,那么此刻你肯定給面試官留了一個好印象,說明你平時對這些源碼級別的研究比較廣泛,透徹。
總結:
最后我基於以上的優化減少了 50% 啟動時間。
面試官:
嗯,研究的挺深的,源碼平時不少看吧。
程序員:
到這里,我知道這一關算是過了!
2、有做過相關的內存優化嗎?
程序員:
有做過,目前的項目內存優化還是挺多的,要不我先說一下優化內存有什么好處吧?咱們不能盲目的去優化!
有的時候對於自己熟悉的領域,一定要主動出擊,自己主導這場面試。
面試官:
可以。
Ps:這里大多數面試官會同意你的請求,除非遇見裝B的。
程序員:
好處:
- 減少 OOM ,可以提高程序的穩定性。
- 減少卡頓,提高應用流暢性。
- 減少內存占用,提高應用后台存活性。
- 減少程序異常,降低應用 Crash 率, 提高穩定性。
那么我基於這四點,我的程序做了如下優化:
-
1.減少 OOM
在應用開發階段我比較喜歡用 LeakCanary 這款性能檢測工具,好處是它能實時的告訴我具體哪個類發現了內存泄漏(如果你對 LeakCanary 的原理了解的話,可以說一說它是怎么檢測的)。
還有我們要明白為什么應用程序會發送 OOM ,又該怎么去避免它?
發生 OOM 的場景是當申請 1M 的內存空間時,如果你要往該內存空間存入 2M 的數據,那么此時就會發生 OOM。
在應用程序中我們不僅要避免直接導致 OOM 的場景還要避免間接導致 OOM 的場景。間接的話也就是要避免內存泄漏的場景。
內存泄漏的場景是這個對象不再使用時,應用完整的執行最后的生命周期,但是由於某些原因,對象雖然已經不再使用,仍然會在內存中存在而導致 GC 不會去回收它,這就意味着發生了內存泄漏。(這里可以介紹下 GC 回收機制,回收算法,知識點盡量往外擴展而不脫離本題)
最后在說一下在實際開發中避免內存泄漏的場景:
-
資源型對象未關閉: Cursor,File
-
注冊對象未銷毀: 廣播,回調監聽
-
類的靜態變量持有大數據對象
-
非靜態內部類的靜態實例
-
Handler 臨時性內存泄漏: 使用靜態 + 弱引用,退出即銷毀
-
容器中的對象沒清理造成的內存泄漏
-
WebView: 使用單獨進程
其實這些都是基礎,把它記下就行了。記得多了在實際開發中就有印象了。
-
-
2.減少卡頓
怎么減少卡頓? 那么我們可以從 2 個原理方面來探討卡頓的根本原因,第一個原理方面是繪制原理,另一個就是刷新原理。
- 繪制原理:
-
刷新原理:
View 的 requestLayout 和 ViewRootImpl##setView 最終都會調用 ViewRootImpl 的 requestLayout 方法,然后通過 scheduleTraversals 方法向 Choreographer 提交一個繪制任務,然后再通過 DisplayEventReceiver 向底層請求 vsync 垂直同步信號,當 vsync 信號來的時候,會通過 JNI 回調回來,在通過 Handler 往消息隊列 post 一個異步任務,最終是 ViewRootImpl 去執行繪制任務,最后調用 performTraversals 方法,完成繪制。
詳細流程可以參考下面流程圖:

卡頓的根本原因:
從刷新原理來看卡頓的根本原理是有兩個地方會造成掉幀:
一個是主線程有其它耗時操作,導致doFrame 沒有機會在 vsync 信號發出之后 16 毫秒內調用;
還有一個就是當前doFrame方法耗時,繪制太久,下一個 vsync 信號來的時候這一幀還沒畫完,造成掉幀。
既然我們知道了卡頓的根本原因,那么我們就可以監控卡頓,從而可以對卡頓優化做到極致。我們可以從下面四個方面來監控應用程序卡頓:
-
基於 Looper 的 Printer 分發消息的時間差值來判斷是否卡頓。
//1. 開啟監聽 Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));//2. 只要分發消息那么就會在之前和之后分別打印消息public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; ... for (;;) { Message msg = queue.next(); // might block ... //分發之前打印 final Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } ... try { //分發消息 msg.target.dispatchMessage(msg); ... //分發之后打印 if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } } }
```
-
基於 Choreographer 回調函數 postFrameCallback 來監控
[圖片上傳中...(image-abbf2c-1585726295362-1)]
-
基於開源框架 BlockCanary 來監控
-
基於開源框架 rabbit-client 來監控
怎么避免卡頓:
一定要避免在主線程中做耗時任務,總結一下 Android 中主線程的場景:
1. UI 生命周期的控制
2. 系統事件的處理
3. 消息處理
4. 界面布局
5. 界面繪制
6. 界面刷新
7. ...
還有一個最重要的就是避免內存抖動,不要在短時間內頻繁的內存分配和釋放。
基於這幾點去說卡頓肯定是沒有問題的。
-
3.減少內存占用
可以從如下幾個方面去展開說明:
-
AutoBoxing(自動裝箱): 能用小的堅決不用大的。
-
內存復用
-
使用最優的數據類型
-
枚舉類型: 使用注解枚舉限制替換 Enum
-
圖片內存優化(這里可以從 Glide 等開源框架去說下它們是怎么設計的)
- 選擇合適的位圖格式
- bitmap 內存復用,壓縮
- 圖片的多級緩存
-
基本數據類型如果不用修改的建議全部寫成 static final,因為 它不需要進行初始化工作,直接打包到 dex 就可以直接使用,並不會在 類 中進行申請內存
-
字符串拼接別用 +=,使用 StringBuffer 或 StringBuilder
-
不要在 onMeause, onLayout, onDraw 中去刷新 UI
-
盡量使用 C++ 代碼轉換 YUV 格式,別用 Java 代碼轉換 RGB 等格式,真的很占用內存
-
-
4.減少程序異常
減少程序異常那么我們可以從穩定性和 Crash 來分別說明。
這個我們將在第四點會詳細的介紹程序的穩定性和 Crash 。
如果說出這些,再實際開發中舉例說明一下怎么解決的應該是沒有問題的。
3、你在項目中有沒有遇見卡頓問題?是怎么排查卡頓?又是怎么優化的?
程序員:
有遇見, 比如在主線程中做耗時操作、頻繁的創建對象和銷毀對象導致 GC 回收頻繁、布局的層級多等。
面試官:
嗯,那具體說說是怎么優化的。
程序員:
這里我們還是可以從顯示原理和優化建議來展開說明,參考如下:
- 顯示原理:
-
繪制原理:
[圖片上傳中...(image-c401b0-1585726295365-7)]
-
刷新原理:
View 的 requestLayout 和 ViewRootImpl##setView 最終都會調用 ViewRootImpl 的 requestLayout 方法,然后通過 scheduleTraversals 方法向 Choreographer 提交一個繪制任務,然后再通過 DisplayEventReceiver 向底層請求 vsync 垂直同步信號,當 vsync 信號來的時候,會通過 JNI 回調回來,在通過 Handler 往消息隊列 post 一個異步任務,最終是 ViewRootImpl 去執行繪制任務,最后調用 performTraversals 方法,完成繪制。
詳細流程可以參考下面流程圖:
[圖片上傳中...(image-ad3389-1585726295365-6)]
-
卡頓的根本原因:
從刷新原理來看卡頓的根本原理是有兩個地方會造成掉幀:
一個是主線程有其它耗時操作,導致doFrame 沒有機會在 vsync 信號發出之后 16 毫秒內調用;
還有一個就是當前 doFrame 方法耗時,繪制太久,下一個 vsync 信號來的時候這一幀還沒畫完,造成掉幀。
既然我們知道了卡頓的根本原因,那么我們就可以監控卡頓,從而可以對卡頓優化做到極致。我們可以從下面四個方面來監控應用程序卡頓:
- 基於 Looper 的 Printer 分發消息的時間差值來判斷是否卡頓。
//1\. 開啟監聽 Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));//2\. 只要分發消息那么就會在之前和之后分別打印消息public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; ... for (;;) { Message msg = queue.next(); // might block ... //分發之前打印 final Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } ... try { //分發消息 msg.target.dispatchMessage(msg); ... //分發之后打印 if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } } }
- 基於 Choreographer 回調函數 postFrameCallback 來監控

-
基於開源框架 BlockCanary 來監控
-
基於開源框架 rabbit-client 來監控
-
怎么可以提高程序運行流暢
1.布局優化:
1.1 布局優化分析工具:

1.2 優化方案:

-
提升動畫性能
- 盡量別用補間動畫,改為屬性動畫,因為通過性能監控發現補間動畫重繪非常頻繁
- 使用硬件加速提高渲染速度,實現平滑的動畫效果。
-
怎么避免卡頓:
一定要避免在主線程中做耗時任務,總結一下 Android 中主線程的場景:
- UI 生命周期的控制
- 系統事件的處理
- 消息處理
- 界面布局
- 界面繪制
- 界面刷新
- ...
基於這幾點去說卡頓肯定是沒有問題的。
4、怎么保證 APP 的穩定運行?
程序員:
保證程序的穩定我們可以從內存、代碼質量、Crash、ANR、后台存活等知識點來展開優化。
面試官:
那你具體說說你是怎么做的?
程序員:
1.內存
可以從第二點內存優化來說明
2.代碼質量
- 團隊之前相互代碼審查,保證了代碼的質量,也可以學習到了其它同事碼代碼的思想。
- 使用 Link 掃描代碼,查看是否有缺陷性。
3. Crash
- 通過實現 Thread.UncaughtExceptionHandler 接口來全局監控異常狀態,發生 Crash 及時上傳日志給后台,並且及時通過插件包修復。
- Native 線上通過 Bugly 框架實時監控程序異常狀況,線下局域網使用 Google 開源的 breakpad 框架。發生異常就搜集日志上傳服務器(這里要注意的是日志上傳的性能問題,后面省電模塊會說明)
4. ANR

5. 后台存活

面試官:
嗯,你對知識點掌握的挺好。
說完這些,這一關也算是過了。
5、說說你在項目中網絡優化?
程序員:
有,這一點其實可以通過 OKHTTP 連接池和 Http 緩存來說一下(當然這里不會再展開分析 OKHTTP 源碼了)
面試官:
那你具體說一下吧
程序員

說了這些之后,再說一下你當前使用網絡框架它們做了哪些優化比如 OKHTTP(Socket 連接池、Http緩存、責任鏈)、Retrofit(動態代理)。說了這些一般這關也算是過了。
6、你在項目中有用過哪些存儲方式? 對它們的性能有過優化嗎?
程序員:
主要用過 sp,File,SQLite 存儲方式。其中對 sp 和 sqlite 做了優化。
面試官:
那你說說都做了哪些優化?
程序員:

這一塊如果你使用過其它第三方的數據庫,可以說說它們的原理和它們存取的方式。
7、你在項目中有做過自定義 View 嗎?有對它做過什么優化?
程序員:
有做過。比如重復繪制,還有大圖長圖有過優化。
面試官:
那具體說一說
程序員:

最后也是結合真實場景具體說一個。
8、你們項目的耗電量怎么樣? 有做過優化嗎?
程序員:
在沒有優化之前持續工作 30 分鍾的耗電量是 8%, 優化后是 4%。
面試官:
那你說一說你是怎么優化的。
程序員:
因為我們產品是一款社交通信的軟件,有音視頻通話、GPS 定位上報、長連接的場景,所以優化起來確實有點困難。不過最后也還是優化了一半的電量下去。主要做了如下優化:

說出這些之后,在結合項目一個真實的優化點來說明一下。
9、有做過日志優化嗎?
程序員:
有優化,在之前沒有考慮任何性能的情況下,我是直接有 log 就寫入文件,盡管我開了線程池去寫文件,只要軟件在運行那么就會頻繁的使 CPU 進行工作。這也間接的導致了耗電。
面試官:
那你具體說一下,最后怎么解決這個問題的?
程序員:

展開上面這些點說明之后,面試官一般不會為難你。
10、你們 APK 有多大?有做過 APK 體積相關的優化嗎?
程序員:
有過優化,在沒有優化之前項目的包體積大小是 80M,優化之后是 50M.
面試官:
說一說是怎么優化的
程序員:

基於這幾點優化方案,一般都能解決 APK 體積問題。最后再把自己項目 APK 體積優化步驟結合上面點說一下就行。
總結
其實性能優化點都是息息相關的,比如卡頓會涉及內存、顯示,啟動也會涉及 APK dex 的影響。所以說性能優化不僅僅是單方面的優化,一定要掌握最基本的優化方案,才能更加深入探討性能原理問題。
在這里也建立大家多看流行開源框架源碼,比如 Glide (內存方面), OKhttp (網絡連接方面) 優化的真的很極致。到這里性能優化方面的知識也就說完了,下來一定好好去消化。
作者:DevYK
鏈接:https://juejin.im/post/5e7f12ba518825736d2780a0
來源:掘金