Android 图片的加载

Android 图片的加载

忽然想起在以前公司做Android的时候,有个哥们在首页放图片的时候,总是因为OOM而崩溃,今天打算正式找一下原因

首先来一个demo如下

本图片格式为1024 * 707,199.62KB

用同样的图片,同样大小的ImageView加载,不同的方法测试,其中第一个图片采用最优加载,第二个图片采用setBackgroundResource,第三个图片采用setBackground,第四个图片采用Glide加载。

查看源码可知,setBackgroundResourcesetBackground最后调用的其实都是setBackgroundDrawable

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setBackgroundResource(@DrawableRes int resid) {
if (resid != 0 && resid == mBackgroundResource) {
return;
}
Drawable d = null;
if (resid != 0) {
d = mContext.getDrawable(resid);
}
setBackground(d); mBackgroundResource = resid; }
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background); }

简单查看setBackgroundDrawable的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public void setBackgroundDrawable(Drawable background) {
computeOpaqueFlags(); if (background == mBackground) {
return;
}
boolean requestLayout = false; mBackgroundResource = 0; /*
* Regardless of whether we're setting a new background or not, we want * to clear the previous drawable. setVisible first while we still have the callback set. */ if (mBackground != null) {
if (isAttachedToWindow()) {
mBackground.setVisible(false, false);
}
mBackground.setCallback(null);
unscheduleDrawable(mBackground);
}
if (background != null) {
Rect padding = sThreadLocal.get();
if (padding == null) {
padding = new Rect();
sThreadLocal.set(padding);
}
resetResolvedDrawablesInternal();
background.setLayoutDirection(getLayoutDirection());
if (background.getPadding(padding)) {
resetResolvedPaddingInternal();
switch (background.getLayoutDirection()) {
case LAYOUT_DIRECTION_RTL:
mUserPaddingLeftInitial = padding.right;
mUserPaddingRightInitial = padding.left;
internalSetPadding(padding.right, padding.top, padding.left, padding.bottom);
break; case LAYOUT_DIRECTION_LTR:
default:
mUserPaddingLeftInitial = padding.left;
mUserPaddingRightInitial = padding.right;
internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
}
mLeftPaddingDefined = false;
mRightPaddingDefined = false;
}
// Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
// if it has a different minimum size, we should layout again if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
// Set mBackground before we set this as the callback and start making other
// background drawable state change calls. In particular, the setVisible call below // can result in drawables attempting to start animations or otherwise invalidate, // which requires the view set as the callback (us) to recognize the drawable as // belonging to it as per verifyDrawable. mBackground = background;
if (background.isStateful()) {
background.setState(getDrawableState());
}
if (isAttachedToWindow()) {
background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
}
applyBackgroundTint(); // Set callback last, since the view may still be initializing.
background.setCallback(this); if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
requestLayout = true;
}
} else {
/* Remove the background */
mBackground = null;
if ((mViewFlags & WILL_NOT_DRAW) != 0
&& (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
/*
* When the background is set, we try to apply its padding to this * View. When the background is removed, we don't touch this View's * padding. This is noted in the Javadocs. Hence, we don't need to * requestLayout(), the invalidate() below is sufficient. */ // The old background's minimum size could have affected this // View's layout, so let's requestLayout requestLayout = true;
}
computeOpaqueFlags(); if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
invalidate(true);
invalidateOutline(); }

这个方法首先查看这个Drawable是否和之前设置的相等,如果一样的话,直接结束方法。然后查看以前设置的mBackground是否为null,如果不为null,则清除之前设置的。接下来就是查看这个Drawable是否为null,如果不为null,就进行一系列的设置,为null的话,就移除之前的。

分析到这里好像也没发现图片大小的关键,不急,继续分析。

图片占用的大小

app启动占用内存

经测试
a. 调用setBackgroundResource

b. 调用setBackground

c. 直接加载src

d. Glide加载

e. 计算相关数值后加载

what!! 怪不得会OOM,直接设置的话,加载一张200K的图片至少占用了24.5mb的内存,不卡才怪。

图片加载的优化

核心思想: 加载所需的尺寸,通过设置BitmapFactory的Options

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Bitmap decodeSimpleBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res,resId,options); }
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int outWidth = options.outWidth;
int outHeight = options.outHeight; int sampleSize = 1; if (outHeight > outWidth || outWidth > outHeight) {
int halfWidth = outWidth / 2;
int halfHeight = outHeight / 2; while ((halfHeight / sampleSize) >= reqHeight &&
(halfWidth / sampleSize) >= reqWidth) {
sampleSize *= 2;
}
}
return sampleSize; }

Options有两个关键参数 ,inJustDecodeBoundsinSampleSize

1
2
/**
* If set to true, the decoder will return null (no bitmap), but * the out... fields will still be set, allowing the caller to query * the bitmap without having to allocate the memory for its pixels. */ public boolean inJustDecodeBounds;

当这个参数设置为true的时候,它不会立刻加载图片,而是允许调用者计算需要的大小。

1
2
/**
* 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. */ public int inSampleSize;

inSampleSize为采样率,总是大于1并且建议为2的倍数。比如一个ImageView为100100像素,图片为200200,那么我们设置采样率为2,就可以。

完整的步骤如下。

  • BitmapFactory的OptionsinJustDecodeBounds设置为true并加载图片。
  • BitmapFactory的Options中取出图片的原始宽高信息
  • 根据规则计算采样率
  • BitmapFactory的OptionsinJustDecodeBounds设置为false,重新加载图片。