Android 性能優化以內存泄漏檢測以及內存優化(下)

  上篇博客咱們寫到了 Android 中內存泄漏的檢測以及相關案例,這篇咱們繼續來分析一下 Android 內存優化的相關內容。
  上篇:Android 性能優化以內存泄漏檢測以及內存優化(上)
  中篇:Android 性能優化以內存泄漏檢測以及內存優化(中)
  下篇:Android 性能優化以內存泄漏檢測以及內存優化(下)
  轉載請註明出處:blog.csdn.net/self_study/…
  對技術感興趣的同鞋加羣544645972一塊兒交流。javascript

Android 內存優化

  上篇博客描述瞭如何檢測和處理內存泄漏,這種問題從某種意義上講是因爲代碼的錯誤致使的,可是也有一些是代碼沒有錯誤,可是咱們能夠經過不少方式去下降內存的佔用,使得應用的總體內存處於一個健康的水平,下面總結一下內存優化的幾個點:html

圖片處理優化

  因爲圖片在應用中使用的較爲頻繁,並且圖片佔用的內存一般來講也比較大,舉個例子來講,如今正常的手機基本都在 1000W 像素左右的水平,較好的基本都在 1600W 像素,這時候拍出來的照片基本都在 34004600 這個水平,按照 ARGB_8888 的標準,一個像素 4 個字節,因此總共有 1600W\4=6400W 字節,總共 64M,也就是說會佔用 64M 的內存,而實際出來的 .png 圖片大小也就才 3M 左右,這是一個很是恐怖的數量,由於對於一個 2G 左右內存的手機來講,一個進程最大可用的內存可能也就在 100M+,一張圖片就可以佔用一半內存,這也就是爲何 decode 一個 bitmap 是發生 OOM 高頻的地方,因此在實際開發過程當中圖片的處理和內存佔用優化也是一個比較重要的地方。
  Android中圖片有四種屬性,分別是:java

  • ALPHA_8:每一個像素佔用1byte內存
  • ARGB_4444:每一個像素佔用2byte內存
  • ARGB_8888:每一個像素佔用4byte內存 (默認)
  • RGB_565:每一個像素佔用2byte內存

大圖片優化

  爲了找出在運行過程當中佔用內存很大的圖片,這個時候就能夠藉助上篇博客介紹到的 MAT 了,按照 Retained Heap 大小進行排序,找出佔用內存比較大的幾個對象,而後經過引用鏈找到持有它的地方,最後看可否有優化的地方。android

圖片分辨率相關

  咱們通常將不一樣分辨率的圖片放置在不一樣的文件夾 hdpi/xhdpi/xxhdpi 下面進行適配,經過 android:background 來設置背景圖片或者使用 BitmapFactory.decodeResource() 方法的時候,圖片默認狀況下會進行縮放,在 Java 層實際調用的是 BitmapFactory 裏的 decodeResourceStream 方法:git

/** * 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 目錄的關係以下:github

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 狀態進行恢復,相比之下就顯得稍微有點慢:

    這裏寫圖片描述

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

      相關文章
      相關標籤/搜索