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。(通过代码的方式获取)
1 2 3 4 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
引用方式
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 目录下图片占用内存是图片实际大小。
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 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(); 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" ); } 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 ; ...... }
drawable 目录下图片占用内存被缩放。
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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 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 (); is = res.openRawResource(id, value); 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 ) { opts = new Options (); } if (opts.inDensity == 0 && value != null ) { final int density = value.density; 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 ) { 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(); 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" ); } 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 ) { 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); if (opts.inScaled || isNinePatch) { outputBitmap.setDensity(targetDensity); } } else if (opts.inBitmap != null ) { outputBitmap.setDensity(Bitmap.getDefaultDensity()); } }
通过 setImageResource,或布局引用,图片不缩放。
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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 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); final Drawable d = a.getDrawable(R.styleable.ImageView_src); ...... } public Drawable getDrawable (@StyleableRes int index) { 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)) { ...... return mResources.loadDrawable(value, value.resourceId, density, mTheme); } return null ; } Drawable loadDrawable (@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density, @Nullable Resources.Theme theme) throws NotFoundException { final boolean useCache = density == 0 || value.density == mMetrics.densityDpi; ...... try { ...... 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 { if (stack.contains(id)) { throw new Exception ("Recursive reference in drawable" ); } stack.push(id); try { if (file.endsWith(".xml" )) { final XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable" ); dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null ); rp.close(); } else { final InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); AssetInputStream ais = (AssetInputStream) is; 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; } public void setImageResource (@DrawableRes int resId) { ...... resolveUri(); ...... } private void resolveUri () { ...... if (mResource != 0 ) { try { d = mContext.getDrawable(mResource); } catch (Exception e) { Log.w(LOG_TAG, "Unable to find resource: " + mResource, e); mResource = 0 ; } } else if (mUri != null ) { ...... } else { return ; } updateDrawable(d); } public final Drawable getDrawable (@DrawableRes int id) { return getResources().getDrawable(id, getTheme()); } 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 ); return impl.loadDrawable(this , value, id, density, theme); } finally { releaseTempTypedValue(value); } }
4. 总结 经过上述实践验证,建议在使用图片时,控制好图片尺寸。避免直接根据 resId 转化成 bitmap 对象。如需实时释放 bitmap 对象,建议通过 BitmapDrawable 取到 bitmap 引用再释放。
另外,以前存在的三个误区请避免。
图片占用的内存只与图片大小有关。非图片文件大小。
图片缩放计算,长scale 款scale = 长 宽*scale^2。
布局中引用的图片以及 setImageResource 方式使用图片,图片不会根据密度缩放。
源码地址
觉得有用?那打赏一个呗。去打赏