Android-認識Bitmap

魯迅認識的那隻猹發表於2018-10-24

Android-認識Bitmap

學習自

  • Android開發藝術探索

例行廢話

在Android的各種APP中都被離不開各種各樣的圖片,有的圖片很大,有的圖片很小不管這樣圖片都是一種很吃記憶體的資源,而在Android中每個APP所持有的資源是非常有限的,所以我們要儘可能的“摳門”一點。本著能省則省的原則,有一個 300 x 300 的圖片現在在一個100 x 100 的ImageView中是一個完全不必要的事情。所以我們為了更節省資源和避免OOM,我們必須對圖片進行處理。在Android中 Bitmap 就代表一個圖片資源。

BitmapFactory和Options

我們記載點陣圖資源都是通過 BitmapFactory 進行載入的,這個類提供了以下的方法。

方法名 描述
decodeResource 從資源中載入圖片
decodeByteArray 從byte陣列中載入圖片
decodeFile 從檔案中載入圖片
decodeStream 從流中載入資源

Options

Options類是BitmapFactory的一個巢狀類,所有的對Bitmap進行特殊處理的操作同時通過這個類的引數和屬性達成的。其中一個Bitmap所佔用的記憶體的大小是由下面的幾個引數影響的。

  • inPreferredConfig Bitmap的色彩模式,不同的色彩模式,每個畫素所佔據的位元組的數量是不一樣的。
  • inSampleSize 取樣率,取樣率越大,Bitmap的寬和高就越小,所佔用的記憶體也就越小。

inPreferredConfig

通過 inPreferredConfig 屬性可以設定Bitmap載入的色彩模式,主要有以下幾種色彩模式

  1. ALPHA_8 每個畫素佔據1byte
  2. ARGB_4444 每個畫素佔據2byte
  3. ARGB_8888 每個畫素佔據4byte
  4. RGB_565 每個畫素佔據2byte

ARGB_8888 的顯式效果最好,同時也是Android預設的色彩模式,但是也最為消耗記憶體。 假設一張1024 x 1024,模式為ARGB_8888的圖片,那麼它佔有的記憶體就是: 1024 x 1024 x 4 byte

//ARGB_8888 模式載入的Bitmap
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.wallbackground)
Log.e("TAG", "Width:${bitmap.width} Helight:${bitmap.height}")
Log.e("TAG", "Original:" + getBitmapSize(bitmap))

//Log資訊如下
//Width:5040 Helight:2835
//Size:57153600

//RGB_565 這裡本想使用 ARGB_4444 的測彩模式的,但是列印出來的一直卻和模式的ARGB_8888相同
//可能也跟具體的裝置有關係吧,所以這裡就是用了 RGB_565 的色彩模式來驗證一下不同的色彩模式
//佔中的記憶體大小不同
val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.RGB_565
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.wallbackground,options)
Log.e("TAG", "Width:${bitmap.width} Helight:${bitmap.height}")
Log.e("TAG", "Size:" + getBitmapSize(bitmap))

//Log資訊如下
//Width:5040 Helight:2835
//Size:28576800

fun getBitmapSize(bitmap: Bitmap): Int {
    return bitmap.rowBytes * bitmap.height
}
複製程式碼

inSampleSize 取樣率

取樣率可以Bitmap記憶體優化的重頭戲,上面也提到了,當使用一個大圖載入到一個小圖中的時候這種情況是完全不需要的,比如說 400 x 400 圖片要顯示在 200 x 200 的ImageView上的時候,那麼只需要記載200 x 200 大小的圖片就行了,而如果直接載入 400 x 400 的圖片就憑白地浪費了一倍的空間。所以我們需要通過設定Bitmap的取樣率將圖片縮小。當然最後經過取樣後的圖片最好不要小於 ImageView 的大小,否則就會造成拉伸效果。

現假設要載入一個400 x 400 的圖片,取樣率預設是 1 也就是載入原圖,如果取樣變為 2 那麼 圖片的寬高都會 / 2 所以所佔據的記憶體也就只有原圖的 1/4(1/取樣率的平方) 了。取樣率越大圖片的寬高越小,佔用的記憶體也就越小。 在官方文件中推薦將取樣率設定為2的N次冪(1,2,3,8..),比如說如果取樣率設為 3 那麼取樣率就是 2 ,但是這真是一個參考,在一些機型上的執行效果並不是如此。

NOTE:

  1. 經過取樣後的Bitmap的大小不應該小於 ImageView的大小。
  2. 關於取樣率的計算的最終結果並不會完全地精確,會有一些上下的浮動。
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.wallbackground)
Log.e("TAG", "Size:" + getBitmapSize(bitmap))
//Log: Size:57153600

val options = BitmapFactory.Options()
options.inSampleSize = 2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.wallbackground, options)
Log.e("TAG", "Size:" + getBitmapSize(bitmap))
//Log: 3573360

fun getBitmapSize(bitmap: Bitmap): Int {
    return bitmap.rowBytes * bitmap.height
}
//Log Size:14293440
複製程式碼

優化Bitmap載入

我想通過上面的介紹你已經大概知道了如果優化Bitmap了吧

  1. 在載入圖片的時候首先獲取到圖片的寬和高和ImageView的寬和高(在這一步並不會載入Bitmap僅僅會獲取Bitmap的寬和高)
  2. 根據圖片的寬和高來和ImageView的取樣率
  3. 以指定的取樣率載入Bitmap

頁面的佈局檔案如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="cn.shycoder.studybitmap.MainActivity">

    <Button
        android:id="@+id/btnLoadImg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="Load Img" />

    <ImageView
        android:id="@+id/iv"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center_horizontal" />
</LinearLayout>
複製程式碼

Activity 的程式碼

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun onClick(view: View) {
        loadImageFromResource(iv, R.drawable.wallbackground)
    }

    /**
     * 從資源中得到一個合適大小的Bitmap
     * */
    private fun loadImageFromResource(imageView: ImageView, resId: Int) {
        Log.e("TAG", "ImageView Width:${imageView.width} Height: ${imageView.height}")

        //1. 獲取Bitmap的寬高
        val options = BitmapFactory.Options()
        // 通過設定此選項,載入Bitmap的時候,僅僅會獲取寬和高並不會真正地載入Bitmap
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(this.resources, resId, options)

        //2. 計算對應的圖片的取樣率
        val inSampleSize = this.calculateInSampleSize(options, imageView.width, imageView.height)

        //3.根據取樣率載入圖片
        //記得取消這個選項
        options.inJustDecodeBounds = false
        options.inSampleSize = inSampleSize
        val bitmap = BitmapFactory.decodeResource(this.resources, resId, options)
        imageView.setImageBitmap(bitmap)
    }

    private fun calculateInSampleSize(options: BitmapFactory.Options, requireWidth: Int,
                                      requireHeight: Int): Int {
        val width = options.outWidth
        val height = options.outHeight
        var inSampleSize = 1
        //如果Bitmap的大小是大於ImageView的大小的
        if (width > requireWidth && height > requireHeight) {
            //取樣率的增加
            while ((width / (inSampleSize + 1) > requireWidth)
                    && (height / (inSampleSize + 1) > requireHeight)) {
                inSampleSize += 1
            }
        }

        return inSampleSize
    }
}

複製程式碼

圖片對比

// 原圖所佔記憶體:60466176
// 經過取樣壓縮後:15148960:
複製程式碼

Snag_484e1caf