在上一篇《Android性能優化(四)以內存優化實戰》中談到那個內存中的大胖子Bitmap,Bitmap對內存的影響極大。javascript
例如:使用Pixel手機拍攝4048x3036像素(1200W)的照片,若是按ARGB_8888來顯示的話,須要48MB的內存空間(4048*3036*4 bytes),這麼大的內存消耗極易引起OOM。本篇文章就來講一說這個大胖子。html
Android Bitmap內存的管理隨着系統的版本迭代也有演進:java
1.在Android 2.2(API8)以前,當GC工做時,應用的線程會暫停工做,同步的GC會影響性能。而Android2.3以後,GC變成了併發的,意味着Bitmap沒有引用的時候其佔有的內存會很快被回收。android
2.在Android 2.3.3(API10)以前,Bitmap的像素數據存放在Native內存,而Bitmap對象自己則存放在Dalvik Heap中。Native內存中的像素數據並不會以可預測的方式進行同步回收,有可能會致使內存升高甚至OOM。而在Android3.0以後,Bitmap的像素數據也被放在了Dalvik Heap中。git
在Android2.3.3以前推薦使用Bitmap.recycle()方法進行Bitmap的內存回收。github
備註:只有當肯定這個Bitmap不被引用的時候才能調用此方法,不然會有「Canvas: trying to use a recycled bitmap」這個錯誤。canvas
官方提供了一個使用Recycle的實例:使用引用計數來判斷Bitmap是否被展現或緩存,判斷可否被回收。數組
Android3.0以後,並無強調Bitmap.recycle();而是強調Bitmap的複用:緩存
使用LruCache對Bitmap進行緩存,當再次使用到這個Bitmap的時候直接獲取,而不用重走編碼流程。性能優化
Android3.0(API 11以後)引入了BitmapFactory.Options.inBitmap字段,設置此字段以後解碼方法會嘗試複用一張存在的Bitmap。這意味着Bitmap的內存被複用,避免了內存的回收及申請過程,顯然性能表現更佳。不過,使用這個字段有幾點限制:
- 聲明可被複用的Bitmap必須設置inMutable爲true;
- Android4.4(API 19)以前只有格式爲jpg、png,同等寬高(要求苛刻),inSampleSize爲1的Bitmap才能夠複用;
- Android4.4(API 19)以前被複用的Bitmap的inPreferredConfig會覆蓋待分配內存的Bitmap設置的inPreferredConfig;
- Android4.4(API 19)以後被複用的Bitmap的內存必須大於須要申請內存的Bitmap的內存;
- Android4.4(API 19)以前待加載Bitmap的Options.inSampleSize必須明確指定爲1。
getByteCount()方法是在API12加入的,表明存儲Bitmap的色素須要的最少內存。API19開始getAllocationByteCount()方法代替了getByteCount()。
API19以後,Bitmap加了一個Api:getAllocationByteCount();表明在內存中爲Bitmap分配的內存大小。
public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer表明存儲Bitmap像素數據的字節數組。
return getByteCount();
}
return mBuffer.length;
}複製代碼
還記得以前我曾言之鑿鑿的說:不考慮壓縮,只是加載一張Bitmap,那麼它佔用的內存 = width * height * 一個像素所佔的內存。
如今想來實在慚愧:說法也對,可是不全對,沒有說明場景,同時也忽略了一個影響項:Density。
BitmapFactory.java
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) {
//inDensity默認爲圖片所在文件夾對應的密度
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//inTargetDensity爲當前系統密度。
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
BitmapFactory.cpp 此處只列出主要代碼。
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
//初始縮放係數
float scale = 1.0f;
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
//縮放係數是當前係數密度/圖片所在文件夾對應的密度;
scale = (float) targetDensity / density;
}
}
//原始解碼出來的Bitmap;
SkBitmap decodingBitmap;
if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
!= SkImageDecoder::kSuccess) {
return nullObjectReturn("decoder->decode returned false");
}
//原始解碼出來的Bitmap的寬高;
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
//要使用縮放係數進行縮放,縮放後的寬高;
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
//源碼解釋爲由於歷史緣由;sx、sy基本等於scale。
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
canvas.scale(sx, sy);
canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
// now create the java bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}複製代碼
此處能夠看出:加載一張本地資源圖片,那麼它佔用的內存 = width * height * nTargetDensity/inDensity * nTargetDensity/inDensity * 一個像素所佔的內存。
實驗:將長爲102四、寬爲594的一張圖片放在xhdpi的文件夾下,使用魅族MX3手機加載。
// 不作處理,默認縮放。
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.resbitmap, options);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
Log.i(TAG, "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);
Log.i(TAG,"===========================================================================");
// 手動設置inDensity與inTargetDensity,影響縮放比例。
BitmapFactory.Options options_setParams = new BitmapFactory.Options();
options_setParams.inDensity = 320;
options_setParams.inTargetDensity = 320;
Bitmap bitmap_setParams = BitmapFactory.decodeResource(getResources(), R.mipmap.resbitmap, options_setParams);
Log.i(TAG, "bitmap_setParams:ByteCount = " + bitmap_setParams.getByteCount() + ":::bitmap_setParams:AllocationByteCount = " + bitmap_setParams.getAllocationByteCount());
Log.i(TAG, "width:" + bitmap_setParams.getWidth() + ":::height:" + bitmap_setParams.getHeight());
Log.i(TAG, "inDensity:" + options_setParams.inDensity + ":::inTargetDensity:" + options_setParams.inTargetDensity);
輸出:
I/lz: bitmap:ByteCount = 4601344:::bitmap:AllocationByteCount = 4601344
I/lz: width:1408:::height:817 // 能夠看到此處:Bitmap的寬高被縮放了440/320=1.375倍
I/lz: inDensity:320:::inTargetDensity:440 // 默認資源文件所處文件夾密度與手機系統密度
I/lz: ===========================================================================
I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
I/lz: width:1024:::height:594 // 手動設置了縮放係數爲1,Bitmap的寬高都不變
I/lz: inDensity:320:::inTargetDensity:320複製代碼
能夠看出:
與BitmapFactory.decodeResource()的調用鏈基本一致,可是少了默認設置density和inTargetDensity(與縮放比例相關)的步驟,也就沒有了縮放比例這一說。
除了加載本地資源文件的解碼方法會默認使用資源所處文件夾對應密度和手機系統密度進行縮放以外,別的解碼方法默認都不會。此時Bitmap默認佔用的內存 = width * height * 一個像素所佔的內存。這也就是上面4.1開頭講的須要注意場景。
Bitmap.Config用來描述圖片的像素是怎麼被存儲的?
ARGB_8888: 每一個像素4字節. 共32位,默認設置。
Alpha_8: 只保存透明度,共8位,1字節。
ARGB_4444: 共16位,2字節。
RGB_565:共16位,2字節,只存儲RGB值。
在上述2.2.2咱們談到了Bitmap的複用,以及複用的限制,Google在《Managing Bitmap Memory》中給出了詳細的複用Demo:
BitmapFactory.Options options = new BitmapFactory.Options();
// 圖片複用,這個屬性必須設置;
options.inMutable = true;
// 手動設置縮放比例,使其取整數,方便計算、觀察數據;
options.inDensity = 320;
options.inTargetDensity = 320;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap, options);
// 對象內存地址;
Log.i(TAG, "bitmap = " + bitmap);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
// 使用inBitmap屬性,這個屬性必須設置;
options.inBitmap = bitmap;
options.inDensity = 320;
// 設置縮放寬高爲原始寬高一半;
options.inTargetDensity = 160;
options.inMutable = true;
Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap_reuse, options);
// 複用對象的內存地址;
Log.i(TAG, "bitmapReuse = " + bitmapReuse);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount());
輸出:
I/lz: bitmap = android.graphics.Bitmap@35ac9dd4
I/lz: width:1024:::height:594
I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse = android.graphics.Bitmap@35ac9dd4 // 兩個對象的內存地址一致
I/lz: width:512:::height:297
I/lz: bitmap:ByteCount = 608256:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse:ByteCount = 608256:::bitmapReuse:AllocationByteCount = 2433024 // ByteCount比AllocationByteCount小複製代碼
能夠看出:
質量壓縮:
它是在保持像素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的,不會減小圖片的像素。進過它壓縮的圖片文件大小會變小,可是解碼成bitmap後佔得內存是不變的。
備註:inSampleSize值的大小不是隨便設、或者越大越好,須要根據實際狀況來設置。
如下是設置inSampleSize值的一個示例:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 設置inJustDecodeBounds屬性爲true,只獲取Bitmap原始寬高,不分配內存;
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 計算inSampleSize值;
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 真實加載Bitmap;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 寬和高比須要的寬高大的前提下最大的inSampleSize
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}複製代碼
這樣使用:mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
備註:
參考:
歡迎關注微信公衆號:按期分享Java、Android乾貨!