前言
筆者在最近在做截屏分享的功能,采用getDrawingCache()發現了兩個問題,特此記錄一下。
View生成Bitmap的兩種方式。
- 利用Canvas繪制出bitmap (測量后)
public static Bitmap getBitmapFromView(View view) {
final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
return bitmap;
}
- 利用Canvas繪制出bitmap (測量前 不需要展示布局)
public static Bitmap getBitmap(View view) {
final int screenWidth = ScreenUtil.getScreenWidth(view.getContext());
final int screenHeight = ScreenUtil.getScreenHeight(view.getContext());
final int widthSpec = View.MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(screenHeight, MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
view.layout(0, 0, screenWidth, screenHeight);
Bitmap bitmap = Bitmap.createBitmap(screenWidth, screenHeight, Config.RGB_565);
final Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
return bitmap;
}
- View # getDrawingCache()
public static Bitmap getBitmapFromView(View view) {
view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
view.setDrawingCacheEnabled(true);
return view.getDrawingCache();
}
public static Bitmap getBitmapFromView(View view) {
view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
view.buildDrawingCache();
return view.getDrawingCache();
}
最終采用了第一種方案,沒什么可說的,不過第二種方案有一定學習價值。筆者當時在使用如下寫法時,發現最終返回的Bitmap結果為null。
public static Bitmap getBitmapFromView(View view) {
view.setDrawingCacheEnabled(true);
return view.getDrawingCache();
}
不知道為什么,所以就看下源碼。
public void setDrawingCacheEnabled(boolean enabled) {
mCachingFailed = false;
setFlags(enabled ? DRAWING_CACHE_ENABLED : 0, DRAWING_CACHE_ENABLED);
}
設置標記位,沒啥說的。getDrawingCache()最終會走到buildDrawingCacheImpl(boolean autoSize)方法中。
private void buildDrawingCacheImpl(boolean autoScale) {
// 省略...
final long projectedBitmapSize = width * height * (opaque && !use32BitCache ? 2 : 4);
final long drawingCacheSize = ViewConfiguration.get(mContext).getScaledMaximumDrawingCacheSize();
if (width <= 0 || height <= 0 || projectedBitmapSize > drawingCacheSize) {
if (width > 0 && height > 0) {
Log.w(VIEW_LOG_TAG, getClass().getSimpleName() + " not displayed because it is"
+ " too large to fit into a software layer (or drawing cache), needs "
+ projectedBitmapSize + " bytes, only "
+ drawingCacheSize + " available");
}
destroyDrawingCache();
mCachingFailed = true;
return;
}
}
這里if判斷中會判斷當前要生成Bitmap的目標View的位圖大小與系統默認支持的最大可生成的緩存大小做對比若目標Bitmap大小大于系統的最大緩存大小,則直接返回Null。我們看下系統最大的支持的緩存Bitmap大小是如何計算的。
final Display display = win.getDefaultDisplay();
final Point size = new Point();
display.getRealSize(size);
mMaximumDrawingCacheSize = 4 * size.x * size.y;
從上述代碼上講,最大緩存大小是 4 * 屏幕寬高 得出。
因此,我們第一個思考,如何去修改最終得目標Bitmap的最終大小呢?使得這個大小小于系統所支持的大小呢?
view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
結論就是提前將測量的尺寸設置為零,然后重新布局一下即可,就繞過去了判斷大小的限制。
那么引發的第二個思考就來了,既然有大小判斷,Google肯定考慮到OOM的問題。那么我們繼續往下讀源碼。
private void buildDrawingCacheImpl(boolean autoScale) {
boolean clear = true;
Bitmap bitmap = autoScale ? mDrawingCache : mUnscaledDrawingCac
if (bitmap == null || bitmap.getWidth() != width || bitmap.getH
Bitmap.Config quality;
if (!opaque) {
switch (mViewFlags & DRAWING_CACHE_QUALITY_MASK) {
case DRAWING_CACHE_QUALITY_AUTO:
case DRAWING_CACHE_QUALITY_LOW:
case DRAWING_CACHE_QUALITY_HIGH:
default:
quality = Bitmap.Config.ARGB_8888;
break;
}
} else {
quality = use32BitCache ? Bitmap.Config.ARGB_8888 : Bit
}
if (bitmap != null) {
bitmap.recycle();
}
try {
// 1
bitmap = Bitmap.createBitmap(mResources.getDisplayMetri
bitmap.setDensity(getResources().getDisplayMetrics().de
if (autoScale) {
mDrawingCache = bitmap;
} else {
mUnscaledDrawingCache = bitmap;
}
if (opaque && use32BitCache) {
bitmap.setHasAlpha(false);
}
} catch (OutOfMemoryError e) {
if (autoScale) {
mDrawingCache = null;
} else {
mUnscaledDrawingCache = null;
}
mCachingFailed = true;
return;
}
clear = drawingCacheBackgroundColor != 0;
}
}
代碼1 處發現在創建Bitmap的時候,try了創建Bitmap過程,然后catch了OutOfMemoryError這個錯誤,筆者在工作中,基本沒有對OOME進行catch過,認為OOM是無法被捕捉的,因為他所屬一種錯誤,繼承于VirtualMachineError這個父類。然后就寫了一個Demo,發現OOM是可以被捕捉的。
try{
varbyte = ByteArray( 10000000* 1024* 1024)
} catch(ignore: OutOfMemoryError) {
// 主動釋放一些內存資源
}
因此就引發了一個思考,什么時候需要將OOM捕捉一下呢?
理論上,Java若拋出了Error的異常,那就說明它的執行狀態已經無法恢復了,此時需要終止線程甚至是終止虛擬機。這是一種不應該被我們應用層去捕獲的異常。
那么產生OOM,原因之一結合我們本文來說,就是Bitmap過大導致,結合需求,若發現View生成的Bitmap過大,那么就不進行將View轉換成Bitmap的操作即可。所以是否需要捕捉OOM,有兩個先決條件:
- 觸發 OOM 的代碼是開發者可控的。
- 在Try包圍的代碼塊中需要進行大段的內存分配,并且有可能導致OOM。
有關于VitualMachineError可以參考這里。