性能優化系列(三)內存性能優化

Android 爲每個應用分配內存時,採用彈性的分配方式,即剛開始並不會給應用分配很多的內存,而是給每一個進程分配一個「夠用」的內存大小,這個大小值是根據每一個設備的實際的物理內存大小來決定的。

隨着應用的運行和使用,Android 會爲應用分配一些額外的內存大小。但是分配的大小是有限度的,系統不可能爲每一個應用分配無限大小的內存。

總之,Android 系統需要最大限度的讓更多的進程存活在內存中,以保證用戶再次打開應用時減少應用的啓動時間,提高用戶體驗。

關於 Android 系統內存管理機制的相關細節,推薦大家閱讀下這兩篇文章:談談 Android 的內存管理機制Android 操作系統的內存回收機制

避免內存溢出

內存溢出(Out Of Memory 簡稱 OOM),簡單地說內存溢出就是指程序運行過程中,申請的內存大於系統能夠提供的內存,導致無法申請到足夠的內存,於是就發生了內存溢出。

減小對象的內存佔用

避免 OOM 的第一步就是要儘量減少新分配出來的對象佔用內存的大小,儘量使用更加輕量的對象。

使用更加輕量的數據結構

例如,我們可以考慮使用 ArrayMap/SparseArray 而不是 HashMap 等傳統數據結構。

通常的 HashMap 的實現方式更加消耗內存,因爲它需要一個額外的實例對象來記錄 Mapping 操作。另外,SparseArray 更加高效在於他們避免了對 key 與 value 的 autobox 自動裝箱,並且避免了裝箱後的解箱。

關於更多ArrayMap/SparseArray的討論,請參考上一篇 性能優化 - 計算性能優化 的內容。

正確的使用枚舉類型

枚舉類型(Enums),是 Java 語言中的一個特性。但 Android 官方強烈建議不要在 Android 應用裏面使用到 Enums,是因爲枚舉類型在編譯之後會生成很多內部類,在移動設備上內存比較珍貴顯然會很佔用內存。想了解枚舉的實現細節可以查看 Java 枚舉類型的實現原理 這篇文章。

所以瞭解了枚舉類型的實現原理可以發現,在 Android 程序裏不是不可以使用枚舉類型,而是推薦使用。合理的使用枚舉類型可以做到一些很優雅的操作,比如單例模式。

public enum  EnumSingleton {
    INSTANCE;
}

我們把字節碼反編譯後可以看到:

public final class EnumSingleton extends Enum<EnumSingleton> {
  public static final EnumSingleton INSTANCE;
  public static EnumSingleton[] values();
  public static EnumSingleton valueOf(String s);
  static {};
}

由反編譯後的代碼可知,INSTANCE 被聲明爲 static 的,在類加載過程中,虛擬機會保證一個類的*<clinit>()* 方法在多線程環境中被正確的加鎖、同步。所以,枚舉實現是在實例化時是線程安全。

Java 虛擬機規範中規定,每一個枚舉類型極其定義的枚舉變量在 JVM 中都是唯一的,因此在枚舉類型的序列化和反序列化上,Java 做了特殊的規定。

在序列化的時候 Java 僅僅是將枚舉對象的 name 屬性輸出到結果中,反序列化的時候則是通過 java.lang.Enum 的 valueOf 方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定製的,因此禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法。

普通的 Java 類的反序列化過程中,會通過反射調用類的默認構造函數來初始化對象。所以,即使單例中構造函數是私有的,也會被反射給破壞掉。由於反序列化後的對象是重新 new 出來的,所以這就破壞了單例。

但是,枚舉的反序列化並不是通過反射實現的。所以,也就不會發生由於反序列化導致的單例破壞問題。

感興趣的可以參看 stackoverflow 上的回答 What is an efficient way to implement a singleton pattern in Java?

減小 Bitmap 對象的內存佔用

Bitmap 是一個極容易消耗內存的大胖子,關於 Bitmap 內存佔用大小詳情請參閱 Android 坑檔案:你的 Bitmap 究竟佔多大內存?,所以減小創建出來的 Bitmap 的內存佔用是很重要的,通常來說有下面 2 個措施:

  • 縮放比例

在把圖片載入內存之前,我們需要先計算出一個合適的縮放比例,避免不必要的大圖載入。

  • 解碼格式

選擇 ALPHA_8/ARGB_4444/ARGB_8888/RBG_565,存在很大差異。

模式 描述 佔用字節
ALPHA_8 Alpha 由 8 位組成 1B
ARGB_4444 4 個 4 位組成 16 位,每個色彩元素站 4 位 2B
ARGB_8888 4 個 8 爲組成 32 位,每個色彩元素站 8 位(默認) 4B
RGB_565 R 爲 5 位,G 爲 6 位,B 爲 5 位共 16 位,沒有Alpha 2B

使用更小的圖片

在設計給到資源圖片的時候,我們需要特別留意這張圖片是否存在可以壓縮的空間,是否可以使用一張更小的圖片。

儘量使用更小的圖片不僅僅可以減少內存的使用,還可以避免出現大量的 InflationException。假設有一張很大的圖片被 XML 文件直接引用,很有可能在初始化視圖的時候就會因爲內存不足而發生 InflationException,這個問題的根本原因其實是發生了 OOM。

  • JPG、PNG、WebP

不瞭解這三種圖片格式的建議看下 JPG 和 PNG 有什麼區別?WebP 原理和 Android 支持現狀介紹 這兩篇文章。

  • 圖片壓縮

圖片壓縮相關知識推薦看下騰訊音樂技術團隊的 Android 中圖片壓縮分析(上)Android 中圖片壓縮分析(下) 兩篇文章。

瞭解了圖片壓縮的相關知識,我們可以自己寫算法來實現圖片壓縮,也可以使用優秀的開源庫,比如:魯班

內存對象的重複利用

除了減小對象對內存的佔用,合理的複用內存對象也是很好避免內存溢出的方式。

大多數對象的複用,最終實施的方案都是利用對象池技術,要麼是在編寫代碼的時候顯式的在程序裏面去創建對象池,然後處理好複用的實現邏輯,要麼就是利用系統框架既有的某些複用特性達到減少對象的重複創建,從而減少內存的分配與回收。

LruCache

在 Android 中最常用的一個緩存算法是 LRU(Least Recently Use),就是當超出緩存容量的時候,就優先淘汰鏈表中最近最少使用的那個數據。

LruCache bitmapCache = new LruCache<String, Bitmap>();
// 根據內存空間設置緩存大小
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int availMemInBytes = am.getMemoryClass() * 1024 *1024;
LruCache bitmapCache = new LruCache<String, Bitmap>(availMemInBytes/8);

使用 LruCache 可以緩存 Bitmap 對象,相同LruCache 只是對內存中對象有效,如果我們想把圖片、視頻等文件緩存在磁盤上可以使用 JakeWharton 大神開源的 DiskLruCache

使用 Glide

Glide 是一個快速高效的 Android 圖片加載庫,注重於平滑的滾動。Glide 提供了易用的 API,高性能、可擴展的圖片解碼管道(decode pipeline),以及自動的資源池技術。

Glide 也是 Google 推薦過的開源項目,詳見:Glide

複用系統自帶的資源

Android 系統本身內置了很多的資源(例如:字符串、顏色、圖片、動畫、樣式以、簡單佈局等等),這些資源都可以在應用程序中直接引用。

這樣做不僅僅可以減少應用程序的自身負重,減小 APK 的大小,另外還可以一定程度上減少內存的開銷,複用性更好。但是也有必要留意 Android 系統的版本差異性,對那些不同系統版本上表現存在很大差異,不符合需求的情況,還是需要應用程序自身內置進去。

複用 ConvertView

在 ListView、GridView 等出現大量重複子組件的視圖裏面對 ConvertView 的複用。

onLowMemory()

OnLowMemory 是 Android 提供的API,在系統內存不足,所有後臺程序(優先級爲 Background 的進程,不是指後臺運行的進程)都被殺死時,系統會調用 OnLowMemory。

系統提供的回調有:Application、Activity、Fragementice、Service、ContentProvider。

onTrimMemory()

OnTrimMemory 是 Android 4.0 之後提供的 API,系統會根據不同的內存狀態來回調。

系統提供的回調有:Application、Activity、Fragementice、Service、ContentProvider。

OnTrimMemory的參數是一個 int 數值,代表不同的內存狀態。

當 App 在前臺運行時,該函數的 level (從低到高)有:

  • TRIM_MEMORY_RUNNING_MODERATE

系統開始運行在低內存狀態下 App 正在運行,不會被殺掉。

  • TRIM_MEMORY_RUNNING_LOW

系統運行在更加低內存狀態下,App 在運行,不會被殺掉 App 可以清理一些資源來保證系統的流暢。

  • TRIM_MEMORY_RUNNING_CRITICAL

系統運行在相當低內存狀態下,App 在運行,且系統不認爲可以殺掉此 App,系統要開始殺掉後臺進程。此時,App 應該去釋放一些不重要的資源。

當 App 在後臺運行時,level 狀態有:

  • TRIM_MEMORY_UI_HIDDEN:

App 的 UI 不可見,App 可以清理 UI 使用的較大的資源。

當 App 進入後臺 LRU List 時:

  • TRIM_MEMORY_BACKGROUND

系統運行在低內存下,App 進程在 LRU List 開始處附近,儘管 App 沒有被殺掉的風險,但是系統也許已經正在殺後臺進程。App 應該清理一些容易恢復的資源。

  • TRIM_MEMORY_MODERATE

系統運行在低內存下,App 進程在 LRU List 中間處附件,App 此時有被殺的可能。

  • TRIM_MEMORY_COMPLETE

系統運行在低內存下,App 是首先被殺的選擇之一,App 應該及時清理掉恢復 App 到前臺狀態,不重要的所有資源。

另外,一個 App 佔用內存越多,則系統清理後臺 LRU List 時,越可能優先被清理。所以,內存使用我們要謹慎使用。

避免在 onDraw 方法裏面執行對象的創建

類似 onDraw 等頻繁調用的方法,一定需要注意避免在這裏做創建對象的操作,因爲他會迅速增加內存的使用,而且很容易引起頻繁的 GC,甚至是內存抖動。

序列化

在 Android 中實現序列化一般用 Serializable 和 Parcelable 兩種方式。

兩者最大的區別在於 存儲媒介的不同,Serializable 使用 I/O 讀寫存儲在硬盤上,而 Parcelable 是直接 在內存中讀寫。很明顯,內存的讀寫速度通常大於 IO 讀寫,所以在 Android 中傳遞數據優先選擇 Parcelable。
Serializable 會使用反射,序列化和反序列化過程需要大量 I/O 操作, Parcelable 自已實現封送和解封(marshalled &unmarshalled)操作不需要用反射,數據也存放在 Native 內存中,效率要快很多。

Parcelable 的性能比 Serializable 好,在內存開銷方面較小,所以在內存間數據傳輸時推薦使用 Parcelable(如 Activity 間傳輸數據)。

而 Serializable 可將數據持久化方便保存,所以在需要保存或網絡傳輸數據時選擇 Serializable,因爲 Android 不同版本 Parcelable 可能不同,所以不推薦使用 Parcelable進行數據持久化.

StringBuilder

在有些時候,代碼中會需要使用到大量的字符串拼接的操作,這種時候有必要考慮使用 StringBuilder 來替代頻繁的 +

避免內存泄漏

內存泄漏(Memory Leak)指程序運行過程中分配內存給臨時變量,用完之後卻沒有被 GC 回收,始終佔用着內存,既不能被使用也不能分配給其他程序,於是就發生了內存泄漏。

內存泄露有時不嚴重且不易察覺,這樣開發者就不知道存在內存泄露,但有時也會很嚴重,甚至會提示你 OOM。

Context 的泄露

在 Android 開發中,最容易引發的內存泄漏問題的是 Context。比如 Activity 的 Context,就包含大量的內存引用,一旦泄漏了 Context,也意味泄漏它指向的所有對象。

造成 Activity 泄漏的常見原因:

靜態引用 Activity

在類中定義了靜態 Activity 變量,把當前運行的 Activity 實例賦值於這個靜態變量。如果這個靜態變量在 Activity 生命週期結束後沒有清空,就導致內存泄漏。

因爲 static 變量是貫穿這個應用的生命週期的,所以被泄漏的 Activity 就會一直存在於應用的進程中,不會被垃圾回收器回收。

static Activity activity; // 這種代碼要避免

單例中保存 Activity

在單例模式中,如果 Activity 經常被用到,那麼在內存中保存一個 Activity 實例是很實用的。

但是由於單例的生命週期是應用程序的生命週期,這樣會強制延長 Activity 的生命週期,這是相當危險而且不必要的,無論如何都不能在單例子中保存類似 Activity 的對象。

public class Singleton {
  private static Singleton instance;
  private Context mContext;
  private Singleton(Context context){
    this.mContext = context;
  }

  public static Singleton getInstance(Context context){
    if (instance == null){
      synchronized (Singleton.class){
        if (instance == null){
          instance = new Singleton(context);
        }
      }
    }
    return instance;
  }
}

在調用 Singleton 的 getInstance() 方法時傳入了 Activity。那麼當 instance 沒有釋放時,這個 Activity 會一直存在,因此造成內存泄露。

考慮使用 Application Context 而不是 Activity Context

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

Inner Classes

內部類的優勢可以提高可讀性和封裝性,而且可以訪問外部類,不幸的是,導致內存泄漏的原因,就是內部類持有外部類實例的強引用(例如在內部類中持有 Activity 對象)。

解決方法:

  • 將內部類變成靜態內部類;
  • 如果有強引用 Activity 中的屬性,則將該屬性的引用方式改爲弱引用;
  • 在業務允許的情況下,當 Activity 執行 onDestory 時,結束這些耗時任務。

避免使用異步回調

異步回調被執行的時間不確定,很有可能發生在 Activity 已經被銷燬之後,這不僅僅很容易引起 crash,還很容易發生內存泄露。

注意臨時 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。

注意 Cursor 對象是否及時關閉

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

注意緩存容器中的對象泄漏

有時候,我們爲了提高對象的複用性把某些對象放到緩存容器中,可是如果這些對象沒有及時從容器中清除,也是有可能導致內存泄漏的。

例如:針對 2.3 的系統,如果把 drawable 添加到緩存容器,因爲 drawable 與 View 的強應用,很容易導致 activity 發生泄漏。而從 4.0 開始,就不存在這個問題。解決這個問題,需要對 2.3 系統上的緩存 drawable 做特殊封裝,處理引用解綁的問題,避免泄漏的情況。

注意 WebView 的泄漏

Android 中的 WebView 存在很大的兼容性問題,不僅僅是 Android 系統版本的不同對 WebView 產生很大的差異,另外不同的廠商出貨的 ROM 裏面 WebView 也存在着很大的差異。更嚴重的是標準的 WebView 存在內存泄露的問題,看這裏 WebView causes memory leak - leaks the parent Activity

所以通常根治這個問題的辦法是爲 WebView 開啓另外一個進程,通過 AIDL 與主進程進行通信, WebView 所在的進程可以根據業務的需要選擇合適的時機進行銷燬,從而達到內存的完整釋放。

Lint Tool

Lint 是Android Studio 提供的代碼掃描分析工具,它可以幫助我們發現代碼結構/質量問題,同時提供一些解決方案,而且這個過程不需要我們手寫測試用例。

Lint 發現的每個問題都有描述信息和等級(和測試發現 bug 很相似),我們可以很方便地定位問題,同時按照嚴重程度進行解決。

點擊 Analyze > Inspect Code 可打開 Lint 工具。

Lint

詳細使用介紹可查看 Android 開發文檔 - 使用 Lint 改進您的代碼Android 性能優化:使用 Lint 優化代碼、去除多餘資源 這兩篇文章,使用比較簡單,網上介紹資源很多,這裏不再詳細介紹。

adb dumpsys

dumpsys 是 Android 系統提供的一個工具,可以查看系統服務的相關信息,dumpsys 通過 adb 命令來調用。

查看指定進程包名的內存使用情況:

$ adb shell dumpsys meminfo <包名>

輸出內容如下:

adb dumpsys

當然 dumpsys 的功能還有很多,可以查看 Android 開發文檔 - dumpsysdumpsys 命令用法 這兩篇文章瞭解更多用法。

Heap Viewer

Heap Viewer 是 DDMS 中的一個工具,實時查看 App 分配的內存大小和空閒內存大小,可幫助查找內存泄露。Heap Viewer 支持 5.0 及以上的系統,現在已經棄用,官方推薦使用 Memory Profiler 來查看 App 內存分配情況。

Memory Profiler

Memory Profiler 是 Android Profiler 中的一個組件,可幫助開發者識別導致應用卡頓、OOM 和內存泄露。 它顯示一個應用內存使用量的實時圖表,可以捕獲堆轉儲、強制執行垃圾回收以及跟蹤內存分配。

可以在 View > Tool Windows > Android Profiler 中打開 Memory Profiler 界面。

Memory Profiler

Memory Profile 的具體使用可查看 Android 開發文檔 - 使用 Memory Profiler 查看 Java 堆和內存分配Android Studio 3.0 利用 Android Profiler 測量應用性能 這兩篇文章。

MAT

MAT(Memory Analyzer Tool),一個基於 Eclipse 的內存分析工具,是一個快速、功能豐富的 Java Heap 分析工具,它可以幫助我們查找內存泄漏和減少內存消耗。

使用內存分析工具從衆多的對象中進行分析,快速的計算出在內存中對象的佔用大小,看看是誰阻止了垃圾收集器的回收工作,並可以通過報表直觀的查看到可能造成這種結果的對象。

Memory Analyzer

當然 MAT 也有獨立的不依賴 Eclipse 的版本,只不過這個版本在調試 Android 內存的時候,需要將 Android Studio 生成的文件進行轉換,纔可以在獨立版本的 MAT 上打開。.

一般情況下使用 Memory Profiler 就可以檢測出內存泄露的大致位置,使用 MAT 可以更詳細的分析內存的具體情況。

MAT 下載 點擊這裏,使用教程可查看 Eclipse Wiki - MemoryAnalyzer,也可以參考 Android 內存優化之一:MAT 使用入門Android內存優化之二:MAT使用進階 這兩篇文章。

LeakCanary

LeakCanary 是一個用於檢測 Android 內存泄漏的開源庫,上面介紹的 Memory Profiler、MAT 使用起來比較複雜,LeakCanary 堪稱傻瓜式的內存泄露檢測工具。

使用方式詳見 https://square.github.io/leakcanary,也可參考 LeakCanary 中文使用說明 這篇文章。

我的 GitHub

github.com/jeanboydev

技術交流羣

歡迎加入技術交流羣,來一起交流學習。

QQ 技術交流羣
微信技術交流羣

我的公衆號

歡迎關注我的公衆號,分享各種技術乾貨,各種學習資料,職業發展和行業動態。

Android 波斯灣

參考資料

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