效能優化04-圖片優化

weixin_33976072發表於2018-05-16

效能優化04-圖片優化

一、圖片壓縮

圖片在APP中通常佔用很大的記憶體,所以經常需要進行圖片壓縮。

常用的圖片壓縮方式:尺寸壓縮、質量壓縮、格式轉換。

1.尺寸壓縮

將一張大圖載入進記憶體時,需要先進行尺寸壓縮,不然很容易導致oom。

尺寸壓縮既會影響圖片的儲存大小,也會影響圖片的記憶體大小。

尺寸壓縮使用 BitmapFactory.Options,載入2次:

  1. 第一次只載入邊框,獲取圖片尺寸;
  2. 設定壓縮比例,載入完整圖片。
/**
 * 尺寸壓縮
 * @param resources 資源
 * @param id        資源id
 * @param newWidth  壓縮後的寬度
 * @param newHeight 壓縮後的高度
 * @return 壓縮後的Btmap
 */
public static Bitmap sizeCompress(Resources resources, int id, float newWidth, float newHeight) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    //開始讀入圖片,此時把options.inJustDecodeBounds 設回true了
    options.inJustDecodeBounds = true;

    Bitmap bitmap = BitmapFactory.decodeResource(resources, id, options);
    options.inJustDecodeBounds = false;
    int w = options.outWidth;
    int h = options.outHeight;
    //縮放比。由於是固定比例縮放,只用高或者寬其中一個資料進行計算即可
    int sacle = (int) (w / newWidth > h / newHeight ? w / newWidth : h / newHeight);
    sacle = sacle <= 0 ? 1 : sacle;
    options.inSampleSize = sacle;//設定縮放比例
    //重新讀入圖片,注意此時已經把options.inJustDecodeBounds 設回false了
    bitmap = BitmapFactory.decodeResource(resources, id, options);
    return bitmap;
}

注意:壓縮比例可以設定任意值,但是實際壓縮比例一定是2的n次方。

2.質量壓縮

質量壓縮只會影響圖片的儲存大小,不會影響圖片的記憶體大小。另外,將質量壓縮後的圖片轉化為二進位制資料進行傳輸時,資料也變小了。比如微信分享圖片。

Bitmap.compress()是系統提供的質量壓縮工具。Bitmap.compress()使用不完整的Skia庫,對jpeg的處理基於libjpeg,對png則是基於libpng。 libjpeg庫壓縮圖片時,可設定哈夫曼編碼。但由於cpu的緣故,7.0之前沒開開啟,7.0以後才開啟。

/**
 * 質量壓縮
 * @param bitmap         原圖
 * @param compressFormat 圖片型別:JPEG,PNG,WEBP;
 * @param quality        質量
 * @return 壓縮後的Bitmap
 */
public static Bitmap qualityCompress(Bitmap bitmap, Bitmap.CompressFormat compressFormat, int quality) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    bitmap.compress(compressFormat, quality, baos);
    byte[] bytes = baos.toByteArray();
    Bitmap bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    return bm;
}

另外,介紹一個第三方的質量壓縮框架LibJpeg-turbo。下載地址:https://libjpeg-turbo.org/

3.格式轉換

安卓常用的圖片格式有JPEG,PNG和WEBP。

JPEG是一種針對照片視訊而廣泛使用的一種壓縮標準方法。

  • 常用的.jpg檔案是有失真壓縮
  • 不支援背景透明
  • 適用於照片等色彩豐富的大圖壓縮
  • 不適用於logo,線圖

PNG即行動式網路圖形(Portable Network Graphics,PNG)是一種無失真壓縮的點陣圖圖形格式,支援索引、灰度、RGB三種顏色方案以及Alpha通道等特性。Android開發中的切圖素材多為.png格式。

  • 支援256色調色盤技術以產生小體積檔案
  • 最高支援48位真彩色影像以及16位灰度影像。
  • 支援Alpha通道的透明/半透明特性。
  • 支援影像亮度的Gamma校準資訊。
  • 支援儲存附加文字資訊,以保留影像名稱、作者、版權、創作時間、註釋等資訊。
  • 使用無失真壓縮。
  • 漸近顯示和流式讀寫,適合在網路傳輸中快速顯示預覽效果後再展示全貌。
  • 使用CRC防止檔案出錯。
  • 最新的PNG標準允許在一個檔案記憶體儲多幅影像。

WEBP是一種同時提供了有失真壓縮與無失真壓縮的圖片檔案格式,派生自視訊編碼格式VP8,是由Google在購買On2 Technologies後發展出來,以BSD授權條款釋出。Android 4.0+預設支援WebP,Android 4.2.1+開始支援無損WebP和帶alpha通道的WebP。

  • 具有更優的影像資料壓縮演算法,能帶來更小的圖片體積,而且擁有肉眼識別無差異的影像質量
  • 無損的WebP圖片比PNG小26%,有損的WebP圖片比JPEG小25-34%
  • 相較編碼JPEG檔案,編碼同樣質量的WebP檔案也需要佔用更多的計算資源
  • 具備了無損和有損的壓縮模式、Alpha 透明以及動畫的特性
  • 在 JPEG 和 PNG 上的轉化效果都非常優秀、穩定和統一
  • 無損WebP支援透明及alpha通道,有損在一定條件下同樣支援

如果不考慮相容性,那麼使用有損的WebP圖片,記憶體最省;使用無損的WebP圖片,效能最優。

另外,將PNG和JPEG轉換層WEBP格式,可以在AndroidStudio上,選中圖片,右鍵選擇Converting Images to Webp來進行轉換。

如果考慮相容性,那麼使用JPG,記憶體更省;使用PNG,效果最好。PNG和JPEG的轉換有很多工具,這裡就不說了。

二、色彩模式

圖片在記憶體中的大小,由畫素和色彩模式決定。

色彩模式可以說是每個畫素所佔的位元組數,決定了圖片的精細度。常見的色彩模式:RGB_565、ARGB_4444、ARGB_8888。

ARGB的含義:A代表alpha 通道,即透明度;R代表Red,紅色;G代表Green,綠色;B代表Blue,藍色。

RGB_565:R佔5位,G佔6位,B佔5位,共16位,即2個位元組;

ARGB_4444:A佔4位,R佔4位,G佔4位,B佔4位,共16位,即2個位元組;

ARGB_8888:A佔8位,R佔8位,G佔8位,B佔8位,共32位,即4個位元組;

RGB_565和ARGB_4444的所佔記憶體小於ARGB_8888,精細度也低於ARGB_8888。不過從肉眼,很難看出三者的差別。

/**
 * 設定圖片色彩模式
 *
 * @param bitmap 點陣圖
 * @param config 色彩模式,常用的:RGB_565,ARGB_4444ARGB_8888。
 * @return 返回圖片大小
 */
public static long setConfig(Bitmap bitmap, Bitmap.Config config) {
    if (bitmap == null) {
        return 0;
    }
    Bitmap newBitmap = bitmap.copy(config, true);
    long size = newBitmap.getByteCount();
    return size;
}

三、圖片快取

展示圖片時,經常需要重新載入圖片。對於重複載入的圖片,如果使用快取,就會提高效能。

1.記憶體快取

當介面重新整理時,就會重新載入圖片。如果重新建立一個Bitmap物件,就很耗效能,這個時候可以使用LruCache來快取Bitmap物件。

LruCache<String, Bitmap> lruCache;
public void test(View view) {
    long maxMemory = Runtime.getRuntime().maxMemory();
    int cacheSize = (int) (maxMemory / 8);
    if (lruCache == null) {
        lruCache = new LruCache<String, Bitmap>(cacheSize){  
        //必須重寫此方法,來測量Bitmap的大小  
        @Override  
        protected int sizeOf(String key, Bitmap value) {  
            return value.getRowBytes() * value.getHeight();  
        };         
    }
    Bitmap bitmap = lruCache.get("T");
    if (bitmap == null) {
        bitmap = ToolBitmap.sizeCompress(getResources(), R.mipmap.wangyiyun_icon, 400, 400);
        lruCache.put("T", bitmap);
    }
    ImageView img = findViewById(R.id.img);
    img.setImageBitmap(bitmap);
}

LruCache使用了Lru演算法(最近使用的優先順序最高,最少使用的優先順序最低),通過LinkedHashMap來存取物件,每次取資料後,把它放在連結串列最後面;每次新增資料時,也是放在連結串列最後面,同時檢查快取大小,如果超過閥值,就移除連結串列最前面的資料。

2.檔案快取

當Activity重新啟動時,可能需要展示之前的圖片,這是時候就需要檔案快取,我們使用第三方的DiskLruCache。

Github地址:https://github.com/JakeWharton/DiskLruCache

使用參考:https://www.jianshu.com/p/0c56dc217917

3.Bitmap複用

Bitmap複用:不再使用的Bitamp可分配給其他圖片使用。

通過bitmap複用,減少頻繁申請記憶體帶來的效能問題。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;//必須設為true,否則不能被複用
//被複用的bitmap
Bitmap oldbitmap = BitmapFactory.decodeResource(getResources(),R.drawable.lance,options);

//複用oldbitmap
options.inBitmap = oldbitmap;
Bitmap newbitmap = BitmapFactory.decodeResource(getResources(),R.drawable.lance,options);

在4.4之前,Bitmap格式必須為jpg、png,inSampleSize為1才能複用,並且複用的bitmap與被複用的bitmap同等寬高。

在4.4以後:被複用的Bitmap的記憶體必須大於等於複用的bitmap的記憶體。

四、超大圖載入

超大圖載入的核心是BitmapRegionDecoder。BitmapRegionDecoder主要用於顯示圖片的某一塊矩形區域。

BitmapRegionDecoder的使用很簡單:

//isShareable為false會複製一張圖片,為true會共用
BitmapRegionDecoder Decoder = BitmapRegionDecodeBitmapRegionDecoder r.newInstance(is, false);
//獲取指定區域的bitmap。mRect為區域的矩陣,mOptions點陣圖片的配置
Bitmap mBitmap = mDecoder.decodeRegion(mRect, mOptions);

我們通常載入超大圖,都是使用自定義控制元件。自定義控制元件有兩個關鍵:

  1. 使用BitmapRegionDecoder載入圖片;
  2. 重寫事件監聽,實現圖片拖動;

下面貼出關鍵程式碼:

/**
 * 設定圖片
 *
 * @param is 圖片的輸入流
 */
public void setImage(InputStream is) {
    mOptions.inJustDecodeBounds = true;//載入邊框
    BitmapFactory.decodeStream(is, null, mOptions);//獲取圖片寬高
    mImageWidth = mOptions.outWidth;
    mImageHeight = mOptions.outHeight;
    mOptions.inMutable = true;//複用Bitmap TODO
    mOptions.inPreferredConfig = Bitmap.Config.RGB_565;//設定畫素格式
    mOptions.inJustDecodeBounds = false;//載入圖片
    try {
        mDecoder = BitmapRegionDecoder.newInstance(is, false);//isShareable為false會複製一張圖片,為true會共用
    } catch (IOException e) {
        e.printStackTrace();
    }
    requestLayout();//重新佈局
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (mDecoder == null) {
        return;
    }

    mViewWidth = getMeasuredWidth();//測量控制元件的寬
    mViewHeight = getMeasuredHeight();//測量控制元件的高
    mSacle = (float) mViewWidth / mImageWidth;

    //獲取載入區域
    mRect.left = 0;
    mRect.top = 0;
    mRect.right = mImageWidth;
    mRect.bottom = (int) (mViewHeight / mSacle);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mDecoder == null) {
        return;
    }
    mOptions.inBitmap = mBitmap;//複用mBitmap
    mBitmap = mDecoder.decodeRegion(mRect, mOptions);
    Matrix matrix = new Matrix();//通過矩陣縮放
    matrix.setScale(mSacle, mSacle);//設定水平和垂直方向的縮放
    canvas.drawBitmap(mBitmap, matrix, null);
}

/**
 * 重寫onScroll,調整圖片的矩陣顯示區域
 */
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    mRect.offset(0, (int) distanceY);//垂直滑動圖片
    if (mRect.bottom > mImageHeight) {
        mRect.top = (int) (mImageHeight - mViewHeight / mSacle);
        mRect.bottom = mImageHeight;
    }
    if (mRect.top < 0) {
        mRect.top = 0;
        mRect.bottom = (int) (mViewHeight / mSacle);
    }
    invalidate();//重繪
    return false;
}

最後

程式碼地址:https://gitee.com/yanhuo2008/Common/blob/master/Tool/src/main/java/gsw/tool/ui/BigView.java

效能優化專題:https://www.jianshu.com/nb/25128595

喜歡請點贊,謝謝!

相關文章