Android 图片适配,真的不是你想像的那样,至少在写这篇文章以前,我陷在一个很大很大的误区中。java
全部关于适配的基本概念,这里很少介绍,资料有不少。下面只介绍点比较重要的部分。android
等级 | 密度 | 比例 |
---|---|---|
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 适配问题。git
如:三星 C9,6英寸屏幕,分辨率 1920x1080 ,按照公式计算屏幕密度 367 dpi ,更接近 320dpi ,所以适配时,会取 xhdpi 目录下的数据。github
但实际中,会取 xxhdpi 数据,由于实际屏幕密度是 420 dpi。(经过代码的方式获取)app
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 。this
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: 560spa
在 S8 上 ,1dp=3.5px ,411dp 约等于 1440px ,表示整个屏幕的宽度。
很庆幸,这两台手机上的适配数据是同样的,高度会存在差别,可是一般都是滚动长页面,或者留白端页面不受太大影响。若刚好是满屏页面,则不适用。
今日头条的适配方案便是经过修改 density 的 值进行适配。不知道什么缘由,他们在《今日头条》7.5 版本中未使用此适配方式。
言归正传,关于图片适配才是咱们的主题。
秉着实践是检验真理的惟一标准这一原则,作了以下实验。三种尺寸的图片,放置在四个目录目录,用三种尺寸的 ImageView ,用三种方式加载图片,检查其内存使用的状况。
图片尺寸
图片目录
ImageView
引用方式
加载 asset 目录下的图片,只能使用 setImageBitmap 的方式。
第一组实验,使用 1 2 3 以及 setImageBitmap ,得出 3x4x3x1 = 36 条数据,以下表。
序号 | 目录 | 分辨率 | 宽度 | 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 |
结果分析:
关于 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 |
结果分析:
实验的最后发现,在布局用使用 android:src 引用图片时,图片内存也不缩放。所以,没有列出实验数据。
基于以上结果,经过分析源码,得以验证。
// 经过流的方式解析图片。
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;
......
}
复制代码
// 只有在使用下面的方式获取 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());
}
}
复制代码
// 布局引用时,在 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);
}
}
复制代码
通过上述实践验证,建议在使用图片时,控制好图片尺寸。避免直接根据 resId 转化成 bitmap 对象。如需实时释放 bitmap 对象,建议经过 BitmapDrawable 取到 bitmap 引用再释放。
另外,之前存在的三个误区请避免。
以为有用?那打赏一个呗。去打赏