來自Android面試官性能優化面試 10 連炮,你能頂住麼?

性能優化是很多 Android 程序員希望徹底掌握的一門技能。很多人都想學好性能優化,希望能夠在自己的工作中靈活運用提高性能,從而爲用戶提供良好的用戶體驗。

然而,很多人在設計技術方案或者編碼時缺乏系統的、方法論級別的指導,導致想做性能優化時缺乏思路。

面試時,性能優化相關的面試題經常會遇到,特別是一些常見的優化。

張工畢業3年了,一直在一家創業公司做Android開發,最近到某知名互聯網公司面試,做了筆試題後, 面試官看了覺得還不錯,於是想進一步考察張工的實際經驗有多少, 就問之前在項目中有做過性能優化嗎?

接下來看看面試官提出的性能優化面試題10連炮你能回答多少吧:

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 實例,並且依次執行 attachBaseContextonCreate 生命週期,由此可見我們不能在這 2 個生命週期裏做主線程耗時操作。

Ps: 這裏會讓面試官感覺你對 App 應用的啓動流程研究的比較深,有過真實的翻閱底層源碼,而並不是背誦答案。

知道了 attachBaseContextonCreate 在應用中最先啓動,那麼我們就可以通過 TreceView 等性能檢測工具,來檢測具體函數耗時時間,然後來對其做具體的優化。

Ps:
1、面試官這裏會覺得你對啓動優化確實瞭解的不錯,有一定的啓動優化經驗。

2、在第五點面試官會覺得你比較關注該圈子的動態,發現好的解決方案,並能用在自己項目上。這一點是加分項!

  1. 項目不及時需要的代碼通過異步加載。
  2. 將對一些使用率不高的初始化,做懶加載。
  3. 將對一些耗時任務通過開啓一個 IntentService來處理。
  4. 還通過 redex 重排列 class 文件,將啓動階段需要用到的文件在 APK 文件中排布在一起,儘可能的利用 Linux 文件系統的 pagecache 機制,用最少的磁盤 IO 次數,讀取儘可能多的啓動階段需要的文件,減少 IO 開銷,從而達到提升啓動性能的目的。
  5. 通過抖音發佈的文章知曉在 5.0 低版本可以做 MultiDex 優化,在第一次啓動的時候,直接加載沒有經過 OPT 優化的原始 DEX,先使得 APP 能夠正常啓動。然後在後臺啓動一個單獨進程,慢慢地做完 DEX 的 OPT 工作,儘可能避免影響到前臺 APP 的正常使用。
  • Application 啓動完之後,AMS 會找出前臺棧頂待啓動的 Activity , 最後也是通過 AIDL 通知 ActivityThread#H 來進行對 Activity 的實例化並依次執行生命週期 onCreateonStartonRemuse 函數,那麼這裏由於 onCreate 生命週期中如果調用了 setContentView 函數,底層就會通過將 XML2View 那麼這個過程肯定是耗時的。所以要精簡 XML 佈局代碼,儘可能的使用 ViewStubincludemerge 標籤來優化佈局。接着在 onResume 聲明週期中會請求 JNI 接收 Vsync (垂直同步刷新的信號) 請求,16ms 之後如果接收到了刷新的消息,那麼就會對 DecorView 進行 onMeasure->onLayout->onDraw 繪製。最後纔是將 Activity 的根佈局 DecorView 添加到 Window 並交於 SurfaceFlinger 顯示。所以這一步除了要精簡 XML 佈局,還有對自定義 View 的測量,佈局,繪製等函數不能有耗時和導致 GC 的操作。最後也可以通過 TreaceView 工具來檢測這三個聲明週期耗時時間,從而進一步優化,達到極限。

這一步給面試官的感覺你對整個 Activity 的啓動和 View 的繪製還有刷新機制都有深入的研究,那麼此刻你肯定給面試官留了一個好印象,說明你平時對這些源碼級別的研究比較廣泛,透徹。

總結:

最後我基於以上的優化減少了 50% 啓動時間。

面試官:

嗯,研究的挺深的,源碼平時不少看吧。

程序員:

到這裏,我知道這一關算是過了!

2、有做過相關的內存優化嗎?

程序員:

有做過,目前的項目內存優化還是挺多的,要不我先說一下優化內存有什麼好處吧?咱們不能盲目的去優化!

有的時候對於自己熟悉的領域,一定要主動出擊,自己主導這場面試。

面試官:

可以。

Ps:這裏大多數面試官會同意你的請求,除非遇見裝B的。

程序員:

好處:

  1. 減少 OOM ,可以提高程序的穩定性。
  2. 減少卡頓,提高應用流暢性。
  3. 減少內存佔用,提高應用後臺存活性。
  4. 減少程序異常,降低應用 Crash 率, 提高穩定性。

那麼我基於這四點,我的程序做了如下優化:

  • 1.減少 OOM

在應用開發階段我比較喜歡用 LeakCanary 這款性能檢測工具,好處是它能實時的告訴我具體哪個類發現了內存泄漏(如果你對 LeakCanary 的原理了解的話,可以說一說它是怎麼檢測的)。

還有我們要明白爲什麼應用程序會發送 OOM ,又該怎麼去避免它?

發生 OOM 的場景是當申請 1M 的內存空間時,如果你要往該內存空間存入 2M 的數據,那麼此時就會發生 OOM。

在應用程序中我們不僅要避免直接導致 OOM 的場景還要避免間接導致 OOM 的場景。間接的話也就是要避免內存泄漏的場景。

內存泄漏的場景是這個對象不再使用時,應用完整的執行最後的生命週期,但是由於某些原因,對象雖然已經不再使用,仍然會在內存中存在而導致 GC 不會去回收它,這就意味着發生了內存泄漏。(這裏可以介紹下 GC 回收機制,回收算法,知識點儘量往外擴展而不脫離本題)

最後在說一下在實際開發中避免內存泄漏的場景:

  1. 資源型對象未關閉: Cursor,File
  2. 註冊對象未銷燬: 廣播,回調監聽
  3. 類的靜態變量持有大數據對象
  4. 非靜態內部類的靜態實例
  5. Handler 臨時性內存泄漏: 使用靜態 + 弱引用,退出即銷燬
  6. 容器中的對象沒清理造成的內存泄漏
  7. WebView: 使用單獨進程

其實這些都是基礎,把它記下就行了。記得多了在實際開發中就有印象了。

  • 2.減少卡頓

怎麼減少卡頓? 那麼我們可以從 2 個原理方面來探討卡頓的根本原因,第一個原理方面是繪製原理,另一個就是刷新原理。

  • 繪製原理:
  • 刷新原理:

View 的 requestLayout 和 ViewRootImpl##setView 最終都會調用 ViewRootImpl 的 requestLayout 方法,然後通過 scheduleTraversals 方法向 Choreographer 提交一個繪製任務,然後再通過 DisplayEventReceiver 向底層請求 vsync 垂直同步信號,當 vsync 信號來的時候,會通過 JNI 回調回來,在通過 Handler 往消息隊列 post 一個異步任務,最終是 ViewRootImpl 去執行繪製任務,最後調用 performTraversals 方法,完成繪製。

詳細流程可以參考下面流程圖:

卡頓的根本原因:

從刷新原理來看卡頓的根本原理是有兩個地方會造成掉幀:

一個是主線程有其它耗時操作,導致doFrame 沒有機會在 vsync 信號發出之後 16 毫秒內調用;

還有一個就是當前doFrame方法耗時,繪製太久,下一個 vsync 信號來的時候這一幀還沒畫完,造成掉幀。

既然我們知道了卡頓的根本原因,那麼我們就可以監控卡頓,從而可以對卡頓優化做到極致。我們可以從下面四個方面來監控應用程序卡頓:

  1. 基於 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);            
             }
       }
 }
​
 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);
 }
 }
 }</pre>

    1. 基於 Choreographer 回調函數 postFrameCallback 來監控



3. 基於開源框架來監控

怎麼避免卡頓:

一定要避免在主線程中做耗時任務,總結一下 Android 中主線程的場景:
1\. UI 生命週期的控制 ​ 2\. 系統事件的處理 ​ 3\. 消息處理 ​ 4\. 界面佈局 ​ 5\. 界面繪製 ​ 6\. 界面刷新 ​ 7\. ... ​
還有一個最重要的就是避免內存抖動,不要在短時間內頻繁的內存分配和釋放。

基於這幾點去說卡頓肯定是沒有問題的。

  • 3.減少內存佔用

可以從如下幾個方面去展開說明:

  1. AutoBoxing(自動裝箱): 能用小的堅決不用大的。

  2. 內存複用

  3. 使用最優的數據類型

  4. 枚舉類型: 使用註解枚舉限制替換 Enum

  5. 圖片內存優化(這裏可以從 Glide 等開源框架去說下它們是怎麼設計的)

    1.  選擇合適的位圖格式
    
    2.  bitmap 內存複用,壓縮
    
    3.  圖片的多級緩存
    
  6. 基本數據類型如果不用修改的建議全部寫成 static final,因爲 它不需要進行初始化工作,直接打包到 dex 就可以直接使用,並不會在 類 中進行申請內存

  7. 字符串拼接別用 +=,使用 StringBuffer 或 StringBuilder

  8. 不要在 onMeause, onLayout, onDraw 中去刷新 UI

  9. 儘量使用 C++ 代碼轉換 YUV 格式,別用 Java 代碼轉換 RGB 等格式,真的很佔用內存

  • 4.減少程序異常

減少程序異常那麼我們可以從穩定性和 Crash 來分別說明。

這個我們將在第四點會詳細的介紹程序的穩定性和 Crash 。

如果說出這些,再實際開發中舉例說明一下怎麼解決的應該是沒有問題的。

3、你在項目中有沒有遇見卡頓問題?是怎麼排查卡頓?又是怎麼優化的?

程序員:

有遇見, 比如在主線程中做耗時操作、頻繁的創建對象和銷燬對象導致 GC 回收頻繁、佈局的層級多等。

面試官:

嗯,那具體說說是怎麼優化的。

程序員:

這裏我們還是可以從顯示原理和優化建議來展開說明,參考如下:

  1. 顯示原理:
  • 繪製原理:
  • 刷新原理:

View 的 requestLayout 和 ViewRootImpl##setView 最終都會調用 ViewRootImpl 的 requestLayout 方法,然後通過 scheduleTraversals 方法向 Choreographer 提交一個繪製任務,然後再通過 DisplayEventReceiver 向底層請求 vsync 垂直同步信號,當 vsync 信號來的時候,會通過 JNI 回調回來,在通過 Handler 往消息隊列 post 一個異步任務,最終是 ViewRootImpl 去執行繪製任務,最後調用 performTraversals 方法,完成繪製。

詳細流程可以參考下面流程圖:

  1. 卡頓的根本原因:

從刷新原理來看卡頓的根本原理是有兩個地方會造成掉幀:

一個是主線程有其它耗時操作,導致doFrame 沒有機會在 vsync 信號發出之後 16 毫秒內調用;

還有一個就是當前 doFrame 方法耗時,繪製太久,下一個 vsync 信號來的時候這一幀還沒畫完,造成掉幀。

既然我們知道了卡頓的根本原因,那麼我們就可以監控卡頓,從而可以對卡頓優化做到極致。我們可以從下面四個方面來監控應用程序卡頓:

1. 基於 Looper 的 Printer 分發消息的時間差值來判斷是否卡頓。

    1. 基於 Choreographer 回調函數 postFrameCallback 來監控
  1. 基於開源框架 BlockCanary 來監控

  2. 基於開源框架 rabbit-client 來監控

  3. 怎麼可以提高程序運行流暢

1.佈局優化:

1.1 佈局優化分析工具:

1.2 優化方案:

  1. 提升動畫性能

    1. 儘量別用補間動畫,改爲屬性動畫,因爲通過性能監控發現補間動畫重繪非常頻繁

    2. 使用硬件加速提高渲染速度,實現平滑的動畫效果。

  2. 怎麼避免卡頓:

一定要避免在主線程中做耗時任務,總結一下 Android 中主線程的場景:

  1. UI 生命週期的控制
  2. 系統事件的處理
  3. 消息處理
  4. 界面佈局
  5. 界面繪製
  6. 界面刷新
  7. ...

基於這幾點去說卡頓肯定是沒有問題的。

4、怎麼保證 APP 的穩定運行?

程序員:

保證程序的穩定我們可以從內存、代碼質量、Crash、ANR、後臺存活等知識點來展開優化。

面試官:

那你具體說說你是怎麼做的?

程序員:

1.內存

可以從第二點內存優化來說明

2.代碼質量

  1. 團隊之前相互代碼審查,保證了代碼的質量,也可以學習到了其它同事碼代碼的思想。

  2. 使用 Link 掃描代碼,查看是否有缺陷性。

3. Crash

  1. 通過實現 Thread.UncaughtExceptionHandler 接口來全局監控異常狀態,發生 Crash 及時上傳日誌給後臺,並且及時通過插件包修復。

  2. 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 (網絡連接方面) 優化的真的很極致。到這裏性能優化方面的知識也就說完了,下來一定好好去消化。

Android及性能優化相關學習資源

其實客戶端開發的知識點就那麼多,面試問來問去還是那麼點東西。所以面試沒有其他的訣竅,只看你對這些知識點準備的充分程度。so,出去面試時先看看自己複習到了哪個階段就好。

這裏再分享一下我面試期間的複習路線:(以下體系的複習資料是我從各路大佬收集整理好的)

《Android開發七大模塊核心知識筆記》

《379頁Android開發面試寶典》

歷時半年,我們整理了這份市面上最全面的安卓面試題解析大全
包含了騰訊、百度、小米、阿里、樂視、美團、58、獵豹、360、新浪、搜狐等一線互聯網公司面試被問到的題目。熟悉本文中列出的知識點會大大增加通過前兩輪技術面試的機率。

如何使用它?

1.可以通過目錄索引直接翻看需要的知識點,查漏補缺。
2.五角星數表示面試問到的頻率,代表重要推薦指數

《333頁Android 性能優化PDF寶典》

從原理到實戰,一應俱全!這份寶典主要涉及以下三個方面:

1、設計思想與代碼質量優化(六大原則、設計模式、數據結構、算法)
2、程序性能優化(啓動速度與執行效率優化、佈局檢測與優化、內存優化、耗電優化、網絡傳輸與數據存儲優化、APK 大小優化)
3、開發效率優化(分佈式版本控制系統 Git、自動化構建系統 Gradle)

資料太多,全部展示會影響篇幅,暫時就先列舉這些部分截圖,以上資源均免費分享,以上內容均放在了開源項目:github 中已收錄,大家可以自行獲取(或者關注主頁掃描加微信獲取)。

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