Android進階學習(12)-- 圖片加載性能優化(1)

前言

最近自己做了一個app,列表中有大量圖片需要加載,毫無任何處理的情況下佔用的內存可達250M之上:
在這裏插入圖片描述
所以需要對所有的圖片進行優化處理,那麼優化主要有以下兩個方面:

  1. 圖片加載時優化
  2. 圖片的緩存

圖片內存

首先需要了解啥圖片的內存是如何計算出來的;我們一半所說的圖片寬高就是鼠標右鍵圖片查看詳細信息那裏的像素
在這裏插入圖片描述
圖片是由一個個像素點構成的,圖片的像素點有以下四種格式:

  1. ALPHA_8 //每個像素佔用1byte內存
  2. RGB_565 //每個像素佔用2byte內存
  3. ARGB_4444 //每個像素佔用2byte內存
  4. ARGB_8888 //每個像素佔用4byte內存

圖片佔用的內存 = 寬 * 高 * 像素點格式
在這裏插入圖片描述
這裏要注意:圖片的內存佔用大小,只和它的像素點格式有關,和它的文件格式無關,.png、.jpg、.webp等同樣的圖片不同格式佔用內存是相同的。
但是,在Android項目中,同樣圖片放在不同的文件目錄,所佔用的內存大小是不同的。在BitmapFactory中的Options有一個屬性, inDensity,表示bitmap的像素密度,它是根據不同的文件目錄去賦值的

drawable-ldpi  	120
drawable-mdpi  	160
drawable-hdpi  	240
drawable-xhdpi 	320
drawable-xxhdpi 480

優化場景

什麼時候需要對圖片優化?比如,一個高清大圖在app中只需要顯示在一個比較小的控件上;又或者比如一些特別長的圖,那麼就需要分段加載,用戶滑動到哪裏就加載那一部分;前面兩種都是加載單個圖片的場景,當有圖片列表時就需要緩存,同樣的圖片不要重複創建bitmap對象或是重複從網絡獲取。針對以上場景逐個分析。

大圖片顯示在小控件

在這裏插入圖片描述
以這張圖片爲例,讓他加載到app中

//XML
<ImageView
        android:layout_marginTop="50dp"
        android:id="@+id/ivCover"
        android:layout_width="300dp"
        android:layout_height="200dp"/>

//Java
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.big);
ivCover.setImageBitmap(bitmap);
Log.e("圖片佔用的內存", bitmap.getByteCount() + " byte");

可以看到,當界面只加載這一張圖片,內存也會飆升
在這裏插入圖片描述
在這裏插入圖片描述 從log中可以看出,這個圖片佔用了50M的內存(log中的byte單位),這種情況下,圖片的寬高遠大於View的寬高,我們就可以對他進行縮放;
新建 ImgUtils,寫入以下代碼:

public class ImgUtils {

    /**
     * 對圖片縮放、降低質量
     * @param context
     * @param resId
     * @param showWidth
     * @param showHeight
     * @return
     */
    public static Bitmap resizeBitmap(Context context, int resId, int showWidth, int showHeight) {
        BitmapFactory.Options mOptions = new BitmapFactory.Options();
        mOptions.inMutable = true;
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(context.getResources(), resId, mOptions);
        int width = mOptions.outWidth;
        int height = mOptions.outHeight;
        mOptions.inSampleSize = calcuteInSampleSize(width, height, showWidth, showHeight);
        mOptions.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(context.getResources(), resId, mOptions);
    }

    /**
     * 計算最大縮放比例
     * @param relWidth 真實寬高
     * @param relHeight 真是寬高
     * @param showWidth 顯示在view中的寬高
     * @param showHeight 顯示在view中的寬高
     * @return
     */
    public static int calcuteInSampleSize(int relWidth, int relHeight, int showWidth, int showHeight) {
        Log.d("ImgUtils", "relWidth : " + relWidth + " relHeight : " + relHeight + " showWidth : " + showWidth + " showHeight : " + showHeight  );
        int inSampleSize = 1;
        if (relWidth > showWidth && relHeight > showHeight) {
            inSampleSize = 2;
            while ((relWidth /= 2) > showWidth && (relHeight /= 2) > showHeight) {
                inSampleSize *= 2;
            }
        }
        Log.d("ImgUtils", "calcuteInSampleSize : " + inSampleSize);
        return inSampleSize;
    }
}

修改activity中的代碼:

Bitmap resizeBitmap = ImgUtils.resizeBitmap(this, R.drawable.big, 300, 200);
Log.e("圖片內存_優化一", resizeBitmap.getByteCount() + " byte");
ivCover.setImageBitmap(resizeBitmap);

運行後會發現native大幅度下降
在這裏插入圖片描述
看一下log
在這裏插入圖片描述
優化了將近二十倍;上面的代碼中,最核心的就是給 Bitmap 設置了 inSampleSize 屬性;inSampleSize 就是取圖片寬高的幾分之一,如果一個圖片的寬高都是100像素,inSampleSize 等於2 的情況下,bitmap分別取圖片寬高的 二分之一,那麼也就意味着圖片佔用的內存是原來的 四分之一;當然,這樣縮放是會造成圖片失真,所有一定要注意 inSampleSize 的大小;

除了設置 inSampleSize,之前還說了圖片的像素格式,RGB_565 佔用 2字節,是ARGB_8888 的一半,也可以修改圖片的像素格式達到減少內存佔用的目的;
在ImgUtils類中的resizeBitmap方法加入以下代碼測試:

public class ImgCacheUtils {

    public static Bitmap resizeBitmap(Context context, int resId, int showWidth, int showHeight) {
        BitmapFactory.Options mOptions = new BitmapFactory.Options();
        mOptions.inMutable = true;
        // 修改圖片像素格式
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(context.getResources(), resId, mOptions);
        int width = mOptions.outWidth;
        int height = mOptions.outHeight;
        mOptions.inSampleSize = calcuteInSampleSize(width, height, showWidth, showHeight);
        mOptions.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(context.getResources(), resId, mOptions);
    }

}

運行後,看一下log
在這裏插入圖片描述
和沒加入修改格式代碼時相比,內存佔用又縮小了一半;同樣,這樣也會讓圖片失真,在處理時一定要考慮圖片的效果問題;

超長圖片處理

遇到特別長的圖片,我們就需要讓他局部加載,就需要我們自定義View;主要實現的功能:只加載屏幕上可見的部分,用戶滑動時改變可見區域,既然滑動,那麼也要實現滑動的邏輯。圖片壓縮處理,依舊採用上面工具類中的方法,對 inSimpleSize 進行修改,和修改圖片的像素格式
新建LongImageView:

public class LongImageView extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {

    //View 滑動相關
    private GestureDetector mGestureDetector;
    private Scroller mScroller;
    //可見的矩形區域
    Rect mRect;
    BitmapFactory.Options mOptions;
    BitmapRegionDecoder mBitmapRegionDecoder;
    //圖片寬高
    int mImageWidth;
    int mImageHeight;
    //View的寬高
    int mViewHeight;
    int mViewWidth;
    //縮放比例
    float mZoom;
    Bitmap bitmap = null;

    public LongImageView(Context context) {
        this(context, null, 0);
    }

    public LongImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LongImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mGestureDetector = new GestureDetector(context, this);
        setOnTouchListener(this);
        mScroller = new Scroller(context);
        mRect = new Rect();
        mOptions = new BitmapFactory.Options();
    }

    public void setImage(InputStream in) {
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(in, null, mOptions);
        //獲取圖片寬高
        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;
        //設置圖片格式
        mOptions.inMutable = true;
        //mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        mOptions.inJustDecodeBounds = false;

        try {
            // 第二個參數爲 false 表示 輸入流關閉時 不受影響
            mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(in, false);
        } catch (IOException e) {
            e.printStackTrace();
        }

        requestLayout();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mBitmapRegionDecoder == null){
            return;
        }
        mOptions.inBitmap = bitmap;
        bitmap = mBitmapRegionDecoder.decodeRegion(mRect, mOptions);

        Matrix matrix = new Matrix();
        matrix.setScale(mZoom * mOptions.inSampleSize, mZoom * mOptions.inSampleSize);
        Log.e("佔用的內存", bitmap.getByteCount() + " byte");
        canvas.drawBitmap(bitmap, matrix, null);
    }

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

        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();

        if (mBitmapRegionDecoder == null){
            return;
        }

        //設置矩形區域
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = mImageWidth;
        //根據圖片縮放程度 計算出圖片顯示的高度
        mZoom = (float)mViewWidth / (float)mImageWidth;
        mRect.bottom = (int) (mViewHeight / mZoom);

        mOptions.inMutable = true;
        mOptions.inSampleSize = ImgUtils.calcuteInSampleSize(mImageWidth, mImageHeight, mViewWidth, mViewHeight);
    }

    /**
     * 用戶 觸摸 屏幕
     * @param e
     * @return
     */
    @Override
    public boolean onDown(MotionEvent e) {
        if (!mScroller.isFinished()){
            mScroller.forceFinished(true);
        }
        return true;
    }

    /**
     * 用戶 觸摸 屏幕 但是 沒有鬆開 拖動
     * @param e
     */
    @Override
    public void onShowPress(MotionEvent e) {

    }

    /**
     * 用戶 觸摸 屏幕 鬆開後
     */
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    /**
     *  用戶 滑動 屏幕
     */
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        mRect.offset(0, (int) distanceY);
        if(mRect.bottom > mImageHeight){
            mRect.bottom = mImageHeight;
            mRect.top = mImageHeight - (int) (mViewHeight / mZoom);
        }
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mZoom);
        }
        invalidate();
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    /**
     *  用戶 觸摸 屏幕 快速滑動後鬆開
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        mScroller.fling(0, mRect.top, 0, (int) - velocityY, 0, 0,
                0, mImageHeight - (int) (mViewHeight / mZoom));
        return false;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        if (mScroller.isFinished()) {
            return;
        }
        //返回 true 表示 在滑動
        if (mScroller.computeScrollOffset()) {
            mRect.top = mScroller.getCurrY();
            mRect.bottom = mRect.top + (int) (mViewHeight / mZoom);
            invalidate();
        }
    }
}

在這個自定義view中,我們藉助了GestureDetector 手勢檢測 和 Scoller,處理用戶觸摸,滑動等事件;圖像局部顯示利用的是BitmapRegionDecoder,看以下他的api:
在這裏插入圖片描述
當用戶滑動時,不斷通過 computeScroll 方法去計算顯示的位置,並且重繪界面,這裏要注意 滑到頂部 和 滑到底部 時的判斷、邏輯處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章