問題
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ā)過程中,我們我們可以看到以下層級:
其中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文件夾里的圖片通常情況下不建議再放到別的文件夾里面翔烁。