1. 程式人生 > >Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)

Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)

Android 記憶體優化

  上篇部落格描述瞭如何檢測和處理記憶體洩漏,這種問題從某種意義上講是由於程式碼的錯誤導致的,但是也有一些是程式碼沒有錯誤,但是我們可以通過很多方式去降低記憶體的佔用,使得應用的整體記憶體處於一個健康的水平,下面總結一下記憶體優化的幾個點:

圖片處理優化

  由於圖片在應用中使用的較為頻繁,而且圖片佔用的記憶體通常來說也比較大,舉個例子來說,現在正常的手機基本都在 1000W 畫素左右的水平,較好的基本都在 1600W 畫素,這時候拍出來的照片基本都在 3400*4600 這個水平,按照 ARGB_8888 的標準,一個畫素 4 個位元組,所以總共有 1600W*4=6400W 位元組,總共 64M,也就是說會佔用 64M 的記憶體,而實際出來的 .png 圖片大小也就才 3M 左右,這是一個非常恐怖的數量,因為對於一個 2G 左右記憶體的手機來說,一個程序最大可用的記憶體可能也就在 100M+,一張圖片就能夠佔用一半記憶體,這也就是為什麼 decode 一個 bitmap 是發生 OOM 高頻的地方,所以在實際開發過程中圖片的處理和記憶體佔用優化也是一個比較重要的地方。
  Android中圖片有四種屬性,分別是:

  • ALPHA_8:每個畫素佔用1byte記憶體
  • ARGB_4444:每個畫素佔用2byte記憶體
  • ARGB_8888:每個畫素佔用4byte記憶體 (預設)
  • RGB_565:每個畫素佔用2byte記憶體

大圖片優化

  為了找出在執行過程中佔用記憶體很大的圖片,這個時候就可以藉助上篇部落格介紹到的 MAT 了,按照 Retained Heap 大小進行排序,找出佔用記憶體比較大的幾個物件,然後通過引用鏈找到持有它的地方,最後看能否有優化的地方。

圖片解析度相關

  我們一般將不同解析度的圖片放置在不同的資料夾 hdpi/xhdpi/xxhdpi 下面進行適配,通過 android:background 來設定背景圖片或者使用 BitmapFactory.decodeResource() 方法的時候,圖片預設情況下會進行縮放,在 Java 層實際呼叫的是 BitmapFactory 裡的 decodeResourceStream 方法:

/**
 * Decode a new Bitmap from an InputStream. This InputStream was obtained from
 * resources, which we pass to be able to scale the bitmap accordingly.
 */
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {

    if (opts == null
) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }

decodeResourceStream 在解析時會將 Bitmap 根據當前裝置螢幕畫素密度 densityDpi 的值進行縮放適配操作,使得解析出來的 Bitmap 與當前裝置的解析度匹配,達到一個最佳的顯示效果,上面也提到過,解析過後 Bitmap 的大小將比原始的大不少,關於 Bitmap 的詳細分析可以看一下這篇部落格:Android 開發繞不過的坑:你的 Bitmap 究竟佔多大記憶體?
  關於 Density、解析度和相關 res 目錄的關係如下:

DensityDpi 解析度 res Density
160dpi 320 x 533 mdpi 1
240dpi 460 x 800 hdpi 1.5
320dpi 720 x 1280 xhdpi 2
480dpi 1080 x 1920 xxhdpi 3
560dpi 1440 x 2560 xxxhdpi 3.5

  舉個例子來說一張 1920x1080 的圖片來說,如果放在 xhdpi 下面,那麼 xhdpi 裝置將其轉成 bitmap 之後的大小是 1920x1080,而 xxhdpi 裝置獲取的大小則是 2520x1418,大小約為前者的 1.7 倍,這些記憶體對於移動裝置來說已經算是比較大的差距。有一點需要提到的是新版本 Android Studio 已經使用 mipmap 來代替了,比起 drawable 官方的解釋是系統會在縮放上提供一定的效能優化:

Mipmapping for drawables

Using a mipmap as the source for your bitmap or drawable is a simple way to provide a quality image and various image scales, which can be particularly useful if you expect your image to be scaled during an animation.

Android 4.2 (API level 17) added support for mipmaps in the Bitmap class—Android swaps the mip images in your Bitmap when you've supplied a mipmap source and have enabled setHasMipMap(). Now in Android 4.3, you can enable mipmaps for a BitmapDrawable object as well, by providing a mipmap asset and setting the android:mipMap attribute in a bitmap resource file or by calling hasMipMap().

但是從用法來說和正常的 drawable 一樣。
  系統也對圖片展示進行了相應的優化,對於類似在 xml 裡面直接通過 android:background 或者 android:src 設定的背景圖片,以 ImageView 為例,最終會呼叫 ResourceImpl(低版本是 Resource) 類中的裡的 loadDrawable 方法,在這個方法中我們可以很清楚的看到系統針對相同的圖片使用享元模式構造了一個全域性的快取 DrawableCache 類的物件:

Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
        boolean useCache) throws NotFoundException {
    try {
        if (TRACE_FOR_PRELOAD) {
            // Log only framework resources
            if ((id >>> 24) == 0x1) {
                final String name = getResourceName(id);
                if (name != null) {
                    Log.d("PreloadDrawable", name);
                }
            }
        }

        final boolean isColorDrawable;
        final DrawableCache caches;
        final long key;
        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
            isColorDrawable = true;
            caches = mColorDrawableCache;
            key = value.data;
        } else {
            isColorDrawable = false;
            caches = mDrawableCache;
            key = (((long) value.assetCookie) << 32) | value.data;
        }

        // First, check whether we have a cached version of this drawable
        // that was inflated against the specified theme. Skip the cache if
        // we're currently preloading or we're not using the cache.
        if (!mPreloading && useCache) {
            final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
            if (cachedDrawable != null) {
                return cachedDrawable;
            }
        }
        .....
}

DrawableCache 類繼承自 ThemedResourceCache 類,來看看這兩個相關類:

/**
 * Class which can be used to cache Drawable resources against a theme.
 */
class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {
    ......
}
/**
 * Data structure used for caching data against themes.
 *
 * @param <T> type of data to cache
 */
abstract class ThemedResourceCache<T> {
    private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
    private LongSparseArray<WeakReference<T>> mUnthemedEntries;
    private LongSparseArray<WeakReference<T>> mNullThemedEntries;
    .....
}

可以看到這個類使用一個 ArrayMap 來儲存一個 Drawable 和這個 Drawable 對應的 Drawable.ConstantState 資訊,相同的圖片對應相同的 Drawable.ConstantState,所以這就可以保證在一些情況下相同的圖片系統只需要儲存一份,從而減少記憶體佔用。我們從這裡可以得到一些啟示,如果我們在某些會重複使用圖片的場景下,自己構造一個 Bitmap 快取器,然後裡面儲存 Bitmap 的 WeakReference,當使用的時候先去快取裡面獲取,獲取不到再做解析的操作。

圖片壓縮

  BitmapFactory 在 decode 圖片的時候,可以帶上一個 Options,這個很多人應該很熟悉,在 Options 中我們可以指定使用一些壓縮的功能:

  • inTargetDensity
  • 表示要被畫出來時的目標畫素密度;
  • inSampleSize
  • 這個值是一個 int,當它小於 1 的時候,將會被當做 1 處理,如果大於 1,那麼就會按照比例(1 / inSampleSize)縮小 bitmap 的寬和高、降低解析度,大於 1 時這個值將會被處置為 2 的指數(3 會被處理為 4,5被處理為8)。例如 width=100,height=100,inSampleSize=2,那麼就會將 bitmap 處理為,width=50,height=50,寬高降為 1/2,畫素數降為 1/4;
  • inJustDecodeBounds
  • 字面意思就可以理解就是隻解析圖片的邊界,有時如果只是為了獲取圖片的大小就可以用這個,而不必直接載入整張圖片;
  • inPreferredConfig
  • 預設會使用 ARGB_8888,在這個模式下一個畫素點將會佔用 4 個位元組,而對一些沒有透明度要求或者圖片質量要求不高的圖片,可以使用 RGB_565,這樣一個畫素只會佔用 2 個位元組,一下就可以省下 50% 記憶體了;
  • inPurgeable 和 inInputShareable
  • 這兩個需要一起使用,BitmapFactory 類的原始碼裡面有註釋,大致意思是表示在系統記憶體不足時是否可以回收這個 Bitmap,有點類似軟引用,但是實際在 5.0 以後這兩個屬性已經被忽略,因為系統認為回收後再解碼實際反而可能會導致效能問題;
  • inBitmap
  • 官方推薦使用的引數,表示重複利用圖片記憶體,減少記憶體分配,在 4.4 以前只有相同大小的圖片記憶體區域可以複用,4.4 以後只要原有的圖片比將要解碼的圖片大就可以實現複用了。
  關於圖片壓縮和圖片記憶體優化的例子可以參考我以前寫的一個部落格:android仿最新版本微信相簿–附原始碼

巨型圖片的處理

  要載入一張巨型的圖片,比如 20000*10000 解析度的,這個時候全放進記憶體是完全不可能的,直接會佔用 800M 記憶體,所以必須要用到上面說到的壓縮比,將其解析度降低到和螢幕匹配,匹配之後如果還要去支援使用者的放大、縮小、左右滑動等操作,這時候就可以使用 BitmapRegionDecoder 這個類去處理圖片了,具體的可以去看看這篇部落格:Android 高清載入巨圖方案 拒絕壓縮圖片,實現的原理就是分割槽域去載入,或者可以去參考這個開源庫:WorldMap

圖片緩衝池

  現在預設的圖片載入工具例如 Universal-ImageLoader 或者 Glide 都會使用一個 LruCache 來管理應用中的圖片快取,一般緩衝池的大小設定為應用可用記憶體的 1/8。

有效利用系統自帶資源

  Android 系統本身內建了大量的資源,比如一些通用的字串、顏色定義、常用 icon 圖片,還有些動畫和頁面樣式以及簡單佈局,如果沒有特別的要求,這些資源都可以在應用程式中直接引用。直接使用系統資源不僅可以在一定程度上減少記憶體的開銷,還可以減少應用程式 APK 的體積。

記憶體抖動造成記憶體碎片優化

  上篇部落格說到過頻繁的 GC 會造成記憶體的抖動,最終會導致記憶體當中存在很多記憶體碎片,雖然總體來說記憶體是可用的,但是當分配記憶體給一個大物件的時候,沒有一塊足夠大的連續區域可以分配給這個物件就會造成 OOM,所以這個時候為了減少記憶體抖動,需要去觀察 Memory Monitor,檢查應用的正常使用過程中有沒有因為頻繁的記憶體分配和釋放導致鋸齒形狀的記憶體圖,如果有的話去檢查相關程式碼,比較容易出現記憶體抖動的地方可能是 convertView 沒有複用、頻繁拼接小的 String 字串、在 for 迴圈中建立物件等等,找到問題所在,解決記憶體抖動。

常用資料結構優化

  ArrayMap 以及 SparseArray 是 Android 系統專門為移動裝置而定製的資料結構,用於在一定情況下取代 HashMap 而達到節省記憶體的目的,對於 key 為 int 的 HashMap 儘量使用 SparceArray 替代(一般 Lint 也會提示開發者將其換成 SparceArray),大概可以省30%的記憶體,而對於其他型別,ArrayMap 對記憶體的節省實際並不明顯,10% 左右,但是資料量在 1000 以上時,查詢速度可能會變慢,具體的可以看看這篇部落格:HashMap,ArrayMap,SparseArray原始碼分析及效能對比

避免建立不必要的物件

  最常見的例子就是當你要頻繁操作一個字串時,使用 StringBuilder 代替 String。對於所有基本型別的組合:int 陣列比 Integer 陣列好,這也概括了一個基本事實,兩個平行的 int 陣列比 (int,int) 物件陣列效能要好很多。總體來說,就是避免建立短命的臨時物件。減少物件的建立就能減少垃圾收集,進而減少對使用者體驗的影響。

儘量避免使用列舉

  Android 平臺上列舉是比較爭議的,在較早的 Android 版本,使用列舉會導致包過大,在某些情況下使用列舉甚至比直接使用 int 包的 size 大了 10 多倍。在 Stackoverflow 上也有很多的討論,大致意思是隨著虛擬機器的優化,目前列舉變數在 Android 平臺效能問題已經不大,而目前 Android 官方建議,使用列舉變數還是需要謹慎,因為列舉變數可能比直接用 int 多使用 2 倍的記憶體,具體的可以看看這個討論:Should I strictly avoid using enums on Android?

減少 View 的層級

  雖然這或多或少有點渲染優化的味道,但是由於 View 也是會佔用一定記憶體的,所以第一步是通過 Hierarchy Viewer 去去掉多餘的 View 層級,第二步是通過使用 ViewStub 去對一些可以延遲載入的 View 做到使用時載入,一定程度上也可以降低記憶體使用。

資料相關

  使用 Protocol Buffer 對資料進行壓縮(關於 Protocol Buffer 和其他工具的對比,可以看看這篇文章:thrift-protobuf-compare),Protocol Buffer 相比於 xml 可以減少 30% 的記憶體使用量;慎用 SharedPreference,因為對於同一個 SP 有時候為了讀取一個欄位可能會將整個 xml 檔案都加入記憶體,因此慎用 SP,或者可以將一個大的 SP 分散為幾個小的 SP;資料庫欄位儘量精簡,表設計合理,只讀取所需要的欄位而不是整個結構都載入到記憶體當中。

dex 優化,程式碼優化,謹慎使用外部庫

  有人覺得程式碼多少與記憶體沒有關係,實際上會有那麼點關係,現在稍微大一點的專案動輒就是百萬行程式碼以上,多 dex 也是常態,不僅佔用 Rom 空間,實際上執行時候需要載入的 dex 也是會佔用記憶體的(幾 M),有時候為了使用一些庫裡的某個功能函式就引入了整個龐大的庫是不太合適的,此時可以考慮抽取必要部分;另外開啟 proguard 優化程式碼,使用 Facebook redex 優化 dex(好像有不少坑)也是一種不錯的方式。

物件池模式享元模式

  對於物件的重複使用來說,物件池模式享元模式再合適不過了,具體的可以去看看我部落格裡面對於這兩個模式的介紹和使用。

  我們都知道 Android 使用者可以隨意在不同的應用之間進行快速切換,系統為了讓 Background 的應用能夠迅速的切換到 Forground,每一個 Background 的應用都會佔用一定的記憶體。Android 系統會根據當前的系統的記憶體使用情況,在一定情況下決定回收部分 Background 的應用記憶體,如果 Background 的應用從暫停狀態直接被恢復到 Forground,能夠獲得較快的恢復體驗,如果 Background 應用是從 Kill 狀態進行恢復,相比之下就顯得稍微有點慢:
這裡寫圖片描述


  • onLowMemory()
  • Android 系統提供了一些回撥來通知當前應用的記憶體使用情況,通常來說當所有的 Background 應用都被 kill 掉的時候,Forground 應用會收到 onLowMemory() 的回撥,在這種情況下需要儘快釋放當前應用的非必須的記憶體資源,從而確保系統能夠繼續穩定執行。
  • onTrimMemory(int)
  • Android 系統從 4.0 開始還提供了 onTrimMemory() 的回撥,當系統記憶體達到某些條件的時候,所有正在執行的應用都會收到這個回撥,同時在這個回撥裡面會傳遞指定的引數,代表不同的記憶體使用情況,收到 onTrimMemory() 回撥的時候,需要根據傳遞的引數型別進行判斷,合理的選擇釋放自身的一些記憶體佔用,一方面可以提高系統的整體執行流暢度,另外也可以避免自己被系統判斷為優先需要殺掉的應用,返回的引數:
因為 onTrimMemory() 的回撥是在 API 14 才被加進來的,對於老的版本,你可以使用 onLowMemory 回撥來進行相容,onLowMemory 相當與 TRIM_MEMORY_COMPLETE。

謹慎使用多程序

  使用多程序可以把應用中的部分元件執行在單獨的程序當中,這樣可以擴大應用的記憶體佔用範圍,但是這個技術必須謹慎使用,絕大多數應用都不應該貿然使用多程序,一方面是因為使用多程序會使得程式碼邏輯更加複雜,另外如果使用不當,它可能反而會導致顯著的記憶體增加。當你的應用需要執行一個常駐後臺的任務,而且這個任務並不輕量,可以考慮使用這個技術,一個典型的例子是建立一個可以長時間後臺播放的 Music Player。如果整個應用都執行在一個程序中,當後臺播放的時候,前臺的那些 UI 資源也沒有辦法得到釋放,類似這樣的應用可以切分成兩個程序:一個用來操作 UI,另外一個給後臺的 Service。

引用