从 setTint 引发的Drawable 的缓存问题
这两天在做一个新需求是突然发现 DrawableCompat.setTint()更改drawable 渲染颜色的方法居然是全局生效的。变相说明getResource().getDrawable() 每次取出drawable的时候都是从一个缓存池里面出去的。
避免缓存的解决方案
第一反应是clone一个新的对象出来进行修改 但是貌似drawable并没有实现Cloneable的借口0那看来得想想别的办法了
看了一会源码果然google在drawable类中已经给我们提供了一个 mutate方法 看注释看似乎这个方法可以返回一个新的drawable对象
/**
* 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;
}
最终的实现是根据不同的drawable类型有不同的实现方式 这里就用bitmapdrawable作为例子 不仅clone了的对象 还禁止了这个对象再次被复制 猜测是为了方式无限clone导致oom
/**
* A mutable BitmapDrawable still shares its Bitmap with any other Drawable
* that comes from the same resource.
*
* @return This drawable.
*/
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
mBitmapState = new BitmapState(mBitmapState);
mMutated = true;
}
return this;
}
drawable的缓存机制
在ResourcesImpl类中 看到 drawable 是根据 theme 和 density判断是否使用缓存的 大部分的机制缓存注释已经说的很明白了
- 1 判断是否符合当前dpi
- 2 判断该theme 是否被缓存 有则直接返回
- 3 从预加载的缓存池中取出 有则直接返回(在我们的app中这个直接忽略)
- 4 调用loadDrawableForCookie
Ps :color 和drawable的缓存都是分开的
@Nullable
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
// If the drawable's XML lives in our current density qualifier,
// it's okay to use a scaled version from the cache. Otherwise, we
// need to actually load the drawable from XML.
final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;
···
// mPreloading值仅在startPreloading及finishPreloading方法中被赋值,根据注释说明这两个方法只会被 zygote进程调用所以我们这里mPreloading始终为false
// First, check whether we have a cached version of this drawable
// that was inflated against the specified theme. Skip the cache if
// we're currently preloading or we're not using the cache.
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null) {
cachedDrawable.setChangingConfigurations(value.changingConfigurations);
return cachedDrawable;
}
}
//这里的TypeValue 是在native层被赋值的。
// 具体是 /frameworks/base/libs/androidfw/AssetManager2.cpp:GetResource方法 有兴趣的可以看一下
final boolean isColorDrawable;
final DrawableCache caches;
//这里的key 也就是这里的TypeValuedata其实就是resID
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;
}
// Next, check preloaded drawables. Preloaded drawables may contain
// unresolved theme attributes.
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);
}
····
// Determine if the drawable has unresolved theme attributes. If it
// does, we'll need to apply a theme and store it in a theme-specific
// cache.
final boolean canApplyTheme = dr != null && dr.canApplyTheme();
if (canApplyTheme && theme != null) {
// 如果有theme的特殊配置也会mutate 防止更改缓存中数据
dr = dr.mutate();
dr.applyTheme(theme);
dr.clearMutated();
}
// 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) {
dr.setChangingConfigurations(value.changingConfigurations);
if (useCache) {
//缓存资源
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
// 目前只有 DrawableContainer(主要是帧动画和statedrawable) 需要。 每次重置新的state
// 要注意帧动画这玩意一开始就会加载所有的图片 及其容易oom
if (needsNewDrawableAfterCache) {
Drawable.ConstantState state = dr.getConstantState();
if (state != null) {
dr = state.newDrawable(wrapper);
}
}
}
}
return 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;
}
//可以忽略preloading部分
if (mPreloading) {
···
} else {
synchronized (mAccessLock) {
// 在这里存进缓存。
caches.put(key, theme, cs, usesTheme);
}
}
}