1:问题及现象
最近新迭代时,遇到一问题分割线颜色与设置颜色不符,透明度引起的问题。一般来说为了项目中简单的分割线写法都如下,直接在xml中
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/black_e"/>
排除各种可能后,猜测是系统帮我们缓存了ColorDrawable对象(background属性会被解析成drawable,至于是哪种drawable,取决于你设置的值)引起的。
通过如下尝试,有如下现象:
1.1:代码view.getBackground()去获取两条相同background为black_e的不同分割线的背景后,发现每个view的背景都是不同的ColorDrawable对象,那么问题到底出在哪,系统是否有混存drawable对象?
2.2: 通过代码view.setBackgroundDrawable(getResources().getDrawable(R.color.black_e))
网上有人遇到类似问题,并解释及给出解决方案,详见链接,这个说法及解释是否可信?
接下来开始从系统源码角度开始分析。
2:分析
2.1 :从xml 出发,android:background
属性最终会在android.view.View
构造时解析成Drawable
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
//...省略
Drawable background = null;
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.View_background:
background = a.getDrawable(attr);
break;
}
//...省略
2.2:由以上关键代码,跟踪到android.content.res.TypedArray
的getDrawablee( int index)
方法,如下
@Nullable
public Drawable getDrawable(@StyleableRes int index) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
return null;
}
关键代码为mResources.loadDrawable(value, value.resourceId, mTheme)
,其中的mResources
为android.content.res.Resources
,
到这里就解释了1.2的原因,因为殊途同归,这种方法和xml 设置背景的方法,最终都是android.content.res.Resources
的loadDrawable方法。
2.3:android.content.res.Resources
的loadDrawabl方法如下
@NonNull
Drawable loadDrawable(@NonNull TypedValue value, int id, @Nullable Theme theme)
throws NotFoundException {
return mResourcesImpl.loadDrawable(this, value, id, theme, true);
}
mResourcesImpl
为 android.content.res.ResourcesImpl
其loadDrawable
方法如下:
Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
boolean useCache) throws NotFoundException {
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;
}
// 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) {
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);
}
Drawable dr;
if (cs != null) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, null);
}
// 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 && useCache) {
dr.setChangingConfigurations(value.changingConfigurations);
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
return dr;
} catch (Exception e) {
//...省略
}
2.3.1 我们的drawable为colorDrawable所以isColorDrawable
值为true,
2.3.2 mPreloading
值仅在startPreloading
及finishPreloading
方法中被赋值,而这2个方法说明如下/** * Start preloading of resource data using this Resources object. Only * for use by the zygote process for loading common system resources. * {@hide} */
,也就是系统zygote 进程调用,当我们app在调用时mPreloading
必定为false
2.3.3 根据以上Resources的调用,传入的useCache
值必定为true
所以必定会进入其中的这部分代码
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}
caches
先不管是啥,从名字上看应该是一个缓存,从这可以看出系统确实有对drawable以id为键进行了缓存。
也就是当我们app非首次获取某一资源时,肯定achedDrawable != null
,即返cachedDrawable
对象;
如果为首次时才接着往下走创建对应新的drawable对象,在随后的cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
代码中做缓存
如果是这样,那为何1.1中得到的2个同id的drawable会为不同对象?
接着我们先看看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, 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);
}
}
}
2.3.2已经知道mPreloading
为false,故走到caches.put(key, theme, cs, usesTheme);
其中的cs
为调用drawable的getConstantState()
方式所获得的Drawable.ConstantState
对象
再查看caches
获取时调用的getInstance
方法相关实现
2.4 caches
为android.content.res.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;
}
原来系统对于drawable做的缓存并非为Drawable对象,而是其对应的Drawable.ConstantState
对象
所以其实当我们非首次获取某一资源时,我们拿到的其实是entry.newDrawable(resources, theme);
这行代码得到的drawable对象。在我们的场景下,entry
为colorDrawable时对应的getConstantState()
方法得到的android.graphics.drawable.ColorDrawable
下的ColorState
内部类,继承于Drawable.ConstantState
对象,它里面存储了ColorDrawable对应的色值。
其中的newDrawable
方法如下``
@Override
public Drawable newDrawable(Resources res) {
return new ColorDrawable(this, res);
}
到这里我们就明白了,1.1的原因了,每次得到的都是一个系统拿了id对应的缓存的色值,去创建的新的ColorDrawable对象。
3:解决方案
3.1 mutate方法是否有效?先看看其源码
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
mColorState = new ColorState(mColorState);
mMutated = true;
}
return this;
}
可以看到其只是新建了一个ColorState
对象返回,所以,如果当整个项目中在用到资源时,都先调用mutate()
方法,那就不会有问题。如果有地方更改了其资源对应的色值,后面用到该资源的方法即使调用了mutate()
也没有效果,因为系统缓存的那份最原始的ColorState
已经被更改了。
3.2 拿到ColorDrawable方法调用其setColor
方法,将自己真正想要的色值设置进去,当然为了不影响其它的资源,视情况调用mutate
方法
网友评论