一、问题
最近在工作中遇到了一个关于Drawable缓存的问题:在调用Drawable.setAlpha()改变透明度后,通过getResources().getDrawable(同一个资源id)获得的Drawable透明度也会跟着改变,猜测是Bitmap缓存的影响,接下来看看源码一探究竟。
二、源码分析
一般我们利用资源id获得Drawable的代码是这样的:
Darwable d = getResources().getDrawable(R.drawable.xxx);
getDarwable方法实现如下:
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
return impl.loadDrawable(this, value, id, theme, true);
} finally {
releaseTempTypedValue(value);
}
}
接下来看看ResourcesImpl的loadDrawable方法,里面有一段:
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}
DrawableCache的getInstance方法如下:
public Drawable getInstance(long key, Resources resources, Resources.Theme theme) {
final Drawable.ConstantState entry = get(key, theme);
if (entry != null) {
return entry.newDrawable(resources, theme);
}
return null;
}
这里从缓存中获得一个ConstantState对象,通过ConstantState的newDrawable方法获得一个新的Drawable对象。
ConstantState是一个Drawable中的一个抽象类,注释中有这么一段:
This abstract class is used by {@link Drawable}s to store shared constant state and data
between Drawables. {@link BitmapDrawable}s created from the same resource will for instance
share a unique bitmap stored in their ConstantState.
那就让我们到BitmapDrawable中看看ConstantState的实现。
看newDrawable方法先。
final static class BitmapState extends ConstantState {
final Paint mPaint;
// Values loaded during inflation.
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 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;
mTargetDensity = bitmapState.mTargetDensity;
mBaseAlpha = bitmapState.mBaseAlpha;
mPaint = new Paint(bitmapState.mPaint);
mRebuildShader = bitmapState.mRebuildShader;
mAutoMirrored = bitmapState.mAutoMirrored;
}
.
.
.
@Override
public Drawable newDrawable() {
return new BitmapDrawable(this, null);
}
@Override
public Drawable newDrawable(Resources res) {
return new BitmapDrawable(this, res);
}
.
.
.
}
从这里我们大致得知,BitmapState持有Bitmap。
最后我们看看Drawable是如何进行缓存的。回到loadDrawable方法中,看到最后有这样的代码:
// If we were able to obtain a drawable, store it in the appropriate
// cache: preload, not themed, null theme, or theme-specific. Don't
// pollute the cache with drawables loaded from a foreign density.
if (dr != null && useCache) {
dr.setChangingConfigurations(value.changingConfigurations);
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
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) {
final int changingConfigs = cs.getChangingConfigurations();
if (isColorDrawable) {
if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
sPreloadedColorDrawables.put(key, cs);
}
} else {
if (verifyPreloadConfig(
changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) {
if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) {
// If this resource does not vary based on layout direction,
// we can put it in all of the preload maps.
sPreloadedDrawables[0].put(key, cs);
sPreloadedDrawables[1].put(key, cs);
} else {
// Otherwise, only in the layout dir we loaded it for.
sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
}
}
}
} else {
synchronized (mAccessLock) {
caches.put(key, theme, cs, usesTheme);
}
}
}
可以看到缓存的是ConstantState的实例。
到这里我们就可以得出结论,同一资源id(并且是同一类型)的Drawable共享ConstantState,ConstantState持有Bitmap,因此Drawable共享Bitmap,所以当对某个Drawable进行setAlpha或其他影响Bitmap属性的操作时,会对同一个资源ID的其他Drawable对象造成影响。
三、解决问题的方法
使用Drawable的mutate方法即可解决问题。
/**
* Make this drawable mutable. This operation cannot be reversed. A mutable
* drawable is guaranteed to not share its state with any other drawable.
* This is especially useful when you need to modify properties of drawables
* loaded from resources. By default, all drawables instances loaded from
* the same resource share a common state; if you modify the state of one
* instance, all the other instances will receive the same modification.
*
* Calling this method on a mutable Drawable will have no effect.
*
* @return This drawable.
* @see ConstantState
* @see #getConstantState()
*/
public @NonNull Drawable mutate() {
return this;
}
看看BitmapDrawable的mutate方法。
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
mBitmapState = new BitmapState(mBitmapState);
mMutated = true;
}
return this;
}
BitmapDrawable中的mutate方法会创建一个新的ConstantState对象,即不再与其他相同资源ID的Drawable共享ConstantState,因此再对这个Drawable作操作就不会影响到其他的Drawable了。