Bitmap和Drawable

問題

1.一張圖在手機內存中占有多大鼎姐?
2.如何優(yōu)化圖片大邪砝А?
3.大圖如何展示,比如世界地圖?
4.Drawable存放位置有什么區(qū)別瑰枫?

為什么要優(yōu)化Bitmap?

Bitmap對內存影響很大丹莲,比如說我們要加載一張4048x3036像素的照片光坝,如果按照ARGB_8888來顯示的話,那么就需要將近47M的內存大猩摹(4048x3036x4bytes)盯另,這么大的消耗很容易引起OutOfMemoryError(OOM)異常,因此必須要對Bitmap進行優(yōu)化洲赵。

一張圖在手機內存中占有多大?

在上一個問題中鸳惯,有寫到不進行壓縮的情況下一張圖所占用的內存:width * height * 一個像素所占用的字節(jié),這種計算方式在絕大部分情況下是正確的但是又不是完全正確叠萍,因為我們遺漏了Density要素芝发。BitmapFactory解碼圖片資源的時候會從BitmapFactory.Options讀取配置信息,而如果加載本地資源文件苛谷,則會在方法鏈過程中寫入Density相關的配置:

    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
        validate(opts);
        if (opts == null) {
            opts = new Options();
        }
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                //inDensity默認文件夾密度
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        if (opts.inTargetDensity == 0 && res != null) {
             //inTargetDensity為屏幕實際密度
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        return decodeStream(is, pad, opts);
    }

上述方法會將inDensity默認設置為圖片所在文件夾密度值辅鲸,將inTargetDensity設置為屏幕實際密度值。后續(xù)方法會走到BitmapFactory.cpp的nativeDecodeStream

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {
...
bitmap = doDecode(env, bufferedStream.release(), padding, options);
}
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
...
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;
            }
...
// Scale is necessary due to density differences.
if (scale != 1.0f) {
   //獲取縮放尺寸
   willScale = true;
   scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
   scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
...
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
...
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
...
}

上述塊中摘取了相關聯的代碼腹殿,可以看到nativeDecodeStream方法調用了doDecode方法独悴,在其中先確認通過Options的屬性確認縮放系數scale例书,然后獲取到縮放后的尺寸,最后完成縮放绵患。因此本地資源文件內存大小的計算方式為size = width * height * (targetDensity / density) * (targetDensity / density) * 像素所占用的字節(jié)雾叭。
加載其余圖片資源和本地資源文件流程大體一致,只是沒有考慮Density元素落蝙,因此計算方式為size = widthheight像素所占用的字節(jié)织狐。

像素的存儲方式
Bitmap.Config 占位 描述
Bitmap.Config.ALPHA_8 1bytes 只存儲alpha信息
Bitmap.Config.RGB_565 2bytes 只存儲RGB信息 R占用5位 G占用6位 B占用5位
Bitmap.Config.ARGB_4444 2bytes 占用 4位 R占用4位 G占用4位 B占用4位
Bitmap.Config.ARGB_8888 4bytes 占用 8位 R占用8位 G占用8位 B占用8位

默認情況下像素是以ARGB_8888方式存儲,如果需要縮略圖等質量不高的圖片筏勒,可以通過降低像素存儲方式來實現移迫。

知道了圖片內存確切的計算方式,那么該如何優(yōu)化圖片呢管行?

1.Bitmap.compress質量壓縮
2.inJustDecodeBounds和inSampleSize結合

第一種方式是質量壓縮
bitmap.compress(Bitmap.CompressFormat.JPEG,100,ous);

第二個參數是質量壓縮的百分比厨埋,100為不壓縮。該方法并不能改變圖片的尺寸和內存大小捐顷,但是可以改變ous的大小荡陷,使用場景是在一些對圖片長度有要求的場景,比如微信分享這些的迅涮。

第二張方式則是常用的內存壓縮方式

設置Options的inJustDecodeBounds字段為true废赞,可以在不將圖片加載到內存的情況下讀取圖片信息,然后通過圖片的尺寸和目標尺寸對比叮姑,計算出inSampleSize的值唉地,然后將inJustDecodeBounds設置為false,加載經過壓縮的圖片到內存中传透。

//內存壓縮
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.download,options);
options.inSampleSize = getSampleSize(options,50,50);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.download,options);
img.setImageBitmap(bitmap);

private int getSampleSize(BitmapFactory.Options options,float realWidth,float realHeight) {
    int bitmapWidth = options.outWidth;
    int bitmapHeight = options.outHeight;
    int sampleSize = 1;
    if (bitmapWidth > realWidth && bitmapHeight > realHeight) {
        int halfWidth = bitmapWidth/2;
        int halfHeight = bitmapHeight/2;
        while (halfWidth/sampleSize > realWidth && halfHeight/sampleSize>realHeight){
            sampleSize *= 2;
        }
     }
    return sampleSize;
}

//BitmapFactory.app中對sampleSize的支持
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
    willScale = true;
    scaledWidth = codec->getInfo().width() / sampleSize;
    scaledHeight = codec->getInfo().height() / sampleSize;
}
//BitmapFactory.app中如果inJustDecodeBounds為true耘沼,則會返回nullptr,不會走下面的createBitmap方法朱盐。
 gOptions_justBoundsFieldID = GetFieldIDOrDie(env, options_class, "inJustDecodeBounds", "Z");

if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
            onlyDecodeSize = true;
}
if (onlyDecodeSize) {
    return nullptr;
}

注:sampleSize會取最接近的2的冪次方的值群嗤。

如何加載大圖

通過上面的學習我們已經知道了如何去優(yōu)化圖片內存了,那么現在有一張超大尺寸的圖(世界地圖)兵琳,我們該如果清晰的加載到手機中呢狂秘?如果用之前的策略去加載的話,的確可以將圖加載到手機中闰围,但是圖片就看不清了赃绊,這不符合我們的預期,所以面對這種情況羡榴,我們考慮有局部加載策略碧查。

局部加載

Bitmap布局加載的核心是BitmapRegionDecoder類,此類通過decodeRegion方法獲取圖片局部區(qū)域的Bitmap實例,從而繪制在View上忠售。

Bitmap bitmap = mDecoder.decodeRegion(mRect,options);

其中mRect是繪制的矩形區(qū)域传惠,options是圖片的配置項。
相關細節(jié)代碼如下:

供外部傳入稻扬,獲取圖片寬高并初始化BitmapRegionDecoder
public void setInputStream(InputStream stream){
        try {
            BitmapFactory.Options tempOption =new BitmapFactory.Options();
            tempOption.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(stream, null, tempOption);
            mImageWidth = tempOption.outWidth;
            mImageHeight = tempOption.outHeight;
            mDecoder = BitmapRegionDecoder.newInstance(stream, false);
            requestLayout();
            invalidate();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
   //測量方法中獲取確切的矩形區(qū)域
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        //默認直接顯示圖片的中心區(qū)域卦方,可以自己去調節(jié)
        mRect.left = imageWidth / 2 - width / 2;
        mRect.top = imageHeight / 2 - height / 2;
        mRect.right = mRect.left + width;
        mRect.bottom = mRect.top + height;
    }
    //onDraw方法繪制
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (null != mDecoder) {
            Bitmap bitmap = mDecoder.decodeRegion(mRect,options);
            canvas.drawBitmap(bitmap,0,0,paint);
        }
    }
   //onTouchEvent刷新矩形區(qū)域,達到移動圖片的效果
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();
                mDownY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:

                if (mImageWidth > getWidth())
                {
                    float moveX = event.getX() - mDownX;
                    mRect.offset((int) -moveX, 0);
                    mDownX = event.getX();
                    checkWidth();
                    invalidate();
                }
                if (mImageHeight > getHeight())
                {
                    float moveY = event.getY() - mDownY;
                    mRect.offset(0, (int) -moveY);
                    mDownY = event.getY();
                    checkHeight();
                    invalidate();
                }
                
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return true;
    }

Drawable存放位置有什么區(qū)別泰佳?

上面我們學習了Bitmap相關的知識盼砍,現在我們來看看drawable的一些注意點:
在Android開發(fā)過程中,我們我們可以看到以下層級:


Android資源結構.png

其中mipmap一般用來存放不同分辨率的App圖標逝她,引用方式和drawable一樣R.mipmap.xxx浇坐。drawable存放我們開發(fā)過程中的不同分辨率的圖片資源,其中各文件夾對應分辨率如下:

密度 建議尺寸
mipmap-mdpi 48 * 48
mipmap-hdpi 72 * 72
mipmap-xhdpi 96 * 96
mipmap-xxhdpi 144 * 144
mipmap-xxxhdpi 192 * 192
dpi范圍 密度
120dpi ldpi
120dpi ~ 160dpi mdpi
160dpi ~ 240dpi hdpi
240dpi ~ 320dpi xhdpi
320dpi ~ 480dpi xxhdpi
480dpi ~ 640dpi xxxhdpi

注意同一張圖片放到不同的文件夾有不同的展現形式黔宛,實際效果就是放到密度越低的文件夾中近刘,展現到手機的尺寸越大,低密度的尺寸在高密度的手機上系統(tǒng)會默認放大臀晃,會導致占用內存增加觉渴,因此圖片優(yōu)先放到高密度的文件夾中。手機優(yōu)先從更高的密度中獲取資源徽惋,如果獲取不到則會從低密度中獲取案淋,獲取順序為:drawable-xxxhdpi->drawable-nodpi ->drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi
drawable-nodpi這個文件夾是一個密度無關的文件夾寂曹,放在這里的圖片系統(tǒng)就不會對它進行自動縮放哎迄,原圖片是多大就會實際展示多大回右。但是要注意一個加載的順序隆圆,drawable-nodpi文件夾是在匹配密度文件夾和更高密度文件夾都找不到的情況下才會去這里查找圖片的,因此放在drawable-nodpi文件夾里的圖片通常情況下不建議再放到別的文件夾里面翔烁。

總結:

1.圖片內存占用大忻煅酢:
1.1本地資源文件 size = width* height * (targetDensity / density) * (targetDensity / density) * 像素所占用的字節(jié)。
1.2其他文件 size = widthheight像素所占用的字節(jié)蹬屹。
2.圖片壓縮:
2.1Bitmap.compress質量壓縮 內存不變化
2.2 inJustDecodeBounds和inSampleSize結合 減小內存
3.大圖加載
BitmapRegionDecoder.decodeRegion(mRect,options);加載局部圖片信息
4.drawable 存放規(guī)則
優(yōu)先存放到對應尺寸的文件夾侣背,如沒有則優(yōu)先存放到高密度文件夾中達到減少內存的效果。 獲取圖片優(yōu)先級慨默,優(yōu)先高密度中獲取贩耐。
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市厦取,隨后出現的幾起案子潮太,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铡买,死亡現場離奇詭異更鲁,居然都是意外死亡,警方通過查閱死者的電腦和手機奇钞,發(fā)現死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門澡为,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人景埃,你說我怎么就攤上這事媒至。” “怎么了谷徙?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵塘慕,是天一觀的道長。 經常有香客問我蒂胞,道長图呢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任骗随,我火速辦了婚禮蛤织,結果婚禮上,老公的妹妹穿的比我還像新娘鸿染。我一直安慰自己指蚜,他們只是感情好,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布涨椒。 她就那樣靜靜地躺著摊鸡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蚕冬。 梳的紋絲不亂的頭發(fā)上免猾,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音囤热,去河邊找鬼猎提。 笑死,一個胖子當著我的面吹牛旁蔼,可吹牛的內容都是我干的锨苏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼棺聊,長吁一口氣:“原來是場噩夢啊……” “哼伞租!你這毒婦竟也來了?” 一聲冷哼從身側響起限佩,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤葵诈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體驯击,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡烁兰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了徊都。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沪斟。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖暇矫,靈堂內的尸體忽然破棺而出主之,到底是詐尸還是另有隱情,我是刑警寧澤李根,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布槽奕,位于F島的核電站,受9級特大地震影響房轿,放射性物質發(fā)生泄漏粤攒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一囱持、第九天 我趴在偏房一處隱蔽的房頂上張望夯接。 院中可真熱鬧,春花似錦纷妆、人聲如沸盔几。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逊拍。三九已至,卻和暖如春际邻,著一層夾襖步出監(jiān)牢的瞬間芯丧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工枯怖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留注整,地道東北人能曾。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓度硝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親寿冕。 傳聞我的和親對象是個殘疾皇子蕊程,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容