Android Bitmap優化

renxhui發表於2019-08-19

概述

在日常開發中我們經常遇到載入圖片報出oom的錯誤,我們要解決這個問題,首先要明白oom代表out of memory 記憶體溢位,因為手機記憶體有限,分給每個應用的記憶體有限,所以要解決這個問題就是要解決圖片佔用記憶體問題 android 中圖片是以bitmap的形式存在的,那麼bitmap中所佔的記憶體,直接影響到了是否oom,我們瞭解一下bitmap的佔用記憶體的計算方法

Bitmap到底佔多大記憶體

從本地載入或者從網路載入可以用下面的公式計算

圖片的長度 * 圖片的寬度 * 一個畫素點佔用的位元組數
複製程式碼

如果從資原始檔夾載入,會怎麼樣?

首先把同一張圖片放進不同的資原始檔夾會發生什麼?

  • 同一張圖片放進不同的資料夾,圖片會被壓縮

看下原始碼

if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
    const int density = env->GetIntField(options, gOptions_densityFieldID);
    const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
    const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
    if (density != 0 && targetDensity != 0 && density != screenDensity) {
        scale = (float) targetDensity / density;
    }
}
...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();

if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
if (willScale) {
    const float sx = scaledWidth / float(decoded->width());
    const float sy = scaledHeight / float(decoded->height());
    bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
    bitmap->allocPixels(&javaAllocator, NULL);
    bitmap->eraseColor(0);
    SkPaint paint;
    paint.setFilterBitmap(true);
    SkCanvas canvas(*bitmap);
    canvas.scale(sx, sy);
    canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
複製程式碼

我們可以看到壓縮比例是由下面的公式得出

 scale = (float) targetDensity / density;
複製程式碼

及縮放的比例和targetDensity,density有關,那麼這個倆個變數又代表著什麼呢?

  • targetDensity:裝置螢幕畫素密度 dpi
  • density:圖片對應的資料夾的畫素密度 dpi

其中density和Bitmap存放的資源目錄有關,不同的資源目錄有不同的值

density 0.75 1 1.5 2 3 3.5 4
densityDpi 120 160 240 320 480 560
DpiFolder ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi

可以得出以下結論

  • 同一張圖片放在不同的資源目錄下,其解析度會有變化
  • Bitmap的解析度越高,其解析後的寬高越小,甚至小於原有的圖片(及縮放),從而記憶體也響應的減少
  • 圖片不放置任何資源目錄時,其使用預設解析度mdpi:160
  • 資源目錄解析度和螢幕解析度一致時,圖片尺寸不會縮放

所以Bitmap在資源目錄中的計算方式為

Bitmap記憶體佔用 ≈ 畫素資料總大小 = 圖片寬 × 圖片高× (當前裝置密度dpi/圖片所在資料夾對應的密度dpi)^2 × 每個畫素的位元組大小
複製程式碼

Bitmap記憶體優化從下面四個方面進行優化

  • 編碼
  • 取樣
  • 複用
  • 匿名共享區

下面我們一個個的來講這些優化

編碼

Android 中提供一下幾種編碼

在這裡插入圖片描述
其中,A代表透明度;R代表紅色;G代表綠色;B代表藍色。

  • ALPHA_8 表示8位Alpha點陣圖,即A=8,一個畫素點佔用1個位元組,它沒有顏色,只有透明度
  • ARGB_4444 表示16位ARGB點陣圖,即A=4,R=4,G=4,B=4,一個畫素點佔4+4+4+4=16位,2個位元組
  • ARGB_8888 表示32位ARGB點陣圖,即A=8,R=8,G=8,B=8,一個畫素點佔8+8+8+8=32位,4個位元組
  • RGB_565 表示16位RGB點陣圖,即R=5,G=6,B=5,它沒有透明度,一個畫素點佔5+6+5=16位,2個位元組

也即是說我們可以通過改變圖片格式,來改變每個畫素佔用位元組數,來改變佔用的記憶體,看下面程式碼

 BitmapFactory.Options options = new BitmapFactory.Options();
        //不獲取圖片,不載入到記憶體中,只返回圖片屬性
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(photoPath, options);
        //圖片的寬高
        int outHeight = options.outHeight;
        int outWidth = options.outWidth;
        Log.d("mmm", "圖片寬=" + outWidth + "圖片高=" + outHeight);
        //圖片格式壓縮
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFile(photoPath, options);
        float bitmapsize = getBitmapsize(bitmap);
        Log.d("mmm","壓縮後:圖片佔記憶體大小" + bitmapsize + "MB / 寬度=" + bitmap.getWidth() + "高度=" + bitmap.getHeight());
複製程式碼

看下log

07-09 11:10:46.042 15312-15312/com.example.jh.rxhapp D/mmm: 原圖:圖片佔記憶體大小=45.776367MB / 寬度=4000高度=3000
07-09 11:10:46.043 15312-15312/com.example.jh.rxhapp D/mmm: 圖片寬=4000圖片高=3000
07-09 11:10:46.367 15312-15312/com.example.jh.rxhapp D/mmm: 壓縮後:圖片佔記憶體大小22.887695MB / 寬度=4000高度=3000
複製程式碼

寬高沒變,我們改變了圖片的格式,從ARGB_8888 變成了RGB_565 ,畫素佔用位元組數減少了一般,根據log 記憶體也減少了一半,這種方式可行

注意:由於ARGB_4444的畫質慘不忍睹,一般假如對圖片沒有透明度要求的話,可以改成RGB_565,相比ARGB_8888將節省一半的記憶體開銷。

取樣

我們瞭解到了計算bitmap的佔用記憶體的方法 ,是以bitmap的寬高和每個畫素佔用的位元組數決定的,下面我們分別講一下倆個 的概念

1 bitmap的寬高

顧名思義,圖片的大小就是bitmap的寬高,按公式我們可以縮減bitmap的寬高來達到壓縮圖片佔用記憶體的目的,看下面程式碼,以縮減寬高來達到壓縮的目的

  BitmapFactory.Options options = new BitmapFactory.Options();
        //不獲取圖片,不載入到記憶體中,只返回圖片屬性
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(photoPath, options);
        //圖片的寬高
        int outHeight = options.outHeight;
        int outWidth = options.outWidth;
        Log.d("mmm", "圖片寬=" + outWidth + "圖片高=" + outHeight);
        //計算取樣率
        int i = utils.computeSampleSize(options, -1, 1000 * 1000);
        //設定取樣率,不能小於1 假如是2 則寬為之前的1/2,高為之前的1/2,一共縮小1/4 一次類推
        options.inSampleSize = i;
        Log.d("mmm", "取樣率為=" + i);
        //圖片格式壓縮
        //options.inPreferredConfig = Bitmap.Config.RGB_565;
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFile(photoPath, options);
        float bitmapsize = getBitmapsize(bitmap);
        Log.d("mmm","壓縮後:圖片佔記憶體大小" + bitmapsize + "MB / 寬度=" + bitmap.getWidth() + "高度=" + bitmap.getHeight());
複製程式碼

看下列印資訊

07-09 11:02:11.714 8010-8010/com.example.jh.rxhapp D/mmm: 原圖:圖片佔記憶體大小=45.776367MB / 寬度=4000高度=3000
07-09 11:02:11.715 8010-8010/com.example.jh.rxhapp D/mmm: 圖片寬=4000圖片高=3000
07-09 11:02:11.715 8010-8010/com.example.jh.rxhapp D/mmm: 取樣率為=4
07-09 11:02:11.944 8010-8010/com.example.jh.rxhapp D/mmm: 壓縮後:圖片佔記憶體大小1.4296875MB / 寬度=1000高度=750
複製程式碼

這種我們根據BitmapFactory 的取樣率進行壓縮 設定取樣率,不能小於1 假如是2 則寬為之前的1/2,高為之前的1/2,一共縮小1/4 一次類推,我們看到log ,確實起到了壓縮的目的

複用

圖片複用指的是inBitmap這個屬性

這個屬性又什麼作用?

不使用這個屬性,你載入三張圖片,系統會給你分配三份記憶體空間,用於分別儲存這三張圖片

如果用了inBitmap這個屬性,載入三張圖片,這三張圖片會指向同一塊記憶體,而不用開闢三塊記憶體空間

inBitmap的限制

  • 3.0-4.3
    • 複用的圖片大小必須相同
    • 編碼必須相同
  • 4.4以上
    • 複用的空間大於等於即可
    • 編碼不必相同
  • 不支援WebP
  • 圖片複用,這個屬性必須設定為true; options.inMutable = true;

匿名共享記憶體(Ashmem)

Android 系統為了程式間共享資料開闢的一塊記憶體區域,由於這塊區域不受應用的Head的大小限制,相當於可以繞開oom,FaceBook的Fresco首次應用到實際中

限制:5.0以後就限制了匿名共享記憶體的使用

圖片到底儲存在哪裡?

2.3- 3.0-4.4 5.0-7.1 8.0
Bitmap物件 java Heap java Heap java Heap
畫素資料 Native Heap java Heap Native Heap
遷移原因 - 解決Native Bitmap記憶體洩露 共享整個系統的記憶體減少OOM

8.0Bitmap的畫素資料儲存在Native,為什麼又改為Native儲存呢?

因為8.0共享了整個系統的記憶體,測試8.0手機如果一直建立Bitmap,如果手機記憶體有1G,那麼你的應用載入1G也不會oom

LRU管理Bitmap

我們可以利用LRU開管理Bitmap,給他設定記憶體最大值,及時回收

圖片的壓縮

圖片的壓縮一般有倆種

  • 通過取樣壓縮,上邊已經講過了
  • 質量壓縮
bitmap.compress(Bitmap.CompressFormat.JPEG, 20, 
new FileOutputStream("sdcard/result.jpg"));
複製程式碼

這個大家用該都用過,這個壓縮是保持畫素的前提下改變圖片的位深及透明度,來達到壓縮的目的,不過這種壓縮不會改變圖片在記憶體中的帶下,而且這種壓縮會導致圖片的失真,但是有沒有壓縮到100k左右,還不失真的方法?

推薦看下這個部落格 www.jianshu.com/p/06a1cae9c…


如何載入高清圖

如果有需求,要求我們既不能壓縮圖片,又不能發生oom怎麼辦,這種情況我們需要載入圖片的一部分割槽域來顯示,下面我們來了解一下BitmapRegionDecoder這個類,載入圖片的一部分割槽域,他的用法很簡單

//支援傳入圖片的路徑,流和圖片修飾符等
   BitmapRegionDecoder mDecoder = BitmapRegionDecoder.newInstance(path, false);
//需要顯示的區域就有由rect控制,options來控制圖片的屬性
    Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
複製程式碼

由於要顯示一部分割槽域,所以要有手勢的控制,方便上下的滑動,需要自定義控制元件,而自定義控制元件的思路也很簡單 1 提供圖片的入口 2 重寫onTouchEvent, 根據手勢的移動更新顯示區域的引數 3 更新區域引數後,重新整理控制元件重新繪製

下面是完整程式碼

public class BigImageView extends View {

    private BitmapRegionDecoder mDecoder;
    private int mImageWidth;
    private int mImageHeight;
    //圖片繪製的區域
    private Rect mRect = new Rect();
    private static final BitmapFactory.Options options = new BitmapFactory.Options();

    static {
        options.inPreferredConfig = Bitmap.Config.RGB_565;
    }

    public BigImageView(Context context) {
        super(context);
        init();
    }

    public BigImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public BigImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

    }

    /**
     * 自定義view的入口,設定圖片流
     *
     * @param path 圖片路徑
     */
    public void setFilePath(String path) {
        try {
            //初始化BitmapRegionDecoder
            mDecoder = BitmapRegionDecoder.newInstance(path, false);
            BitmapFactory.Options options = new BitmapFactory.Options();
            //便是隻載入圖片屬性,不載入bitmap進入記憶體
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(path, options);
            //圖片的寬高
            mImageWidth = options.outWidth;
            mImageHeight = options.outHeight;
            Log.d("mmm", "圖片寬=" + mImageWidth + "圖片高=" + mImageHeight);

            requestLayout();
            invalidate();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //獲取本view的寬高
        int measuredHeight = getMeasuredHeight();
        int measuredWidth = getMeasuredWidth();


        //預設顯示圖片左上方
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = mRect.left + measuredWidth;
        mRect.bottom = mRect.top + measuredHeight;
    }

    //第一次按下的位置
    private float mDownX;
    private float mDownY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();
                mDownY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float moveX = event.getX();
                float moveY = event.getY();
                //移動的距離
                int xDistance = (int) (moveX - mDownX);
                int yDistance = (int) (moveY - mDownY);
                Log.d("mmm", "mDownX=" + mDownX + "mDownY=" + mDownY);
                Log.d("mmm", "movex=" + moveX + "movey=" + moveY);
                Log.d("mmm", "xDistance=" + xDistance + "yDistance=" + yDistance);
                Log.d("mmm", "mImageWidth=" + mImageWidth + "mImageHeight=" + mImageHeight);
                Log.d("mmm", "getWidth=" + getWidth() + "getHeight=" + getHeight());
                if (mImageWidth > getWidth()) {
                    mRect.offset(-xDistance, 0);
                    checkWidth();
                    //重新整理頁面
                    invalidate();
                    Log.d("mmm", "重新整理寬度");
                }
                if (mImageHeight > getHeight()) {
                    mRect.offset(0, -yDistance);
                    checkHeight();
                    invalidate();
                    Log.d("mmm", "重新整理高度");
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
        }
        return true;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    /**
     * 確保圖不劃出螢幕
     */
    private void checkWidth() {


        Rect rect = mRect;
        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        if (rect.right > imageWidth) {
            rect.right = imageWidth;
            rect.left = imageWidth - getWidth();
        }

        if (rect.left < 0) {
            rect.left = 0;
            rect.right = getWidth();
        }
    }

    /**
     * 確保圖不劃出螢幕
     */
    private void checkHeight() {

        Rect rect = mRect;
        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        if (rect.bottom > imageHeight) {
            rect.bottom = imageHeight;
            rect.top = imageHeight - getHeight();
        }

        if (rect.top < 0) {
            rect.top = 0;
            rect.bottom = getHeight();
        }
    }
}

複製程式碼

程式碼行有註釋,應該很好理解了

參考https://www.jianshu.com/p/3f6f6e4f1c88 www.jianshu.com/p/e49ec7d05…

相關文章