0. 前言
Android 图片适配,真的不是你想像的那样,至少在写这篇文章之前,我陷在一个很大很大的误区中。
1. 关于适配
所有关于适配的基本概念,这里不多介绍,资料有很多。下面只介绍点比较重要的部分。
等级 | 密度 | 比例 |
---|---|---|
ldpi | 120dpi | 1dp=0.75px |
mdpi | 160dpi | 1dp=1px |
hdpi | 240dpi | 1dp=1.5px |
xhdpi | 320dpi | 1dp=2px |
xxhdpi | 480dpi | 1dp=3px |
xxxhdpi | 640dpi | 1dp=4px |
上面这张表介绍了 dpi 与 px 之间的关系。而多数手机厂商没有严格按照上述规范生产屏幕,才会有如今令人恶心的 Android 适配问题。
如:三星 C9,6英寸屏幕,分辨率 1920x1080 ,按照公式计算屏幕密度 367 dpi ,更接近 320dpi ,因此适配时,会取 xhdpi 目录下的数据。
但实际中,会取 xxhdpi 数据,因为实际屏幕密度是 420 dpi。(通过代码的方式获取)
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
Log.d(TAG, "onCreate: "+dm.density);
Log.d(TAG, "onCreate: "+dm.densityDpi);
2019-11-29 14:50:03.879 28034-28034/com.flueky.demo D/MainActivity: onCreate: 2.625
2019-11-29 14:50:03.879 28034-28034/com.flueky.demo D/MainActivity: onCreate: 420
2.625 是 420/160 的结果。表示在 C9 上,1dp=2.625 px ,411dp 约等于 1080px ,表示整个屏幕的宽度。
如:三星 S8,5.8英寸屏幕,分辨率 2960x1440 ,屏幕密度 568 dpi,接近 640 dpi ,因此适配时,会取 xxxhdpi 目录下数据。
但实际中,会取 xxhdpi 数据,因为实际屏幕密度是 560 dpi 。
2019-11-29 14:59:49.604 20793-20793/com.flueky.demo D/MainActivity: onCreate: 3.5
2019-11-29 14:59:49.604 20793-20793/com.flueky.demo D/MainActivity: onCreate: 560
在 S8 上 ,1dp=3.5px ,411dp 约等于 1440px ,表示整个屏幕的宽度。
很庆幸,这两台手机上的适配数据是一样的,高度会存在差异,但是通常都是滚动长页面,或者留白端页面不受太大影响。若恰好是满屏页面,则不适用。
今日头条的适配方案即是通过修改 density 的 值进行适配。不知道什么原因,他们在《今日头条》7.5 版本中未使用此适配方式。
2. 图片适配
言归正传,关于图片适配才是我们的主题。
秉着实践是检验真理的唯一标准这一原则,做了如下实验。三种尺寸的图片,放置在四个目录目录,用三种尺寸的 ImageView ,用三种方式加载图片,检查其内存使用的情况。
-
图片尺寸
- large 1600x900 ,占用内存 1600x900x4/1024/1024 = 5.49m
- middle 800x450 ,占用内存 800x450x4/1024/1024 = 1.37m
- small 400x225 ,占用内存 400x225x4/1024/1024 = 0.34m
-
图片目录
- asset
- drawable hdpi
- drawable xhdpi
- drawable xxhdpi
-
ImageView
- wrap-content
- 280dp
- 160dp
-
引用方式
- android:src
- setImageResource
- setImageBitmap
加载 asset 目录下的图片,只能使用 setImageBitmap 的方式。
第一组实验,使用 1 2 3 以及 setImageBitmap ,得出 3x4x3x1 = 36 条数据,如下表。
- B 表示内存中图片的 bitmap 大小。
- G 表示内存中 Graphics 占用的空间。
- N 表示内存中 Native 占用的空间。
- 序号 0 表示,未使用图片时的情况。
- 实验基于屏幕密度 540dpi 的设备。
序号 | 目录 | 分辨率 | 宽度 | B | G | N |
---|---|---|---|---|---|---|
0 | - | - | - | - | 1.8m | 7.8m |
1 | asset | 1600x900 | wrap | 5.49m | 8.7m | 14.6m |
2 | asset | 1600x900 | w280 | 5.49m | 8.7m | 14.7m |
3 | asset | 1600x900 | w160 | 5.49m | 8.6m | 13.2m |
4 | asset | 800x450 | wrap | 1.37m | 3.8m | 9.3m |
5 | asset | 800x450 | w280 | 1.37m | 3.8m | 9.2m |
6 | asset | 800x450 | w160 | 1.37m | 3.8m | 9.3m |
7 | asset | 400x225 | wrap | 0.34m | 2.6m | 8.2m |
8 | asset | 400x225 | w280 | 0.34m | 2.6m | 8.2m |
9 | asset | 400x225 | w160 | 0.34m | 2.6m | 8.2m |
10 | hdpi | 1600x900 | wrap | 27.8m | 37.1m | 37.3m |
11 | hdpi | 1600x900 | w280 | 27.8m | 37.1m | 31.7m |
12 | hdpi | 1600x900 | w160 | 27.8m | 31.7m | 36.9m |
13 | hdpi | 800x450 | wrap | 6.95m | 9.7m | 14.9m |
14 | hdpi | 800x450 | w280 | 6.95m | 9.7m | 14.8m |
15 | hdpi | 800x450 | w160 | 6.95m | 9.7m | 15.3m |
16 | hdpi | 400x225 | wrap | 1.73m | 4.1m | 9.9m |
17 | hdpi | 400x225 | w280 | 1.73m | 4m | 9.7m |
18 | hdpi | 400x225 | w160 | 1.73m | 4.1m | 10.1m |
19 | xhdpi | 1600x900 | wrap | 15.6m | 18.9m | 24.9m |
20 | xhdpi | 1600x900 | w280 | 15.6m | 18.9m | 24.7m |
21 | xhdpi | 1600x900 | w160 | 15.6m | 18.9m | 24.7m |
22 | xhdpi | 800x450 | wrap | 3.9m | 6.3m | 12.4m |
23 | xhdpi | 800x450 | w280 | 3.9m | 6.3m | 11.5m |
24 | xhdpi | 800x450 | w160 | 3.9m | 6.3m | 12.2m |
25 | xhdpi | 400x225 | wrap | 0.97m | 3.2m | 9m |
26 | xhdpi | 400x225 | w280 | 0.97m | 3.2m | 8.8m |
27 | xhdpi | 400x225 | w160 | 0.97m | 3.2m | 9.1m |
28 | xxhdpi | 1600x900 | wrap | 6.95m | 9.7m | 16.7m |
29 | xxhdpi | 1600x900 | w280 | 6.95m | 9.7m | 16m |
30 | xxhdpi | 1600x900 | w160 | 6.95m | 9.7m | 16m |
31 | xxhdpi | 800x450 | wrap | 1.73m | 4.1m | 9.7m |
32 | xxhdpi | 800x450 | w280 | 1.73m | 4.1m | 9.7m |
33 | xxhdpi | 800x450 | w160 | 1.73m | 4.1m | 9.6m |
34 | xxhdpi | 400x225 | wrap | 0.43m | 2.6m | 8.4m |
35 | xxhdpi | 400x225 | w280 | 0.43m | 2.6m | 8.4m |
36 | xxhdpi | 400x225 | w160 | 0.43m | 2.6m | 8.7m |
结果分析:
- 使用的图片越大,越耗内存。实验数据:1/4/7。
- 图片内存与其显示大小无关。实验数据:1/2/3,4/5/6,7/8/9。误区1:图片显示区域越大,越耗内存。
- 加载 asset 目录的图片,图片占用内存等于实际大小,实验数据:1/2/3,4/5/6,7/8/9。计算方式:l x w x 4,长乘宽乘 4 (每个像素点占用 4 字节)。
- 加载 drawable 目录的图片,图片占用内存存在缩放。如:large 占用内存 5.49m , hdpi 对应 240 dpi 。因此图片实际占用内存 5.49 x (540/240)^2 = 27.79m 。误区2:5.49 x (540/240) = 12.35m。
关于 B/G/N 之间的关系还未研究透彻,如有了解还请告知。
第二组实验基于屏幕密度 360dpi 的设备,排除多数无用项。
序号 | 目录 | 分辨率 | 宽度 | B | G | N |
---|---|---|---|---|---|---|
37 | - | - | - | - | 1.8m | 7.4m |
38 | asset | 1600x900 | w160 | 5.49m | 8.7m | 14.7m |
39 | asset | 800x450 | w280 | 1.37m | 3.8m | 9.3m |
40 | asset | 400x225 | wrap | 0.34m | 2.6m | 8.3m |
41 | hdpi | 1600x900 | wrap | 12.3m | 15.4m | 21.4m |
41 | hdpi | 1600x900 | w280 | 12.3m | 15.4m | 21.3m |
42 | hdpi | 1600x900 | w160 | 12.3m | 15.4m | 21.4m |
43 | hdpi | 800x450 | w280 | 3.08m | 5.9m | 11m |
44 | hdpi | 400x225 | w160 | 0.77m | 3m | 8.8m |
45 | xhdpi | 1600x900 | wrap | 6.95m | 9.7m | 16m |
46 | xhdpi | 1600x900 | w280 | 6.95m | 9.7m | 16.1m |
47 | xhdpi | 1600x900 | w160 | 6.95m | 9.7m | 16.1m |
48 | xhdpi | 800x450 | w280 | 1.73m | 4.1m | 9.7m |
49 | xhdpi | 400x225 | w160 | 0.43m | 2.6m | 8.3m |
50 | xxhdpi | 1600x900 | wrap | 3.08m | 5.9m | 12.3m |
51 | xxhdpi | 1600x900 | w280 | 3.08m | 5.9m | 12.4m |
52 | xxhdpi | 1600x900 | w160 | 3.08m | 5.9m | 12.2m |
53 | xxhdpi | 800x450 | w280 | 0.77m | 3m | 8.7m |
54 | xxhdpi | 400x225 | w160 | 0.19m | 2.4m | 8.1m |
结果分析:
- 图片内存与屏幕密度无关。
第三组实验基于屏幕密度 540 dpi 的设备,使用 setImageResource 方式加载图片。
序号 | 目录 | 分辨率 | 宽度 | B | G | N |
---|---|---|---|---|---|---|
55 | hdpi | 1600x900 | w160 | 5.49m | 8.7m | 19m |
56 | hdpi | 800x450 | wrap | 1.37m | 3.8m | 9.3m |
57 | hdpi | 400x225 | w280 | 0.34m | 2.6m | 8.2m |
58 | xhdpi | 1600x900 | w280 | 5.49m | 8.7m | 19.9m |
59 | xhdpi | 800x450 | w160 | 1.37m | 3.8m | 9.3m |
60 | xhdpi | 400x225 | wrap | 0.34m | 2.6m | 8.6m |
61 | xxhdpi | 1600x900 | wrap | 5.49m | 8.7m | 14.6m |
62 | xxhdpi | 800x450 | w280 | 1.37m | 3.9m | 9.6m |
63 | xxhdpi | 400x225 | w160 | 0.34m | 2.6m | 8.3m |
结果分析:
- 使用 setImageResource 加载图片,没有对图片进行缩放。实验数据:55/58/61。误区3:使用不同屏幕密度下的图片存在缩放情况。
实验的最后发现,在布局用使用 android:src 引用图片时,图片内存也不缩放。因此,没有列出实验数据。
3. 源码分析
基于以上结果,通过分析源码,得以验证。
- asset 目录下图片占用内存是图片实际大小。
// 通过流的方式解析图片。
bitmap = BitmapFactory.decodeStream(getAssets().open("test.jpg"));
public static Bitmap decodeStream(InputStream is) {
return decodeStream(is, null, null);
}
/**
* 实际执行到下面的代码
*/
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
@Nullable Options opts) {
......
Bitmap bm = null;
Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try {
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
// 解析 asset 目录下的 文件,opts == null ,所以按照设备的 density 解析。
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
// 解密普通的文件流
bm = decodeStreamInternal(is, outPadding, opts);
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
// 更新 bitmap 的 density
setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}
return bm;
}
private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
// opts==null,因此未做处理。
if (outputBitmap == null || opts == null) return;
......
}
- drawable 目录下图片占用内存被缩放。
// 只有在使用下面的方式获取 bitmap 会缩放。
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
public static Bitmap decodeResource(Resources res, int id) {
return decodeResource(res, id, null);
}
public static Bitmap decodeResource(Resources res, int id, Options opts) {
validate(opts);
Bitmap bm = null;
InputStream is = null;
try {
final TypedValue value = new TypedValue();
// 根据 id 得到文件流,AssetInputStream
is = res.openRawResource(id, value);
// 根据流得到 bitmap
bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
......
}
return bm;
}
@Nullable
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
// 生成 Option
opts = new Options();
}
// 以 设备 320dpi ,图片在 xxhdpi 为例
if (opts.inDensity == 0 && value != null) {
final int density = value.density; // density = 480
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
// res.getDisplayMetrics().densityDpi = 320
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
@Nullable
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
@Nullable Options opts) {
......
Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try {
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
// opts inDensity 480 ,inTargetDensity 320 ,因此需要缩放。
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
bm = decodeStreamInternal(is, outPadding, opts);
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
// 根据 opts 设置图片的 density
setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}
return bm;
}
private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
if (outputBitmap == null || opts == null) return;
final int density = opts.inDensity;
if (density != 0) {
// 先设置成 480
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}
byte[] np = outputBitmap.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
// 由于支持缩放,再设置成 320
if (opts.inScaled || isNinePatch) {
outputBitmap.setDensity(targetDensity);
}
} else if (opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
outputBitmap.setDensity(Bitmap.getDefaultDensity());
}
}
- 通过 setImageResource,或布局引用,图片不缩放。
// 布局引用时,在 ImageView 的构造函数中加载图片
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
......
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
// 得到 Drawable 对象,如果使用 png 或 jpg 等图片,则是 BitmapDrawable
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
......
}
// TypedArray 类
public Drawable getDrawable(@StyleableRes int index) {
// 注意此处的 density 是 0
return getDrawableForDensity(index, 0);
}
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
......
// density = 0 ,执行下面代码
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
return null;
}
// ResourcesImpl 类
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
// useCache = true,后面的代码忽略
final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;
......
try {
......
// 读加载过的 BitmapDrawable
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null) {
cachedDrawable.setChangingConfigurations(value.changingConfigurations);
return cachedDrawable;
}
}
final Drawable.ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
Drawable dr;
boolean needsNewDrawableAfterCache = false;
if (cs != null) {
......
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
// 最终执行到此处加载图片
dr = loadDrawableForCookie(wrapper, value, id, density);
}
......
return dr;
} catch (Exception e) {
......
}
}
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density) {
......
final Drawable dr;
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
LookupStack stack = mLookupStack.get();
try {
// Perform a linear search to check if we have already referenced this resource before.
if (stack.contains(id)) {
throw new Exception("Recursive reference in drawable");
}
stack.push(id);
try {
// 处理使用 shape selector 等 使用 xml 生成的资源文件
if (file.endsWith(".xml")) {
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
rp.close();
} else {
// 通过 asset 的方式读取资源 file:///res/drawable-xhdpi/test.jpg
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
AssetInputStream ais = (AssetInputStream) is;
// 解析得到 BitmapDrawable
dr = decodeImageDrawable(ais, wrapper, value);
}
} finally {
stack.pop();
}
} catch (Exception | StackOverflowError e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
.......
return dr;
}
// 使用 setImageResource 方式同布局引用一致。
public void setImageResource(@DrawableRes int resId) {
......
resolveUri();
......
}
private void resolveUri() {
......
if (mResource != 0) {
try {
// 读取 Drawable
d = mContext.getDrawable(mResource);
} catch (Exception e) {
Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);
// Don't try again.
mResource = 0;
}
} else if (mUri != null) {
......
} else {
return;
}
updateDrawable(d);
}
// Context 类
public final Drawable getDrawable(@DrawableRes int id) {
return getResources().getDrawable(id, getTheme());
}
// Resources 类
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
return getDrawableForDensity(id, 0, theme);
}
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValueForDensity(id, density, value, true);
// 依然执行到 ResourcesImpl.loadDrawable 且 density = 0
return impl.loadDrawable(this, value, id, density, theme);
} finally {
releaseTempTypedValue(value);
}
}
4. 总结
经过上述实践验证,建议在使用图片时,控制好图片尺寸。避免直接根据 resId 转化成 bitmap 对象。如需实时释放 bitmap 对象,建议通过 BitmapDrawable 取到 bitmap 引用再释放。
另外,以前存在的三个误区请避免。
- 图片占用的内存只与图片大小有关。非图片文件大小。
- 图片缩放计算,长scale款scale = 长宽*scale^2。
- 布局中引用的图片以及 setImageResource 方式使用图片,图片不会根据密度缩放。
觉得有用?那打赏一个呗。去打赏