Android Bitmap面面观
在日常開(kāi)發(fā)中,可以說(shuō)和Bitmap低頭不見(jiàn)抬頭見(jiàn),基本上每個(gè)應(yīng)用都會(huì)直接或間接的用到,而這里面又涉及到大量的相關(guān)知識(shí)。
所以這里把Bitmap的常用知識(shí)做個(gè)梳理,限于經(jīng)驗(yàn)和能力,不做太深入的分析。
1. 區(qū)別decodeResource()和decodeFile()
這里的區(qū)別不是指方法名和參數(shù)的區(qū)別,而是對(duì)于解碼后圖片尺寸在處理上的區(qū)別:
decodeFile()用于讀取SD卡上的圖,得到的是圖片的原始尺寸
decodeResource()用于讀取Res、Raw等資源,得到的是圖片的原始尺寸 * 縮放系數(shù)
可以看的出來(lái),decodeResource()比decodeFile()多了一個(gè)縮放系數(shù),縮放系數(shù)的計(jì)算依賴(lài)于屏幕密度,當(dāng)然這個(gè)參數(shù)也是可以調(diào)整的:
| 1 2 3 4 5 6 7 8 | // 通過(guò)BitmapFactory.Options的這幾個(gè)參數(shù)可以調(diào)整縮放系數(shù) public class BitmapFactory { public static class Options { public boolean inScaled; // 默認(rèn)true public int inDensity; // 無(wú)dpi的文件夾下默認(rèn)160 public int inTargetDensity; // 取決具體屏幕 } } |
我們分具體情況來(lái)看,現(xiàn)在有一張720x720的圖片:
inScaled屬性
如果inScaled設(shè)置為false,則不進(jìn)行縮放,解碼后圖片大小為720x720; 否則請(qǐng)往下看。
如果inScaled設(shè)置為true或者不設(shè)置,則根據(jù)inDensity和inTargetDensity計(jì)算縮放系數(shù)。
默認(rèn)情況
把這張圖片放到drawable目錄下, 默認(rèn):
以720p的紅米3為例子,縮放系數(shù) = inTargetDensity(具體320 / inDensity(默認(rèn)160)= 2 = density,解碼后圖片大小為1440x1440。
以1080p的MX4為例子,縮放系數(shù) = inTargetDensity(具體480 / inDensity(默認(rèn)160)= 3 = density, 解碼后圖片大小為2160x2160。
*dpi文件夾的影響
把圖片放到drawable或者draw這樣不帶dpi的文件夾,會(huì)按照上面的算法計(jì)算。
如果放到xhdpi會(huì)怎樣呢? 在MX4上,放到xhdpi,解碼后圖片大小為1080 x 1080。
因?yàn)榉诺接衐pi的文件夾,會(huì)影響到inDensity的默認(rèn)值,放到xhdpi為160 x 2 = 320; 所以縮放系數(shù) = 480(屏幕) / 320 (xhdpi) = 1.5; 所以得到的圖片大小為1080 x 1080。
手動(dòng)設(shè)置縮放系數(shù)
如果你不想依賴(lài)于這個(gè)系統(tǒng)本身的density,你可以手動(dòng)設(shè)置inDensity和inTargetDensity來(lái)控制縮放系數(shù):
| 1 2 3 4 5 6 7 8 9 10 11 12 | BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = false; options.inSampleSize = 1; options.inDensity = 160; options.inTargetDensity = 160; bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.origin, options); // MX4上,雖然density = 3 // 但是通過(guò)設(shè)置inTargetDensity / inDensity = 160 / 160 = 1 // 解碼后圖片大小為720x720 System.out.println("w:" + bitmap.getWidth() + ", h:" + bitmap.getHeight()); |
2. recycle()方法
官方說(shuō)法
首先,Android對(duì)Bitmap內(nèi)存(像素?cái)?shù)據(jù))的分配區(qū)域在不同版本上是有區(qū)分的:
As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.
從3.0開(kāi)始,Bitmap像素?cái)?shù)據(jù)和Bitmap對(duì)象一起存放在Dalvik堆中,而在3.0之前,Bitmap像素?cái)?shù)據(jù)存放在Native內(nèi)存中。
所以,在3.0之前,Bitmap像素?cái)?shù)據(jù)在Nativie內(nèi)存的釋放是不確定的,容易內(nèi)存溢出而Crash,官方強(qiáng)烈建議調(diào)用recycle()(當(dāng)然是在確定不需要的時(shí)候);而在3.0之后,則無(wú)此要求。
參考鏈接:Managing Bitmap Memory
一點(diǎn)討論
3.0之后官方無(wú)recycle()建議,是不是就真的不需要recycle()了呢?
在醫(yī)生的這篇文章:Bitmap.recycle引發(fā)的血案?最后指出:“在不兼容Android2.3的情況下,別在使用recycle方法來(lái)管理Bitmap了,那是GC的事!”。文章開(kāi)頭指出了原因在于recycle()方法的注釋說(shuō)明:
| 1 2 3 4 5 6 | /** * ... This is an advanced call, and normally need not be called, * since the normal GC process will free up this memory when * there are no more references to this bitmap. */ public void recycle() {} |
事實(shí)上這個(gè)說(shuō)法是不準(zhǔn)確的,是不能作為recycle()方法不調(diào)用的依據(jù)的。
因?yàn)閺腸ommit history中看,這行注釋早在08年初始化代碼的就有了,但是早期的代碼并沒(méi)有因此不需要recycle()方法了。
如果3.0之后真的完全不需要主動(dòng)recycle(),最新的AOSP源碼應(yīng)該有相應(yīng)體現(xiàn),我查了SystemUI和Gallery2的代碼,并沒(méi)有取締Bitmap的recycle()方法。
所以,我個(gè)人認(rèn)為,如果Bitmap真的不用了,recycle一下又有何妨?
PS:至于醫(yī)生說(shuō)的那個(gè)bug,顯然是一種優(yōu)化策略,APP開(kāi)發(fā)中加個(gè)兩個(gè)bitmap不相等的判斷條件即可。
3. Bitmap到底占多大內(nèi)存
這個(gè)已經(jīng)有一篇bugly出品的絕好文章講的很清楚:
Android 開(kāi)發(fā)繞不過(guò)的坑:你的 Bitmap 究竟占多大內(nèi)存?
4. inBitmap
BitmapFactory.Options.inBitmap是Android3.0新增的一個(gè)屬性,如果設(shè)置了這個(gè)屬性則會(huì)重用這個(gè)Bitmap的內(nèi)存從而提升性能。
但是這個(gè)重用是有條件的,在Android4.4之前只能重用相同大小的Bitmap,Android4.4+則只要比重用Bitmap小即可。
在官方網(wǎng)站有詳細(xì)介紹,這里列舉示例代碼的兩個(gè)方法了解一下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { // inBitmap only works with mutable bitmaps, so force the decoder to // return mutable bitmaps. options.inMutable = true; if (cache != null) { // Try to find a bitmap to use for inBitmap. Bitmap inBitmap = cache.getBitmapFromReusableSet(options); if (inBitmap != null) { // If a suitable bitmap has been found, // set it as the value of inBitmap. options.inBitmap = inBitmap; } } } static boolean canUseForInBitmap( Bitmap candidate, BitmapFactory.Options targetOptions) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // From Android 4.4 (KitKat) onward we can re-use // if the byte size of the new bitmap is smaller than // the reusable bitmap candidate // allocation byte count. int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); return byteCount <= candidate.getAllocationByteCount(); } // On earlier versions, // the dimensions must match exactly and the inSampleSize must be 1 return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1; } |
參考鏈接:
Managing Bitmap Memory
Bitmap對(duì)象的復(fù)用
5. LRU緩存算法
LRU,Least Recently Used,Discards the least recently used items first。
在最近使用的數(shù)據(jù)中,丟棄使用最少的數(shù)據(jù)。與之相反的還有一個(gè)MRU,丟棄使用最多的數(shù)據(jù)。
這就是著名的局部性原理。
實(shí)現(xiàn)思路
1.新數(shù)據(jù)插入到鏈表頭部;
2.每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問(wèn)),則將數(shù)據(jù)移到鏈表頭部;
3.當(dāng)鏈表滿的時(shí)候,將鏈表尾部的數(shù)據(jù)丟棄。
LruCache
在Android3.1和support v4中均提供了Lru算法的實(shí)現(xiàn)類(lèi)LruCache。
內(nèi)部使用LinkedHashMap實(shí)現(xiàn)。
DiskLruCache
LruCache的所有對(duì)象和數(shù)據(jù)都是在內(nèi)存中(或者說(shuō)LinkedHashMap中),而DiskLruCache是磁盤(pán)緩存,不過(guò)它的實(shí)現(xiàn)要稍微復(fù)雜一點(diǎn)。
使用DiskLruCache后就不用擔(dān)心文件或者圖片太多占用過(guò)多磁盤(pán)空間,它能把那些不常用的圖片自動(dòng)清理掉。
DiskLruCache系統(tǒng)中并沒(méi)有正式提供,需要另外下載:?DiskLruCache
6. 計(jì)算inSampleSize
使用Bitmap節(jié)省內(nèi)存最重要的技巧就是加載合適大小的Bitmap,因?yàn)橐袁F(xiàn)在相機(jī)像素,很多照片都巨無(wú)霸的大,這些大圖直接加載到內(nèi)存,最容易OOM。
加載合適的Bitmap需要先讀取Bitmap的原始大小,按縮小了合適的倍數(shù)的大小進(jìn)行加載。
那么,這個(gè)縮小的倍數(shù)的計(jì)算就是inSampleSize的計(jì)算。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 根據(jù)maxWidth, maxHeight計(jì)算最合適的inSampleSize public static int $sampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) { // raw height and width of image int rawWidth = options.outWidth; int rawHeight = options.outHeight; // calculate best sample size int inSampleSize = 0; if (rawHeight > maxHeight || rawWidth > maxWidth) { float ratioWidth = (float) rawWidth / maxWidth; float ratioHeight = (float) rawHeight / maxHeight; inSampleSize = (int) Math.min(ratioHeight, ratioWidth); } inSampleSize = Math.max(1, inSampleSize); return inSampleSize; } |
關(guān)于inSampleSize需要注意,它只能是2的次方,否則它會(huì)取最接近2的次方的值。
7. 縮略圖
為了節(jié)省內(nèi)存,需要先設(shè)置BitmapFactory.Options的inJustDecodeBounds為true,這樣的Bitmap可以借助decodeFile方法把高和寬存放到Bitmap.Options中,但是內(nèi)存占用為空(不會(huì)真正的加載圖片)。
有了具備高寬信息的Options,結(jié)合上面的inSampleSize算法算出縮小的倍數(shù),我們就能加載本地大圖的某個(gè)合適大小的縮略圖了。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | /** * 獲取縮略圖 * 支持自動(dòng)旋轉(zhuǎn) * 某些型號(hào)的手機(jī)相機(jī)圖片是反的,可以根據(jù)exif信息實(shí)現(xiàn)自動(dòng)糾正 * @return */ public static Bitmap $thumbnail(String path, int maxWidth, int maxHeight, boolean autoRotate) { int angle = 0; if (autoRotate) { angle = ImageLess.$exifRotateAngle(path); } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 獲取這個(gè)圖片的寬和高信息到options中, 此時(shí)返回bm為空 Bitmap bitmap = BitmapFactory.decodeFile(path, options); options.inJustDecodeBounds = false; // 計(jì)算縮放比 int sampleSize = $sampleSize(options, maxWidth, maxHeight); options.inSampleSize = sampleSize; options.inPreferredConfig = Bitmap.Config.RGB_565; options.inPurgeable = true; options.inInputShareable = true; if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); } bitmap = BitmapFactory.decodeFile(path, options); if (autoRotate && angle != 0) { bitmap = $rotate(bitmap, angle); } return bitmap; } |
系統(tǒng)內(nèi)置了一個(gè)ThumbnailUtils也能生成縮略圖,細(xì)節(jié)上不一樣但原理是相同的。
8. Matrix變形
學(xué)過(guò)線性代數(shù)或者圖像處理的同學(xué)們一定深知Matrix的強(qiáng)大,很多常見(jiàn)的圖像變換一個(gè)Matrix就能搞定,甚至更復(fù)雜的也是如此。
// Matrix matrix = new Matrix();
// 每一種變化都包括set,pre,post三種,分別為設(shè)置、矩陣先乘、矩陣后乘。
平移:matrix.setTranslate()
縮放:matrix.setScale()
旋轉(zhuǎn):matrix.setRotate()
斜切:matrix.setSkew()
下面我舉兩個(gè)例子說(shuō)明一下。
旋轉(zhuǎn)
借助Matrix的postRotate方法旋轉(zhuǎn)一定角度。
| 1 2 3 4 5 6 7 8 9 10 | Matrix matrix = new Matrix(); // angle為旋轉(zhuǎn)的角度 matrix.postRotate(angle); Bitmap rotatedBitmap = Bitmap.createBitmap(originBitmap, 0, 0, originBitmap.getWidth(), originBitmap.getHeight(), matrix, true); |
縮放
借助Matrix的postScale方法旋轉(zhuǎn)一定角度。
| 1 2 3 4 5 6 7 8 9 10 | Matrix matrix = new Matrix(); // scaleX,scaleY分別為為水平和垂直方向上縮放的比例 matrix.postScale(scaleX, scaleY); Bitmap scaledBitmap = Bitmap.createBitmap(originBitmap, 0, 0, originBitmap.getWidth(), originBitmap.getHeight(), matrix, true); |
Bitmap本身也帶了一個(gè)縮放方法,不過(guò)是把bitmap縮放到目標(biāo)大小,原理也是用Matrix,我們封裝一下:
| 1 2 3 4 5 | // 水平和寬度縮放到指定大小,注意,這種情況下圖片很容易變形 Bitmap scaledBitmap = Bitmap.createScaledBitmap(originBitmap, dstWidth, dstHeight, true); |
通過(guò)組合可以實(shí)現(xiàn)更多效果。
9. 裁剪
圖片的裁剪的應(yīng)用場(chǎng)景還是很多的:頭像剪切,照片裁剪,圓角,圓形等等。
矩形
矩陣形狀的裁剪比較簡(jiǎn)單,直接用createBitmap方法即可:
| 1 2 3 4 5 6 | Canvas canvas = new Canvas(originBitmap); draw(canvas); // 確定裁剪的位置和裁剪的大小 Bitmap clipBitmap = Bitmap.createBitmap(originBitmap, left, top, clipWidth, clipHeight); |
圓角
對(duì)于圓角我們需要借助Xfermode和PorterDuffXfermode,把圓角矩陣套在原Bitmap上取交集得到圓角Bitmap。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 準(zhǔn)備畫(huà)筆 Paint paint = new Paint(); paint.setAntiAlias(true); // 準(zhǔn)備裁剪的矩陣 Rect rect = new Rect(0, 0, originBitmap.getWidth(), originBitmap.getHeight()); RectF rectF = new RectF(new Rect(0, 0, originBitmap.getWidth(), originBitmap.getHeight())); Bitmap roundBitmap = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(roundBitmap); // 圓角矩陣,radius為圓角大小 canvas.drawRoundRect(rectF, radius, radius, paint); // 關(guān)鍵代碼,關(guān)于Xfermode和SRC_IN請(qǐng)自行查閱 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(originBitmap, rect, rect, paint); |
圓形
和上面的圓角裁剪原理相同,不過(guò)畫(huà)的是圓形套在上面。
為了從中間裁剪出圓形,我們需要計(jì)算繪制原始Bitmap的left和top值。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | int min = originBitmap.getWidth() > originBitmap.getHeight() ? originBitmap.getHeight() : originBitmap.getWidth(); Paint paint = new Paint(); paint.setAntiAlias(true); Bitmap circleBitmap = Bitmap.createBitmap(min, min, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(circleBitmap); // 圓形 canvas.drawCircle(min / 2, min / 2, min / 2, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); // 居中顯示 int left = - (originBitmap.getWidth() - min) / 2; int top = - (originBitmap.getHeight() - min) / 2; canvas.drawBitmap(originBitmap, left, top, paint); |
從圓角、圓形的處理上我們應(yīng)該能看的出來(lái)繪制任意多邊形都是可以的。
10. 保存Bitmap
很多圖片應(yīng)用都支持裁剪功能,濾鏡功能等等,最終還是需要把處理后的Bitmap保存到本地,不然就是再?gòu)?qiáng)大的功能也是白忙活了。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public static String $save(Bitmap bitmap, Bitmap.CompressFormat format, int quality, File destFile) { try { FileOutputStream out = new FileOutputStream(destFile); if (bitmap.compress(format, quality, out)) { out.flush(); out.close(); } if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); } return destFile.getAbsolutePath(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } |
如果想更穩(wěn)定或者更簡(jiǎn)單的保存到SDCard的包名路徑下,可以再封裝一下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 保存到本地,默認(rèn)路徑/mnt/sdcard/[package]/save/,用隨機(jī)UUID命名文件 public static String $save(Bitmap bitmap, Bitmap.CompressFormat format, int quality, Context context) { if (!Environment.getExternalStorageState() .equals(Environment.MEDIA_MOUNTED)) { return null; } File dir = new File(Environment.getExternalStorageDirectory() + "/" + context.getPackageName() + "/save/"); if (!dir.exists()) { dir.mkdirs(); } File destFile = new File(dir, UUID.randomUUID().toString()); return $save(bitmap, format, quality, destFile); } |
11. 巨圖加載
巨圖加載,當(dāng)然不能使用常規(guī)方法,必OOM。
原理比較簡(jiǎn)單,系統(tǒng)中有一個(gè)類(lèi)BitmapRegionDecoder:
| 1 2 3 4 5 6 7 8 9 10 11 12 | public static BitmapRegionDecoder newInstance(byte[] data, int offset, int length, boolean isShareable) throws IOException { } public static BitmapRegionDecoder newInstance( FileDescriptor fd, boolean isShareable) throws IOException { } public static BitmapRegionDecoder newInstance(InputStream is, boolean isShareable) throws IOException { } public static BitmapRegionDecoder newInstance(String pathName, boolean isShareable) throws IOException { } |
可以按區(qū)域加載:
| 1 2 | public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) { } |
微博的大圖瀏覽也是通過(guò)這個(gè)BitmapRegionDecoder實(shí)現(xiàn)的,具體可自行查閱。
12. 顏色矩陣ColorMatrix
圖像處理其實(shí)是一門(mén)很深?yuàn)W的學(xué)科,所幸Android提供了顏色矩陣ColorMatrix類(lèi),可實(shí)現(xiàn)很多簡(jiǎn)單的特效,以灰階效果為例子:
| 1 2 3 4 5 6 7 8 9 10 11 | Bitmap grayBitmap = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.RGB_565); Canvas canvas = new Canvas(grayBitmap); Paint paint = new Paint(); ColorMatrix colorMatrix = new ColorMatrix(); // 設(shè)置飽和度為0,實(shí)現(xiàn)了灰階效果 colorMatrix.setSaturation(0); ColorMatrixColorFilter colorMatrixColorFilter = new ColorMatrixColorFilter(colorMatrix); paint.setColorFilter(colorMatrixColorFilter); canvas.drawBitmap(originBitmap, 0, 0, paint); |
除了飽和度,我們還能調(diào)整對(duì)比度,色相變化等等。
13. ThumbnailUtils剖析
ThumbnailUtils是系統(tǒng)提供的一個(gè)專(zhuān)門(mén)生成縮略圖的方法,我專(zhuān)門(mén)寫(xiě)了一篇文章分析,內(nèi)容較多,請(qǐng)移步:理解ThumbnailUtils
14. 小結(jié)
既然與Bitmap經(jīng)常打交道,那就把它都理清楚弄明白,這是很有必要的。
難免會(huì)有遺漏,歡迎留言,我會(huì)酌情補(bǔ)充。
本文部分代碼已經(jīng)集成到LessCode,歡迎Follow參考。
原文地址: http://jayfeng.com/2016/03/22/Android-Bitmap%E9%9D%A2%E9%9D%A2%E8%A7%82/#more總結(jié)
以上是生活随笔為你收集整理的Android Bitmap面面观的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Android APP终极瘦身指南
- 下一篇: 你应该知道的那些Android小经验