Android效能優化 - Bitmap優化
在日常開發的APP,大部分時候需要想使用者展示圖片資訊,圖片最終對應Android中的Bitmap物件。而對於APP端來說Bitmap又是一個比較麻煩的問題,主要表現在Bitmap是非常佔用記憶體的物件,處理不當將導致APP執行卡頓甚至出現OOM。 Google在其官方有針對Bitmap的使用專門寫了一個專題Displaying Bitmaps Efficiently
一、主動釋放Bitmap資源
當你確定這個Bitmap資源不會再被使用的時候(當然這個Bitmap不釋放可能會讓程式下一次啟動或者resume快一些,但是其佔用的記憶體資源太大,可能導致程式在後臺的時候被殺掉,反而得不償失),我們建議手動呼叫recycle()方法,釋放其Native記憶體:
if(bitmap != null && !bitmap.isRecycled()){ bitmap.recycle(); bitmap = null; }
呼叫bitmap.recycle之後,這個Bitmap如果沒有被引用到,那麼就會被垃圾回收器回收。如果不主動呼叫這個方法,垃圾回收器也會進行回收工作,只不過垃圾回收器的不確定性太大,依賴其自動回收不靠譜(比如垃圾回收器一次性要回收好多Bitmap,那麼需要的時間就會很多,導致回收的時候會卡頓)。所以我們需要主動呼叫recycle。
二、主動釋放ImageView的圖片資源
由於我們在實際開發中,很多情況是在xml佈局檔案中設定ImageView的src或者在程式碼中呼叫ImageView.setImageResource/setImageURI/setImageDrawable等方法設定影象,下面程式碼可以回收這個ImageView所對應的資源:
private static void recycleImageViewBitMap(ImageView imageView) { if (imageView != null) { BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable(); rceycleBitmapDrawable(bd); } } private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) { if (bitmapDrawable != null) { Bitmap bitmap = bitmapDrawable.getBitmap(); rceycleBitmap(bitmap); } bitmapDrawable = null; } private static void rceycleBitmap(Bitmap bitmap) { if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); bitmap = null; } }
三、主動釋放ImageView的背景資源
如果你的ImageView是有Background,那麼下面的程式碼可以釋放他:
public static void recycleBackgroundBitMap(ImageView view) { if (view != null) { BitmapDrawable bd = (BitmapDrawable) view.getBackground(); rceycleBitmapDrawable(bd); } } public static void recycleImageViewBitMap(ImageView imageView) { if (imageView != null) { BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable(); rceycleBitmapDrawable(bd); } } private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) { if (bitmapDrawable != null) { Bitmap bitmap = bitmapDrawable.getBitmap(); rceycleBitmap(bitmap); } bitmapDrawable = null; }
四、儘量少用Png圖,多用NinePatch的圖
現在手機的解析度越來越高,圖片資源在被載入後所佔用的記憶體也越來越大,所以要儘量避免使用大的PNG圖,在產品設計的時候就要儘量避免用一張大圖來進行展示,儘量多用NinePatch資源。
Android中的NinePatch指的是一種拉伸後不會變形的特殊png圖,NinePatch的拉伸區域可以自己定義。這種圖的優點是體積小,拉伸不變形,可以適配多機型。Android SDK中有自帶NinePatch資源製作工具,Android-Studio中在普通png圖片點選右鍵可以將其轉換為NinePatch資源,使用起來非常方便。
五、使用大圖之前,儘量先對其進行壓縮
圖片有不同的形狀與大小。在大多數情況下它們的實際大小都比需要呈現出來的要大很多。例如,系統的Gallery程式會顯示那些你使用裝置camera拍攝的圖片,但是那些圖片的解析度通常都比你的裝置螢幕解析度要高很多。
考慮到程式是在有限的記憶體下工作,理想情況是你只需要在記憶體中載入一個低解析度的版本即可。這個低解析度的版本應該是與你的UI大小所匹配的,這樣才便於顯示。一個高解析度的圖片不會提供任何可見的好處,卻會佔用寶貴的(precious)的記憶體資源,並且會在快速滑動圖片時導致(incurs)附加的效率問題。
5.1 圖片大小壓縮:
直接使用ImageView顯示bitmap會佔用較多資源,特別是圖片較大的時候,可能導致崩潰。
使用BitmapFactory.Options設定inSampleSize, 這樣做可以減少對系統資源的要求。
屬性值inSampleSize表示縮圖大小為原始圖片大小的幾分之一,即如果這個值為2,則取出的縮圖的寬和高都是原始圖片的1/2,圖片大小就為原始大小的1/4。
BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options(); bitmapFactoryOptions.inJustDecodeBounds = true; bitmapFactoryOptions.inSampleSize = 2; // 這裡一定要將其設定回false,因為之前我們將其設定成了true // 設定inJustDecodeBounds為true後,decodeFile並不分配空間,即,BitmapFactory解碼出來的Bitmap為Null,但可計算出原始圖片的長度和寬度 options.inJustDecodeBounds = false; Bitmap bmp = BitmapFactory.decodeFile(sourceBitmap, options);
5.2 圖片畫素壓縮:
Android中圖片有四種屬性,分別是:
ALPHA_8:每個畫素佔用1byte記憶體
ARGB_4444:每個畫素佔用2byte記憶體
ARGB_8888:每個畫素佔用4byte記憶體 (預設)
RGB_565:每個畫素佔用2byte記憶體
Android預設的顏色模式為ARGB_8888,這個顏色模式色彩最細膩,顯示質量最高。但同樣的,佔用的記憶體也最大。 所以在對圖片效果不是特別高的情況下使用RGB_565(565沒有透明度屬性),如下:
public static Bitmap readBitMap(Context context, intresId) { BitmapFactory.Options opt = newBitmapFactory.Options(); opt.inPreferredConfig = Bitmap.Config.RGB_565; opt.inPurgeable = true; opt.inInputShareable = true; //獲取資源圖片 InputStreamis = context.getResources().openRawResource(resId); returnBitmapFactory.decodeStream(is, null, opt); }
其他知識點:Android 關於dp dip sp px dpi density解析
-
1.px
px即畫素(Pixel),1px代表了手機螢幕上一個物理的畫素點。由於以px為單位的控制元件在不同手機上顯示大小不一定相同,故Android不推薦使用px來設定控制元件大小。
-
2.解析度
解析度通常表示為橫軸畫素長度和縱軸畫素長度的乘積,如320*480等。
-
3.dpi/densityDpi
dpi的全稱是Dots Per Inch,即點每英寸,一般被稱為畫素密度,它代表了一英寸裡面有多少個畫素點。計算方法為螢幕總畫素點(即解析度的乘積除以螢幕大小),常見的取值有120,160,240。
-
4.density
density直譯為密度,它的計算公式為螢幕dpi除以160點每英寸,由於單位除掉了,故density只是一個比值,常見取值為1.0,1.5等。在Android中我們可以通過下面程式碼獲取當前螢幕的density:
getResources().getDisplayMetrics().density;
簡單來說,可以理解為 density 的數值是 1dp=density px;densityDpi 是螢幕每英寸對應多少個點(不是畫素點),在 DisplayMetrics 當中,這兩個的關係是線性的:
density 1 1.5 2 3 3.5 4 densityDpi 160 240 320 480 560 640 -
5.dp(dip)
dp,也叫做dip,全稱為Density independent pixels,叫做裝置獨立畫素。他是Android為了解決眾多手機dpi不同所定義的單位,dp是一種虛擬抽象的畫素單位,他的計算公式為:px = dp * (dpi / 160) = dp * density。因此在dpi大小為160的手機上,1dp = 1px,而在dpi大小為320的手機上,1dp = 2px,即在螢幕越大的手機上,1dp代表的畫素也越大。因此我們定義控制元件大小的時候應該使用dp代替使用px。
-
6.sp
sp是Android中定義字型大小的一種單位,全稱為Scaled Pixels,叫做放大畫素。sp會根據使用者手機上設定的字型大小而改變,在使用者手機字型大小設定為正常的情況下,1sp = 1dp。sp與px之間的密度比例可以通過如下程式碼獲取:
getResources().getDisplayMetrics().scaledDensity;
-
7.資原始檔解析度
一般而言,我們存放資原始檔的目錄(res)會有多個子目錄,這些子目錄代表了不同系統螢幕解析度:
密度 | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi |
---|---|---|---|---|---|---|
中文 | 低解析度 | 中解析度 | 高解析度 | 超高解析度 | 超超高解析度 | 超超超高解析度 |
dpi | 120以下 | 120~160 | 160~240 | 240~320 | 320~480 | 480~640 |
解析度 | 240*320 | 320*480 | 480*800 | 720*1280 | 1080*1920 | 3840*2160 |
-
8.找不到對應解析度資原始檔情況
對於drawable資源,當應用在裝置對應dpi目錄下沒有找到某個資源時,遵循“先高再低”原則,會從附近的解析度獲取圖片,然後按比例進行縮放:
比如,當前為xhdpi裝置,並且只有以下幾個目錄,則drawable的尋找順序為:
xhdpi->xxhdpi->xxxhdpi(如果沒有更高的了)->nodpi(如果有的話)->hdpi->mdpi,如果在xxhdpi中找到目標圖片,則壓縮2/3來使用,如果在mdpi中找到圖片,則放大2倍來使用。
因此,以現在主流裝置來說一般可能在drawable-xxhdpi放置一份即可,這樣可以儘量避免Android為我們放大圖片所導致的OOM
對於values資源,當應用裝置在當前dpi對應目錄的demins.xml中沒有找到目標條目時,採用“就近匹配”原則:
比如,當前為hdpi裝置,並且只有以下幾個目錄,則values的尋找順序為:
hdpi->xhdpi->mdpi->values,即先向上級dpi目錄查詢,再向下級dpi目錄查詢,最後一路向下查詢到values目錄,如果values下都找不到,就只有找values-ldpi,當然,現在有這個目錄的應用不多了。
單位換算
-
計算dpi
例:一個手機螢幕4英寸(對角線長度),解析度480×800,dpi如何計算?
dpi(畫素密度) = 對角線畫素數量 ÷ 對角線長度
對角線畫素數量:利用勾股定理,通過螢幕解析度480×800計算
dpi = 233畫素/英寸
density = (233 px/inch)/(160 px/inch)=1.46
-
計算dp與px
dp = (dpi / (160畫素/英寸)) px = density px
如1中的dp = 1.46px,意為在dip為233的螢幕上,1dp = 1.46px(畫素)
此時螢幕的相對解析度為:
寬度 = (480 / 1.46)dp = 329dp
高度 = (800 / 1.46)dp = 548dp
【附錄】

資料圖
需要資料的朋友可以加入Android架構交流QQ群聊:513088520
點選連結加入群聊【Android移動架構總群】: 加入群聊
獲取免費學習視訊,學習大綱另外還有像高階UI、效能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等Android高階開發資料免費分享。