圖片基礎知識梳理(3) Bitmap&BitmapFactory 解析

澤毛發表於2017-12-21

一、概述

今天這篇文章我們來了解一下兩個類:

  • Bitmap
  • BitmapFactory

二、Bitmap

2.1 建立Bitmap

通過Bitmap的原始碼,我們可以看到它內部提供了很多.createBitmap(xxx)的靜態方法,我們可以通過這些方法來獲得一個Bitmap

圖片基礎知識梳理(3)   Bitmap&BitmapFactory 解析
上述的方法最終可以分為以下三類:

  • 通過一個已有的Bitmap建立
  • 建立一個空的Bitmap
  • 建立一個新的Bitmap,該Bitmap每個畫素點的顏色通過一個colors[]陣列指定。

下面,我們來看一下這三類方法對於Bitmap的生產過程:

第一類

    public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height,
            Matrix m, boolean filter) {

        checkXYSign(x, y);
        checkWidthHeight(width, height);
        //新的bitmap範圍不能大於原始的bitmap
        if (x + width > source.getWidth()) {
            throw new IllegalArgumentException("x + width must be <= bitmap.width()");
        }
        if (y + height > source.getHeight()) {
            throw new IllegalArgumentException("y + height must be <= bitmap.height()");
        }

        //如果滿足下面這些條件,那麼直接返回原始的bitmap
        if (!source.isMutable() && x == 0 && y == 0 && width == source.getWidth() &&
                height == source.getHeight() && (m == null || m.isIdentity())) {
            return source;
        }

        int neww = width;
        int newh = height;
        Canvas canvas = new Canvas();
        Bitmap bitmap;
        Paint paint;
        //生成bitmap對應區域
        Rect srcR = new Rect(x, y, x + width, y + height);
        //原始bitmap對應區域
        RectF dstR = new RectF(0, 0, width, height);

        Config newConfig = Config.ARGB_8888;
        //獲得原始bitmap的config
        final Config config = source.getConfig();
        // GIF files generate null configs, assume ARGB_8888
        if (config != null) {
            switch (config) {
                case RGB_565:
                    newConfig = Config.RGB_565;
                    break;
                case ALPHA_8:
                    newConfig = Config.ALPHA_8;
                    break;
                //noinspection deprecation
                case ARGB_4444:
                case ARGB_8888:
                default:
                    newConfig = Config.ARGB_8888;
                    break;
            }
        }
        //如果不需要變換,那麼建立一個空的bitmap.
        if (m == null || m.isIdentity()) {
            bitmap = createBitmap(neww, newh, newConfig, source.hasAlpha());
            paint = null;   // not needed
        } else {
            //根據Matrix,對原始的bitmap進行一些變換操作.
            final boolean transformed = !m.rectStaysRect();

            RectF deviceR = new RectF();
            m.mapRect(deviceR, dstR);

            neww = Math.round(deviceR.width());
            newh = Math.round(deviceR.height());

            bitmap = createBitmap(neww, newh, transformed ? Config.ARGB_8888 : newConfig,
                    transformed || source.hasAlpha());

            canvas.translate(-deviceR.left, -deviceR.top);
            canvas.concat(m);

            paint = new Paint();
            paint.setFilterBitmap(filter);
            if (transformed) {
                paint.setAntiAlias(true);
            }
        }
        //返回bitmap的這些屬性和原始bitmap相同
        bitmap.mDensity = source.mDensity;
        bitmap.setHasAlpha(source.hasAlpha());
        bitmap.setPremultiplied(source.mRequestPremultiplied);

        //設定canvas對應的bitmap為返回的bitmap
        canvas.setBitmap(bitmap);

        //通過canvas把原始的bitmap繪製上去.
        canvas.drawBitmap(source, srcR, dstR, paint);

        //重新置為空.
        canvas.setBitmap(null);
        return bitmap;
    }
複製程式碼
  • 方法作用:返回原始的Bitmap中一個不可改變的子集,返回的Bitmap有可能是原始的Bitmap(原始的Bitmap不可改變,並且大小和請求的新的Bitmap大小和原來一樣),也有可能是複製出來的,它和原始的Bitmapdensity相同。
  • 引數說明:
  • source:原始的Bitmap
  • x, y:在原始的Bitmap中的起始座標。
  • width, height:返回的Bitmap的寬高,如果超過了原始Bitmap的範圍,那麼會丟擲異常。
  • mMatrix型別,表示需要的變換
  • filter:是否需要優化,只有當m不只有平移操作時才去進行。

第二類

    private static Bitmap createBitmap(DisplayMetrics display, int width, int height, Config config, boolean hasAlpha) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("width and height must be > 0");
        }
        Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true);
        if (display != null) {
            bm.mDensity = display.densityDpi;
        }
        bm.setHasAlpha(hasAlpha);
        if (config == Config.ARGB_8888 && !hasAlpha) {
            nativeErase(bm.mNativePtr, 0xff000000);
        }
        return bm;
    }
複製程式碼
  • 方法作用:返回一個可變的bitmap,它的density由傳入的DisplayMetrics指定。
  • 引數說明:
  • displayBitmap將要被繪製的Display metrics
  • width, heightbitmap的寬高
  • config:配置資訊,對應ARGB_8888那些。
  • hasAlpha:如果bitmap的屬性是ARGB_8888,那麼這個標誌為可以用來把bitmap標誌為透明,它會把bitmap中的黑色畫素轉換為透明。

第三類

    public static Bitmap createBitmap(DisplayMetrics display, int colors[],
            int offset, int stride, int width, int height, Config config) {

        checkWidthHeight(width, height);
        if (Math.abs(stride) < width) {
            throw new IllegalArgumentException("abs(stride) must be >= width");
        }
        int lastScanline = offset + (height - 1) * stride;
        int length = colors.length;
        if (offset < 0 || (offset + width > length) || lastScanline < 0 ||
                (lastScanline + width > length)) {
            throw new ArrayIndexOutOfBoundsException();
        }
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("width and height must be > 0");
        }
        Bitmap bm = nativeCreate(colors, offset, stride, width, height,
                            config.nativeInt, false);
        if (display != null) {
            bm.mDensity = display.densityDpi;
        }
        return bm;
    }
複製程式碼
  • 方法作用:返回一個不可變的bitmap物件,它的長寬由width/height指定,每個畫素點的顏色通過colos[]陣列得到,初始的density來自於DisplayMetrics
  • 方法引數:
  • displayBitmap將要被繪製的Display metrics
  • colors:用來初始化畫素點的顏色
  • offset:第一個畫素點的顏色在陣列當中跳過的個數。
  • stride:兩行之間需要跳過的顏色個數。
  • width/height:寬高。
  • config:對應ARGB_8888那些。

2.2 壓縮bitmap

    public boolean compress(CompressFormat format, int quality, OutputStream stream) {
        checkRecycled("Can't compress a recycled bitmap");
        // do explicit check before calling the native method
        if (stream == null) {
            throw new NullPointerException();
        }
        if (quality < 0 || quality > 100) {
            throw new IllegalArgumentException("quality must be 0..100");
        }
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
        boolean result = nativeCompress(mNativePtr, format.nativeInt,
                quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        return result;
    }
複製程式碼
  • 方法作用:把當前這個bitmap的壓縮版本寫入到某個輸出流當中,如果返回true,那麼這個bitmap可以被BitmapFactory.decodeStream()恢復。需要注意的是,並不是所有的bitmap都支援所有的格式,因此,通過BitmapFactory恢復回來的bitmap有可能和原來不同。
  • 方法引數:
  • CompressFormat是一個列舉型別,它的值有JPEG/PNG/WEBP
  • quality對應0-100
  • stream則是壓縮後結果的輸出流。

2.3 回收bitmap

    public void recycle() {
        if (!mRecycled && mNativePtr != 0) {
            if (nativeRecycle(mNativePtr)) {
                // return value indicates whether native pixel object was actually recycled.
                // false indicates that it is still in use at the native level and these
                // objects should not be collected now. They will be collected later when the
                // Bitmap itself is collected.
                mBuffer = null;
                mNinePatchChunk = null;
            }
            mRecycled = true;
        }
    }
複製程式碼

recycle方法主要做幾件事:

  • 釋放和這個bitmap關聯的native物件
  • 清除畫素資料mBuffer的引用,但是這一過程不是同步的,它只是將引用置為空,等待垃圾回收器將它回收。
  • 在呼叫這個方法之後,mRecycled標誌位就為true,之後如果再呼叫bitmap的方法,那麼很有可能發生異常。
  • 一般情況下,我們不需要手動呼叫這個方法,因為當這個bitmap不被引用時,垃圾回收器就會自動回收它所佔用的記憶體。

2.4 獲取Bitmap所佔記憶體

  • getAllocationByteCount() 返回儲存這個bitmap物件所需要的記憶體,當我們對bitmap所佔記憶體區域進行復用的時候,這個函式的返回結果可能要大於getByteCount的值,否則,它和getByteCount的值是相同的。 這個值,在bitmap整個生命週期之內都不會改變。
    public final int getAllocationByteCount() {
        if (mBuffer == null) {
            return getByteCount();
        }
        return mBuffer.length;
    }
複製程式碼
  • getByteCount 表示儲存bitmap畫素所需要的最小位元組,自從4.4之後,這個就不能用來確定bitmap佔用的記憶體了,需要用getAllocationByteCount
    public final int getByteCount() {
        // int result permits bitmaps up to 46,340 x 46,340
        return getRowBytes() * getHeight();
    }
複製程式碼

2.5 獲取縮放後大小

圖片基礎知識梳理(3)   Bitmap&BitmapFactory 解析
我們可以通過上面這六個方法獲得縮放後的寬高,它們的原理就是傳入一個目標的density,然後和當前bitmapdensity進行比較,然後算出一個縮放的倍數,在和原來的大小相乘。目標density的來源有以下三個:

  • 直接傳入
  • Canvasdensity
  • DisplayMetricsdensity

計算的規則為:

    static public int scaleFromDensity(int size, int sdensity, int tdensity) {
        if (sdensity == DENSITY_NONE || tdensity == DENSITY_NONE || sdensity == tdensity) {
            return size;
        }

        // Scale by tdensity / sdensity, rounding up.
        return ((size * tdensity) + (sdensity >> 1)) / sdensity;
    }
複製程式碼

三、BitmapFactory

BitmapFactory用來從多種不同的來源獲得Bitmap

  • 檔案、檔案描述符
  • 資原始檔Resource
  • byte[]陣列
  • 輸入流InputStream
    圖片基礎知識梳理(3)   Bitmap&BitmapFactory 解析

3.1 BitmapFactory.Options

  • Bitmap inBitmap 如果給Options設定了這個Bitmap,那麼在通過這個Options解碼的時候,解碼方法返回的bitmap會嘗試複用這個Options中的bitmap,如果不能複用,那麼解碼方法會返回null,並丟擲異常,它要求複用的bitmap是可變的。 在4.4以後,只要求新申請的bitmapgetByteCount()小於等於Options中的bitmapgetAllocationByteCount()就可以。 在4.4以前,格式必須是jpeg/png,並且要求兩個bitmap相同並且inSampleSize1
  • boolean inJustDecodeBounds 如果設為true,那麼解碼方法的返回值null,但是它會設定outXXX的值,這樣呼叫者就可以在不用解碼整張圖片的前提下查詢到這個bitmap的長寬。
  • int inSampleSize 對原來的圖片進行取樣,如果inSampleSize4,那麼圖片的長寬會縮短為原來的1/4,這樣就可以減少bitmap佔用的記憶體。
  • Bitmap.Config inPreferredConfig 圖片解碼的格式要求。
  • 縮放相關的標誌:inScaledinDensityinTargetDensityinScreenDensity 首先,只有在inScaledtrue的時候,縮放的機制才會生效,這個值預設是true的。
  • inDensity 我們先討論一下inDensity,當我們沒有給density賦值的時候,系統會給我們初始化它:
        //如果沒有設定density
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density; //這裡的TypeValue會根據存放資料夾的不同而不同.
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; //如果density為0,那麼把density設定為160.
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density; //否則,設定為value中的density.
            }
        }
複製程式碼
  • inTargetDensity 再來看一下inTargetDensity,它得到的就是螢幕的density.
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
複製程式碼
  • inScreenDensity 最後inScreenDensity沒有被賦予預設值,也就是說它為0,如果我們期望圖片不要被縮放,那麼就要給它設定為手機的density

這三者的關係是:inDensity不為0並且inTargetDensity不為0inDensityinScreenDensity不相等時,會對圖片進行縮放,縮放倍數為inTargetDensity/inDensity

這樣說可能比較抽象,我們舉一個實際的例子,假如我們的手機的density320dpi的,那麼inTargetDensity就等於320,這時候我們把某張圖片資源放在了drawable-xxxhpi下,那麼inDensity的值就為640,我們沒有設定inScreenDensity,那麼它的預設值是0,這時候滿足:

inDensity != 0 && inTargetDensity != 0 && inDensity != inScreenDensity
複製程式碼

圖片就會進行縮放,縮放的倍數就為320/640,也就是說最終得到的bitmap的長寬是原來的一半。

  • outXXX 這個返回的結果和inJustDecodeBounds有關,如果inJustDecodeBoundstrue,那麼返回的是沒有經過縮放的大小,如果為false,那麼就是縮放後的大小。

3.2 獲取bitmap方法

下面是BitmapFactory提供的方法:

圖片基礎知識梳理(3)   Bitmap&BitmapFactory 解析
所有的獲取bitmap最終都是呼叫了一下四個方法Native方法其中之一,可以看到它可以從這些來源讀取:

  • file
  • byte[]
  • InputStream

圖片基礎知識梳理(3)   Bitmap&BitmapFactory 解析

其中有個需要注意的是Rect,這是一個傳入的值,在讀取資源完畢後,它會寫入讀取資源的padding,如果沒有那麼為[-1, -1, -1,- 1],而如果返回的bitmap為空,那麼傳入的值不會改變。

四、Bitmap的轉換方法

public class BitmapConvertUtils {

    public static Bitmap fromResourceIdAutoScale(Resources resources, int resourceId, BitmapFactory.Options options) {
        return BitmapFactory.decodeResource(resources, resourceId, options);
    }

    public static Bitmap fromResourceIdNotScale(Resources resources, int resourceId, Rect rect, BitmapFactory.Options options) {
        InputStream resourceStream = null;
        Bitmap bitmap = null;
        try {
            resourceStream = resources.openRawResource(resourceId);
            bitmap = BitmapFactory.decodeStream(resourceStream, rect, options);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (resourceStream != null) {
                    resourceStream.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }

    public static Bitmap fromAssert(Context context, String assertFilePath, Rect rect, BitmapFactory.Options options) {
        Bitmap bitmap = null;
        InputStream assertStream = null;
        AssetManager assetManager = context.getAssets();
        try {
            assertStream = assetManager.open(assertFilePath);
            bitmap = BitmapFactory.decodeStream(assertStream, rect, options);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (assertStream != null) {
                    assertStream.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        return bitmap;
    }

    public static Bitmap fromByteArray(byte[] byteArray, int offset, int length, BitmapFactory.Options options) {
        return BitmapFactory.decodeByteArray(byteArray, offset, length, options);
    }

    public static Bitmap fromFile(String filePath, BitmapFactory.Options options) {
        return BitmapFactory.decodeFile(filePath, options);
    }

    public static Bitmap fromDrawable(Drawable drawable) {
        int width = drawable.getIntrinsicWidth();
        int height = drawable.getIntrinsicHeight();
        Bitmap bitmap = Bitmap.createBitmap(width, height, drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
        if (bitmap != null) {
            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, width, height);
            drawable.draw(canvas);
            return bitmap;
        }
        return null;
    }

    public static Bitmap fromView(View view) {
        view.clearFocus();
        view.setPressed(false);
        boolean willNotCache = view.willNotCacheDrawing();
        view.setWillNotCacheDrawing(false);
        int color = view.getDrawingCacheBackgroundColor();
        view.setDrawingCacheBackgroundColor(color);
        if (color != 0) {
            view.destroyDrawingCache();
        }
        view.buildDrawingCache();
        Bitmap cacheBitmap = view.getDrawingCache();
        if (cacheBitmap == null) {
            return null;
        }
        Bitmap bitmap = Bitmap.createBitmap(cacheBitmap);
        view.destroyDrawingCache();
        view.setWillNotCacheDrawing(willNotCache);
        view.setDrawingCacheBackgroundColor(color);
        return bitmap;
    }

    public static Bitmap fromInputStream(InputStream inputStream) {
        return BitmapFactory.decodeStream(inputStream);
    }

    public static byte[] toByteArray(Bitmap bitmap, Bitmap.CompressFormat format, int quality) {
        byte[] bytes = null;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        bitmap.compress(format, quality, outputStream);
        bytes = outputStream.toByteArray();
        try {
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bytes;
    }

    public static Drawable toDrawable(Resources resources, Bitmap bitmap) {
        return new BitmapDrawable(resources, bitmap);
    }

    public static void toFile(Bitmap bitmap, Bitmap.CompressFormat format, int quality, String path) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(path);
            bitmap.compress(format, quality, fileOutputStream);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
}
複製程式碼

相關文章