图片来源有网络图片 手机存储图片 资源图片 以前一般网络图片用图片加载库来加载 手机存储图片一般用BitmapFactory来加载 资源图片分为xml里的src与background 和hdpi或xhdpi里的图片 xml里的framework用a.getDrawable来加载 hdpi里的图片我们一般用setImageResource或setBackgroundResource来设置 其实它们3个都是调用Resource.loadDrawable(采用享元设计模式)通过BitmapFactory.decodeResourceStream来加载的
以上几种方式如果不通过jni,都会调用BitmapFactory.decodeStream来加载图片 如果图片分辨率过高可能会出现已升OOM异常 因为图片是由一个个的像素点构成,加载过程会创建一个二维数组,在数组中图片分辨率为x,y,每一个像素点由ARGB组成,占据2或4个字节 因此理论消耗内存公式如下
bitmap内存大小 = 分辨率x * 分辨率y * 像素大小即色彩模式(ARGB_8888为4字节 ARGB_4444与RGB_565为2字节)
如1200 * 900 的图片 以ARGB_8888模式加载 内存大小为1200 * 800 * 4 = 3840000 约为3.66MiB
图片加载时如果设置了密度的话可能会对图片分辨率进行成倍的缩放(图片目录密度大于设备密度分辨率缩小 图片目录密度小于设备密度分辨率放大) 从而影响内存大小 所以公式做如下修改
scale = (float) targetDensity / density;
//这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
int bitmapWidth = decodingBitmap.width();
int bitmapHeight = decodingBitmap.height();
int scaledWidth = int(scaledWidth * scale + 0.5f);
int scaledHeight = int(scaledHeight * scale + 0.5f);
bitmapSize = scaledWidth * scaledHeight * pixelByte;
bitmapSize = int(bitmapWidth * scale + 0.5f) * int(bitmapHeight * scale + 0.5f) * pixelByte;
bitmapSize = int(bitmapWidth * bitmapHeight * scale² + bitmapWidth * scale *0.5f + bitmapHeight * scale * 0.5f + 0.5²) * pixelByte
bitmapSize = int(bitmapWidth * bitmapHeight * scale² + (bitmapWidth + bitmapHeight) * scale /2 + 0.25) * pixelByte
bitmap内存大小 = (原始分辨率x * 原始分辨率y * scale² + (原始分辨率x+原始分辨率y) * scale / 2 +0.5²) * 采样率² * 像素大小
bitmap内存大小 = 原始分辨率x * 原始分辨率y * 像素大小* 最终分辨率x/原始分辨率x * 最终分辨率y/原始分辨率y / 采样率²
估算 内存大小=原始分辨率x * 原始分辨率y * scale² / 采样率² * 像素大小
如果资源图片没有放到对应密度文件夹下 很可能造成OOM或图片显得很虚 所以要根据图片的分辨率与目的和内容来确认放到哪个密度的文件夹下 最后能用color就用color 能用shape就用shape 能用.9就用.9 都不能则用图片
如果觉得图片太大 想要成倍的缩小 可以设置采样率 该值为2的整数倍 如果为2 将缩小4倍 4将缩小16倍 该值越大图片越虚需合理设置
采样率和密度都是通过BitmapFactory.Option来设置 如采样率option.inSampleSize 设备密度dpi option.inTargetDensity 图片目录密度dpi option.inDensity
如果不能确定缩小的倍数 可以先设置option.inJustDecodeBounds为true来获取图片的宽高等信息(不生成bitmap) 在通过计算获取inSampleSize
1
有些图片加载库已支持加载本地文件和资源图片 现可以统一使用图片加载库加载 如Glide
ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi 代表不同的密度下的图片 如密度160 为mdpi 在此密度下1dp=1px 1dp=density px
dpiFolder | resolution | icon size | densityDpi | ratio | density |
---|---|---|---|---|---|
ldpi | 320*240 | 36*36 | 120 | 3 | 0.75 |
mdpi | 480*320 | 48*48 | 160 | 4 | 1 |
hdpi | 480*800 | 72*72 | 240 | 6 | 1.5 |
xhdpi | 1280*720 | 96*96 | 320 | 8 | 2 |
xxhdpi | 1920*1080 | 144*144 | 480 | 12 | 3 |
xxxhdpi | 3840*2160 | 192*192 | 640 | 16 | 4 |
注:Android studio mipmap文件夹只存放启动图标icon
分析源码之前先介绍下View的下面两个方法
-
requestLayout()
这个方法会根据View树逐级向上传递,最后由ViewRootImpl.requestLayout方法处理该事件。ViewRootImpl.requestLayout中会调用scheduleTraversals()方法,这是一个异步方法,会调用performTraversals()方法。在performTraversals()方法中会先后调用performMeasure()和performLayout()方法。然后视条件调用performDraw()方法。 -
invalidate()
该方法会不断向父容器请求刷新并计算需要重绘的区域,最后会传递到ViewRootImpl中触发performTraversals方法,然后调用performDraw()进行重绘。
ImageView setImageBitmap 把bitmap构建成Drawable 在调用setImageDrawable方法 把drawable赋值给mDrawable 如果drawable的宽或高有改变重新布局 最后重新绘制 即通过onDrawable把mDrabable画到canvas上 最终显示到界面上
//ImageView
public Drawable getDrawable() {
if (mDrawable == mRecycleableBitmapDrawable) {
// Consider our cached version dirty since app code now has a reference to it
//只有在调用setImageBitmap后 没为mDrawable设置新值时 该条件才满足
//只有在这里会把mRecycleableBitmapDrawable置为null
mRecycleableBitmapDrawable = null;
}
return mDrawable;
}
//ImageView
public void setImageBitmap(Bitmap bm) {
// Hacky fix to force setImageDrawable to do a full setImageDrawable
// instead of doing an object reference comparison
mDrawable = null;
//setImageBitmap时复用drawable
if (mRecycleableBitmapDrawable == null) {
mRecycleableBitmapDrawable = new ImageViewBitmapDrawable(
mContext.getResources(), bm);
} else {
mRecycleableBitmapDrawable.setBitmap(bm);
}
setImageDrawable(mRecycleableBitmapDrawable);
}
//ImageView
public void setImageDrawable(@Nullable Drawable drawable) {
if (mDrawable != drawable) {
mResource = 0;
mUri = null;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
updateDrawable(drawable);
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
//重新绘制
invalidate();
}
}
更新drawable并初始化 如设置回调 宽高 范围 等 干掉oldDrawable的回调
//ImageView
private void updateDrawable(Drawable d) {
if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {
//不是通过setImageBitmap进来的把mRecycleableBitmapDrawable的bitmap置为null
//让mRecycleableBitmapDrawable里bitmap可以被垃圾回收
mRecycleableBitmapDrawable.setBitmap(null);
}
if (mDrawable != null) {
//干掉旧drawable的ImageView的弱引用
mDrawable.setCallback(null);
unscheduleDrawable(mDrawable);
}
mDrawable = d;
if (d != null) {
d.setCallback(this);
d.setLayoutDirection(getLayoutDirection());
if (d.isStateful()) {
d.setState(getDrawableState());
}
d.setVisible(getVisibility() == VISIBLE, true);
d.setLevel(mLevel);
mDrawableWidth = d.getIntrinsicWidth();
mDrawableHeight = d.getIntrinsicHeight();
applyImageTint();
applyColorMod();
configureBounds();
} else {
mDrawableWidth = mDrawableHeight = -1;
}
}
public void unscheduleDrawable(Drawable who) {
if (mAttachInfo != null && who != null) {
mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
Choreographer.CALLBACK_ANIMATION, null, who);
}
}
xml中的android:src设置图片 先通过Resources.loadDrawable来加载资源图片 在通过setImageDrawble来设置图片 代码片段如下
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ImageView, defStyleAttr, defStyleRes);
Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
a.recycle();
}
updateDrawable已分析 Resources.loadDrawble 等setImageResource时候在分析
//TypedArray
public Drawable getDrawable(int index) {
if (mRecycled) {
//调用过recycle方法
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;
}
xml中的android:background设置背景 先通过Resources.loadDrawable来加载资源图片 在通过setBackgroundDrawable来设置背景给mBackground 视情况决定是否重新布局 最后重新绘制 即在draw方法中掉用drawBackground 把mBackground画到canvas上 代码片段如下
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
Drawable background = null;
final int N = a.getIndexCount();
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;
}
}
if (background != null) {
setBackground(background);
}
}
a.getDrawable已分析 见上面 下面分析setBackground
//View
/**
* @param background The Drawable to use as the background, or null to remove the background
*/
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background)
}
//View
public void setBackgroundDrawable(Drawable background) {
//计算mPrivateFlags mPrivateFlags影响draw方法里是否调用drawBackground
computeOpaqueFlags();
if (background == mBackground) {
return;
}
boolean requestLayout = false;
mBackgroundResource = 0;
/*
* Regardless of whether we're setting a new background or not, we want
* to clear the previous drawable.
* 清除之前的背景回调
*/
if (mBackground != null) {
mBackground.setCallback(null);
unscheduleDrawable(mBackground);
}
if (background != null) {
......
// Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
// if it has a different minimum size, we should layout again
//没有旧背景或新背景最小值跟旧背景最小值不同时需要重新布局
if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
background.setCallback(this);
if (background.isStateful()) {
background.setState(getDrawableState());
}
background.setVisible(getVisibility() == VISIBLE, false);
mBackground = background;
applyBackgroundTint();
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
requestLayout = true;
}
}else{
mBackground = null;
if ((mViewFlags & WILL_NOT_DRAW) != 0
&& (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
//需要重新布局
requestLayout = true;
}
//计算mPrivateFlags mPrivateFlags影响draw方法里是否调用drawBackground
computeOpaqueFlags();
//根据布局标志 决定是否重新布局
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
//重新绘制
invalidate(true);
}
setBackgroundResource 会通过Resource.loadDrawable来加载背景图片 接着用setBackground来设置背景图片
//View
public void setBackgroundResource(@DrawableRes int resid) {
//resId != 0才往下执行 所以setBackgroundResource(0) 并不能清除背景图片 可以使用setBackground(null)来清除
if (resid != 0 && resid == mBackgroundResource) {
return;
}
Drawable d = null;
if (resid != 0) {
//会走到Resources.loadDrawable
d = mContext.getDrawable(resid);
}
setBackground(d);
mBackgroundResource = resid;
}
setImageUri 调用updateDrawable(null)清理mDrawable 接著把mResource置为0 uri赋值给mUri 在调用resolveUri来获取并设置drawable 如果drawable的宽或高有改变重新布局 最后重新绘制
//ImageView
public void setImageURI(@Nullable Uri uri) {
if (mResource != 0 ||
(mUri != uri &&
(uri == null || mUri == null || !uri.equals(mUri)))) {
//干掉imageView建立的对mDrawable的引用 同时也是为了可以执行resolveUri方法
updateDrawable(null);
//让resolveUri方法通过mUri来加载drawable
mResource = 0;
mUri = uri;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
//加载并跟新drawable mResource为0 mUri不为null根据mUri加载drawable
resolveUri();
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}
当mUri以android.resource开头时调用的是Resource.getDrawable 如果以file://或content://开头的话先把支援加载成文件或流 再通过BitmapFactory加载成bitmap 在包装成drawable
//ImageView
private void resolveUri() {
if (mDrawable != null) {
return;
}
Resources rsrc = getResources();
if (rsrc == null) {
return;
}
Drawable d = null;
if (mResource != 0) {
......
} else if (mUri != null) {
通过内容观察者或文件加载Drawable
String scheme = mUri.getScheme();
if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
try {
//mUri以android.resource://开头
//格式1 android.resource://package_name/id_number
//如 android.resource://com.example.myapp/drawable/{{drawable_id}}
//如 android.resource://com.example.myapp/raw/{{my_resource}}
// Load drawable through Resources, to get the source density information
//通过mUri的authority(如com.example.myapp)获取资源对象和资源编号
//OpenResourceIdResult是资源对象和资源编号的包装类
ContentResolver.OpenResourceIdResult r =
mContext.getContentResolver().getResourceId(mUri);
//r.r为资源对象 r.id为资源id 即resources.getDrawable(resId,mContext.getTheme());
d = r.r.getDrawable(r.id, mContext.getTheme());
} catch (Exception e) {
Log.w("ImageView", "Unable to open content: " + mUri, e);
}
} else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
|| ContentResolver.SCHEME_FILE.equals(scheme)) {
//mUri以content://或file://开头
InputStream stream = null;
try {
//1.通过mUri的authority(如com.example.myapp)获取资源对象和资源编号
//2.在通过获取的资源对象打开资源编号所对应的资源获取流对象(r.r.openRawResource(r.id);)
stream = mContext.getContentResolver().openInputStream(mUri);
//1.通过BitmapFactory.decodeResourceStream创建bitmap
//2.调用drawableFromBitmap 把bitmap包装成drawable
d = Drawable.createFromStream(stream, null);
} catch (Exception e) {
Log.w("ImageView", "Unable to open content: " + mUri, e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
Log.w("ImageView", "Unable to close content: " + mUri, e);
}
}
}
} else {
//以文件形式加载Drawable
//1.通过BitmapFactory.decodeFile加载mUri获取bitmap
//2.调用drawableFromBitmap 把bitmap包装成drawable
d = Drawable.createFromPath(mUri.toString());
}
if (d == null) {
System.out.println("resolveUri failed on bad bitmap uri: " + mUri);
// Don't try again.
//通过mUri加载失败 把mUri置为空
mUri = null;
}
} else {
return;
}
//更新drawable
updateDrawable(d);
}
setImageResource 调用updateDrawable(null)清理mDrawable 接着把resId赋值给mResource mUri置为null 在调用resolveUri来获取并设置drawable 如果drawable的宽或高有改变重新布局 最后重新绘制
public void setImageResource(@DrawableRes int resId) {
// The resource configuration may have changed, so we should always
// try to load the resource even if the resId hasn't changed.
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
//干掉imageView建立的对mDrawable的引用 同时也是为了可以执行resolveUri方法
updateDrawable(null);
//让resolveUri方法通过mResource来加载drawable
mResource = resId;
mUri = null;
//如果mDrawable不为null直接返回
//如果mResource不为0 通过mResource加载drawable
//如果mUri不为null通过mUri加载drawable
//都不满足返回
//更新drawable
resolveUri();
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
//宽高有改变重新布局
requestLayout();
}
//重新绘制
invalidate();
}
首先通过mResource或者mUri加载出相应的drawable 接着更新drawable
//ImageView
private void resolveUri() {
if (mDrawable != null) {
return;
}
Resources rsrc = getResources();
if (rsrc == null) {
return;
}
Drawable d = null;
if (mResource != 0) {
//通过图片资源id加载Drawable
try {
//会走到Resources.loadDrawable方法里
d = mContext.getDrawable(mResource);
} catch (Exception e) {
Log.w("ImageView", "Unable to find resource: " + mResource, e);
// Don't try again.
mUri = null;
}
} else if (mUri != null) {
......
} else {
return;
}
//更新drawable
updateDrawable(d);
}
//Context
public final Drawable getDrawable(int id) {
return getResources().getDrawable(id, getTheme());
}
//Resources
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
TypedValue value;
synchronized (mAccessLock) {
value = mTmpValue;
if (value == null) {
value = new TypedValue();
} else {
mTmpValue = null;
}
getValue(id, value, true);
}
final Drawable res = loadDrawable(value, id, theme);
synchronized (mAccessLock) {
if (mTmpValue == null) {
mTmpValue = value;
}
}
return res;
}
首先通过key和theme到可绘制缓存中获取ConstantState 如果找到根据cs创建drawable并返回 接着根据key去预加载缓存中获取ConstantState 如果找到根据cs创建drawable 否则根据资源加载创建drawable 检查drawable是否应用主题 如果没有根据drawable的cs重行创建个cs并应用主题 并缓存cs
//Resource
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
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;
//可绘制的缓存 存储以下3个集合
//主题不为null的arrayMap key为theme 值为LongSparseArray 命名为mThemedEntries
//主题为null的LongSparseArray缓存 命名为mNullThemedEntries
//没有主题的LongSparseArray缓存 命名为mUnthemedEntries
//LongSparseArray的值为Drawable.ConstantState的弱引用
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.
if (!mPreloading) {
//检查是否有缓存的ConstantState 如果有根据ConstantState创建drawable返回
//1.如果thme不为null 以theme为键到mThemedEntries中获取 为null获取mNullThemedEntries 获取的集合命名为cache
//2.cache不为null 根据key到cache中获取对应的ConstantState 赋值给cs
//3.如果cs为null 根据key到mUnthemedEntries中去找对应的ConstantState 赋值给cs
//4.如果cs不为空调用cs.newDrawable 来创建个drawable并返回 通过cs创建的drawable公用同一个cs
final Drawable cachedDrawable = caches.getInstance(key, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}
// Next, check preloaded drawables. These may contain unresolved theme
// attributes.
//从预加载的可绘制的缓存中获取ConstantState
final ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
Drawable dr;
if (cs != null) {
//如果cs不为null 根据cs创建drawable
dr = cs.newDrawable(this);
} else if (isColorDrawable) {
//如果是颜色资源 创建个colorDrawable
dr = new ColorDrawable(value.data);
} else {
//根据xml或资源流来加载drawable
dr = loadDrawableForCookie(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) {
//根据cs重新创建个cs对象 并赋值给dr
dr = dr.mutate();
dr.applyTheme(theme);
//把mutate的标志清除
dr.clearMutated();
}
// If we were able to obtain a drawable, store it in the appropriate
// cache: preload, not themed, null theme, or theme-specific.
if (dr != null) {
dr.setChangingConfigurations(value.changingConfigurations);
//缓存cs 如果mPreloading为true缓存到预加载的可绘制的缓存中 否则存储到可绘制的缓存caches中
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
return dr;
}
缓存cs
//Resource
private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
Theme theme, boolean usesTheme, long key, Drawable dr) {
final ConstantState cs = dr.getConstantState();
if (cs == null) {
return;
}
if (mPreloading) {
//把cs缓存到预加载缓存中
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 {
把cs缓存到可绘制缓存中
synchronized (mAccessLock) {
caches.put(key, theme, cs, usesTheme);
}
}
}
可绘制缓存
/**
* Data structure used for caching data against themes.
*
* @param <T> type of data to cache
*/
abstract class ThemedResourceCache<T> {
//先找主题缓存 在找无主题缓存
//主题key不为null的缓存
private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
//无主题缓存
private LongSparseArray<WeakReference<T>> mUnthemedEntries;
//主题key为null的缓存
private LongSparseArray<WeakReference<T>> mNullThemedEntries;
/**
* Adds a new theme-dependent entry to the cache.
*
* @param key a key that uniquely identifies the entry
* @param theme the theme against which this entry was inflated, or
* {@code null} if the entry has no theme applied
* @param entry the entry to cache
*/
public void put(long key, @Nullable Theme theme, @NonNull T entry) {
put(key, theme, entry, true);
}
/**
* Adds a new entry to the cache.
*
* @param key a key that uniquely identifies the entry
* @param theme the theme against which this entry was inflated, or
* {@code null} if the entry has no theme applied
* @param entry the entry to cache
* @param usesTheme {@code true} if the entry is affected theme changes,
* {@code false} otherwise
*/
public void put(long key, @Nullable Theme theme, @NonNull T entry, boolean usesTheme) {
if (entry == null) {
return;
}
synchronized (this) {
final LongSparseArray<WeakReference<T>> entries;
if (!usesTheme) {
entries = getUnthemedLocked(true);
} else {
entries = getThemedLocked(theme, true);
}
if (entries != null) {
entries.put(key, new WeakReference<>(entry));
}
}
}
/**
* Returns an entry from the cache.
*
* @param key a key that uniquely identifies the entry
* @param theme the theme where the entry will be used
* @return a cached entry, or {@code null} if not in the cache
*/
@Nullable
public T get(long key, @Nullable Theme theme) {
// The themed (includes null-themed) and unthemed caches are mutually
// exclusive, so we'll give priority to whichever one we think we'll
// hit first. Since most of the framework drawables are themed, that's
// probably going to be the themed cache.
synchronized (this) {
final LongSparseArray<WeakReference<T>> themedEntries = getThemedLocked(theme, false);
if (themedEntries != null) {
final WeakReference<T> themedEntry = themedEntries.get(key);
if (themedEntry != null) {
return themedEntry.get();
}
}
final LongSparseArray<WeakReference<T>> unthemedEntries = getUnthemedLocked(false);
if (unthemedEntries != null) {
final WeakReference<T> unthemedEntry = unthemedEntries.get(key);
if (unthemedEntry != null) {
return unthemedEntry.get();
}
}
}
return null;
}
/**
* Prunes cache entries that have been invalidated by a configuration
* change.
*
* @param configChanges a bitmask of configuration changes
*/
public void onConfigurationChange(int configChanges) {
prune(configChanges);
}
/**
* Returns whether a cached entry has been invalidated by a configuration
* change.
*
* @param entry a cached entry
* @param configChanges a non-zero bitmask of configuration changes
* @return {@code true} if the entry is invalid, {@code false} otherwise
*/
protected abstract boolean shouldInvalidateEntry(@NonNull T entry, int configChanges);
/**
* Returns the cached data for the specified theme, optionally creating a
* new entry if one does not already exist.
*
* @param t the theme for which to return cached data
* @param create {@code true} to create an entry if one does not already
* exist, {@code false} otherwise
* @return the cached data for the theme, or {@code null} if the cache is
* empty and {@code create} was {@code false}
*/
@Nullable
private LongSparseArray<WeakReference<T>> getThemedLocked(@Nullable Theme t, boolean create) {
if (t == null) {
if (mNullThemedEntries == null && create) {
mNullThemedEntries = new LongSparseArray<>(1);
}
return mNullThemedEntries;
}
if (mThemedEntries == null) {
if (create) {
mThemedEntries = new ArrayMap<>(1);
} else {
return null;
}
}
final ThemeKey key = t.getKey();
LongSparseArray<WeakReference<T>> cache = mThemedEntries.get(key);
if (cache == null && create) {
cache = new LongSparseArray<>(1);
final ThemeKey keyClone = key.clone();
mThemedEntries.put(keyClone, cache);
}
return cache;
}
/**
* Returns the theme-agnostic cached data.
*
* @param create {@code true} to create an entry if one does not already
* exist, {@code false} otherwise
* @return the theme-agnostic cached data, or {@code null} if the cache is
* empty and {@code create} was {@code false}
*/
@Nullable
private LongSparseArray<WeakReference<T>> getUnthemedLocked(boolean create) {
if (mUnthemedEntries == null && create) {
mUnthemedEntries = new LongSparseArray<>(1);
}
return mUnthemedEntries;
}
/**
* Prunes cache entries affected by configuration changes or where weak
* references have expired.
*
* @param configChanges a bitmask of configuration changes, or {@code 0} to
* simply prune missing weak references
* @return {@code true} if the cache is completely empty after pruning
*/
private boolean prune(int configChanges) {
synchronized (this) {
if (mThemedEntries != null) {
for (int i = mThemedEntries.size() - 1; i >= 0; i--) {
if (pruneEntriesLocked(mThemedEntries.valueAt(i), configChanges)) {
mThemedEntries.removeAt(i);
}
}
}
pruneEntriesLocked(mNullThemedEntries, configChanges);
pruneEntriesLocked(mUnthemedEntries, configChanges);
return mThemedEntries == null && mNullThemedEntries == null
&& mUnthemedEntries == null;
}
}
private boolean pruneEntriesLocked(@Nullable LongSparseArray<WeakReference<T>> entries,
int configChanges) {
if (entries == null) {
return true;
}
for (int i = entries.size() - 1; i >= 0; i--) {
final WeakReference<T> ref = entries.valueAt(i);
if (ref == null || pruneEntryLocked(ref.get(), configChanges)) {
entries.removeAt(i);
}
}
return entries.size() == 0;
}
private boolean pruneEntryLocked(@Nullable T entry, int configChanges) {
return entry == null || (configChanges != 0
&& shouldInvalidateEntry(entry, configChanges));
}
}
/**
* Class which can be used to cache Drawable resources against a theme.
*/
class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {
private final Resources mResources;
/**
* Creates a cache for the given Resources instance.
*
* @param resources the resources to use when creating new instances
*/
public DrawableCache(Resources resources) {
mResources = resources;
}
/**
* If the resource is cached, creates and returns a new instance of it.
*
* @param key a key that uniquely identifies the drawable resource
* @param theme the theme where the resource will be used
* @return a new instance of the resource, or {@code null} if not in
* the cache
*/
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;
}
@Override
public boolean shouldInvalidateEntry(Drawable.ConstantState entry, int configChanges) {
return Configuration.needNewResources(configChanges, entry.getChangingConfigurations());
}
}