前言
Bitmap是Android中一種重要的圖片處理機制,它可以用來擷取圖片的相關資訊,同時可以對圖片進行裁剪、縮放等操作,也可以指定圖片格式進行儲存。相信對於OOM再熟悉不過了,OOM的產生是一個非常頭疼的事情,如果在載入圖片的時候未對大圖進行處理,它將會佔用非常大的記憶體,這樣就非常容易產生OOM。所以我們必須要有意識的對大圖進行壓縮載入,這樣才能更好的保證App的正常運行與效能的穩定。
Bitmap大小計算
那麼如果計算一張圖片載入過程中所佔的記憶體大小呢?在這之前,我們先來瞭解一下關於Bitmap兩個主要配置。
CompressFormat
這是用來指定Bitmap的圖片的壓縮格式,在Bitmap中是一個Enum結構,主要表現為以下三種格式。
public enum CompressFormat { JPEG (0), PNG (1), WEBP (2); CompressFormat(int nativeInt) { this.nativeInt = nativeInt; } final int nativeInt; }
JPEG: 以JPEG演算法進行壓縮,壓縮後的圖片格式可以為.jpeg或者.jpg,這是一種有損壓縮,沒有透明度。
PNG:以PNG演算法進行壓縮,壓縮後的圖片格式是.png,這是一種無損壓縮,可以有透明度。
WEBP:以WEBP演算法進行壓縮,壓縮後的圖片格式是.webp,這是一種有損壓縮。相同品質下,webp比jpeg映像小40%,但webp圖片的編碼時間比jpeg長8倍。
Config
這是關於Bitmap像素儲存的方式配置,不同的像素儲存,對圖片的品質也會有不同的影響。在Bitmap中是一個Enum結構,主要表現於以下四種格式。
ALPHA_8:每一個像素都只儲存單一的透明度,即只有透明度,總共佔8位,1位元組。
ARGB_4444:每一個像素都以A(透明度)R(Red)G(Green)B(Blue)四部分組成,每部分佔4位,總共佔16位,2位元組。由於這種格式的圖片品質太差,所以中API 13就已經廢棄了,推薦使用ARGB_8888。
ARGB_8888:每一個像素都以A(透明度)R(Red)G(Green)B(Blue)四部分組成,每部分佔8位,總共32位,4位元組。
RGB_565:每一個像素都以R(Red)G(Green)B(Blue)三部分組成,各個部分分別佔5位,6位,5位,總共16位,2位元組。
所以如果為了防止OOM對圖片進行壓縮,一般會使用RGB_565格式,因為ALPHA_8隻有透明度,對於正常圖片未意義;ARGB_4444顯示的圖片品質太差;ARGB_8888佔用的記憶體最多。
如果載入的圖片的寬度為1080、高度為675、Config為ARGB_8888。那麼它占的記憶體為:1080 x 675 x 4 = 2916000.折算成M為2916000 / 1024 / 1024 = 2.78M。一種圖片就近3M,如果載入10張或者100張,所佔的記憶體可想而知。這樣的話會很容易將記憶體消耗殆盡,同時對於Android App來說根本就不需要這麼高清的圖片,所以我們在載入圖片的時候可以對其進行相應的處理,例如:對寬高進行縮放,亦或者將Config改我RGB_565。這樣不僅有效減小了記憶體的佔用,同時也不影響圖片的清晰度的展示。
下面我們來看下如何通過Bitmap與BitmapFactory來對圖片進行處理
Bitmap相關
對於使用Bitmap進行圖片的壓縮處理,它主要提供了以下有效方法。
| status return |
method name |
| boolean |
compress(Bitmap.CompressFormat format, int quality, OutputStream stream) |
| static Bitmap |
createBitmap(DisplayMetrics display, int[] colors, int width, int height, Bitmap.Config config) |
| static Bitmap |
createBitmap(DisplayMetrics display, int[] colors, int offset, int stride, int width, int height, Bitmap.Config config) |
| static Bitmap |
createBitmap(Bitmap source, int x, int y, int width, int height) |
| static Bitmap |
createBitmap(Bitmap src) |
| static Bitmap |
createBitmap(DisplayMetrics display, int width, int height, Bitmap.Config config) |
| static Bitmap |
createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter) |
| static Bitmap |
createBitmap(int width, int height, Bitmap.Config config) |
| static Bitmap |
createBitmap(int[] colors, int offset, int stride, int width, int height, Bitmap.Config config) |
| static Bitamp |
createBitmap(int[] colors, int width, int height, Bitmap.Config config) |
| static Bitmap |
createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter) |
compress
compress方法通過指定圖片的CompressFormat格式與壓縮百分比來對圖片進行壓縮處理,同時將壓縮的圖片儲存到指定的outputStream中。我們來看下具體用法
val fs = FileOutputStream(path)val out = ByteArrayOutputStream()scaleBitmap.compress(Bitmap.CompressFormat.JPEG, 30, out)LogUtils.d("compressBitmap jpeg of byteCount %d.", out.toByteArray().size)fs.write(out.toByteArray())fs.close()
在這裡要注意quality代表百分比,值為0~100,值越小壓縮的後的大小就越小。例如上面的樣本,quality為30,則代表對原圖進行壓縮70%,保留30%。同時Bitmap.CompressFormat在前面已經詳細介紹了,要注意不同的格式圖片的展示效果也不同,例如JPEG的格式圖片是沒有透明度的。
特別注意的:對於Bitmap.CompressFormat.PNG類型的格式,quality將失去效果,因為其格式是無損的壓縮;再者,使用compress方法並不是對顯示處理的圖片進行了壓縮,它只是對原圖進行壓縮後儲存到本地磁碟中,並不改變顯示的圖片。主要用途作用於儲存圖片到本地,以便下次載入,減小圖片在本地磁碟所佔的磁碟大小。
createBitmap
對於createBitmap方法,Bitmap中提供了9種不同資料來源的壓縮處理方法。分別有通過colors數組、Bitmap與DisplayMetrics等來決定。
例如使用colors數組
val colors = intArrayOf(Color.RED, Color.GREEN, Color.BLUE, Color.GREEN, Color.BLUE, Color.RED, Color.BLUE, Color.RED, Color.GREEN)val displayMetricsBitmap = Bitmap.createBitmap(DisplayMetrics(),colors,3, 3,Bitmap.Config.ARGB_8888)LogUtils.d("displayMetricsBitmap of byteCount %d and rowBytes %d", displayMetricsBitmap.byteCount, displayMetricsBitmap.rowBytes)
這裡建立了一個3 x 3的圖片,Config為ARGB_8888,所以最終圖片在記憶體中的大小為3 x 3 x 4 = 36位元組。而圖片的展示效果是通過colors數組中的顏色也實現的,3 x 3 = 9 分別對應colors中的9個像素點的色值。所以colors的大小最小必須大於等於9,即寬*高的大小。為何說最小,因為Bitmap還提供了offset與stride參數的重載方法。這兩個參數分別代表在colors中的開始點的位移量與取值的步伐,即每個取值點間的跨度。
在實際是使用createBitmap最多的還是用它的Bitmap重載方法,主要用來對原圖片進行裁剪。我們直接看它的使用方式:
//bitmapval bitmapBitmap = Bitmap.createBitmap(scaleBitmap, 150, 0, 100, 100)image_view?.setImageBitmap(scaleBitmap)image_view_text.text = "width: " + scaleBitmap.width + " height: " + scaleBitmap.heightsub_image_view.setImageBitmap(bitmapBitmap)sub_image_view_text.text = "startX: 150 startY: 0\n" + "width: " + bitmapBitmap.width + " height: " + bitmapBitmap.height
主要參數是原Bitmap,我們所有的操作都是在原Bitmap中進行的。其中x = 150、y = 0代表從原Bitmap中的座標(150,0)開始進行裁剪;width = 100、height = 100,裁剪後返回新的的Bitmap,且大小為100 x 100。
if (!source.isMutable() && x == 0 && y == 0 && width == source.getWidth() && height == source.getHeight() && (m == null || m.isIdentity())) { return source; }
注意,如果原Bitmap是不可變的,同時需要的圖參數與原圖片相同,那麼它會直接返回原Bitmap。是否可變可以通過Bitmap.isMutable判斷。
看下上面的代碼所展示的:
最後通過傳遞Bitmap參數還有一個選擇性參數Matrix,它主要用於對Bitmap進行矩陣變換。
createScaledBitmap
該方法相對上面兩種就簡單多了,它目的是對原Bitmap進行指定的寬高進行縮放,最終返回新的Bitmap。
注意:如果傳入的寬高與原Bitmap相同,它將返回原Bitmap對象。
//createScaledBitmapval createScaledBitmap = Bitmap.createScaledBitmap(scaleBitmap, 500, 300, false)image_view?.setImageBitmap(scaleBitmap)image_view_text.text = "width: " + scaleBitmap.width + " height: " + scaleBitmap.heightsub_image_view.setImageBitmap(createScaledBitmap)sub_image_view_text.text = "width: " + createScaledBitmap.width + " height: " + createScaledBitmap.height
再來看下其實現源碼
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight, boolean filter) { Matrix m = new Matrix(); final int width = src.getWidth(); final int height = src.getHeight(); if (width != dstWidth || height != dstHeight) { final float sx = dstWidth / (float) width; final float sy = dstHeight / (float) height; m.setScale(sx, sy); } return Bitmap.createBitmap(src, 0, 0, width, height, m, filter); }
一目瞭然,內部就是使用到了Matrix,運用Matrix的知識進行寬高縮放;最後再調用前面所分析的createBitmap方法。只不過這裡指定了初始點(0,0)而已,再傳入原Bitmap的寬高與產生的Matrix。因此createBitmap方法中的注意點也是應用到createScaledBitmap中。
BitmapFactory相關
BitmapFactory主要用來解碼Bitmap,通過不同的資源類型,例如:files、streams與byte-arrays。既然是繼續資源的解碼,自然可以在解碼的過程中進行一些圖片壓縮處理。來看下它提供的主要解碼方法。
| status return |
method name |
| static Bitmap |
decodeByteArray(byte[] data, int offset, int length, BitmapFactory.Options opts) |
| static Bitmap |
decodeByteArray(byte[] data, int offset, int length) |
| static Bitmap |
decodeFile(String pathName) |
| static Bitmap |
decodeFile(String pathName, BitmapFactory.Options opts) |
| static Bitmap |
decodeFileDescriptor(FileDescriptor fd) |
| static Bitmap |
decodeFileDescriptor(FileDescriptor fd, Rect outPadding, BitmapFactory.Options opts) |
| static Bitmap |
decodeResource(Resources res, int id, BitmapFactory.Options opts) |
| static Bitmap |
decodeResource(Resources res, int id) |
| static Bitmap |
decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, BitmapFactory.Options opts) |
| static Bitmap |
decodeStream(InputStream is) |
| static Bitmap |
decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) |
直接通過方法名就能很方便的分辨出使用哪種資源類型進行圖片解碼操作。例如:decodeByteArray方法是通過byte數組作為解析源,同時在解碼過程中可以通過設定offset與length來控制解碼的起始點與解碼的大小。因此如果能夠精確控制offset與length也就能夠做到圖片的裁剪效果。decodeFileDescriptor方法是通過檔案描述符進行解碼Bitmap。一般用不到,下面詳細分析幾種常用的方法。
decodeFile
該方法是通過檔案路徑來解碼出Bitmap
//decodeFileval decodeFileOptions = BitmapFactory.Options()val decodeFileBitmap = BitmapFactory.decodeFile(mRootPath +"bitmap", decodeFileOptions)decodeFileOptions.inSampleSize = 2val decodeFileScaleBitmap = BitmapFactory.decodeFile(mRootPath + "bitmap", decodeFileOptions)image_view?.setImageBitmap(decodeFileBitmap)image_view_text.text = "width: " + decodeFileBitmap.width + " height: " + decodeFileBitmap.heightsub_image_view.setImageBitmap(decodeFileScaleBitmap)sub_image_view_text.text = "width: " + decodeFileScaleBitmap.width + " height: " + decodeFileScaleBitmap.height
下面的圖片比上面的圖片寬高都縮小了一半,對圖片進行了壓縮操作。通過代碼發現,下面的圖片解碼時設定了
decodeFileOptions.inSampleSize = 2
這裡就涉及到了靜態內部類BitmapFactory.Options,可以看上面的表發現大多數方法都有這個參數,它是一個可選項。主要用途是在圖片解碼過程中對圖片的原有屬性進行修改。它的參數配置大多數以in首碼開頭,下面列舉一些常用的配置設定屬性。
| type |
name |
description |
|
| boolean |
inJustDecodeBounds |
如果為true,解碼後不會返回Bitmap對象,但Bitmap寬高將返回到options.outWidth與options.outHeight中;反之返回。主要用於只需擷取解碼後的Bitmap的大小。 |
|
| boolean |
inMutable |
為true,代表返回可變屬性的Bitmap,反之不可變 |
|
| boolean |
inPreferQualityOverSpeed |
為true,將在解碼過程中犧牲解碼的速度來擷取更高品質的Bitmap |
|
| Bitmap.Config |
inPreferredConfig |
根據指定的Config來進行解碼,例如:Bitmap.Config.RGB_565等 |
|
| int |
inSampleSize |
如果值大於1,在解碼過程中將按比例返回占更小記憶體的Bitmap。例如值為2,則對寬高進行縮放一半。 |
|
| boolean |
inScaled |
如果為true,且inDesity與inTargetDensity都不為0,那麼在載入過程中將會根據inTargetDensityl來縮放,在drawn中不依靠於圖片自身的縮放屬性。 |
|
| int |
inDensity |
Bitmap自身的密度 |
|
| int |
inTargetDensity |
Bitmap drawn過程中使用的密度 |
|
我們在來看下decodeFile的源碼
public static Bitmap decodeFile(String pathName, Options opts) { validate(opts); Bitmap bm = null; InputStream stream = null; try { stream = new FileInputStream(pathName); bm = decodeStream(stream, null, opts); } catch (Exception e) { /* do nothing. If the exception happened on open, bm will be null. */ Log.e("BitmapFactory", "Unable to decode stream: " + e); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { // do nothing here } } } return bm; }
內部根據檔案路徑建立FileInputStream,最終調用decodeStream方法進解碼圖片。
decodeStream & decodeResourceStream
至於decodeStream內部則是根據不同的InputStream類型調用不同的native方法。如果為AssetManager.AssetInputStrea類型則調用
nativeDecodeAsset(asset, outPadding, opts);
否則調用
nativeDecodeStream(is, tempStorage, outPadding, opts);
還有對應的decodeResourceStream方法內部也是調用了decodeStream.
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { validate(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); }
decodeResourceStream內部做了對Bitmap的密度適配,最後再調用decodeStream,這樣decodeStream也分析完畢。
decodeResource
decodeResource用法也很簡單,傳入相應本地的資源檔即可,需要縮放的話配置options參數。
val options = BitmapFactory.Options()val bitmap = BitmapFactory.decodeResource(resources, R.drawable.yaodaoji, options)
隨便看下它的源碼
public static Bitmap decodeResource(Resources res, int id, Options opts) { validate(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; }
通過res.openRawResource(id, value)來擷取InputStream,最後再調用decodeResourceStream(res, value, is, null, opts)方法。這樣就簡單了,又回到了上面分析的方法中去了。
總結
下面來做個總結,對於圖片壓縮主要使用到Bitmap與BitmapFactory這兩個類,同時在使用這兩個類之前也要對Bitmap中的CompressFormat與Config;BitmapFactory中的BitmapFactory.Options有所瞭解。然後再結合他們中的方法進行相應的壓縮、裁剪操作。其實只要掌握幾個常用的方法在日常的使用中就足夠了。
最後如有有不足之處,希望指出!
推薦
tensorflow-梯度下降,有這一篇就足夠了
Android共用動畫相容實現
Kotlin最佳實務
RecyclerView下拉重新整理與上拉更多
七大排序演算法總結