概述
在Android中,出于对内存优化的考虑,对于图片的存储使用了缓存机制,资源id相同的图片使用了同一个位图信息,如果对这些机制不了解的话开发过程中就会造成一些困扰。本文通过实例和分析Drawable的缓存机制源码的方式来介绍一下Drawable的缓存机制,并且了解一下Drawable.mutate()的用法。
问题演示
下面我们通过一个实例来演示一个我们在使用Drawable过程中经常会遇到的一个问题。
首先贴出UI布局文件,这里放了两个 ImageView
,它们的寬高不一样,而且对他们加以蓝色的背景。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<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
设置一个显示图片。1
2
3
4
5
6final 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
设置图片。
1 | ...... |
看一下效果图,第一个图片的现实效果和实例1变的不一样了,你也许会感觉这个很正常,因为同一个Drawable
对象设置给两个大小不同的ImageView
,第二个尺寸改变以后第一个也跟着改变了。
实例3
那么我们再实例化一个Drawable
对象设置给第二个ImageView
。
1 | final BitmapDrawable firstDrawable = (BitmapDrawable) getResources() |
看一下效果图,这下显示正常了,这个也可以理解,两个不同的Drawable
对象设置给不同的ImageView
,他们互不干涉。那么真的是这样的吗?再接着往下面看。
实例4
我们在上面的代码的基础上把第二个Drawable
的 alpha 设置为15 0 。
1 | ...... |
看下面效果图,奇怪的现象发生了,第一个图片也变成半透明的了,为什么呢?
问题1:为什么设置第二个图片的 alpha 会对第一个图片有影响?
实例5
你也许听说过 mutate()
的作用,那么现在我们改一下代码:
1 | ...... |
看下面效果图,现在正常了。
问题2: mutate()
方法是做什么的?
实例6
下面我们再对代码稍作修改:
1 | final BitmapDrawable firstDrawable = (BitmapDrawable) getResources() |
这样两个图片也能正常显示出来了。
修改一下最后一行代码:
1 | Drawable drawable = firstDrawable.getConstantState().newDrawable(); |
这样的效果仍然是两个图片都是半透明的。
也需要调用drawable.mutate().setAlpha(150);
才能使第二个半透明,第一个没有半透明。
问题3: Drawable.getConstantState().newDrawable()
又是怎么回事?
问题分析
首先通过实例3我们可以得到这样的结论:分别两次调用getResources().getDrawable(R.drawable.test_mutate)
肯定不是指向同一个对象的。为了验证真实性,我们添加Log。
1 | Log.e("Test","firstDrawable = "+firstDrawable+", secondDrawable = "+secondDrawable); |
有下面打印:
1 | 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()
方法:
1 | ... |
这里会从caches
里面获取曾经加载过的资源,如果找到就直接返回缓存。具体这个缓存是怎么放进去的我们就不再详细分析了。前面我们也说了,firstDrawable
和secondDrawable
是不同的对象,那他们在这个缓存里肯定也不是同一个Drawable
了。
再直接往下看,DrawableCache
是什么呢?DrawableCache
继承自ThemedResourceCache
。
下来看一下DrawableCache
的getInstance()
方法:
1 | public Drawable getInstance(long key, Resources.Theme theme) { |
现在我们知道了,caches
里面缓存的不是Drawable
对象,而是Drawable.ConstantState
对象。
1 | public static abstract class ConstantState { |
ConstantState
类是一个抽象类,BitmapDrawable.BitmapState
便是它的实现类之一。由于getResources().getDrawable(R.drawable.test_mutate)
得到的是BitmapDrawable
,那么我们就重点分析这个类。
1 | final static class BitmapState extends ConstantState { |
在newDrawable()
方法里面返回的是一个新的BitmapDrawable
对象,但是所有相同资源的BitmapDrawable
对象共用同一个BitmapState
对象。我们注意到BitmapState
的mBitmap
属性,这也验证了前面的猜想,它们共用同一个Bitmap
。
1 | private BitmapDrawable(BitmapState state, Resources res) { |
那么我们setAlpha()
操作实际改变的是mBitmapState
的属性值,这也就不难理解问题1了,因为它们用的是同一个BitmapState
对象。
为了验证这个结论,我们添加打印:
1 | Log.e("Test","firstDrawable = "+firstDrawable.getConstantState()+", secondDrawable = "+secondDrawable.getConstantState()); |
打印如下:
1 | 4433 4433 E Test : firstDrawable = android.graphics.drawable.BitmapDrawable$BitmapState@3109294, secondDrawable = android.graphics.drawable.BitmapDrawable$BitmapState@3109294 |
它们确实是指向同一个对象的。
它们的关系可以用下图表示:
问题2
接下来再来分析问题2: mutate()
方法是做什么的?
我们先来看一下Drawable
中对这个方法的解释:
1 | /** |
mutate()
返回的Drawable
对象不再与同资源的其他Drawable
共用 state,那么它的属性改变后就不再影响其他的Drawable
了。
在BitmapDrawable
的mutate()
方法里面确实又新建了一个BitmapState
对象。
1 | /** |
它们的关系可以用下图表示:
注意: 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
开始分析,这个方法的调用流程如图:
1 | ├── ImageView.setImageDrawable |
1 | private void configureBounds() { |
在configureBounds()
里面根据不同的ScaleType
会进行不同的变换,包括设置绘制边界、缩放、位移、绘制是的矩阵变换等等。
在onDraw()
方法中再把这个Drawable
绘制到Canvas
上,这些改变变化的只是Drawable
本身,而对ConstantState
不会有改变。
为了验证这个结论,我们在实例3代码基础上,添加一些Log。
1 | public void refresh(View v){ |
打印如下:
1 | 21313 21313 E Test : 1 rect1 = Rect(0, 0 - 200, 400) |
它们的Drawable.mBounds
是不同的。
参考文章
https://android-developers.googleblog.com/2009/05/drawable-mutations.html