天天看點

安卓記憶體優化-bitmap優化

Bitmap常用方法:

  • public boolean compress

将位圖的壓縮到指定的OutputStream,可以了解成将Bitmap儲存到檔案中! format:格式,PNG,JPG等; quality:壓縮品質,0-100,0表示最低畫質壓縮,100最大品質(PNG無損,會忽略品質設定) stream:輸出流 傳回值代表是否成功壓縮到指定流!

  • void recycle():

回收位圖占用的記憶體空間,把位圖示記為Dead

  • boolean isRecycled():

判斷位圖記憶體是否已釋放

  • int getWidth():

擷取位圖的寬度

  • int getHeight():

擷取位圖的高度

  • boolean isMutable():

圖檔是否可修改

  • int getScaledWidth(Canvas canvas):

擷取指定密度轉換後的圖像的寬度

  • int getScaledHeight(Canvas canvas):

擷取指定密度轉換後的圖像的高度

  • Bitmap createBitmap(Bitmap src):

以src為原圖生成不可變得新圖像

  • Bitmap createScaledBitmap(Bitmap src, int dstWidth,int dstHeight, boolean filter):

以src為原圖,建立新的圖像,指定新圖像的高寬以及是否變。

  • Bitmap createBitmap(int width, int height, Config config):

建立指定格式、大小的位圖,一般建立空的Bitmap

  • Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)

以source為原圖,建立新的圖檔,指定起始坐标以及新圖像的高寬。

BitmapFactory

options

  • inJustDecodeBounds:

如果将這個值置為true,那麼在解碼的時候将不會傳回bitmap,隻會傳回這個bitmap的尺寸。這個屬性的目的是,如果你隻想知道一個bitmap的尺寸,但又不想将其加載到記憶體時。這是一個非常有用的屬性。

  • inSampleSize:

這個值是一個int,當它小于1的時候,将會被當做1處理,如果大于1,那麼就會按照比例(1 / inSampleSize)縮小bitmap的寬和高、降低分辨率,大于1時這個值将會被處置為2的倍數。例如,width=100,height=100,inSampleSize=2,那麼就會将bitmap處理為,width=50,height=50,寬高降為1 / 2,像素數降為1 / 4。

  • inPreferredConfig:

這個值是設定色彩模式,預設值是ARGB_8888,在這個模式下,一個像素點占用4bytes空間,一般對透明度不做要求的話,一般采用RGB_565模式,這個模式下一個像素點占用2bytes。

  • inPremultiplied:

這個值和透明度通道有關,預設值是true,如果設定為true,則傳回的bitmap的顔色通道上會預先附加上透明度通道。

  • inDither:

這個值和抖動解碼有關,預設值為false,表示不采用抖動解碼。

  • inDensity:

表示這個bitmap的像素密度(對應的是DisplayMetrics中的densityDpi,不是density)。

  • inTargetDensity:

表示要被畫出來時的目标像素密度(對應的是DisplayMetrics中的densityDpi,不是density)。

  • inScreenDensity:

表示實際裝置的像素密度(對應的是DisplayMetrics中的densityDpi,不是density)。

  • inScaled:

設定這個Bitmap是否可以被縮放,預設值是true,表示可以被縮放。

  • inPurgeable和inInputShareable:

這兩個值一般是一起使用,設定為true時,前者表示空間不夠是否可以被釋放,後者表示是否可以共享引用。這兩個值在Android5.0後被棄用。

  • inPreferQualityOverSpeed:

這個值表示是否在解碼時圖檔有更高的品質,僅用于JPEG格式。如果設定為true,則圖檔會有更高的品質,但是會解碼速度會很慢。

  • outWidth和outHeight:

表示這個Bitmap的寬和高,一般和inJustDecodeBounds一起使用來獲得Bitmap的寬高,但是不加載到記憶體。

工廠方法:

  • decodeByteArray(byte[] data, int offset,int length):從指定位元組數組的offset位置開始,将長度為length的位元組資料解析成Bitmap對象。
  • decodeFIle(String pathName):從pathName指定的檔案中解析、建立Bitmap對象。
  • decodeFileDescriptor(FileDescriptor fd):用于從FileDescriptor對應的檔案中解析、建立Bitmap對象。
  • decodeResource(Resource res,int id):用于根據給定的資源ID從指定的資源檔案中解析、建立Bitmap對象。
  • decodeStream(InputStream is):用于從指定輸入流中介解析、建立Bitmap對象。

圖檔占用記憶體

如1920*1080圖檔占用多少記憶體?

每個像素的位元組大小

每個像素的位元組大小由bitmap的可配置的參數Config來決定。

Bitmap中,存在一個枚舉類Config,定義了Android中支援Bitmap配置:

Config 占用位元組大小(byte) 說明
ALPHA_8 1 單透明通道
RGB_565 2 簡單RGB色調
ARGB_4444 2 已廢棄
ARGB_8888 4 24位真彩色
RGBA_F16 8 Android 8.0新增(更豐富色彩表現HDR)
HARDWARE Special Android 8.0新增(Bitmap直接存儲在graphicmemory)

Bitmap加載方式

從擷取方式分:

(1) 以檔案流的方式

假設在sdcard下有 test.png圖檔

FileInputStream fis = new FileInputStream(“/sdcard/test.png”);

Bitmap bitmap=BitmapFactory.decodeStream(fis);

(2) 以R檔案的方式

假設 res/drawable下有 test.jpg檔案

Bitmap bitmap =BitmapFactory.decodeResource(getResources(), R.drawable.test);

BitmapDrawable bitmapDrawable = (BitmapDrawable) getResources().getDrawable(R.drawable. test );

Bitmap bitmap = bitmapDrawable.getBitmap();

(3) 以ResourceStream的方式,不用R檔案

Bitmap bitmap=BitmapFactory.decodeStream(getClass().getResourceAsStream(“/res/drawable/test.png”));

(4) 以檔案流+ R檔案 的方式

InputStream in = getResources(). openRawResource(R.drawable. test );

Bitmap bitmap = BitmapFactory. decodeStream(in);

InputStream in = getResources(). openRawResource(R.drawable. test );

BitmapDrawable bitmapDrawable = new BitmapDrawable(in);

Bitmap bitmap = bitmapDrawable.getBitmap();

注意: openRawResource可以打開 drawable, sound, 和raw資源,但不能是string和color。

從資源存放路徑分:

(1) 圖檔放在sdcard中

Bitmap imageBitmap = BitmapFactory.decodeFile(path);// (path 是圖檔的路徑,跟目錄是/sdcard)

(2)圖檔在項目的res檔案夾下面

ApplicationInfo appInfo = getApplicationInfo();

//得到該圖檔的id(name 是該圖檔的名字,“drawable” 是該圖檔存放的目錄,appInfo.packageName是應用程式的包)

int resID = getResources().getIdentifier(fileName, “drawable”, appInfo.packageName);

Bitmap imageBitmap2 = BitmapFactory. decodeResource(getResources(), resID);

(3) 圖檔放在src目錄下

String path = “com/xiangmu/test.png”; //圖檔存放的路徑

InputStream in = getClassLoader().getResourceAsStream(path); //得到圖檔流

Bitmap imageBitmap3 = BitmapFactory. decodeStream(in);

(4) 圖檔放在 Assets目錄

InputStream in = getResources().getAssets().open(fileName);

Bitmap imageBitmap4 = BitmapFactory.decodeStream(in);

Bitmap Drawable byte[] InputStream 互相轉換方法

// 将byte[]轉換成InputStream  
    public InputStream Byte2InputStream(byte[] b) {  
        ByteArrayInputStream bais = new ByteArrayInputStream(b);  
        return bais;  
    }  

    // 将InputStream轉換成byte[]  
    public byte[] InputStream2Bytes(InputStream is) {  
        String str = "";  
        byte[] readByte = new byte[1024];  
        int readCount = -1;  
        try {  
            while ((readCount = is.read(readByte, 0, 1024)) != -1) {  
                str += new String(readByte).trim();  
            }  
            return str.getBytes();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  

    // 将Bitmap轉換成InputStream  
    public InputStream Bitmap2InputStream(Bitmap bm) {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        bm.compress(Bitmap.CompressFormat.JPEG, 100, baos);  
        InputStream is = new ByteArrayInputStream(baos.toByteArray());  
        return is;  
    }  

    // 将Bitmap轉換成InputStream  
    public InputStream Bitmap2InputStream(Bitmap bm, int quality) {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        bm.compress(Bitmap.CompressFormat.PNG, quality, baos);  
        InputStream is = new ByteArrayInputStream(baos.toByteArray());  
        return is;  
    }  

    // 将InputStream轉換成Bitmap  
    public Bitmap InputStream2Bitmap(InputStream is) {  
        return BitmapFactory.decodeStream(is);  
    }  

    // Drawable轉換成InputStream  
    public InputStream Drawable2InputStream(Drawable d) {  
        Bitmap bitmap = this.drawable2Bitmap(d);  
        return this.Bitmap2InputStream(bitmap);  
    }  

    // InputStream轉換成Drawable  
    public Drawable InputStream2Drawable(InputStream is) {  
        Bitmap bitmap = this.InputStream2Bitmap(is);  
        return this.bitmap2Drawable(bitmap);  
    }  

    // Drawable轉換成byte[]  
    public byte[] Drawable2Bytes(Drawable d) {  
        Bitmap bitmap = this.drawable2Bitmap(d);  
        return this.Bitmap2Bytes(bitmap);  
    }  

    // byte[]轉換成Drawable  
    public Drawable Bytes2Drawable(byte[] b) {  
        Bitmap bitmap = this.Bytes2Bitmap(b);  
        return this.bitmap2Drawable(bitmap);  
    }  

    // Bitmap轉換成byte[]  
    public byte[] Bitmap2Bytes(Bitmap bm) {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        bm.compress(Bitmap.CompressFormat.PNG, 100, baos);  
        return baos.toByteArray();  
    }  

    // byte[]轉換成Bitmap  
    public Bitmap Bytes2Bitmap(byte[] b) {  
        if (b.length != 0) {  
            return BitmapFactory.decodeByteArray(b, 0, b.length);  
        }  
        return null;  
    }  

    // Drawable轉換成Bitmap  
    public Bitmap drawable2Bitmap(Drawable drawable) {  
        Bitmap bitmap = Bitmap  
                .createBitmap(  
                        drawable.getIntrinsicWidth(),  
                        drawable.getIntrinsicHeight(),  
                        drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888  
                                : Bitmap.Config.RGB_565);  
        Canvas canvas = new Canvas(bitmap);  
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),  
                drawable.getIntrinsicHeight());  
        drawable.draw(canvas);  
        return bitmap;  
    }  

    // Bitmap轉換成Drawable  
    public Drawable bitmap2Drawable(Bitmap bitmap) {  
        BitmapDrawable bd = new BitmapDrawable(bitmap);  
        Drawable d = (Drawable) bd;  
        return d;  
    }  
           

Bitmap常見操作

縮放

/**
     * 根據給定的寬和高進行拉伸
     *
     * @param origin    原圖
     * @param newWidth  新圖的寬
     * @param newHeight 新圖的高
     * @return new Bitmap
     */
    private Bitmap scaleBitmap(Bitmap origin, int newWidth, int newHeight) {
        if (origin == null) {
            return null;
        }
        int height = origin.getHeight();
        int width = origin.getWidth();
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);// 使用後乘
        Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
        if (!origin.isRecycled()) {
            origin.recycle();
        }
        return newBM;
    }

    /**
     * 按比例縮放圖檔
     *
     * @param origin 原圖
     * @param ratio  比例
     * @return 新的bitmap
     */
    private Bitmap scaleBitmap(Bitmap origin, float ratio) {
        if (origin == null) {
            return null;
        }
        int width = origin.getWidth();
        int height = origin.getHeight();
        Matrix matrix = new Matrix();
        matrix.preScale(ratio, ratio);
        Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
        if (newBM.equals(origin)) {
            return newBM;
        }
        origin.recycle();
        return newBM;
    }

           

裁剪

/**
 * 裁剪
 *
 * @param bitmap 原圖
 * @return 裁剪後的圖像
 */
private Bitmap cropBitmap(Bitmap bitmap) {
    int w = bitmap.getWidth(); // 得到圖檔的寬,高
    int h = bitmap.getHeight();
    int cropWidth = w >= h ? h : w;// 裁切後所取的正方形區域邊長
    cropWidth /= 2;
    int cropHeight = (int) (cropWidth / 1.2);
    return Bitmap.createBitmap(bitmap, w / 3, 0, cropWidth, cropHeight, null, false);
}
           

旋轉

/**
 * 選擇變換
 *
 * @param origin 原圖
 * @param alpha  旋轉角度,可正可負
 * @return 旋轉後的圖檔
 */
private Bitmap rotateBitmap(Bitmap origin, float alpha) {
    if (origin == null) {
        return null;
    }
    int width = origin.getWidth();
    int height = origin.getHeight();
    Matrix matrix = new Matrix();
    matrix.setRotate(alpha);
    // 圍繞原地進行旋轉
    Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
    if (newBM.equals(origin)) {
        return newBM;
    }
    origin.recycle();
    return newBM;
}
           

偏移

/**
 * 偏移效果
 * @param origin 原圖
 * @return 偏移後的bitmap
 */
private Bitmap skewBitmap(Bitmap origin) {
    if (origin == null) {
        return null;
    }
    int width = origin.getWidth();
    int height = origin.getHeight();
    Matrix matrix = new Matrix();
    matrix.postSkew(-0.6f, -0.3f);
    Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
    if (newBM.equals(origin)) {
        return newBM;
    }
    origin.recycle();
    return newBM;
}
           

獲得圓角圖檔

private Bitmap bimapRound(Bitmap mBitmap,float index){

Bitmap bitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Config.ARGB_4444);

Canvas canvas = new Canvas(bitmap);
    Paint paint = new Paint();
    paint.setAntiAlias(true);

    //設定矩形大小
    Rect rect = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
    RectF rectf = new RectF(rect);

    // 相當于清屏
    canvas.drawARGB(0, 0, 0, 0);
    //畫圓角
    canvas.drawRoundRect(rectf, index, index, paint);
    // 取兩層繪制,顯示上層
    paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));

    // 把原生的圖檔放到這個畫布上,使之帶有畫布的效果
    canvas.drawBitmap(mBitmap, rect, rect, paint);
    return bitmap;

}
           

Bitmap記憶體模型

Bitmap 在記憶體中的組成部分,在任何系統版本中都會存在以下 3 個部分:

  • 1、Java Bitmap 對象: 位于 Java 堆,即我們熟悉的

    android.graphics.Bitmap.java

  • 2、Native Bitmap 對象: 位于 Native 堆,以

    Bitmap.cpp

    為代表,除此之外還包括與 Skia 引擎相關的 SkBitmap、SkBitmapInfo 等一系列對象;
  • 3、圖檔像素資料: 圖檔解碼後得到的像素資料。

其中,Java Bitmap 對象和 Native Bitmap 對象是分别存儲在 Java 堆和 Native 堆的,毋庸置疑。唯一有操作性的是 3、圖檔像素資料,不同系統版本采用了不同的配置設定政策,分為 3 個曆史時期:

  • 時期 1 - Android 3.0 以前: 像素資料存放在 Native 堆(這部分系統版本的市場占有率已經非常低,後文我們不再考慮);
  • 時期 2 - Android 8.0 以前: 從 Android 3.0 到 Android 7.1,像素資料存放在 Java 堆;
  • 時期 3 - Android 8.0 以後: 從 Android 8.0 開始,像素資料重新存放在 Native 堆。另外還新增了 Hardware Bitmap 硬體位圖,可以減少圖檔記憶體配置設定并提高繪制效率。

Bitmap的記憶體回收

  1. 2.3.3以前Bitmap的像素記憶體是配置設定在natvie上,而且不确定什麼時候會被回收。根據官方文檔的說法我們需要手動調用Bitmap.recycle()去回收:
  2. 3.0之後沒有強調Bitmap.recycle();而是強調Bitmap的複用

如何複用

  • 1.使用LruCache和DiskLruCache做記憶體和磁盤緩存;
  • 2.使用Bitmap複用,同時針對版本進行相容(inMutable和inBitmap)
  • 3.使用inTempStorage

擷取Bitmap的大小

/** 
  * 得到bitmap的大小 
  */  
 public static int getBitmapSize(Bitmap bitmap) {  
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {    //API 19  
         return bitmap.getAllocationByteCount();  
     }  
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {//API 12  
         return bitmap.getByteCount();  
     }  
     // 在低版本中用一行的位元組x高度  
     return bitmap.getRowBytes() * bitmap.getHeight();
 }  
           

getByteCount()與getAllocationByteCount()的差別 一般情況下兩者是相等的;如果通過複用Bitmap來解碼圖檔,如果被複用的Bitmap的記憶體比待配置設定記憶體的Bitmap大,那麼getByteCount()表示新解碼圖檔占用記憶體的大小(并非實際記憶體大小,實際大小是複用的那個Bitmap的大小),getAllocationByteCount()表示被複用Bitmap真實占用的記憶體大小(getByteCount永遠小于等于getAllocationByteCount)

Bitmap占用記憶體大小計算

在BitmapFactory.cpp中的 doDecode 方法

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 = size.width();
int scaledHeight = size.height();
bool willScale = false;

// Apply a fine scaling step if necessary.
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
    willScale = true;
    scaledWidth = codec->getInfo().width() / sampleSize;
    scaledHeight = codec->getInfo().height() / sampleSize;
}

...
if (willScale) {
    // Set the allocator for the outputBitmap.
    SkBitmap::Allocator* outputAllocator;
    if (javaBitmap != nullptr) {
        outputAllocator = &recyclingAllocator;
    } else {
        outputAllocator = &defaultAllocator;
    }

    SkColorType scaledColorType = decodingBitmap.colorType();
    // FIXME: If the alphaType is kUnpremul and the image has alpha, the
    // colors may not be correct, since Skia does not yet support drawing
    // to/from unpremultiplied bitmaps.
    outputBitmap.setInfo(
            bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
    if (!outputBitmap.tryAllocPixels(outputAllocator)) {
        // This should only fail on OOM.  The recyclingAllocator should have
        // enough memory since we check this before decoding using the
        // scaleCheckingAllocator.
        return nullObjectReturn("allocation failed for scaled bitmap");
    }

    SkPaint paint;
    // kSrc_Mode instructs us to overwrite the uninitialized pixels in
    // outputBitmap.  Otherwise we would blend by default, which is not
    // what we want.
    paint.setBlendMode(SkBlendMode::kSrc);
    paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering

    SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
    canvas.scale(scaleX, scaleY);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
}
           

從上述代碼可以看出bitmap最終通過canvas繪制出來,而canvas在繪制之前,有一個scale的操作,scale的值由

scale = (float) targetDensity / density;
           

決定,即縮放的倍率和targetDensity和density相關,而這兩個參數都是從傳入的options中擷取到的

  • inDensity: Bitmap位圖自身的密度,分辨率
  • inTargetDensity: Bitmap最終繪制的目标位置的分辨率
  • inScreenDensity: 裝置螢幕分辨率

其中inDensit和圖檔存在的資源檔案的目錄有關,同一張圖檔放在不同目錄下會有不同的值:

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

總結:

  1. 圖檔放在drawable中,等同于放在drawable-mdpi中。原因為:drawable目錄不與有螢幕密度特性,是以采用基準值,即mdpi
  2. 圖檔放在某個特性的drawable中,比如drawable-hdpi,如果裝置的螢幕密度高于目前drawable目錄所代表的密度,則圖檔會被放大,否則被縮小。

​ 放大或縮小比例 = 裝置螢幕密度 / drawable目錄所代表的螢幕密度

是以,bitmap占用公式,從之前

Bitmap記憶體占用 ≈ 像素資料總大小 = 橫向像素數量 x 縱向像素密度 x 每個像素的位元組大小
           

細化為

Bitmap記憶體占用 ≈ 像素資料總大小 = 圖檔寬 x 圖檔高 x(裝置分辨率/資源目錄分辨率)² x 每個像素的位元組大小
           

如要進行bitmap的解碼

try{
decode bitmap;
}catch(OutOfMemoryError e){//oom
品質壓縮
再次解碼
}
           

通過epic的hook架構可以對bitmap操作時進行優化處理

記憶體優化小結:

  1. 裝置分級,不能脫離裝置,針對不同機型做優化
  2. Bitmap優化 統一圖檔庫 線上線下監控 hook
  3. 記憶體洩漏工具使用
  4. glide