Bitmap是Android中一種重要的圖片處理機制,它可以用來獲取圖片的相關信息,同時可以對圖片進行裁剪、縮放等操作,也可以指定圖片格式進行保存。相信對於OOM再熟悉不過了,OOM的產生是一個非常頭疼的事情,如果在加載圖片的時候未對大圖進行處理,它將會佔用非常大的內存,這樣就非常容易產生OOM。所以我們必須要有意識的對大圖進行壓縮加載,這樣才能更好的確保App的正常運作與效能的穩定性。
那麼如果計算一張圖片載入過程中所佔的記憶體大小呢?在這之前,我們先來了解一下關於Bitmap兩個主要配置。
這是用來指定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倍。
這是關於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進行圖片的壓縮處理,它主要提供了以下有效方法。
狀態傳回 | 方法名稱 |
---|---|
#boolean | #compress(Bitmap .CompressFormat 格式,int 質量,OutputStream 流) |
靜態位圖 | createBitmap(DisplayMetrics 顯示,int[] 顏色,int 寬度,int 高度,Bitmap.Config config ) |
靜態位圖 | createBitmap(DisplayMetrics 顯示,int[] 顏色,int 偏移量,int 步長,int 寬度,int 高度,Bitmap. Config 設定) |
靜態位圖 | createBitmap(位圖來源, int x, int y, int width, int height) |
#靜態位圖 | createBitmap(Bitmap src) |
靜態位圖 | createBitmap(DisplayMetrics display, int width, int height, Bitmap.Config config) |
靜態位圖 | createBitmap(點陣圖來源,int x,int y,int寬度,int高度,矩陣m,布林過濾器) |
static Bitmap | #createBitmap(int width, int height, Bitmap.Config config) |
#static Bitmap | createBitmap(int[] colors, int偏移量,int 步幅,int 寬度,int 高度,Bitmap.Config config) |
靜態Bitamp | #createBitmap(int[] 顏色,int 寬度,int 高度,Bitmap .Config config) |
靜態位圖 | createScaledBitmap(位圖src,int dstWidth,int dstHeight,布林過濾器) |
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方法,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重载方法,主要用来对原图片进行裁剪。我们直接看它的使用方式:
//bitmap val bitmapBitmap = Bitmap.createBitmap(scaleBitmap, 150, 0, 100, 100) image_view?.setImageBitmap(scaleBitmap) image_view_text.text = "width: " + scaleBitmap.width + " height: " + scaleBitmap.height sub_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进行矩阵变换。
该方法相对上面两种就简单多了,它目的是对原Bitmap进行指定的宽高进行缩放,最终返回新的Bitmap。
注意:如果传入的宽高与原Bitmap相同,它将返回原Bitmap对象。
//createScaledBitmap val createScaledBitmap = Bitmap.createScaledBitmap(scaleBitmap, 500, 300, false) image_view?.setImageBitmap(scaleBitmap) image_view_text.text = "width: " + scaleBitmap.width + " height: " + scaleBitmap.height sub_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主要用来解码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。一般用不到,下面详细分析几种常用的方法。
该方法是通过文件路径来解码出Bitmap
//decodeFile val decodeFileOptions = BitmapFactory.Options() val decodeFileBitmap = BitmapFactory.decodeFile(mRootPath +"bitmap", decodeFileOptions) decodeFileOptions.inSampleSize = 2 val decodeFileScaleBitmap = BitmapFactory.decodeFile(mRootPath + "bitmap", decodeFileOptions) image_view?.setImageBitmap(decodeFileBitmap) image_view_text.text = "width: " + decodeFileBitmap.width + " height: " + decodeFileBitmap.height sub_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内部则是根据不同的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用法也很简单,传入相应本地的资源文件即可,需要缩放的话配置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下拉刷新与上拉更多
七大排序算法总结
相关推荐:
mysql创建Bitmap_Join_Indexes中的约束与索引
以上是Bitmap的圖片壓縮匯總 - Android成長之路的詳細內容。更多資訊請關注PHP中文網其他相關文章!