這可能是最好的效能優化教程(三)

nanchen2251發表於2019-02-28

這可能是最好的效能優化教程系列專欄
這可能是最好的效能優化教程(一)
這可能是最好的效能優化教程(二)
這可能是最好的效能優化教程(三)

前言

記憶體洩漏從來都是我們老生常談的話題,無論是 Android Studio 自帶的記憶體洩漏分析工具還是專業的 Eclipse MAT 抑或是備受青睞的第三方外掛 LeakCanary,都為我們的記憶體洩漏檢測提供了便利。如果從根源上解決記憶體洩漏,記憶體優化必不可少。所以本章節我們參考扔物線胡凱的記憶體優化策略,直接拿出一章節來談記憶體優化。

記憶體優化基本可以分為下面幾個方面

  • 減少物件的記憶體佔用
  • 對記憶體物件進行復用
  • 避免物件的記憶體洩漏
  • 記憶體使用策略優化

減少物件的記憶體佔用

避免在 Android 裡面使用 Enum

Enum 是 Java 中包含固定常量的資料型別,當需要知道預先定製的幾個值,這幾個值表示一些資料類,我們都可以使用 Enum。我們一般用 Enum 做一些編譯時檢查,以避免傳入不合法的引數。

但 Enum 的每個物件都是 Object,在 Android 官網上就早已明確指出應該在 Android 開發中避免使用 Enum,因為與靜態常量想必,它對記憶體的佔用是要大很多的。

因此在實際開發中,我更加傾向於介面變數,因為介面會自動把成員變數設定為 static 和 final 的,這一點可以防止某些情況下錯誤地新增新的常量,這也使得程式碼看起來更加簡單和清晰。

使用更加輕量的資料結構

前面第一節已經說過,我們應該更加傾向於考慮使用 ArrayMapSparseArray 而不是 HashMap 等傳統資料結果,前面已經用圖示演示了 HashMap 的簡要工作原理,相比起 Android 系統專門為移動作業系統編寫的 ArrayMap 容器,在大多數情況下,都顯示效率低下,更佔記憶體。通常的 HashMap 的實現方式更加消耗記憶體,因為它需要一個額外的例項物件來記錄 Mapping 操作。另外,SparseArray 更加高效在於他們避免了對 keyvalueautobox 自動裝箱,並且避免了裝箱後的解箱。

使用更小的圖片

在設計給到資源圖片的時候,我們需要特別留意這張圖片是否存在可以壓縮的空間,是否可以使用一張更小的圖片。儘量使用更小的圖片不僅僅可以減少記憶體的使用,還可以避免出現大量的 InflationException。假設有一張很大的圖片被 XML 檔案直接引用,很有可能在初始化檢視的時候就會因為記憶體不足而發生 InflationException,這個問題的根本原因其實是發生了 OOM。

減少 Bitmap 物件的記憶體佔用

Bitmap是一個極容易消耗記憶體的大胖子,減小建立出來的Bitmap的記憶體佔用是很重要的,通常來說有下面2個措施:

  • inSampleSize:縮放比例,在把圖片載入記憶體之前,我們需要先計算出一個合適的縮放比例,避免不必要的大圖載入。
  • decode format:解碼格式,選擇 ARGB_8888 / RBG_565 / ARGB_4444 / ALPHA_8,存在很大差異。

儘量地採用 int 型別

Android 系統中 float 型別的資料存取速度是 int 型別的一半,儘量優先採用 int 型別。而同樣能作為整數的代名詞,採用 int 替換 Integer 會讓你的記憶體開銷更小。

對記憶體物件進行復用

複用系統自帶的資源

Android 系統本身內建了很多的資源,例如字串 / 顏色 / 圖片 / 動畫 / 樣式以及簡單佈局等等,這些資源都可以在應用程式中直接引用。這樣做不僅僅可以減少應用程式的自身負重,減小 APK 的大小,另外還可以一定程度上減少記憶體的開銷,複用性更好。但是也有必要留意 Android 系統的版本差異性,對那些不同系統版本上表現存在很大差異,不符合需求的情況,還是需要應用程式自身內建進去。

注意 ListView / GridView 的 Adapter 對 ConvertView 進行復用

這個貌似沒啥好說的,太基礎了,而且我們可能現在更加青睞於 RecyclerView

儘量的採用 StringBuilder

這個也特別基礎,我們點到為止。大概就是儘量的採用 StringBuilder / StringBuffer 來替換我們頻繁的字串拼接。

儘量使用原字串的 subString

當從已經存在的資料集中抽取出 String 的時候,嘗試返回原資料的 subString 物件,而不要建立一個重複的物件。

避免在 onDraw() 裡面執行物件的建立

類似 onDraw() 等頻繁呼叫的方法,一定需要注意避免在這裡做建立物件的操作,因為他會迅速增加記憶體的使用,而且很容易引起頻繁的 gc,甚至是記憶體抖動。

避免物件的記憶體洩漏

記憶體物件的洩漏,會導致一些不再使用的物件無法及時釋放,這樣一方面佔用了寶貴的記憶體空間,很容易導致後續需要分配記憶體的時候,空閒空間不足而出現 OOM。顯然,這還使得每級 Generation 的記憶體區域可用空間變小,gc 就會更容易被觸發,容易出現記憶體抖動,從而引起效能問題。

注意 Activity 的洩漏

通常來說,Activity 的洩漏是記憶體洩漏裡面最嚴重的問題,它佔用的記憶體多,影響面廣,我們需要特別注意以下兩種情況導致的 Activity 洩漏:

  • 內部類引用導致 Activity 的洩漏
    最典型的場景是 Handler 導致的 Activity 洩漏,如果 Handler 中有延遲的任務或者是等待執行的任務佇列過長,都有可能因為 Handler 繼續執行而導致 Activity 發生洩漏。此時的引用關係鏈是 Looper -> MessageQueue -> Message -> Handler -> Activity。為了解決這個問題,可以在 UI 退出之前,執行 remove Handler 訊息佇列中的訊息與 runnable 物件。或者是使用 Static + WeakReference 的方式來達到斷開 Handler 與 Activity 之間存在引用關係的目的。

  • Activity Context 被傳遞到其他例項中,這可能導致自身被引用而發生洩漏。
    內部類引起的洩漏不僅僅會發生在 Activity 上,其他任何內部類出現的地方,都需要特別留意!我們可以考慮儘量使用 static 型別的內部類,同時使用 WeakReference 的機制來避免因為互相引用而出現的洩露。

儘量地採用 Application Context

對於大部分非必須使用 Activity Context 的情況(Dialog 的 Context 就必須是Activity Context),我們都可以考慮使用 Application Context 而不是 Activity 的 Context,這樣可以避免不經意的 Activity 洩露。

而且如果習慣 Glide 的童鞋可能會發現,Glide 需要傳遞的 Context 如果是 Activity 的 Context ,那麼在 Activity 被銷燬後還沒載入出來的話還會引發崩潰。所以,請在使用 Glide 或者 Toast 等的時候,直接傳遞 Application Context 吧。

注意 Cursor 物件是否及時關閉

在程式中我們經常會進行查詢資料庫的操作,但時常會存在不小心使用 Cursor 之後沒有及時關閉的情況。這些 Cursor 的洩露,反覆多次出現的話會對記憶體管理產生很大的負面影響,我們需要謹記對 Cursor 物件的及時關閉。

注意 WebView 的洩漏

Android中 的 WebView 存在很大的相容性問題,不僅僅是 Android 系統版本的不同對 WebView 產生很大的差異,另外不同的廠商出貨的 ROM 裡面 WebView 也存在著很大的差異。更嚴重的是標準的 WebView 存在記憶體洩露的問題,看這裡。所以通常根治這個問題的辦法是為 WebView 開啟另外一個程式,通過 AIDL 與主程式進行通訊,WebView 所在的程式可以根據業務的需要選擇合適的時機進行銷燬,從而達到記憶體的完整釋放。

注意臨時 Bitmap 物件的及時回收

雖然在大多數情況下,我們會對 Bitmap 增加快取機制,但是在某些時候,部分 Bitmap 是需要及時回收的。例如臨時建立的某個相對比較大的 Bitmap 物件,在經過變換得到新的 Bitmap 物件之後,應該儘快回收原始的 Bitmap,這樣能夠更快釋放原始 Bitmap 所佔用的空間。

需要特別留意的是 Bitmap 類裡面提供的 createBitmap() 方法:


這個函式返回的 Bitmap 有可能和 source bitmap 是同一個,在回收的時候,需要特別檢查 source bitmap 與 return bitmap 的引用是否相同,只有在不等的情況下,才能夠執行 source bitmap 的 recycle() 方法。

注意監聽器的登出

在 Android 程式裡面存在很多需要 register 與 unregister 的監聽器,我們需要確保在合適的時候及時 unregister 那些監聽器。自己手動 add 的 listener,需要記得及時 remove 這個 listener。

記憶體使用策略優化

謹慎使用 large heap

Android 裝置根據硬體與軟體的設定差異而存在不同大小的記憶體空間,他們為應用程式設定了不同大小的 Heap 限制閾值。你可以通過呼叫 getMemoryClass() 來獲取應用的可用 Heap 大小。在一些特殊的情景下,你可以通過在 manifest 的 application 標籤下新增 largeHeap = true 的屬性來為應用宣告一個更大的 heap 空間。然後,你可以通過 getLargeMemoryClass() 來獲取到這個更大的 heap size 閾值。然而,宣告得到更大 Heap 閾值的本意是為了一小部分會消耗大量 RAM 的應用 ( 例如一個大圖片的編輯應用 ) 。不要輕易的因為你需要使用更多的記憶體而去請求一個大的 Heap Size。只有當你清楚的知道哪裡會使用大量的記憶體並且知道為什麼這些記憶體必須被保留時才去使用 large heap。因此請謹慎使用 large heap 屬性。使用額外的記憶體空間會影響系統整體的使用者體驗,並且會使得每次 gc 的執行時間更長。在任務切換時,系統的效能會大打折扣。另外, large heap 並不一定能夠獲取到更大的 heap。在某些有嚴格限制的機器上,large heap 的大小和通常的 heap size 是一樣的。因此即使你申請了 large heap,你還是應該通過執行 getMemoryClass() 來檢查實際獲取到的 heap 大小。

資原始檔需要選擇合適的資料夾進行存放

我們知道 hdpi / xhdpi / xxhdpi 等等不同 dpi 的資料夾下的圖片在不同的裝置上會經過 scale 的處理。例如我們只在 hdpi 的目錄下放置了一張 100 x 100 的圖片,那麼根據換算關係,xxhdpi 的手機去引用那張圖片就會被拉伸到 200 x 200。需要注意到在這種情況下,記憶體佔用是會顯著提高的。對於不希望被拉伸的圖片,需要放到 assets 或者 nodpi 的目錄下。

Try catch某些大記憶體分配的操作

在某些情況下,我們需要事先評估那些可能發生 OOM 的程式碼,對於這些可能發生 OOM 的程式碼,加入 catch 機制,可以考慮在 catch 裡面嘗試一次降級的記憶體分配操作。例如 decode bitmap 的時候,catch 到 OOM,可以嘗試把取樣比例再增加一倍之後,再次嘗試 decode。

謹慎使用 static 物件

因為 static 的生命週期過長,和應用的程式保持一致,使用不當很可能導致物件洩漏,在 Android 中應該謹慎使用 static 物件。

特別留意單例物件中不合理的持有

雖然單例模式簡單實用,提供了很多便利性,但是因為單例的生命週期和應用保持一致,使用不合理很容易出現持有物件的洩漏。特別是持有 Context 的引用,需要謹慎對待

優化佈局層次,減少記憶體消耗

越扁平化的檢視佈局,佔用的記憶體就越少,效率越高。我們需要儘量保證佈局足夠扁平化,當使用系統提供的 View 無法實現足夠扁平的時候考慮使用自定義 View 來達到目的。

謹慎使用多程式

使用多程式可以把應用中的部分元件執行在單獨的程式當中,這樣可以擴大應用的記憶體佔用範圍,但是這個技術必須謹慎使用,絕大多數應用都不應該貿然使用多程式,一方面是因為使用多程式會使得程式碼邏輯更加複雜,另外如果使用不當,它可能反而會導致顯著增加記憶體。當你的應用需要執行一個常駐後臺的任務,而且這個任務並不輕量,可以考慮使用這個技術。

一個典型的例子是建立一個可以長時間後臺播放的 Music Player。如果整個應用都執行在一個程式中,當後臺播放的時候,前臺的那些 UI 資源也沒有辦法得到釋放。類似這樣的應用可以切分成 2 個程式:一個用來操作 UI,另外一個給後臺的 Service。

寫在最後

記憶體優化並不就是說程式佔用的記憶體越少就越好,如果因為想要保持更低的記憶體佔用,而頻繁觸發執行 gc 操作,在某種程度上反而會導致應用效能整體有所下降,這裡需要綜合考慮做一定的權衡。

如果想第一時間收到更新資訊的可以關注我的簡書:簡書地址
你也可以選擇關注我的公眾號:nanchen

相關文章