作者:DevYK
前言
如果你已經有 2 - 3 年以上開發經驗還不懂的怎么去優化自己的專案,那就有點說不過去了,下面是我自己總結的一套入門級別的 Android 性能優化,
如果你正在找作業, 那么你需要一份 Android高級開發面試寶典
一、 你對 APP 的啟動有過研究嗎? 有做過相關的啟動優化嗎?
程式員:
之前做熱修復的時候研究過 Application 的啟動原理,專案中也做過一些啟動優化,
面試官:
哦,你之前研究過熱修復? (這個時候有可能就會深入的問問熱修復的原理,這里咱們就不討論熱修復原理) 那你說說對啟動方面都做了哪些優化?
程式員:
1、我發現程式在冷啟動的時候,會有 1s 左右的白屏閃現,低版本是黑屏的現象,在這期間我通過翻閱系統主題原始碼,發現了系統 AppTheme 設定了一個 windowBackground ,
由此推斷就是這個屬性搗的鬼,開始我是通過設定 windowIsTranslucent 透明屬性,發現雖然沒有了白屏,但是中間還是有一小段不可見,這個用戶體驗還是不好的,
最后我觀察了市面上大部分的 Android 軟體在冷啟動的時候都會有一個 Splash 的廣告頁,同時在增加一個倒數的計時器,最后才進入到登錄頁面或者主頁面,
我最后也是這樣做的,原因是這樣做的好處可以讓用戶先基于廣告對本 APP 有一個基本認識,而且在倒數的時候也預留給咱們一些對插件和一些必須或者耗時的初始化做一些準備,
Ps:這里會讓面試官感覺你是一個注重用戶體驗的
2、通過翻閱 Application 啟動的原始碼,當我們點擊桌面圖示進入我們軟體應用的時候,會由 AMS 通過 Socket 給 Zygote 發送一個 fork 子行程的訊息,
當 Zygote fork 子行程完成之后會通過反射啟動 ActivityThread##main 函式,最后又由 AMS 通過 aidl 告訴 ActivityThread##H 來反射啟動創建Application 實體,并且依次執行 attachBaseContext 、onCreate 生命周期,由此可見我們不能在這 2 個生命周期里做主執行緒耗時操作,
Ps: 這里會讓面試官感覺你對 App 應用的啟動流程研究的比較深,有過真實的翻閱底層原始碼,而并不是背誦答案,
3、知道了 attachBaseContext 、onCreate 在應用中最先啟動,那么我們就可以通過 TreceView 等性能檢測工具,來檢測具體函式耗時時間,然后來對其做具體的優化,
Ps:1. 面試官這里會覺得你對啟動優化確實了解的不錯,有一定的啟動優化經驗,
- 在第五點面試官會覺得你比較關注該圈子的動態,發現好的解決方案,并能用在自己專案上,這一點是加分項!
- 專案不及時需要的代碼通過異步加載,
- 將對一些使用率不高的初始化,做懶加載,
- 將對一些耗時任務通過開啟一個 IntentService來處理,
- 還通過 redex 重排列 class 檔案,將啟動階段需要用到的檔案在 APK 檔案中排布在一起,盡可能的利用 Linux 檔案系統的 pagecache 機制,用最少的磁盤 IO 次數,讀取盡可能多的啟動階段需要的檔案,減少 IO 開銷,從而達到提升啟動性能的目的,
- 通過抖音發布的文章知曉在 5.0 低版本可以做 MultiDex 優化,在第一次啟動的時候,直接加載沒有經過 OPT 優化的原始 DEX,先使得 APP 能夠正常啟動,然后在后臺啟動一個單獨行程,慢慢地做完 DEX 的 OPT 作業,盡可能避免影響到前臺 APP 的正常使用,
4、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% 啟動時間,
面試官:
嗯,研究的挺深的,原始碼平時不少看吧,
程式員:
到這里,我知道這一關算是過了!
二、有做過相關的記憶體優化嗎?
程式員:
有做過,目前的專案記憶體優化還是挺多的,要不我先說一下優化記憶體有什么好處吧?咱們不能盲目的去優化!
有的時候對于自己熟悉的領域,一定要主動出擊,自己主導這場面試,
面試官:
可以,
Ps:這里大多數面試官會同意你的請求,除非遇見裝B的,
程式員:
好處:
- 減少 OOM ,可以提高程式的穩定性,
- 減少卡頓,提高應用流暢性,
- 減少記憶體占用,提高應用后臺存活性,
- 減少程式例外,降低應用 Crash 率, 提高穩定性,
那么我基于這四點,我的程式做了如下優化:
- 1.減少 OOM
在應用開發階段我比較喜歡用 LeakCanary 這款性能檢測工具,好處是它能實時的告訴我具體哪個類發現了記憶體泄漏(如果你對 LeakCanary 的原理了解的話,可以說一說它是怎么檢測的),
還有我們要明白為什么應用程式會發送 OOM ,又該怎么去避免它?發生 OOM 的場景是當申請 1M 的記憶體空間時,如果你要往該記憶體空間存入 2M 的資料,那么此時就會發生 OOM,
在應用程式中我們不僅要避免直接導致 OOM 的場景還要避免間接導致 OOM 的場景,
間接的話也就是要避免記憶體泄漏的場景,記憶體泄漏的場景是這個物件不再使用時,應用完整的執行最后的生命周期,但是由于某些原因,物件雖然已經不再使用,仍然會在記憶體中存在而導致 GC 不會去回收它,這就意味著發生了記憶體泄漏,
(這里可以介紹下 GC 回識訓制,回收演算法,知識點盡量往外擴展而不脫離本題)
最后再說一下在實際開發中避免記憶體泄漏的場景:
其實這些都是基礎,把它記下就行了,記得多了在實際開發中就有印象了,
- 資源型物件未關閉: Cursor,File
- 注冊物件未銷毀: 廣播,回呼監聽
- 類的靜態變數持有大資料物件
- 非靜態內部類的靜態實體
- Handler 臨時性記憶體泄漏: 使用靜態 + 弱參考,退出即銷毀
- 容器中的物件沒清理造成的記憶體泄漏
- WebView: 使用單獨行程
- 2.減少卡頓
怎么減少卡頓? 那么我們可以從 2 個原理方面來探討卡頓的根本原因:
第一個原理方面是繪制原理,另一個就是重繪原理,
卡頓的根本原因:
從重繪原理來看卡頓的根本原理是有兩個地方會造成掉幀:
一個是主執行緒有其它耗時操作,導致doFrame 沒有機會在 vsync 信號發出之后 16 毫秒內呼叫;
還有一個就是當前doFrame方法耗時,繪制太久,下一個 vsync 信號來的時候這一幀還沒畫完,造成掉幀,
既然我們知道了卡頓的根本原因,那么我們就可以監控卡頓,從而可以對卡頓優化做到極致,
我們可以從下面三個方面來監控應用程式卡頓:
**怎么避免卡頓:**一定要避免在主執行緒中做耗時任務,總結一下 Android 中主執行緒的場景,
還有一個最重要的就是避免記憶體抖動,不要在短時間內頻繁的記憶體分配和釋放,
基于這幾點去說卡頓肯定是沒有問題的,
- UI 生命周期的控制
- 系統事件的處理
- 訊息處理
- 界面布局
- 界面繪制
- 界面重繪
- …
1、基于 Looper 的 Printer 分發訊息的時間差值來判斷是否卡頓,
2、基于 Choreographer 回呼函式 postFrameCallback 來監控
3、基于開源框架 BlockCanary 來監控
4、基于開源框架 rabbit-client 來監控
- 繪制原理:
- 重繪原理:
View 的 requestLayout 和 ViewRootImpl##setView 最終都會呼叫 ViewRootImpl 的 requestLayout 方法,然后通過 scheduleTraversals 方法向 Choreographer 提交一個繪制任務,然后再通過 DisplayEventReceiver 向底層請求 vsync 垂直同步信號,當 vsync 信號來的時候,會通過 JNI 回呼回來,在通過 Handler 往訊息佇列 post 一個異步任務,最終是 ViewRootImpl 去執行繪制任務,最后呼叫 performTraversals 方法,完成繪制,
詳細流程可以參考下面流程圖:
- 3.減少記憶體占用
可以從如下幾個方面去展開說明:
- 選擇合適的位圖格式
- bitmap 記憶體復用,壓縮
- 圖片的多級快取
-
AutoBoxing(自動裝箱): 能用小的堅決不用大的,
-
記憶體復用
-
使用最優的資料型別
-
列舉型別: 使用注解列舉限制替換 Enum
-
圖片記憶體優化(這里可以從 Glide 等開源框架去說下它們是怎么設計的)
-
基本資料型別如果不用修改的建議全部寫成 static final,因為 它不需要進行初始化作業,直接打包到 dex 就可以直接使用,并不會在 類 中進行申請記憶體
-
字串拼接別用 +=,使用 StringBuffer 或 StringBuilder
-
不要在 onMeause, onLayout, onDraw 中去重繪 UI
-
g 圖片轉換為 webp 格式圖片
-
盡量使用 C++ 代碼轉換 YUV 格式,別用 Java 代碼轉換 RGB 等格式,真的很占用記憶體
-
4.減少程式例外
減少程式例外那么我們可以從穩定性和 Crash 來分別說明,
這個我們將在第四點會詳細的介紹程式的穩定性和 Crash ,
如果說出這些,再實際開發中舉例說明一下怎么解決的應該是沒有問題的,
三、你在專案中有沒有遇見卡頓問題?是怎么排查卡頓?又是怎么優化的?
程式員:
有遇見, 比如在主執行緒中做耗時操作、頻繁的創建物件和銷毀物件導致 GC 回收頻繁、布局的層級多等,
面試官:
嗯,那具體說說是怎么優化的,
程式員:
這里我們還是可以從顯示原理和優化建議來展開說明,參考如下:
1、顯示原理:
- 繪制原理:
- 重繪原理:
View 的 requestLayout 和 ViewRootImpl##setView 最終都會呼叫 ViewRootImpl 的 requestLayout 方法,然后通過 scheduleTraversals 方法向 Choreographer 提交一個繪制任務,然后再通過 DisplayEventReceiver 向底層請求 vsync 垂直同步信號,當 vsync 信號來的時候,會通過 JNI 回呼回來,再通過 Handler 往訊息佇列 post 一個異步任務,最終是 ViewRootImpl 去執行繪制任務,最后呼叫 performTraversals 方法,完成繪制,
詳細流程可以參考下面流程圖:
2、卡頓的根本原因:
從重繪原理來看卡頓的根本原理是有兩個地方會造成掉幀:
一個是主執行緒有其它耗時操作,導致doFrame 沒有機會在 vsync 信號發出之后 16 毫秒內呼叫;
還有一個就是當前 doFrame 方法耗時,繪制太久,下一個 vsync 信號來的時候這一幀還沒畫完,造成掉幀,
既然我們知道了卡頓的根本原因,那么我們就可以監控卡頓,從而可以對卡頓優化做到極致,我們可以從下面三個方面來監控應用程式卡頓:
- 基于 Looper 的 Printer 分發訊息的時間差值來判斷是否卡頓,
- 基于 Choreographer 回呼函式 postFrameCallback 來監控
-
基于開源框架 BlockCanary 來監控
-
基于開源框架 rabbit-client 來監控
3、怎么可以提高程式運行流暢
1.布局優化:
1.1 布局優化分析工具:
1.2 優化方案:
4、提升影片性能
- 盡量別用補間影片,改為屬性影片,因為通過性能監控發現補間影片重繪非常頻繁
- 使用硬體加速提高渲染速度,實作平滑的影片效果,
5、怎么避免卡頓:
一定要避免在主執行緒中做耗時任務,總結一下 Android 中主執行緒的場景:
- UI 生命周期的控制
- 系統事件的處理
- 訊息處理
- 界面布局
- 界面繪制
- 界面重繪
- …
基于這幾點去說卡頓肯定是沒有問題的,
四、怎么保證 APP 的穩定運行?
程式員:
保證程式的穩定我們可以從記憶體、代碼質量、Crash、ANR、后臺存活等知識點來展開優化,
面試官:
那你具體說說你是怎么做的?
程式員:
1.記憶體
可以從第二點記憶體優化來說明
2.代碼質量
- 團隊之前相互代碼審查,保證了代碼的質量,也可以學習到了其它同事碼代碼的思想,
- 使用 Link 掃描代碼,查看是否有缺陷性,
3. Crash
- 通過實作 Thread.UncaughtExceptionHandler 介面來全域監控例外狀態,發生 Crash 及時上傳日志給后臺,并且及時通過插件包修復,
- Native 線上通過 Bugly 框架實時監控程式例外狀況,線下局域網使用 Google 開源的 breakpad 框架,發生例外就搜集日志上傳服務器(這里要注意的是日志上傳的性能問題,后面省電模塊會說明)
4. ANR
5. 后臺存活
面試官:
嗯,你對知識點掌握的挺好,
說完這些,這一關也算是過了,
五、說說你在專案中網路優化?
程式員:
有,這一點其實可以通過 OKHTTP 連接池和 Http 快取來說一下(當然這里不會再展開分析 OKHTTP 原始碼了)
面試官:
那你具體說一下吧
程式員:
說了這些之后,再說一下你當前使用網路框架它們做了哪些優化比如 OKHTTP(Socket 連接池、Http快取、責任鏈)、Retrofit(動態代理),說了這些一般這關也算是過了,
六、你在專案中有用過哪些存盤方式? 對它們的性能有過優化嗎?
程式員:
主要用過 sp,File,SQLite 存盤方式,其中對 sp 和 sqlite 做了優化,
面試官:
那你說說都做了哪些優化?
程式員:
這一塊如果你使用過其它第三方的資料庫,可以說說它們的原理和它們存取的方式,
七、你在專案中有做過自定義 View 嗎?有對它做過什么優化?
程式員:
有做過,比如重復繪制,還有大圖長圖有過優化,
面試官:
那具體說一說
程式員:
最后也是結合真實場景具體說一個,
八、你們專案的耗電量怎么樣? 有做過優化嗎?
程式員:
在沒有優化之前持續作業 30 分鐘的耗電量是 8%, 優化后是 4%,
面試官:
那你說一說你是怎么優化的,
程式員:
因為我們產品是一款社交通信的軟體,有音視頻通話、GPS 定位上報、長連接的場景,所以優化起來確實有點困難,不過最后也還是優化了一半的電量下去,主要做了如下優化:
說出這些之后,再結合專案一個真實的優化點來說明一下,
九、有做過日志優化嗎?
程式員:
有優化,在之前沒有考慮任何性能的情況下,我是直接有 log 就寫入檔案,盡管我開了執行緒池去寫檔案,只要軟體在運行那么就會頻繁的使 CPU 進行作業,這也間接的導致了耗電,
面試官:
那你具體說一下,最后怎么解決這個問題的?
程式員:
展開上面這些點說明之后,面試官一般不會為難你,
十、你們 APK 有多大?有做過 APK 體積相關的優化嗎?
程式員:
有過優化,在沒有優化之前專案的包體積大小是 80M,優化之后是 50M.
面試官:
說一說是怎么優化的
程式員:
基于這幾點優化方案,一般都能解決 APK 體積問題,最后再把自己專案 APK 體積優化步驟結合上面點說一下就行,
總結
其實性能優化點都是息息相關的,比如卡頓會涉及記憶體、顯示,啟動也會涉及 APK dex 的影響,所以說性能優化不僅僅是單方面的優化,一定要掌握最基本的優化方案,才能更加深入探討性能原理問題,
在這里也建議大家多看流行開源框架原始碼,比如 Glide (記憶體方面), OKhttp (網路連接方面) 優化的真的很極致,到這里性能優化方面的知識也就說完了,下來一定好好去消化,
最后分享一份全面的性能優化資料(篇幅原因只能展示部分)
當然還有大佬收錄整理的Android學習PDF+架構視頻+原始碼筆記,高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料
這些都是我現在閑暇還會反復翻閱的精品資料,里面對近幾年的大廠面試高頻知識點都有詳細的講解,相信可以有效的幫助大家掌握知識、理解原理,
當然你也可以拿去查漏補缺,提升自身的競爭力,
相信它會給大家帶來很多識訓,如果你有需要的話,可以點擊獲取!
喜歡本文的話,不妨順手給我點個贊、評論區留言或者轉發支持一下唄~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/76447.html
標籤:其他
