面試官: 說一下你做過哪些效能優化?

DevYK發表於2020-03-28

前言

如果你已經有 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 (網路連線方面) 優化的真的很極致。到這裡效能優化方面的知識也就說完了,下來一定好好去消化。

所有 xmind 原圖點選獲得

關於我

掃碼關注我的公眾號,讓我們離得更進一些!

面試官: 說一下你做過哪些效能優化?

相關文章