Android Drawable cache source code analysis
Android generally obtains Drawable through Resources. getDrawable (int), and Framework returns a top-level abstract Drawable object. In the Framework, the system uses the metadata-sharing method to save memory. To prove this, let's write a small demo:
We have introduced a simple image test.png in our androidproject. Since we only want to share the meta conclusion, we define a simple Activity and rewrite its onCreate method:
List
list = new ArrayList
(); Bitmap bitmap = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); for (int i = 0; i < 10; i ++) { bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); list.add(bitmap); } ImageView iv = new ImageView(this); iv.setImageBitmap(bitmap); this.setContentView(iv); }
You may wonder why you need a list to store Bitmap. This is important to avoid memory release caused by GC. After printing out our memory, we will find that the actual memory occupied by 10 bitmaps is 26364 kb. We are converting to Drawable:
List
list = new ArrayList
(); Drawable bitmap = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); for (int i = 0; i < 10; i ++) { bitmap = this.getResources().getDrawable(R.drawable.test); list.add(bitmap); } ImageView iv = new ImageView(this); iv.setImageDrawable(bitmap); this.setContentView(iv); }
We printed the memory and found that the memory has been reduced to 7844 kb. This part of data basically proves our conclusion. Is it possible that Resources caches the same drawable. Of course not. You can write a simple code to test it:
Drawable d1 = this.getResources().getDrawable(R.drawable.test); Drawable d2 = this.getResources().getDrawable(R.drawable.test); System.out.println(">>>d1 == d2 ? = "+(d1 == d2));
You will find that the output is false. In fact, we have basically reached a consensus on the metadata. The combination mode is also introduced when Framwork is used to wrap Drawable. The Framework caches the Core Metadata of your Drawable.
Resources.java Drawable loadDrawable(TypedValue value, int id) throws NotFoundException {...Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key);...}
The Code shows that the system mainly divides Drawable into two categories. In fact, there is also a type of Drawable that is familiar with the pre-loading class, but it is not the focus of our discussion, because our load does not belong to the color type Drawable, our corresponding metadata pool is implemented by the mDrawableCache object.
Resources.javaprivate Drawable getCachedDrawable( LongSparseArray
> drawableCache, long key) { synchronized (mAccessLock) { WeakReference
wr = drawableCache.get(key); if (wr != null) { // we have the key Drawable.ConstantState entry = wr.get(); if (entry != null) { //Log.i(TAG, "Returning cached drawable @ #" + // Integer.toHexString(((Integer)key).intValue()) // + " in " + this + ": " + entry); return entry.newDrawable(this); } else { // our entry has been purged drawableCache.delete(key); } } } return null; }
By calling the code, we can find that our stored in the data pool is not our Drawable object, but an object called Drawable. ConstantState, and is encapsulated with weak references. ConstantState is an abstract class with multiple sub-classes.
public static abstract class ConstantState { /** * Create a new drawable without supplying resources the caller * is running in. Note that using this means the density-dependent * drawables (like bitmaps) will not be able to update their target * density correctly. One should use {@link #newDrawable(Resources)} * instead to provide a resource. */ public abstract Drawable newDrawable(); /** * Create a new Drawable instance from its constant state. This * must be implemented for drawables that change based on the target * density of their caller (that is depending on whether it is * in compatibility mode). */ public Drawable newDrawable(Resources res) { return newDrawable(); } /** * Return a bit mask of configuration changes that will impact * this drawable (and thus require completely reloading it). */ public abstract int getChangingConfigurations(); /** * @hide */ public Bitmap getBitmap() { return null; } }
Because BitmapDrawable is used, the ConstantState corresponding to BitmapDrawable is BitmapState.
final static class BitmapState extends ConstantState { Bitmap mBitmap; int mChangingConfigurations; int mGravity = Gravity.FILL; Paint mPaint = new Paint(DEFAULT_PAINT_FLAGS); Shader.TileMode mTileModeX = null; Shader.TileMode mTileModeY = null; int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; boolean mRebuildShader; boolean mAutoMirrored; BitmapState(Bitmap bitmap) { mBitmap = bitmap; } BitmapState(BitmapState bitmapState) { this(bitmapState.mBitmap); mChangingConfigurations = bitmapState.mChangingConfigurations; mGravity = bitmapState.mGravity; mTileModeX = bitmapState.mTileModeX; mTileModeY = bitmapState.mTileModeY; mTargetDensity = bitmapState.mTargetDensity; mPaint = new Paint(bitmapState.mPaint); mRebuildShader = bitmapState.mRebuildShader; mAutoMirrored = bitmapState.mAutoMirrored; } @Override public Bitmap getBitmap() { return mBitmap; } @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; } }
We can see the newDrawable method corresponding to BitmapState. It passes itself as a parameter to the BitmapDrawable object, that is, BitmapDrawble combines the same BitmapState. In this way, the same Bitmap resource is reused.
So far, I believe everyone understands how Bitmap is retrieved from the cache just like me. Let's take a look at how ConstantState is saved.
Resources.loadDrawable(){... InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); // System.out.println("Opened file " + file + ": " + is); // MIUI MOD: // dr = Drawable.createFromResourceStream(this, value, is, file, null); dr = createFromResourceStream(this, value, is, file, id); is.close();... if (dr != null) { dr.setChangingConfigurations(value.changingConfigurations); cs = dr.getConstantState(); if (cs != null) { 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. final LongSparseArray
preloads = sPreloadedDrawables[mConfiguration.getLayoutDirection()]; preloads.put(key, cs); } } } } else { synchronized (mAccessLock) { //Log.i(TAG, "Saving cached drawable @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + cs); if (isColorDrawable) { mColorDrawableCache.put(key, new WeakReference
(cs)); } else { mDrawableCache.put(key, new WeakReference
(cs)); } } } }...}
It can be seen that when you generate a Drawable, The Drawable ConstantState will be removed from the Drawable and then put into your Cache pool.