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

Shawn_Dut發表於2017-04-10

  上篇部落格我們寫到了 Android 中記憶體洩漏的檢測以及相關案例,這篇我們繼續來分析一下 Android 記憶體優化的相關內容。
  上篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
  中篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
  下篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)
  轉載請註明出處:blog.csdn.net/self_study/…
  對技術感興趣的同鞋加群544645972一起交流。

Android 記憶體優化

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

圖片處理優化

  由於圖片在應用中使用的較為頻繁,而且圖片佔用的記憶體通常來說也比較大,舉個例子來說,現在正常的手機基本都在 1000W 畫素左右的水平,較好的基本都在 1600W 畫素,這時候拍出來的照片基本都在 34004600 這個水平,按照 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 的體積:

  • 利用系統定義的 ID
  • 比如我們有一個定義 ListView 的 xml 檔案,一般的,我們會寫類似下面的程式碼片段:
<ListView  
    android:id="@+id/mylist"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"/>複製程式碼

這裡我們定義了一個 ListView,定義它的 id 是 "@+id/mylist",實際上,如果沒有特別的需求,就可以利用系統定義的 ID,類似下面的樣子:

<ListView  
    android:id="@android:id/list"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"/>複製程式碼

在 xml 檔案中引用系統的 ID,只需要加上 “@android:” 字首即可。如果是在Java程式碼中使用系統資源,和使用自己的資源基本上是一樣的。不同的是,需要使用 android.R 類來使用系統的資源,而不是使用應用程式指定的 R 類。這裡如果要獲取 ListView 可以使用 android.R.id.list 來獲取;

  • 利用系統的圖片資源
  • 這樣做的好處,一個是美工不需要重複的做一份已有的圖片了,可以節約不少工時,另一個是能保證我們的應用程式的風格與系統一致;
  • 利用系統的字串資源
  • 如果使用系統的字串,預設就已經支援多語言環境了,直接去使用 @android:string/yes 和 @android:string/no,在簡體中文環境下會顯示“確定”和“取消”,在英文環境下會顯示 “OK” 和 “Cancel”;
  • 利用系統的 Style
  • 假設佈局檔案中有一個 TextView,用來顯示視窗的標題,使用中等大小字型,可以使用下面的程式碼片段來定義 TextView 的 Style:
    <TextView  
        android:id="@+id/title"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:textAppearance="?android:attr/textAppearanceMedium" />複製程式碼

    其中 android:textAppearance="?android:attr/textAppearanceMedium" 就是使用系統的 style,需要注意的是使用系統的 style 必須在想要使用的資源前面加 “?android:” 作為字首,而不是 “@android:”;

  • 利用系統的顏色定義
  • 除了上述的各種系統資源以外,還可以使用系統定義好的顏色,在專案中最常用的,就是透明色的使用 android:background ="@android:color/transparent"

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

      上篇部落格說到過頻繁的 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?

    儘量使用系統類庫

      選擇系統類庫中的程式碼而非自己重寫,第一個可以節省少部分記憶體,第二個考慮到系統空閒時會用匯編程式碼呼叫來替代系統類庫中方法,這可能比 JIT 中生成的等價的最好的 Java 程式碼還要快:

    • 當你在處理字串的時候,不要吝惜使用 String.indexOf(),String.lastIndexOf() 等特殊實現的方法,這些方法都是使用 C/C++ 實現的,比起 Java 迴圈快 10 到 100 倍;
    • System.arraycopy 方法在有 JIT 的 Nexus One 上,自行編碼的迴圈快 9 倍;
    • android.text.format 包下的 Formatter 類,提供了 IP 地址轉換、檔案大小轉換等方法,DateFormat 類,提供了各種時間轉換,都是非常高效的方法;
    • TextUtils 類,對於字串處理 Android 為我們提供了一個簡單實用的 TextUtils 類,如果處理比較簡單的內容不用去思考正規表示式不妨試試這個 android.text.TextUtils 的類;
    • 高效能 MemoryFile 類,很多人抱怨 Android 處理底層 I/O 效能不是很理想,如果不想使用 NDK 則可以通過 MemoryFile 類實現高效能的檔案讀寫操作。MemoryFile 適用於哪些地方呢?對於 I/O 需要頻繁操作的,主要是和外部儲存相關的 I/O 操作,MemoryFile 通過將 NAND 或 SD 卡上的檔案,分段對映到記憶體中進行修改處理,這樣就用高速的 RAM 代替了 ROM 或 SD 卡,效能自然提高不少,對於 Android 手機而言同時還減少了電量消耗。該類實現的功能不是很多,直接從 Object 上繼承,通過 JNI 的方式直接在 C 底層執行。

    減少 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(好像有不少坑)也是一種不錯的方式。

    物件池模式享元模式

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

    onLowMemory() 與 onTrimMemory())

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

    Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)
    這裡寫圖片描述

    • 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。

      引用

      blog.csdn.net/luoshengyan…
      blog.csdn.net/luoshengyan…
      blog.csdn.net/luoshengyan…
      blog.csdn.net/luoshengyan…
      blog.csdn.net/luoshengyan…
      blog.csdn.net/a396901990/…
      mp.weixin.qq.com/s?__biz=MzA…
      geek.csdn.net/news/detail…
      www.jianshu.com/p/216b03c22…
      zhuanlan.zhihu.com/p/25213586
      joyrun.github.io/2016/08/08/…
      www.cnblogs.com/larack/p/60…
      source.android.com/devices/tec…
      blog.csdn.net/high2011/ar…
      gityuan.com/2015/10/03/…
      www.ayqy.net/blog/androi…
      developer.android.com/studio/prof…
      zhuanlan.zhihu.com/p/26043999
      www.csdn.net/article/201…

      相關文章