面試官: 說一下你做過哪些性能優化? 這篇帶你搞懂性能優化

前言

如果你已經有 2 - 3 年以上開發經驗還不懂的怎麼去優化自己的項目,那就有點說不過去了,下面是我自己總結的一套通用級別的 Android 性能優化。如果圖片不清晰文末可以下載原始 xmind 圖。

如果你正在找工作, 那麼你需要一份 Android 高級開發面試寶典

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

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

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

    1. 項目不及時需要的代碼通過異步加載。

    2. 將對一些使用率不高的初始化,做懶加載。

    3. 將對一些耗時任務通過開啓一個 IntentService來處理。

    4. 還通過 redex 重排列 class 文件,將啓動階段需要用到的文件在 APK 文件中排布在一起,儘可能的利用 Linux 文件系統的 pagecache 機制,用最少的磁盤 IO 次數,讀取儘可能多的啓動階段需要的文件,減少 IO 開銷,從而達到提升啓動性能的目的。

    5. 通過抖音發佈的文章知曉在 5.0 低版本可以做 MultiDex 優化,在第一次啓動的時候,直接加載沒有經過 OPT 優化的原始 DEX,先使得 APP 能夠正常啓動。然後在後臺啓動一個單獨進程,慢慢地做完 DEX 的 OPT 工作,儘可能避免影響到前臺 APP 的正常使用。

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

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

  4. 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 個原理方面來探討卡頓的根本原因,第一個原理方面是繪製原理,另一個就是刷新原理。

    1. 繪製原理:

    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);
                  }
              }
          }
      
    2. 基於 Choreographer 回調函數 postFrameCallback 來監控

    3. 基於開源框架 BlockCanary 來監控

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

    怎麼避免卡頓:

    一定要避免在主線程中做耗時任務,總結一下 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. 開啓監聽
        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);
                  }
              }
          }
      
    2. 基於 Choreographer 回調函數 postFrameCallback 來監控

    3. 基於開源框架 BlockCanary 來監控

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

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

    1.佈局優化:

    1.1 佈局優化分析工具:

    1.2 優化方案:

  3. 提升動畫性能

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

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

  4. 怎麼避免卡頓:

    一定要避免在主線程中做耗時任務,總結一下 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 (網絡連接方面) 優化的真的很極致。到這裏性能優化方面的知識也就說完了,下來一定好好去消化。

對本文感興趣的小夥伴,可以加入粉絲交流裙,扣扣掃碼可以直接加入

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