Bitmap的圖片壓縮彙總

怪談時間發表於2018-03-29

前言

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過載方法,主要用來對原圖片進行裁剪。我們直接看它的使用方式:

//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的圖片壓縮彙總

最後通過傳遞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
複製程式碼

Bitmap的圖片壓縮彙總

再來看下其實現原始碼

    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
複製程式碼

Bitmap的圖片壓縮彙總

下面的圖片比上面的圖片寬高都縮小了一半,對圖片進行了壓縮操作。通過程式碼發現,下面的圖片解碼時設定了

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下拉重新整理與上拉更多

七大排序演算法總結

相關文章