概述
每個 Drawable 都有一個對應的ConstantState
,這個 state 保存了 Drawable 所有的關鍵信息。由於 Drawable 的廣泛使用,系統爲了優化性能(節省內存佔用),相同資源的 Drawable 都共享同一個ConstantState
。這個的含義用較爲白話的方式解釋爲:假使有一個View,內部邏輯加載了一個 Drawable,即使多次創建這個 View 的實例,但每個 View 實例獲取的 Drawable 都是同一個。
複用 State
這種優化也會導致一些問題,當我們修改了 Drawable 的屬性,比如透明度,那麼會影響到其他 View 實例中 Drawable 的透明度值,因爲他們的狀態是共享的。
這個問題常見於修改了某些 View 背景的透明度。如 View 背景初始爲白色,當更改了其透明度後,其他背景同樣爲白色的 View 也會受到影響。又或者對於同一個資源在多個地方使用了,在A地方進行透明度修改也會影響到其餘使用的地方。
// 導致其他用到 Drawable 也受影響的代碼
view.getBackground().setAlpha(0);
解決方案:
drawable.mutate().setAlpha(0) // 通過 mutate() 方法,複製一份 ConstantState 進行修改避免影響到其他地方
源碼解析
以下基於 API 33 源碼進行分析
drawable加載流程
// Resources#getDrawable
ppublic Drawable getDrawable(@DrawableRes int id) throws NotFoundException {
final Drawable d = getDrawable(id, null);
if (d != null && d.canApplyTheme()) {
Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme "
+ "attributes! Consider using Resources.getDrawable(int, Theme) or "
+ "Context.getDrawable(int).", new RuntimeException());
}
return d;
}
// Resources#getDrawable
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
return getDrawableForDensity(id, 0, theme);
}
// Resources#getDrawableForDensity
@Nullable
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 loadDrawable(value, id, density, theme);
} finally {
releaseTempTypedValue(value);
}
}
// Resources#loadDrawable
@NonNull
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
throws NotFoundException {
return mResourcesImpl.loadDrawable(this, value, id, density, theme);
}
``
實際調用入口爲ResourcesImpl#loadDrawable
,其大致做了以下事情:
- 判斷是否能夠使用緩存
- 能夠使用並命中緩存的話,取出對應的 ConstantState 並創建一個 Drawable
- 不能使用或沒有命中緩存的,走 Drawable 創建流程。創建完成後,對於能夠使用緩存的,將創建的 Drawable 對應的 ConstantState 加入緩存池中
// ResourcesImpl#loadDrawable
@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.
// 判斷是否能夠使用緩存,通常我們使用的 Resouces#getDrawable 方法 density 爲0,因此 useCache 爲true
final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;
// Pretend the requested density is actually the display density. If
// the drawable returned is not the requested density, then force it
// to be scaled later by dividing its density by the ratio of
// requested density to actual device density. Drawables that have
// undefined density or no density don't need to be handled here.
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 {
if (TRACE_FOR_PRELOAD) {
// Log only framework resources
if ((id >>> 24) == 0x1) {
final String name = getResourceName(id);
if (name != null) {
Log.d("PreloadDrawable", name);
}
}
}
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;
}
// 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;
}
}
// 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);
}
// 判斷預加載的 ConstantState 是否存在,不存在創建 Drawable
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);
}
// DrawableContainer' constant state has drawables instances. In order to leave the
// constant state intact in the cache, we need to create a new DrawableContainer after
// added to cache.
if (dr instanceof DrawableContainer) {
needsNewDrawableAfterCache = true;
}
// 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) {
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);
// 使用緩存,調用cacheDrawable進行緩存,實際緩存的是drawable的ConstantState對象
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) {
String name;
try {
name = getResourceName(id);
} catch (NotFoundException e2) {
name = "(missing name)";
}
// The target drawable might fail to load for any number of
// reasons, but we always want to include the resource name. // Since the client already expects this method to throw a // NotFoundException, just throw one of those. final NotFoundException nfe = new NotFoundException("Drawable " + name
+ " with resource ID #0x" + Integer.toHexString(id), e);
nfe.setStackTrace(new StackTraceElement[0]);
throw nfe;
}
}
// ResourcesImpl#cacheDrawable
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, ActivityInfo.CONFIG_LAYOUT_DIRECTION, value.resourceId, "drawable")) {
if ((changingConfigs & ActivityInfo.CONFIG_LAYOUT_DIRECTION) == 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
,其是Drawable
的抽象內部類。
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;
}
}
現在通過較爲常見的BitmapDrawable
相關聯的BitmapState
來了解一下ConstantState
的實際用途。
final static class BitmapState extends ConstantState {
final Paint mPaint;
// Values loaded during inflation.
int[] mThemeAttrs = null;
Bitmap mBitmap = null;
ColorStateList mTint = null;
BlendMode mBlendMode = DEFAULT_BLEND_MODE;
int mGravity = Gravity.FILL;
float mBaseAlpha = 1.0f;
Shader.TileMode mTileModeX = null;
Shader.TileMode mTileModeY = null;
// The density to use when looking up the bitmap in Resources. A value of 0 means use
// the system's density. int mSrcDensityOverride = 0;
// The density at which to render the bitmap.
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;
mBlendMode = bitmapState.mBlendMode;
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);
}
}
從ResourcesImpl#loadDrawable
方法我們得知,當能夠使用並命中緩存時,會調用ConstantState#newDrawable
方法得到一個 Drawable 對象。而這個方法對應到BitmapState
的newDrawable
,其實現方式就是創建了一個BitmapDrawable
並把自身當作參數傳遞進去實現了狀態共享。
// BitmapDrawable
private BitmapDrawable(BitmapState state, Resources res) {
init(state, res);
}
// BitmapDrawable#init
private void init(BitmapState state, Resources res) {
mBitmapState = state;
updateLocalState(res);
if (mBitmapState != null && res != null) {
mBitmapState.mTargetDensity = mTargetDensity;
}
}
綜上源碼分析,我們知道了 Drawable 的一整個複用流程的大致邏輯。