Author:楊空明 Date:2018-11-15
1.何為bitmap窖壕?
2.開發(fā)中bitmap遇到的那些問題
3.bitmap幾種壓縮方式
4.android中加載大圖片的正確方式
5.Android Skia 圖像引擎
1.何為bitmap匙铡?
我們可以稱之為位圖,是一種存儲像素的數(shù)據(jù)結(jié)構(gòu)攀例,通過這個對象我們可以獲取到一系列和圖片相關(guān)的屬性脑蠕, 并且可以對圖像進(jìn)行處理,比如切割蔬将,放大等等,相關(guān)操作央星。
1.1怎么計算一張圖片占用的內(nèi)存大邢蓟场?
bitmap 在內(nèi)存空間中所占用的內(nèi)存計算是這樣的:
bitmap 的寬 x 高 x 每個像素所占的字節(jié)
其中每個像素占用的字節(jié)可以通過Bitmap.Config動態(tài)配置
Config | 每個像素占用的字節(jié) | 說明 |
---|---|---|
ALPHA_8 | 1 bytes | 每個像素僅僅儲存透明度通道 |
RGB_565 | 2 bytes | 每個像素的RGB通道會保存莉给,透明度不會保存毙石,紅色通道5位,有2^5 =32種表現(xiàn)形式颓遏,綠色通道6位徐矩,有2^6 =64種表現(xiàn)形式;藍(lán)色通道5位叁幢,有2^5=32種表現(xiàn)形式 |
ARGB_4444 | 2 bytes | 每個像素的ARGB通道都會保存滤灯,透明度/紅色/綠色/藍(lán)色通道4位,有2^4=16種表現(xiàn)形式 |
ARGB_8888 | 4 bytes | 每個像素的ARGB通道都會保存曼玩,透明度/紅色/綠色/藍(lán)色通道8位鳞骤,有2^8=256種表現(xiàn)形式 |
有什么區(qū)別呢?最簡單的黍判,當(dāng)一個顏色表現(xiàn)形式越多豫尽,那么畫面整體的色彩就會更豐富,圖片質(zhì)量就會越高顷帖,當(dāng)然拂募,圖片占用的儲存空間也越大。
1.2圖片存在形式
1.文件形式(即以二進(jìn)制形式存在于硬盤上)
2.流的形式(即以二進(jìn)制形式存在于內(nèi)存中)
3.Bitmap形式
這三種形式的區(qū)別: 文件形式和流的形式對圖片體積大小并沒有影響,也就是說,如果你手機SD卡上的如果是100K,那么通過流的形式讀到內(nèi)存中,也一定是占100K的內(nèi)存,注意是流的形式,不是Bitmap的形式,當(dāng)圖片以Bitmap的形式存在時,其占用的內(nèi)存會瞬間變大, 我做分享的時候一個9.9M的圖片保存在手機相冊中顯示是238KB窟她,80M的圖片在手機相冊中顯示是1.24M。
2.開發(fā)中bitmap遇到的問題
在Android的開發(fā)中蔼水,我們經(jīng)痴鹛牵回去處理一些圖片相關(guān)的問題,比如當(dāng)加載圖片到內(nèi)存中產(chǎn)生的OOM(OutOfMemory)異常趴腋、圖片太大壓縮造成失真吊说,圖片不顯示论咏,圖片壓縮之后出現(xiàn)黑色,分享圖片漸變色出現(xiàn)色塊等颁井。
3.bitmap幾種壓縮方式
bitmap 的寬 x 高 x 每個像素所占的字節(jié) 從這個公式可以看出想要壓縮圖片大小有兩種方式:
1.減小圖片的長寬
2.減小圖片每個像素占用的字節(jié)數(shù)
3.1質(zhì)量壓縮
bitmap.compress(CompressFormat format, int quality, OutputStream stream);
圖片格式 | 說明 |
---|---|
PNG | 它是一種無損數(shù)據(jù)壓縮位圖圖形文件格式厅贪。這也就是說PNG 只支持無損壓縮。對于PNG 格式是有8 位雅宾、24位养涮、32位的三種形式的。區(qū)別就是對透明度的支持眉抬。 |
JPG | 其實就是 JPEG的另一種叫法 |
JPEG | 它是一種有損壓縮的圖片格式 |
WEBP | Google 開發(fā)出的一種支持alpha 通道的有損壓縮和無損壓縮贯吓。同等質(zhì)量情況下比 JPEG和PNG小 25%~45%.WebP格式圖像的編碼時間“比JPEG格式圖像長8倍”(占用cpu,節(jié)省內(nèi)存 |
GIF | 它是動態(tài)圖片的一種格式蜀变,和PNG 一樣是一種無損壓縮悄谐。 |
SVG | 是一種無損、矢量圖(放大不失真) |
圖片的格式有很多種库北,除了我們熟知的 JPG爬舰、PNG、GIF寒瓦,還有Webp情屹,BMP,TIFF孵构,CDR 等等幾十種屁商,用于不同的場景或平臺。
這些格式可以分為兩大類:有損壓縮和無損壓縮颈墅。
1.有損壓縮:是對圖像數(shù)據(jù)進(jìn)行處理蜡镶,去掉那些圖像上會被人眼忽略的細(xì)節(jié),然后使用附近的顏色通過漸變或其他形式進(jìn)行填充恤筛。這樣既能大大降低圖像信息的數(shù)據(jù)量官还,又不會影響圖像的還原效果。最典型的有損壓縮格式是 jpg毒坛。
2.無損壓縮:先判斷圖像上哪些區(qū)域的顏色是相同的望伦,哪些是不同的,然后把這些相同的數(shù)據(jù)信息進(jìn)行壓縮記錄煎殷,(例如一片藍(lán)色的天空之需要記錄起點和終點的位置就可以了)屯伞,而把不同的數(shù)據(jù)另外保存(例如天空上的白云和漸變等數(shù)據(jù))。常見的無損壓縮格式是 png豪直,gif劣摇。
Android 原生支持的格式只有 JPEG、PNG弓乙、GIF末融、WEBP(android 4.0 加入)钧惧、BMP。而在android層代碼中我們只能調(diào)用的編碼方式只有PNG勾习、JPEG浓瞪、和WEBP 三種。不過目前來說android 還不支持對GIF 這種的動態(tài)編碼巧婶。
注意 :我們?nèi)粘K械?.png乾颁、.jpg、.jpeg 等等指的是圖像數(shù)據(jù)經(jīng)過壓縮編碼后在媒體上的封存形式粹舵,是不能和PNG 钮孵、JPG、JEPG 混為一談的眼滤。
通過此種方式巴席,圖片的大小是沒有變的,因為質(zhì)量壓縮不會減少圖片的像素诅需,它是在保持像素的前提下改變圖片的位深及透明度等漾唉,來達(dá)到壓縮圖片的目的,這也是為什么該方法叫質(zhì)量壓縮方法堰塌。圖片的長赵刑,寬,像素都不變场刑,那么bitmap所占內(nèi)存大小是不會變的般此。
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 的倍數(shù))個像素最后對應(yīng)一個像素,由于采樣率設(shè)置為 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);
采樣前:
采樣后:
可以看到處理之后的圖片不是像鄰近采樣一樣純粹的一種顏色茅茂,而是兩種顏色的混合。雙線性采樣使用的是雙線性內(nèi)插值算法太抓,這個算法不像鄰近點插值算法一樣空闲,直接粗暴的選擇一個像素,而是參考了源像素相應(yīng)位置周圍 2x2 個點的值走敌,根據(jù)相對位置取對應(yīng)的權(quán)重进副,經(jīng)過計算之后得到目標(biāo)圖像。
3.2.3鄰近采樣和雙線性采樣對比
鄰近采樣的方式是最快的,因為它直接選擇其中一個像素作為生成像素影斑,但是生成的圖片可能會相對比較失真,產(chǎn)生比較明顯的鋸齒机打,最具有代表性的就是處理文字比較多的圖片在展示效果上的差別矫户,對比:
3.3像素壓縮
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565; //將格式設(shè)置成RGB_565
bm = BitmapFactory.decodeFile( "/DCIM/Camera/test.jpg", options);
注意:由于ARGB_4444的畫質(zhì)慘不忍睹,一般假如對圖片沒有透明度要求的話残邀,可以改成RGB_565皆辽,相比ARGB_8888將節(jié)省一半的內(nèi)存開銷。
4.Android中如何加載大圖片和長圖片
如果我們要加載的圖片遠(yuǎn)遠(yuǎn)大于ImageView的大小芥挣,直接用ImageView去展示的話驱闷,就會帶來不好的視覺效果,也會占用太多的內(nèi)存和性能開銷空免。甚至這張圖片足夠大到導(dǎo)致程序oom崩潰
1.壓縮
2.局部展示
有時候我們通過壓縮可以取得很好的效果空另,但有時候效果就不那么美好了,例如長圖像清明上河圖蹋砚,像這類的長圖扼菠,如果我們直接壓縮展示的話匈棘,這張圖完全看不清射富,很影響體驗。這時我們就可以采用局部展示查刻,然后滑動查看的方式去展示圖片墨坚。
public class LargeImageView extends View implements GestureDetector.OnGestureListener {
int bitmapLeft;
Paint paint;
private BitmapRegionDecoder mDecoder;
/**
* 繪制的區(qū)域
*/
public volatile Rect mRect = new Rect();
// private int mScaledTouchSlop;
// 分別記錄上次滑動的坐標(biāo)
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();
//設(shè)置為true后泽篮。不加載到內(nèi)存就能獲取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;
}
/**
* 移動的時候更新圖片顯示的區(qū)域
*
* @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區(qū)域
mRect.offset(-deltaX, 0);
//檢查是否到達(dá)圖片最右端
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);
//是否到達(dá)最底部
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;
}
}
根據(jù)上述源碼:
在setInputStream方法里面初始BitmapRegionDecoder,獲取圖片的實際寬高;
onMeasure方法里面給Rect賦初始化值咪辱,控制開始顯示的圖片區(qū)域;
onTouchEvent監(jiān)聽用戶手勢振劳,修改Rect參數(shù)來修改圖片展示區(qū)域,并且進(jìn)行邊界檢測油狂,最后invalidate;
在onDraw里面根據(jù)Rect獲取Bitmap并且繪制历恐。
5、Android Skia 圖像引擎
Skia 是一個 Google 自己維護(hù)的 c++ 實現(xiàn)的圖像引擎专筷,實現(xiàn)了各種圖像處理功能弱贼,并且廣泛地應(yīng)用于谷歌自己和其它公司的產(chǎn)品中(如:Chrome、Firefox磷蛹、 Android等)吮旅,基于它可以很方便為操作系統(tǒng)、瀏覽器等開發(fā)圖像處理功能。
Skia 在 Android 中提供了基本的畫圖和簡單的編解碼功能庇勃,可以掛接其他的第三方編碼解碼庫或者硬件編解碼庫檬嘀,例如 libpng 和 libjpeg,libgif 等等责嚷。因此鸳兽,這個函數(shù)調(diào)用bitmap.compress(Bitmap.CompressFormat.JPEG...),實際會調(diào)用 libjpeg.so 動態(tài)庫進(jìn)行編碼壓縮罕拂。
最終 Android 編碼保存圖片的邏輯是 Java 層函數(shù)→Native 函數(shù)→Skia函數(shù)→對應(yīng)第三庫函數(shù)(例如 libjpeg)揍异。所以 skia 就像一個膠水層,用來鏈接各種第三方編解碼庫爆班,不過 Android 也會對這些庫做一些修改衷掷,比如修改內(nèi)存管理的方式等等。
Android 在之前從某種程度來說使用的算是 libjpeg 的功能閹割版柿菩,壓縮圖片默認(rèn)使用的是 standard huffman戚嗅,而不是 optimized huffman,也就是說使用的是默認(rèn)的哈夫曼表碗旅,并沒有根據(jù)實際圖片去計算相對應(yīng)的哈夫曼表渡处,Google 在初期考慮到手機的性能瓶頸,計算圖片權(quán)重這個階段非常占用 CPU 資源的同時也非常耗時祟辟,因為此時需要計算圖片所有像素 argb 的權(quán)重医瘫,這也是 Android 的圖片壓縮率對比 iOS 來說差了一些的原因之一。