概要
在Android中,出於對內存優化的考慮,對於圖片的存儲使用了緩存機制,資源id相同的圖片使用了同一個位圖信息,如果對這些機制不瞭解的話開發過程中就會造成一些困擾。本文通過實例和分析Drawable的緩存機制源碼的方式來介紹一下Drawable的緩存機制,並且瞭解一下Drawable.mutate()的用法。
問題演示
下面我們通過一個實例來演示一個我們在使用Drawable過程中經常會遇到的一個問題。
首先貼出UI佈局文件,這裏放了兩個 ImageView
,它們的寬高不一樣,而且對他們加以藍色的背景。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:mz="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="40dp"
android:orientation="vertical">
<ImageView
android:id="@+id/first"
android:layout_width="100dp"
android:layout_height="200dp"
android:scaleType="fitXY"
android:background="#1E90FF"/>
<ImageView
android:id="@+id/second"
android:layout_width="200dp"
android:layout_height="100dp"
android:layout_marginTop="50dp"
android:scaleType="fitXY"
android:background="#1E90FF"/>
</LinearLayout>
實例1
首先我們給第一個ImageView
設置一個顯示圖片。
final BitmapDrawable firstDrawable = (BitmapDrawable) getResources()
.getDrawable(R.drawable.test_mutate);
mFirstImage = (ImageView) findViewById(R.id.first);
mSecondImage = (ImageView) findViewById(R.id.second);
mFirstImage.setImageDrawable(firstDrawable);
看下面的效果,因爲第二個我們沒有設置前景圖片,因此會現實背景圖片。這個很正常,我們不會有什麼疑問。
實例2
接下來我們在原來代碼的基礎上添加下面代碼,爲第二個ImageView
設置圖片。
......
mSecondImage.setImageDrawable(firstDrawable);
看一下效果圖,第一個圖片的現實效果和實例1變的不一樣了,你也許會感覺這個很正常,因爲同一個Drawable
對象設置給兩個大小不同的ImageView
,第二個尺寸改變以後第一個也跟着改變了。
實例3
那麼我們再實例化一個Drawable
對象設置給第二個ImageView
。
final BitmapDrawable firstDrawable = (BitmapDrawable) getResources()
.getDrawable(R.drawable.test_mutate);
final BitmapDrawable secondDrawable = (BitmapDrawable) getResources()
.getDrawable(R.drawable.test_mutate);
mFirstImage = (ImageView) findViewById(R.id.first);
mSecondImage = (ImageView) findViewById(R.id.second);
mFirstImage.setImageDrawable(firstDrawable);
mSecondImage.setImageDrawable(secondDrawable);
看一下效果圖,這下顯示正常了,這個也可以理解,兩個不同的Drawable
對象設置給不同的ImageView
,他們互不干涉。那麼真的是這樣的嗎?再接着往下面看。
實例4
我們在上面的代碼的基礎上把第二個Drawable
的 alpha 設置爲15 0 。
......
secondDrawable.setAlpha(150);
看下面效果圖,奇怪的現象發生了,第一個圖片也變成半透明的了,爲什麼呢?
問題1:爲什麼設置第二個圖片的 alpha 會對第一個圖片有影響?
實例5
你也許聽說過 mutate()
的作用,那麼現在我們改一下代碼:
......
secondDrawable.mutate().setAlpha(150);
看下面效果圖,現在正常了。
問題2: mutate()
方法是做什麼的?
實例6
下面我們再對代碼稍作修改:
final BitmapDrawable firstDrawable = (BitmapDrawable) getResources()
.getDrawable(R.drawable.test_mutate);
mFirstImage = (ImageView) findViewById(R.id.first);
mSecondImage = (ImageView) findViewById(R.id.second);
mFirstImage.setImageDrawable(firstDrawable);
mSecondImage.setImageDrawable(firstDrawable.getConstantState().newDrawable());
這樣兩個圖片也能正常顯示出來了。
修改一下最後一行代碼:
Drawable drawable = firstDrawable.getConstantState().newDrawable();
mSecondImage.setImageDrawable(drawable);
drawable.setAlpha(150);
這樣的效果仍然是兩個圖片都是半透明的。
也需要調用drawable.mutate().setAlpha(150);
才能使第二個半透明,第一個沒有半透明。
問題3: Drawable.getConstantState().newDrawable()
又是怎麼回事?
問題分析
首先通過實例3我們可以得到這樣的結論:分別兩次調用getResources().getDrawable(R.drawable.test_mutate)
肯定不是指向同一個對象的。爲了驗證真實性,我們添加Log。
Log.e("Test","firstDrawable = "+firstDrawable+", secondDrawable = "+secondDrawable);
有下面打印:
3110 3110 E Test : firstDrawable = android.graphics.drawable.BitmapDrawable@3109294, secondDrawable = android.graphics.drawable.BitmapDrawable@d2fb13d
那麼,firstDrawable
和secondDrawable
肯定不是指向同一個對象了。
問題1
我們來分析問題1爲什麼設置第二個圖片的 alpha 會對第一個圖片有影響?
兩個完全不同的ImageView
因爲設置了資源id相同的圖片就產生了關聯,現在我們可以猜想,firstDrawable
和secondDrawable
肯定存在某種聯繫的。此時我們可能立刻想到爲了優化性能,Android內部是不是針對相同的資源使用了同一份位圖信息呢?是不是有什麼緩存機制呢?帶着這個疑問我們先來分析Resources
的源碼。
在Resources
類中,我們找到了loadDrawable()
方法:
...
final DrawableCache caches;
...
final Drawable cachedDrawable = caches.getInstance(key, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
...
這裏會從caches
裏面獲取曾經加載過的資源,如果找到就直接返回緩存。具體這個緩存是怎麼放進去的我們就不再詳細分析了。前面我們也說了,firstDrawable
和secondDrawable
是不同的對象,那他們在這個緩存裏肯定也不是同一個Drawable
了。
再直接往下看,DrawableCache
是什麼呢?DrawableCache
繼承自ThemedResourceCache
。
下來看一下DrawableCache
的getInstance()
方法:
public Drawable getInstance(long key, Resources.Theme theme) {
final Drawable.ConstantState entry = get(key, theme);
if (entry != null) {
return entry.newDrawable(mResources, theme);
}
return null;
}
現在我們知道了,caches
裏面緩存的不是Drawable
對象,而是Drawable.ConstantState
對象。
public static abstract class ConstantState {
public abstract Drawable newDrawable();
public Drawable newDrawable(Resources res) {
return newDrawable();
}
public Drawable newDrawable(Resources res, Theme theme) {
return newDrawable(null);
}
public abstract int getChangingConfigurations();
public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
return 0;
}
protected final boolean isAtlasable(Bitmap bitmap) {
return bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888;
}
public boolean canApplyTheme() {
return false;
}
}
ConstantState
類是一個抽象類,BitmapDrawable.BitmapState
便是它的實現類之一。由於getResources().getDrawable(R.drawable.test_mutate)
得到的是BitmapDrawable
,那麼我們就重點分析這個類。
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 mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
boolean mAutoMirrored = false;
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 boolean canApplyTheme() {
return mThemeAttrs != null || mTint != null && mTint.canApplyTheme();
}
@Override
public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
if (isAtlasable(mBitmap) && atlasList.add(mBitmap)) {
return mBitmap.getWidth() * mBitmap.getHeight();
}
return 0;
}
@Override
public Drawable newDrawable() {
return new BitmapDrawable(this, null);
}
@Override
public Drawable newDrawable(Resources res) {
return new BitmapDrawable(this, res);
}
@Override
public int getChangingConfigurations() {
return mChangingConfigurations
| (mTint != null ? mTint.getChangingConfigurations() : 0);
}
}
在newDrawable()
方法裏面返回的是一個新的BitmapDrawable
對象,但是所有相同資源的BitmapDrawable
對象共用同一個BitmapState
對象。我們注意到BitmapState
的mBitmap
屬性,這也驗證了前面的猜想,它們共用同一個Bitmap
。
private BitmapDrawable(BitmapState state, Resources res) {
mBitmapState = state;
updateLocalState(res);
}
@Override
public void setAlpha(int alpha) {
final int oldAlpha = mBitmapState.mPaint.getAlpha();
if (alpha != oldAlpha) {
mBitmapState.mPaint.setAlpha(alpha);
invalidateSelf();
}
}
那麼我們setAlpha()
操作實際改變的是mBitmapState
的屬性值,這也就不難理解問題1了,因爲它們用的是同一個BitmapState
對象。
爲了驗證這個結論,我們添加打印:
Log.e("Test","firstDrawable = "+firstDrawable.getConstantState()+", secondDrawable = "+secondDrawable.getConstantState());
打印如下:
4433 4433 E Test : firstDrawable = android.graphics.drawable.BitmapDrawable$BitmapState@3109294, secondDrawable = android.graphics.drawable.BitmapDrawable$BitmapState@3109294
它們確實是指向同一個對象的。
它們的關係可以用下圖表示:
問題2
接下來再來分析問題2: 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 Drawable mutate() {
return this;
}
mutate()
返回的Drawable
對象不再與同資源的其他Drawable
共用 state,那麼它的屬性改變後就不再影響其他的Drawable
了。
在BitmapDrawable
的mutate()
方法裏面確實又新建了一個BitmapState
對象。
/**
* 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;
}
它們的關係可以用下圖表示:
注意: mutate操作是不可逆轉的,已經調用過mutate()
方法的BitmapDrawable
對象再調用mutate()
是不起作用的。這點在代碼中可以清楚的看到。
問題3
記下來分析問題3: Drawable.getConstantState().newDrawable()
又是怎麼回事?
經過上面的源碼分析,這個很容易就理解了,它就是獲得Drawable
的ConstantState
來重新實例化一個Drawable
,兩個Drawable
還是共用一個ConstantState
。
這個和重新調用getResources().getDrawable(R.drawable.test_mutate)
原理是一樣的。
附加問題
那爲什麼設置 alpha 兩個圖片互有影響,而在實例3中第二個Drawable
大小尺寸改變卻沒有影響呢?
這就要附帶分析一下ImageView
的ScaleType
原理。
我們從ImageView.setImageDrawable
開始分析,這個方法的調用流程如圖:
├── ImageView.setImageDrawable
└── ImageView.updateDrawable
└── configureBounds()
private void configureBounds() {
...
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
}
...
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
int saveCount = canvas.getSaveCount();
canvas.save();
if (mCropToPadding) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
scrollX + mRight - mLeft - mPaddingRight,
scrollY + mBottom - mTop - mPaddingBottom);
}
canvas.translate(mPaddingLeft, mPaddingTop);
if (mDrawMatrix != null) {
canvas.concat(mDrawMatrix);
}
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
在configureBounds()
裏面根據不同的ScaleType
會進行不同的變換,包括設置繪製邊界、縮放、位移、繪製是的矩陣變換等等。
在onDraw()
方法中再把這個Drawable
繪製到Canvas
上,這些改變變化的只是Drawable
本身,而對ConstantState
不會有改變。
爲了驗證這個結論,我們在實例3代碼基礎上,添加一些Log。
public void refresh(View v){
Log.e("Test","1 rect1 = "+mFirstImage.getDrawable().getBounds());
Log.e("Test","2 rect2 = "+mSecondImage.getDrawable().getBounds());
}
打印如下:
21313 21313 E Test : 1 rect1 = Rect(0, 0 - 200, 400)
21313 21313 E Test : 2 rect2 = Rect(0, 0 - 400, 200)
它們的Drawable.mBounds
是不同的。