平台的版本的变化会引入新的特性,和对现有API的优化。
对于Android 4.1 (JellyBean)的新特性请参考官方文档android-4.1,由于本文的重点不是对4.1新特性进行介绍,所以暂时省略这部分内容。
众所周知对于已有项目的维护,有一项必不可少的工作就是对app进行平台版本的迁移工作,对于身处移动平台的Android更是如此。这期间会碰见许多"莫名其妙"的奇怪问题,下文会结合一次实际的平台迁移工作小结下一部分4.1下API的变化以及可能引起的问题。
案例1. 莫名其妙的 "java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics......"
相信各位看官对这个exception一定不会陌生,正如exception 本身所描述的那样,当我们试图在Canvas上Draw一个回收过的bitmap时就会抛出这样的异常。
同样的apk在4.1版本以下运行都是没有问题的,但一旦运行到4.1版本的操作系统下就会抛出这个异常,这应该是一个由版本迁移引起的API变化的问题,那么到底是哪个API引起的问题呢?怎样定位到这个有问题的API呢?
bitmap.recycle()!既然是回收bitmap引起的问题,那就从所有调用bitmap.recycle()方法的地方入手,根据重现步骤反相定位有问题的代码。
按照这样的解决思路我很快的揪出了这个引起问题的API
Bitmap result = Bitmap.createScaledBitmap(bitmap, dstWidth, dstHeight, true);
首先说说这个API在JellyBean下发生了什么样的变化?
这个Bitmap的静态方法createScaledBitmap(......)是用来根据一个源bitmap,和缩放的大小来创建一个缩放的bitmap。
那么请考虑这样的一种特殊情况,当作为参数传进来的源bitmap的宽度高度与同样是参数的缩放的宽度高度一致的时候(换句话说就是没有对源bitmap进行缩放的时候),这个API会返回什么样的结果呢?
Source bitmap hash code | Returned bitmap hash code | |
JellyBean | 42041a00 | 42041a00 |
ICS | 41cd8888 | 41dfa4c8 |
Gingerbread | 41d37750 | 41c2a3d1 |
各位看官看出有什么问题么?
对。那就是在JellyBean之前的版本返回的这个实际上没有缩放的bitmap与源bitmap实际上并不是同一个对象实例,但JellyBean对于这样的特殊用例给出了优化方案,那就是将这个源bitmap实例返回,减少创建不必要的内存。
那么为什么会引起draw 回收bitmap的问题呢?
请各位再次看看这个出问题的API,实际上当调用这个API的时候是想获得一个新的bitmap对象,这个新的bitmap对象可能是以缓存的形式存在,在某个时点会为了对bitmap资源进行管理,会调用bitmap。recycle() 方法进行释放。
注意:而实际上并不希望回收源bitmap,如果回收了这个源bitmap很有可能就会造成上面提到的那个Exception。
说到现在,那么这个问题就很好解释了,在JellyBean下如果调用方法,并将这个返回的bitmap缓存起来,在某个时点调用bitmap.recycle()方法进行手动回收,回收这个缓存的同时也会将源bitmap回收掉了,悲剧就发生了。
怎样解决?
两种解决方案;
1. 保持原先代码逻辑不变,只是对变化了实现的API进行一层方法封装
public Bitmap createScaledBitmap(Bitmap sourceBitmap, int scaledWidth, int scaledHeight, boolean filter){
Bitmap scaledBitmap = null;
if (null != sourceBitmap){
if(sourceBitmap.getWidth() == scaledWidth && sourceBitmap.getHeight() == scaledHeight){
scaledBitmap = Bitmap.createBitmap(sourceBitmap);
}else{
scaledBitmap = Bitmap.createScaledBitmap(sourceBitmap, scaledWidth, scaledHeight, filter);
}
}
return scaledBitmap;
}
2. 设计好bitmap资源缓存回收机制,依赖GC和缓存回收机制回收资源;不再调用bitmap。recycle()方法回收bitmap。
案例2. ListView 中的RadioButton 点击后选中状态下UI没有更新
如同上个案例,这个问题同样也只会在JellyBean 下发生,在排除了UI展示所需的数据错误后,这个问题最终定位在了UI层中ListView中的UI未被更新。
为什么会在JellyBean中出现这个问题呢?
有两方面的原因:
首先,ListView中在处理点击事件的部分,framework层的实现有了变化,在JellyBean之前ListView在触发了Click事件后会调用Listview的requestLayout()方法,从而可能会导致ListView item的UI更新,而JellyBean出于优化的考虑,不再主动调用requestLayout()方法更新UI,也就是是说需要开发者自己负责去在数据变化的时候更新UI。
其次,基于第一个原因,当用户点击ListView中的item导致数据变化的时候没有调用adapter的notifyDatasetChanged()方法通知UI改变,这个原因是自身代码的问题,今后要引以为戒。
案例3. One table, many user
多可用户可以共享一个平板,这就像Windows下面的多个用户一样,不同的用户可以有不同的桌面,不同的widgets,不同的界面设置等信息,不同用户之间的信息是分隔开的,也就是说,这个用户的配置不会影响到其他用户。
每一个用户会共享应用程序,A用户装了一个程序,B用户也要可以装,但实现在平板上面,只会有一个程序,所有用户都共享这个程序。
对于开发者来说,一般我们不需要针对多用户做特别的操作,这里要说一点,特别重要:
在4.2上面,应用程序的数据存储路径再也不是/data/data/package-name了,而是/data/user/user-id/package-name。
因此,我们程序中如果需要存储一些配置文件到/data目录,一定不能直接是写死的字符串,而是始终都应该从程序中去动态获取,获取方法如下:
String dataDir = context.getApplicationInfo.dataDir
记住:
1,不要把存储路径写死。
2,不要把存储路径写成绝对路径。
3,动态根据程序来取存储路径,这样可以应对不同的系统版本。
总结
1,在Jelly Bean下,Bitmap.createScaledBitmap并不会总是返回一个新的Bitmap,如果指定的大小与源图片一样,那么它直接返回源图片。
2,在Android开发过程中,尽量不要自己去调用Bitmap.recycle()方法来回收bitmap所占的内在,因为,你很有可能不清楚这个bitmap是否还有对象在使用,比如如果这个bitmap设置为View的背景图片时。
3,在AdapterView(ListView, GridView, Gallery)在,如果当我们更改了数据后,一定要调用BaseAdapter#notifyDatasetChange()方法来更新UI,这一点务必明确。