Bitmap的圖片壓縮彙總

前言

Bitmap是Android中一種重要的圖片處理機制,它能夠用來獲取圖片的相關信息,同時能夠對圖片進行裁剪、縮放等操做,也能夠指定圖片格式進行保存。相信對於OOM再熟悉不過了,OOM的產生是一個很是頭疼的事情,若是在加載圖片的時候未對大圖進行處理,它將會佔用很是大的內存,這樣就很是容易產生OOM。因此咱們必需要有意識的對大圖進行壓縮加載,這樣才能更好的保證App的正常運行與性能的穩定。android

Bitmap大小計算

那麼若是計算一張圖片加載過程當中所佔的內存大小呢?在這以前,咱們先來了解一下關於Bitmap兩個主要配置。git

CompressFormat

這是用來指定Bitmap的圖片的壓縮格式,在Bitmap中是一個Enum結構,主要表現爲如下三種格式。github

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結構,主要表現於如下四種格式。web

  • 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、高度爲67五、Config爲ARGB_8888。那麼它佔的內存爲:1080 x 675 x 4 = 2916000.折算成M爲2916000 / 1024 / 1024 = 2.78M。一種圖片就近3M,若是加載10張或者100張,所佔的內存可想而知。這樣的話會很容易將內存消耗殆盡,同時對於Android App來講根本就不須要這麼高清的圖片,因此咱們在加載圖片的時候能夠對其進行相應的處理,例如:對寬高進行縮放,亦或者將Config改我RGB_565。這樣不只有效的減少了內存的佔用,同時也不影響圖片的清晰度的展現。api

下面咱們來看下如何經過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中。咱們來看下具體用法app

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的格式圖片是沒有透明度的。ide

特別注意的:對於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重載方法,主要用來對原圖片進行裁剪。咱們直接看它的使用方式:

//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判斷。

看下上面的代碼所展現的效果圖:

clipboard.png

最後經過傳遞Bitmap參數還有一個可選參數Matrix,它主要用於對Bitmap進行矩陣變換。

createScaledBitmap

該方法相對上面兩種就簡單多了,它目的是對原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

clipboard.png

再來看下其實現源碼

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

//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

clipboard.png

下面的圖片比上面的圖片寬高都縮小了一半,對圖片進行了壓縮操做。經過代碼發現,下面的圖片解碼時設置了

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有所瞭解。而後再結合他們中的方法進行相應的壓縮、裁剪操做。其實只要掌握幾個經常使用的方法在平常的使用中就足夠了。

最後若有有不足之處,但願指出!

點擊跳轉到項目地址

關注

clipboard.png

推薦

tensorflow-梯度降低,有這一篇就足夠了
Android共享動畫兼容實現
Kotlin最佳實踐
RecyclerView下拉刷新與上拉更多
七大排序算法總結

相關文章
相關標籤/搜索