Android Bitmap那些事

1.何爲bitmap?

2.開發中bitmap遇到的那些問題

3.bitmap幾種壓縮方式

4.android中加載大圖片的正確方式

5.Android Skia 圖像引擎

1.何爲bitmap?

咱們能夠稱之爲位圖,是一種存儲像素的數據結構,經過這個對象咱們能夠獲取到一系列和圖片相關的屬性, 而且能夠對圖像進行處理,好比切割,放大等等,相關操做。html

1.1怎麼計算一張圖片佔用的內存大小?

bitmap 在內存空間中所佔用的內存計算是這樣的:android

bitmap 的寬 x 高 x 每一個像素所佔的字節c++

其中每一個像素佔用的字節能夠經過Bitmap.Config動態配置算法

Config 每一個像素佔用的字節 說明
ALPHA_8 1 bytes 每一個像素僅僅儲存透明度通道
RGB_565 2 bytes 每一個像素的RGB通道會保存,透明度不會保存,紅色通道5位,有2^5 =32種表現形式,綠色通道6位,有2^6 =64種表現形式;藍色通道5位,有2^5=32種表現形式
ARGB_4444 2 bytes 每一個像素的ARGB通道都會保存,透明度/紅色/綠色/藍色通道4位,有2^4=16種表現形式
ARGB_8888 4 bytes 每一個像素的ARGB通道都會保存,透明度/紅色/綠色/藍色通道8位,有2^8=256種表現形式

有什麼區別呢?最簡單的,當一個顏色表現形式越多,那麼畫面總體的色彩就會更豐富,圖片質量就會越高,固然,圖片佔用的儲存空間也越大。canvas

1.2圖片存在形式

1.文件形式(即以二進制形式存在於硬盤上)瀏覽器

2.流的形式(即以二進制形式存在於內存中)數據結構

3.Bitmap形式ide

這三種形式的區別: 文件形式和流的形式對圖片體積大小並無影響,也就是說,若是你手機SD卡上的若是是100K,那麼經過流的形式讀到內存中,也必定是佔100K的內存,注意是流的形式,不是Bitmap的形式,當圖片以Bitmap的形式存在時,其佔用的內存會瞬間變大, 我作分享的時候一個9.9M的圖片保存在手機相冊中顯示是238KB,80M的圖片在手機相冊中顯示是1.24M。函數

2.開發中bitmap遇到的問題

在Android的開發中,咱們常常回去處理一些圖片相關的問題,好比當加載圖片到內存中產生的OOM(OutOfMemory)異常、圖片太大壓縮形成失真,圖片不顯示,圖片壓縮以後出現黑色,分享圖片漸變色出現色塊等。工具

3.bitmap幾種壓縮方式

bitmap 的寬 x 高 x 每一個像素所佔的字節 從這個公式能夠看出想要壓縮圖片大小有兩種方式:

1.減少圖片的長寬
2.減少圖片每一個像素佔用的字節數

3.1質量壓縮

bitmap.compress(CompressFormat format, int quality, OutputStream stream);

圖片格式 說明
PNG 它是一種無損數據壓縮位圖圖形文件格式。這也就是說PNG 只支持無損壓縮。對於PNG 格式是有8 位、24位、32位的三種形式的。區別就是對透明度的支持。
JPG 其實就是 JPEG的另外一種叫法
JPEG 它是一種有損壓縮的圖片格式
WEBP Google 開發出的一種支持alpha 通道的有損壓縮和無損壓縮。同等質量狀況下比 JPEG和PNG小 25%~45%.WebP格式圖像的編碼時間「比JPEG格式圖像長8倍」(佔用cpu,節省內存
GIF 它是動態圖片的一種格式,和PNG 同樣是一種無損壓縮。
SVG 是一種無損、矢量圖(放大不失真)

WEBP的體積對比

圖片的格式有不少種,除了咱們熟知的 JPG、PNG、GIF,還有Webp,BMP,TIFF,CDR 等等幾十種,用於不一樣的場景或平臺。

圖片常見格式

這些格式能夠分爲兩大類:有損壓縮和無損壓縮。

1.有損壓縮:是對圖像數據進行處理,去掉那些圖像上會被人眼忽略的細節,而後使用附近的顏色經過漸變或其餘形式進行填充。這樣既能大大下降圖像信息的數據量,又不會影響圖像的還原效果。最典型的有損壓縮格式是 jpg。

2.無損壓縮:先判斷圖像上哪些區域的顏色是相同的,哪些是不一樣的,而後把這些相同的數據信息進行壓縮記錄,(例如一片藍色的天空之須要記錄起點和終點的位置就能夠了),而把不一樣的數據另外保存(例如天空上的白雲和漸變等數據)。常見的無損壓縮格式是 png,gif。

Android 原生支持的格式只有 JPEG、PNG、GIF、WEBP(android 4.0 加入)、BMP。而在android層代碼中咱們只能調用的編碼方式只有PNG、JPEG、和WEBP 三種。不過目前來講android 還不支持對GIF 這種的動態編碼。

注意 :咱們平常全部的 .png、.jpg、.jpeg 等等指的是圖像數據通過壓縮編碼後在媒體上的封存形式,是不能和PNG 、JPG、JEPG 混爲一談的。

quality=100

quality=50

經過此種方式,圖片的大小是沒有變的,由於質量壓縮不會減小圖片的像素,它是在保持像素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的,這也是爲何該方法叫質量壓縮方法。圖片的長,寬,像素都不變,那麼bitmap所佔內存大小是不會變的。

3.2尺寸壓縮

3.2.1鄰近採樣

BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2; //inSampleSize 爲壓縮比 此處爲1/2

bm = BitmapFactory.decodeFile("/DCIM/Camera/test.jpg", options);

採樣前:

採樣前

採樣後:

採樣後

接着咱們來看看 inSampleSzie 的官方描述:

If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1. Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.
複製代碼

從官方的inSampleSzie描述看咱們能夠看到 x(x 爲 2 的倍數)個像素最後對應一個像素,因爲採樣率設置爲 1/2,因此是兩個像素生成一個像素。鄰近採樣的方式比較粗暴,直接選擇其中的一個像素做爲生成像素,另外一個像素直接拋棄,這樣就形成了圖片變成了純綠色,也就是紅色像素被拋棄。

3.2.2雙線性採樣

1.Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),bit.getHeight(), matrix, true);

2.Bitmap.createScaledBitmap(bitmapOld, 150, 150, true);

採樣前:

採樣前

採樣後:

採樣後

能夠看處處理以後的圖片不是像鄰近採樣同樣純粹的一種顏色,而是兩種顏色的混合。雙線性採樣使用的是雙線性內插值算法,這個算法不像鄰近點插值算法同樣,直接粗暴的選擇一個像素,而是參考了源像素相應位置周圍 2x2 個點的值,根據相對位置取對應的權重,通過計算以後獲得目標圖像。

3.2.3鄰近採樣和雙線性採樣對比

鄰近採樣的方式是最快的,由於它直接選擇其中一個像素做爲生成像素,可是生成的圖片可能會相對比較失真,產生比較明顯的鋸齒,最具備表明性的就是處理文字比較多的圖片在展現效果上的差異,對比:

原圖

鄰近採樣

雙線性採樣對比

3.3像素壓縮

BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; //將格式設置成RGB_565 bm = BitmapFactory.decodeFile( "/DCIM/Camera/test.jpg", options);

注意:因爲ARGB_4444的畫質慘不忍睹,通常假如對圖片沒有透明度要求的話,能夠改爲RGB_565,相比ARGB_8888將節省一半的內存開銷。
複製代碼

4.Android中如何加載大圖片和長圖片

若是咱們要加載的圖片遠遠大於ImageView的大小,直接用ImageView去展現的話,就會帶來很差的視覺效果,也會佔用太多的內存和性能開銷。甚至這張圖片足夠大到致使程序oom崩潰

1.壓縮

2.局部展現

有時候咱們經過壓縮能夠取得很好的效果,但有時候效果就不那麼美好了,例如長圖像清明上河圖,像這類的長圖,若是咱們直接壓縮展現的話,這張圖徹底看不清,很影響體驗。這時咱們就能夠採用局部展現,而後滑動查看的方式去展現圖片。

public class LargeImageView extends View implements GestureDetector.OnGestureListener {

int bitmapLeft;
Paint paint;
private BitmapRegionDecoder mDecoder;

/**
 * 繪製的區域
 */
public volatile Rect mRect = new Rect();

//    private int mScaledTouchSlop;

// 分別記錄上次滑動的座標
private int mLastX = 0;
private int mLastY = 0;
/**
 * 圖片的寬度和高度
 */
public int mImageWidth, mImageHeight;
private GestureDetector mGestureDetector;
private BitmapFactory.Options options;


public LargeImageView(Context context) {
    this(context, null);
}

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

public LargeImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context) {
    options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.ARGB_4444;


 //        mScaledTouchSlop = ViewConfiguration.get(getContext())
//                .getScaledTouchSlop();
    //初始化手勢控制器
    mGestureDetector = new GestureDetector(context, this);
    paint = new Paint();
    paint.setColor(Color.TRANSPARENT);
    paint.setAntiAlias(true);
}

/**
 * setInputStream裏面去得到圖片的真實的寬度和高度,以及初始化咱們的mDecoder。
 *
 * @param is
 */
public void setInputStream(InputStream is) {
    try {
        mDecoder = BitmapRegionDecoder.newInstance(is, false);
        BitmapFactory.Options bfOptions = new BitmapFactory.Options();
        //設置爲true後。不加載到內存就能獲取Bitmap尺寸大小。
        bfOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is, null, bfOptions);

        mImageWidth = mDecoder.getWidth();
        mImageHeight = mDecoder.getHeight();

        requestLayout();
        invalidate();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (is != null) {
                is.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


@Override
public boolean onTouchEvent(MotionEvent ev) {
    //把觸摸事件交給手勢控制器處理
    return mGestureDetector.onTouchEvent(ev);
}


@Override
public boolean onDown(MotionEvent e) {
    mLastX = (int) e.getRawX();
    mLastY = (int) e.getRawY();
    return true;
}

@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) {
    int x = (int) e2.getRawX();
    int y = (int) e2.getRawY();
    move(x, y);

    return true;
}

/**
 * 移動的時候更新圖片顯示的區域
 *
 * @param x
 * @param y
 */
private void move(int x, int y) {

    boolean isInvalidate = false;

    int deltaX = x - mLastX;
    int deltaY = y - mLastY;
    //若是圖片寬度大於屏幕寬度
    if (mImageWidth > getWidth()) {
        //移動rect區域
        mRect.offset(-deltaX, 0);
        //檢查是否到達圖片最右端
        if (mRect.right > mImageWidth) {
            mRect.right = mImageWidth;
            mRect.left = mImageWidth - getWidth();
        }

        //檢查左端
        if (mRect.left < 0) {
            mRect.left = 0;
            mRect.right = getWidth();
        }
        isInvalidate = true;

    }
    //若是圖片高度大於屏幕高度
    if (mImageHeight > getHeight()) {
        mRect.offset(0, -deltaY);

        //是否到達最底部
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = mImageHeight;
            mRect.top = mImageHeight - getHeight();
        }

        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = getHeight();
        }
        isInvalidate = true;

    }

    if (isInvalidate) {
        invalidate();
    }

    mLastX = x;
    mLastY = y;
}

@Override
public void onLongPress(MotionEvent e) {
    mLastX = (int) e.getRawX();
    mLastY = (int) e.getRawY();
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    int x = (int) e2.getRawX();
    int y = (int) e2.getRawY();
    move(x, y);
    return true;
}


@SuppressLint("CheckResult")
@Override
protected void onDraw(final Canvas canvas) {
    //顯示圖片
    if (null != mDecoder) {
        Bitmap bm = mDecoder.decodeRegion(mRect, options);
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        canvas.drawBitmap(bm, bitmapLeft, 0, null);
    }
}

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

    int width = getMeasuredWidth();
    int height = getMeasuredHeight();

    bitmapLeft = width / 2 - mImageWidth / 2;//圖片距離左邊大小
    mRect.left = 0;
    mRect.top = 0;
    mRect.right = mImageWidth;
    mRect.bottom = mRect.top + height;
}
}
複製代碼

根據上述源碼:

在setInputStream方法裏面初始BitmapRegionDecoder,獲取圖片的實際寬高; onMeasure方法裏面給Rect賦初始化值,控制開始顯示的圖片區域; onTouchEvent監聽用戶手勢,修改Rect參數來修改圖片展現區域,而且進行邊界檢測,最後invalidate; 在onDraw裏面根據Rect獲取Bitmap而且繪製。

五、Android Skia 圖像引擎

Skia 是一個 Google 本身維護的 c++ 實現的圖像引擎,實現了各類圖像處理功能,而且普遍地應用於谷歌本身和其它公司的產品中(如:Chrome、Firefox、 Android等),基於它能夠很方便爲操做系統、瀏覽器等開發圖像處理功能。

Skia 在 Android 中提供了基本的畫圖和簡單的編解碼功能,能夠掛接其餘的第三方編碼解碼庫或者硬件編解碼庫,例如 libpng 和 libjpeg,libgif 等等。所以,這個函數調用bitmap.compress(Bitmap.CompressFormat.JPEG...),實際會調用 libjpeg.so 動態庫進行編碼壓縮。

最終 Android 編碼保存圖片的邏輯是 Java 層函數→Native 函數→Skia函數→對應第三庫函數(例如 libjpeg)。因此 skia 就像一個膠水層,用來連接各類第三方編解碼庫,不過 Android 也會對這些庫作一些修改,好比修改內存管理的方式等等。

Android 在以前從某種程度來講使用的算是 libjpeg 的功能閹割版,壓縮圖片默認使用的是 standard huffman,而不是 optimized huffman,也就是說使用的是默認的哈夫曼表,並無根據實際圖片去計算相對應的哈夫曼表,Google 在初期考慮到手機的性能瓶頸,計算圖片權重這個階段很是佔用 CPU 資源的同時也很是耗時,由於此時須要計算圖片全部像素 argb 的權重,這也是 Android 的圖片壓縮率對比 iOS 來講差了一些的緣由之一。

圖片格式

阿里圖片庫

壓縮圖片工具類

圖好快

在線摳圖

爲何Android的圖片質量會比iPhone的差?

Android中的圖片壓縮技術詳解

相關文章
相關標籤/搜索