Android Drawable 缓存浅析

Posted by Codyi96 on 2020-08-16

问题描述

先来看这样一份代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Act1 extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_act1);

View vAct = findViewById(R.id.view_act);
Drawable dAct = getResources().getDrawable(R.drawable.cheese, null);
dAct.setTint(getColor(R.color.black));
vAct.setBackground(dAct);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Act2 extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_act2);

View vAct = findViewById(R.id.view_act);
Drawable dAct = getResources().getDrawable(R.drawable.cheese, null);
vAct.setBackground(dAct);
}
}

上面的代码定义了两个页面,每个页面都展示了一个图片。不同的是,第一个页面使用着色器为图片涂上了黑色。
效果如下:

可以看到,着色器的配置影响了相同DrawableID的其他控件。

一探究竟

要想弄清其中的原因,还是得看源码才行。(以下源码基于API 26)

先看看调用入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Resources.java
...
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);
}
}

Resources仅仅是一个简单的封装类,作用只是创建一个新的TypedValue以及调用ResourcesImpl.loadDrawable()而已,具体还是要看ResourcesImpl

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
// ResourcesImpl.java
@Nullable
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;

if (density > 0 && value.density > 0 && value.density != TypedValue.DENSITY_NONE) {
if (value.density == density) {
value.density = mMetrics.densityDpi;
} else {
value.density = (value.density * mMetrics.densityDpi) / density;
}
}

try {

...
final boolean isColorDrawable;
final DrawableCache caches;
final long key;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
isColorDrawable = true;
caches = mColorDrawableCache;
key = value.data;
} else {
isColorDrawable = false;
caches = mDrawableCache;
key = (((long) value.assetCookie) << 32) | value.data;
}

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) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, density, null);
}

...
if (dr != null) {
dr.setChangingConfigurations(value.changingConfigurations);
if (useCache) {
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
if (needsNewDrawableAfterCache) {
Drawable.ConstantState state = dr.getConstantState();
if (state != null) {
dr = state.newDrawable(wrapper);
}
}
}
}

return dr;
} catch (Exception e) {
...
}
}

private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
Resources.Theme theme, boolean usesTheme, long key, Drawable dr) {
final Drawable.ConstantState cs = dr.getConstantState();
if (cs == null) {
return;
}

if (mPreloading) {
...
} else {
synchronized (mAccessLock) {
caches.put(key, theme, cs, usesTheme);
}
}
}

Drawable加载部分的代码比较长,这里省略了一些和本次主题无关的代码。
可以看到,这里有一个DrawableCache的缓存设计,通过TypedValue计算Drawable对应的Key,然后从缓存中尝试命中,如果命中则直接返回。否则,使用loadDrawableForCookie()通过XmlResourceParserDrawable.createFromResourceStream()获得一个全新的Drawable对象,并通过cacheDrawable()存入缓存。

🍎值得注意的是,缓存并不是直接存储了Drawable对象,而是存储了其中的一些状态(dr.getConstantState()),即Drawable.ConstantState:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Drawable.java
public static abstract class ConstantState {
public abstract @NonNull Drawable newDrawable();
public @NonNull Drawable newDrawable(@Nullable Resources res) {
return newDrawable();
}
public @NonNull Drawable newDrawable(@Nullable Resources res,
@Nullable @SuppressWarnings("unused") Theme theme) {
return newDrawable(res);
}
public abstract @Config int getChangingConfigurations();
public boolean canApplyTheme() {
return false;
}
}

Drawable.ConstantState是一个虚拟类,具体实现还得看它的实现类BitmapDrawable.BitmapState(这里的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
// BitmapDrawable.java
public class BitmapDrawable extends Drawable {
final static class BitmapState extends ConstantState {
final Paint mPaint;
int[] mThemeAttrs = null;
Bitmap mBitmap = null;
ColorStateList mTint = null;
Mode mTintMode = DEFAULT_TINT_MODE;
int mGravity = Gravity.FILL;
float mBaseAlpha = 1.0f;
Shader.TileMode mTileModeX = null;
Shader.TileMode mTileModeY = null;
int mSrcDensityOverride = 0;
int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;

boolean mAutoMirrored = false;

@Config int mChangingConfigurations;
boolean mRebuildShader;

BitmapState(Bitmap bitmap) {
mBitmap = bitmap;
mPaint = new Paint(DEFAULT_PAINT_FLAGS);
}

BitmapState(BitmapState bitmapState) {
mBitmap = bitmapState.mBitmap;
mTint = bitmapState.mTint;
mTintMode = bitmapState.mTintMode;
mThemeAttrs = bitmapState.mThemeAttrs;
mChangingConfigurations = bitmapState.mChangingConfigurations;
mGravity = bitmapState.mGravity;
mTileModeX = bitmapState.mTileModeX;
mTileModeY = bitmapState.mTileModeY;
mSrcDensityOverride = bitmapState.mSrcDensityOverride;
mTargetDensity = bitmapState.mTargetDensity;
mBaseAlpha = bitmapState.mBaseAlpha;
mPaint = new Paint(bitmapState.mPaint);
mRebuildShader = bitmapState.mRebuildShader;
mAutoMirrored = bitmapState.mAutoMirrored;
}

@Override
public boolean canApplyTheme() {
return mThemeAttrs != null || mTint != null && mTint.canApplyTheme();
}

@Override
public Drawable newDrawable() {
return new BitmapDrawable(this, null);
}

@Override
public Drawable newDrawable(Resources res) {
return new BitmapDrawable(this, res);
}

@Override
public @Config int getChangingConfigurations() {
return mChangingConfigurations
| (mTint != null ? mTint.getChangingConfigurations() : 0);
}
}
}

可以看到,状态信息包括了着色器ColorStateList mTint,因此对页面一的图片着色才会影响到页面二的图片。
你可能会疑惑ColorStateList类和着色器有什么关系,我们来看一下Drawable.setTint()方法:

1
2
3
public void setTint(@ColorInt int tintColor) {
setTintList(ColorStateList.valueOf(tintColor));
}

用户通过setTint()配置的着色器颜色,实际上会被封装成ColorStateList对象配置到Drawable中。换句话说,正是ColorStateList对象使图片被泼上颜色的。

我们来总结一下导致本文开头那个奇怪现象的原因: 页面一和页面二使用同一个DrawableID获取Drawable对象,当页面一创建Drawable时,该对象内的getConstantState()值,即BitmapDrawable.ConstantState被缓存。随后,接下来的setTint()方法修改了ConstantState中的mTint。因此,当页面二获取Drawable对象时,就直接使用了缓存数据,获取到了已被上色的图片。

禁用缓存

解决方法也很简单,修改一行代码即可:

1
2
3
4
// Act1.java
...
- Drawable dAct = getResources().getDrawable(R.drawable.cheese, null);
+ Drawable dAct = getResources().getDrawable(R.drawable.cheese, null).mutate();

在页面一中,对获取到的Drawable使用mutate()封装即可。如此一来,后续对此Drawable对象调用setTint()就只会影响当前Drawable对象了。

更多信息可以查阅官网的方法描述,简而言之mutate()的作用就是禁止当前实例与其他实例共享状态

拓展

本文仅针对位图进行分析,因此仅涉及BitmapDrawable。实际上Drawable的衍生类还有ColorDrawable等等,有兴趣可以进一步探索。此外,Drawable还有预加载机制,预加载的图片资源将缓存在sPreloadedDrawables中,也是一个值得探索的课题。