一.Bitmap
內容如下:
1.Bitmap的生成
2.bitmap縮放、等圖像變換
3.bitmap模糊處理
4.bitmap保存圖像文件
5.Bitmap的防止內存泄露小方法
6.小知識點
1.Bitmap的生成
/** * 由本地文件路徑、網絡url或者項目的資源文件,生成Bitmap(舊,極端情況下可能造成OOM) * @param filePath */ private void productBitmap(String filePath){ Bitmap des_bitmap = null; BitmapFactory.Options options = new BitmapFactory.Options(); // options.inPreferredConfig //本地文件路徑或者網絡url Uri uri = Uri.parse(filePath); des_bitmap = BitmapFactory.decodeFile(uri.toString(),options); //項目資源文件 des_bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher); //流,例如文件流 FileInputStream fis = null; try { fis = new FileInputStream(filePath); des_bitmap = BitmapFactory.decodeStream(fis,null,options); fis.close(); fis = null; } catch (Exception e) { e.printStackTrace(); }finally { try { if (fis != null) fis.close(); } catch (Exception e) { } } if(iv_bitmap_test!=null) { iv_bitmap_test.setImageBitmap(des_bitmap); }else{ iv_bitmap_test = (ImageView) findViewById(R.id.iv_bitmap_test); iv_bitmap_test.setImageBitmap(des_bitmap); } }
/** * 獲取截屏 * * @param view 要截屏的view * @return 位圖 */ private Bitmap getScreenShot(View view) { View v = view; v.setDrawingCacheEnabled(true); Bitmap bitmap = Bitmap.createBitmap(v.getDrawingCache()); return bitmap; }
2.bitmap縮放、等圖像變換
(1)長寬相同比例縮放
用BitmapFactory的decodeFile方法,然后通過傳遞進去 BitmapFactory.Option類型的參數進行取縮略圖,在Option中,屬性值inSampleSize表示縮略圖大小為原始圖片大小的幾分之一,即如果這個值為2,則取出的縮略圖的寬和高都是原始圖片的1/2,圖片大小就為原始大小的1/4。
經過閱讀文檔發現,Options中有個屬性inJustDecodeBounds,文檔中的是這么說的:
If set to true, the decoder will return null (no bitmap), but the out... fields will still be set, allowing the caller to query the bitmap without having to allocate the memory for its pixels.
意思就是說如果該值設為true那么將不返回實際的bitmap對象,不給其分配內存空間但是可以得到一些解碼邊界信息即圖片大小等信息。因此我們可以通過設置inJustDecodeBounds為true,獲取到outHeight(圖片原始高度)和 outWidth(圖片的原始寬度),然后計算一個inSampleSize(縮放值),就可以取圖片了,這里要注意的是,inSampleSize 可能等於0,必須做判斷。也就是說先將Options的屬性inJustDecodeBounds設為true,先獲取圖片的基本大小信息數據(信息沒有保存在bitmap里面,而是保存在options里面),通過options.outHeight和 options. outWidth獲取的大小信息以及自己想要到得圖片大小計算出來縮放比例inSampleSize,然后緊接着將inJustDecodeBounds設為false,就可以根據已經得到的縮放比例得到自己想要的圖片縮放圖了。
/** * 由File轉成長寬固定相同比例的bitmap。實現長寬等比例縮放 * @return */ private Bitmap resizeBitmap(String filePath){ Bitmap des_bitmap ; BitmapFactory.Options options = new BitmapFactory.Options(); //表示縮略圖大小為原始圖片大小的幾分之一,即如果這個值為2,則取出的縮略圖的寬和高都是原始圖片的1/2,圖片大小就為原始大小的1/4 options.inJustDecodeBounds = true; des_bitmap = BitmapFactory.decodeFile(filePath,options);//des_bitmap為null.因為options.inJustDecodeBounds = true,節省內存 //長寬按照相同比例縮放 int height = options.outHeight; int width = options.outWidth; int scale;//縮放值,因為為等比例縮放. if(height > width){//以高為准 scale = (int) (height/200.0f);//該200說明目標大小為200PX }else{ scale = (int) (width/200.0f);//該200說明目標大小為200PX } if(scale<=0){ scale = 1; } options.inSampleSize = scale; options.inJustDecodeBounds = false; Bitmap new_bitmap = BitmapFactory.decodeFile(filePath,options);//des_bitmap不為null if(des_bitmap!=new_bitmap){ des_bitmap.recycle();//注意銷毀不用的bitmap } return new_bitmap; }
(2)Bitmap與Matrix搭配,實現縮放或者其他形變例如旋轉
如果對Matrix矩陣類沒有多少概念或者概念不全,可進入Matrix詳情了解
/** * 由Bitmap轉成指定大小的Bitmap,實現縮放成指定准確大小Bitmap * @param filePath * @param sWidth 目標寬,單位為px * @param sHeight 目標高,單位為px */ private Bitmap resizeBitmap(String filePath,int sWidth,int sHeight){ //縮放到指定的長寬 Bitmap mBitmap; BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = false; mBitmap = BitmapFactory.decodeFile(filePath,options); if(mBitmap == null){ return null; } int bmpWidth = mBitmap.getWidth(); int bmpHeight = mBitmap.getHeight(); // 縮放圖片的尺寸 float scaleWidth = (float) sWidth / bmpWidth; // 按固定大小縮放 sWidth 寫多大就多大 float scaleHeight = (float) sHeight / bmpHeight; // Matrix matrix = new Matrix();
//matrix.postScale(1, -1); //鏡像垂直翻轉
//matrix.postScale(-1, 1); //鏡像水平翻轉
//matrix.postRotate(-90); //旋轉-90度
matrix.postScale(scaleWidth, scaleHeight);// 產生縮放后的Bitmap對象
Bitmap resizeBitmap = Bitmap.createBitmap(mBitmap, 0, 0, bmpWidth, bmpHeight, matrix, false);
mBitmap.recycle();
return resizeBitmap; }
在控件畫布上的使用:
重要的方法有
Bitmap.createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter){}
canvas.drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) {}
Matrix mIconMatrix= new Matrix(); mIconMatrix.postScale(0.71f,0.71f); final Bitmap start = BitmapFactory.decodeResource(getResources(), R.drawable.run_start); bitmapStart = Bitmap.createBitmap(start,0,0,start.getWidth(),start.getHeight(),mIconMatrix,false); protected void onDraw(Canvas canvas){ mIconMatrix.postTranslate(123,123);//matrix的一系列變換 canvas.drawBitmap(bitmapStart, mIconMatrix, null); }
3.bitmap模糊處理
(一)自定義的模糊
見下篇文章
(二)高斯模糊
下篇文章介紹
4.bitmap保存圖像文件
(一)保存
/** * 保存內存中的bitmap到本地文件 * @param bitmap 要保存的bitmap * @param localPath 保存在本地的文件名,絕對路徑 * */ public static void saveBitmap2File(Bitmap bitmap,String localPath){ if (bitmap == null) { return; } try { File file = new File(localPath); FileOutputStream fos = new FileOutputStream(file); assert bitmap != null; bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);//100表示正常質量,如果壓縮之類的更改壓縮質量即可 fos.flush(); fos.close(); } catch (Exception e) { e.printStackTrace(); } }
(二)壓縮
/** * 圓片壓縮 * * @param bitmap * @return */ private Bitmap compressImage(Bitmap image) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//質量壓縮方法,這里100表示不壓縮,把壓縮后的數據存放到baos中 Log.e("size", baos.toByteArray().length/1024+""); ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把壓縮后的數據baos存放到ByteArrayInputStream中 Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream數據生成圖片 return bitmap; }
(三)Bitmap<->bytes轉換
1. public static Bitmap Bytes2Bimap(byte[] b) { 2. if (b.length != 0) { 3. return BitmapFactory.decodeByteArray(b, 0, b.length); 4. } else { 5. return null; 6. } 7. } 1. public static byte[] Bitmap2Bytes(Bitmap bm) { 2. ByteArrayOutputStream baos = new ByteArrayOutputStream(); 3. bm.compress(Bitmap.CompressFormat.PNG, 100, baos); 4. return baos.toByteArray(); 5. } 6. 1. public static Bitmap drawableToBitmap(Drawable drawable) { 2. 3. Bitmap bitmap = Bitmap 4. .createBitmap( 5. drawable.getIntrinsicWidth(), 6. drawable.getIntrinsicHeight(), 7. drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 8. : Bitmap.Config.RGB_565); 9. Canvas canvas = new Canvas(bitmap); 10. // canvas.setBitmap(bitmap); 11. drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), 12. drawable.getIntrinsicHeight()); 13. drawable.draw(canvas); 14. return bitmap; 15. }
5.Bitmap的防止內存泄露小方法
(一)Bitmap占用了多少內存
①獲取bitmap的內存大小
/** * Returns the minimum number of bytes that can be used to store this bitmap's pixels. * * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can * no longer be used to determine memory usage of a bitmap. See {@link * #getAllocationByteCount()}.</p> */ public final int getByteCount() { // int result permits bitmaps up to 46,340 x 46,340 return getRowBytes() * getHeight(); }
getByteCount()方法能獲取該bitmap所占的最小內存,不過API19即4.4以后通過getAllocationByteCount()獲取內存大小。
兩者可能存在不一樣的原因是因為如果bitmap重復利用的話,先存儲形狀大的圖片再存儲形狀小些的圖片,對於小的圖片,通過getAllocationCount()獲取到的大小會比getByteCount()要大。
/** * Returns the size of the allocated memory used to store this bitmap's pixels. * * <p>This can be larger than the result of {@link #getByteCount()} if a bitmap is reused to * decode other bitmaps of smaller size, or by manual reconfiguration. See {@link * #reconfigure(int, int, Config)}, {@link #setWidth(int)}, {@link #setHeight(int)}, {@link * #setConfig(Bitmap.Config)}, and {@link BitmapFactory.Options#inBitmap * BitmapFactory.Options.inBitmap}. If a bitmap is not modified in this way, this value will be * the same as that returned by {@link #getByteCount()}.</p> * * <p>This value will not change over the lifetime of a Bitmap.</p> * * @see #reconfigure(int, int, Config) */ public final int getAllocationByteCount() { if (mBuffer == null) { // native backed bitmaps don't support reconfiguration, // so alloc size is always content size return getByteCount(); } return mBuffer.length; }
②深入
在getByteCount()中,getHeight()標示bitmap的高度,單位為px。而getRowBytes()呢?往下
/** * Return the number of bytes between rows in the bitmap's pixels. Note that * this refers to the pixels as stored natively by the bitmap. If you call * getPixels() or setPixels(), then the pixels are uniformly treated as * 32bit values, packed according to the Color class. * * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, this method * should not be used to calculate the memory usage of the bitmap. Instead, * see {@link #getAllocationByteCount()}. * * @return number of bytes between rows of the native bitmap pixels. */ public final int getRowBytes() { if (mRecycled) { Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!"); } return nativeRowBytes(mNativePtr); }
進入Android5.0源碼進行JNI分析
\frameworks\base\core\jni\Android\graphics\Bitmap.cpp:
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) { SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle) return static_cast<jint>(bitmap->rowBytes()); }
我們發現rowBytes()其實就是對SKBitmap的指針引用。因為bitmap指向的是skBitmap的所在地址。所以bitmap的大小即為SkBitmap的值。
SkBitmap.h:
/** Return the number of bytes between subsequent rows of the bitmap. */ size_t rowBytes() const { return fRowBytes; }
SkBitmap.cpp:
size_t SkBitmap::ComputeRowBytes(Config c, int width) { return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width); } static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) { return width * SkColorTypeBytesPerPixel(ct); } SkImageInfo.h static int SkColorTypeBytesPerPixel(SkColorType ct) { static const uint8_t gSize[] = { 0, // Unknown 1, // Alpha_8 2, // RGB_565 2, // ARGB_4444 4, // RGBA_8888 4, // BGRA_8888 1, // kIndex_8 }; SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1), size_mismatch_with_SkColorType_enum); SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize)); return gSize[ct]; }
我發現大小即是width和一個ColorTypeBytesPerPixel的乘積。ColorTypeBytesPerPixel為bitmap的圖片分辨率類型,在BitmapFactory.Option的inPreferredConfig屬性決定,即不同圖片
分辨率類型決定了不同的內存大小(例如ARGB_8888類型的分辨率圖片每單位width下占4字節,該類型也是Bitmap默認的屬性)。即rowBytes等於‘4*width’ Bytes。
但是通過計算,發現通過計算內存=4*width*height,仍然不對。
因此我猜想圖片的內存大小除了圖片自身的分辨率以外,還跟設備的顯示密度、以及資源來源有關。
③再深入
我們以BitmapFactory.decodeResource()加載項目資源文件為例

/** * Synonym for opening the given resource and calling * {@link #decodeResourceStream}. * * @param res The resources object containing the image data * @param id The resource id of the image data * @param opts null-ok; Options that control downsampling and whether the * image should be completely decoded, or just is size returned. * @return The decoded bitmap, or null if the image data could not be * decoded, or, if opts is non-null, if opts requested only the * size be returned (in opts.outWidth and opts.outHeight) */ public static Bitmap decodeResource(Resources res, int id, Options opts) { Bitmap bm = null; InputStream is = null; try { final TypedValue value = new TypedValue(); is = res.openRawResource(id, value); bm = decodeResourceStream(res, value, is, null, opts); } catch (Exception e) { /* do nothing. If the exception happened on open, bm will be null. If it happened on close, bm is still valid. */ } finally { try { if (is != null) is.close(); } catch (IOException e) { // Ignore } } if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } return bm; }
本質就兩步:
1.調用Resource.openRawResource 方法,這個方法調用完成之后會對 TypedValue 進行賦值,其中包含了原始資源的 density 等信息
2.調用 decodeResourceStream 對原始資源進行解碼和適配。這個過程實際上就是原始資源的 density 到屏幕 density 的一個映射。
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); }
又回到了decodeStream(),我試過直接加載文件圖片時調用該方法,參數為文件輸入流。decodeStream(fis,null,option)。即參數Rect直接設為null。
與本例子不同的,即通過decodeResourceStream(),設置了BitmapFactory.Option的inDensity屬性和inTargetDensity屬性。
inDensity表示資源文件密度,如果對應資源目錄為hdpi的話,就是240.
inTargetDensity標示顯示的密度,如果是三星S6為640.
重要的是nativeDecodeStream方法,重要的的是其中的doDecode方法。
BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) { ...... if (env->GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env->GetIntField(options, gOptions_densityFieldID);//對應hdpi的時候,是240 const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//三星s6的為640 const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } } } const bool willScale = scale != 1.0f; ...... SkBitmap decodingBitmap; if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) { return nullObjectReturn("decoder->decode returned false"); } //這里這個deodingBitmap就是解碼出來的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); } if (willScale) { const float sx = scaledWidth / float(decodingBitmap.width()); const float sy = scaledHeight / float(decodingBitmap.height()); // TODO: avoid copying when scaled size equals decodingBitmap size SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType()); // FIXME: If the alphaType is kUnpremul and the image has alpha, the // colors may not be correct, since Skia does not yet support drawing // to/from unpremultiplied bitmaps. outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight, colorType, decodingBitmap.alphaType())); if (!outputBitmap->allocPixels(outputAllocator, NULL)) { return nullObjectReturn("allocation failed for scaled bitmap"); } // If outputBitmap's pixels are newly allocated by Java, there is no need // to erase to 0, since the pixels were initialized to 0. if (outputAllocator != &javaAllocator) { outputBitmap->eraseColor(0); } SkPaint paint; paint.setFilterLevel(SkPaint::kLow_FilterLevel); SkCanvas canvas(*outputBitmap); canvas.scale(sx, sy); canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint); } ...... }
outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
colorType, decodingBitmap.alphaType()));
我們看到最終輸出的 outputBitmap 的大小是scaledWidth*scaledHeight,我們把這兩個變量計算的片段拿出來給大家一看就明白了:
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) { scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); }
在522*686的PNG 圖片,我把它放到 drawable-xxhdpi 目錄下,在三星s6上加載,占用內存2547360B。
scaledWidth = int( 522 * 640 / 480f + 0.5) = int(696.5) = 696
scaledHeight = int( 686 * 640 / 480f + 0.5) = int(915.16666…) = 915
下面就是見證奇跡的時刻:
915 * 696 * 4 = 2547360
④總結
Bitmap的內存使用與圖片的分辨率屬性有關以外,還跟所在的文件目錄,投影的設備密度有關。
6.附加小知識
(一)Bitmap加水印
/** * 添加文字到圖片,類似水印文字 * * @param gContext * @param gResId * @param gText * @return */ public static Bitmap drawTextToBitmap(Context gContext, int gResId, String gText) { Resources resources = gContext.getResources(); float scale = resources.getDisplayMetrics().density; Bitmap bitmap = BitmapFactory.decodeResource(resources, gResId); android.graphics.Bitmap.Config bitmapConfig = bitmap.getConfig(); // set default bitmap config if none if (bitmapConfig == null) { bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888; } // resource bitmaps are imutable, // so we need to convert it to mutable one bitmap = bitmap.copy(bitmapConfig, true); Canvas canvas = new Canvas(bitmap); // new antialised Paint Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); // text color - #3D3D3D paint.setColor(Color.rgb(61, 61, 61)); // text size in pixels paint.setTextSize((int) (14 * scale * 5)); // text shadow paint.setShadowLayer(1f, 0f, 1f, Color.WHITE); // draw text to the Canvas center Rect bounds = new Rect(); paint.getTextBounds(gText, 0, gText.length(), bounds); //int x = (bitmap.getWidth() - bounds.width()) / 2; //int y = (bitmap.getHeight() + bounds.height()) / 2; //draw text to the bottom int x = (bitmap.getWidth() - bounds.width()) / 10 * 9; int y = (bitmap.getHeight() + bounds.height()) / 10 * 9; canvas.drawText(gText, x, y, paint); return bitmap; }