Android Bitmap实战技巧
注:本文大量参考谷歌官方文档自http://developer.android.com/intl/zh-cn/training/displaying-bitmaps/index.html。如果你自学能力还可以或者英文理解能力不错可以直接去看原版的。
如果你时间宝贵,想直接看结论和我个人理解的心得,也可以继续往下看。此外要着重说一下,现在网上其实有很多库,包括facebook的fresco啊,square的那些android 上的图片处理库
基本上都帮我们把这些事情做好了。但是原理大致上是相同的,如果你只想最简单的调用一下他们的api的话,其实这个文章可以不用看的,如果你想改写他们的库,或者自己写一个轻量级的库
这个文章还是挺有用的。
1.首先我们来看看加载大图片的问题。
假设我们有一台galaxy nexus手机,你看啊,用他拍照 一张像素 2592*1936像素,如果我们用 http://developer.android.com/intl/zh-cn/reference/android/graphics/Bitmap.Config.html
ARGB_8888来加载这个图片,也就是一个像素点 用4个byte来表示的话 就是 2592*1936*4 大概是19mb的内存,一张图片19mb啊~~当然现在android机器900元左右的内存都很大,差不多每个app
能有64mb的内存使用,但是你一个图片就将近20mb,就有点不讲道理了。
好,我们先看看第一段代码的解析:
1 BitmapFactory.Options options = new BitmapFactory.Options(); 2 //这个属性设置为true就是deocde的时候 返回的bitmap是null,但是这种decode方法 3 //无论你原始图片有多大,哪怕是一亿像素 都不会oom!他的作用就是可以利用这个属性 4 //去读取你原始图片的信息,注意是原始图片,而不是系统加载过的图片,我们都知道 5 //如果你把一张图片放在mdpi的下面,手机是xxhdpi的话 图片在显示的过程中会自动放大 6 //但是在这里用这个属性的时候 是不care 你图片放在哪个路径下的,也不care你手机的dpi 7 //他就只单纯的关心原始图片的原始属性 8 options.inJustDecodeBounds = true; 9 BitmapFactory.decodeResource(getResources(), R.mipmap.dd 10 , options); 11 //原始图片的宽高。 12 int height = options.outHeight; 13 int width = options.outWidth; 14 //这个按照通俗的理解就是 把图片的后缀名告诉你 比如jpeg png 这种 15 String imageType = options.outMimeType;
那这段代码有什么用呢?实际上可以用他作为图片缩放的基准标准。我们可以想一下,假设我们现在有一张1024*768的图片。
但是我们给他的显示区域 算出来 只有128*96。你说在这种情况下,你从resource解析出来 的bitmap还是1024*768.不是很蠢么?
我们可以算一下 缩放对图片占用内存大小的贡献。
我们假设现在有一张图片是2048*1536,我们用argb8888来解析他,那他占的内存是多少呢?就是2048*1536*4/1024=12.28mb,
假设我们现在缩放4倍,那就是512*384*4/1024=0.768mb.相差了16倍。
有些人可能理解不透这一点。我现在用个极简的例子来说明下:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root" android:layout_margin="30dp" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_red_light" tools:context=".MainActivity"> <ImageView android:layout_width="10dp" android:layout_height="10dp" android:id="@+id/iv" android:src="@drawable/gg"/> </FrameLayout>
你看啊,我用的这个图片gg 是一张1920*1080 像素的高清大图,加载出来以后占用内存 整个app大概是49.85mb!但是你发现没有,我们的iv 宽高都是10dp啊 没多大,这个就是显示的时候极大的浪费了。
当然了 你就算把宽高全部改成wrap_content 甚至是match 占用的内存也是49.85mb 不会有任何区别的~。也就是说imageview 等系统控件 在加载图片的时候 是不会帮你在bitmap层面上进行缩放的
他缩放只是matrix缩放,对内存占用是没有任何影响的,这一点一定要注意。那当然了,我们一般 在显示一张图片的时候 是可以估算他的大小的,位置什么的 也可以固定,所以在显示大图的时候 我们还是
最好对他进行缩放,比如这里 我们只想让这个图 显示10dp 的区域大小么,在我这个手机上dpi的尺寸的话 也就是20*20 像素点的区域了,所以我们就手动加载一张大约20*20的像素图 就可以极大节省我们的内存了
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 先把inJustDecodeBounds设置为true 取得原始图片的属性 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 然后算一下我们想要的最终的属性 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 在decode的时候 别忘记直接 把这个属性改为false 否则decode出来的是null options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // 先从options 取原始图片的 宽高 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; //一直对这个图片进行宽高 缩放,每次都是缩放1倍,然后这么叠加,当发现叠加以后 也就是缩放以后的宽或者高小于我们想要的宽高 //这个缩放就结束 跳出循环 然后就可以得到我们极限的inSampleSize值了。 while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
然后开始加载:
1 iv=(ImageView)this.findViewById(R.id.iv); 2 //这种加载方式最终我们的app 占用内存大小仅仅是9.85mb左右,而下面那个注释掉的加载方式,就和你在xml里直接写id的方式是一样的 3 //占用内存将近50mb! 4 iv.setImageBitmap(decodeSampledBitmapFromResource(getResources(),R.drawable.gg,20,20)); 5 //iv.setImageBitmap(BitmapFactory.decodeResource(getResources(),R.drawable.gg));
2.如何正确加载Bitmap。
上文,我们讲述了 如何在android里 正确的加载大图,但是实际上那部分代码还是有不完善的地方,我们都知道bitmap的decode方法 有很多种,除了能decode本地的资源图片以外,还可以decode byte。
直接了当的说 就是可以decode 流,可以从网络中获取图片。试想一下 如果还是按照我们上文所说的直接在ui 线程里decode 那就很容易发生anr了。
于是有人就说 我们可以用aysnctask。然后很多新手就会这么写:
1 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { 2 private final ImageView iv; 3 private int data = 0; 4 5 public BitmapWorkerTask(ImageView imageView) { 6 iv = imageView; 7 } 8 9 // Decode image in background. 10 @Override 11 protected Bitmap doInBackground(Integer... params) { 12 data = params[0]; 13 return decodeSampledBitmapFromResource(getResources(), data, 500, 500); 14 } 15 16 @Override 17 protected void onPostExecute(Bitmap bitmap) { 18 iv.setImageBitmap(bitmap); 19 } 20 }
可以看一下这段代码有什么问题,首先你这个task 是一个内部类,大家都知道内部类对象是持有外部类的引用的。我们可以设想一个场景,假设你doInBackGround 这个方法里 decode 是从网络中decode 耗时10s
好,这个时候用户点击跳转 跳转到你这个界面了,然后不到10s中 他又点了返回,此时你的逻辑是点击返回 就finish这个activity。但是此时这个task还在后台跑,他里面还持有着这个imageview的强引用!
这回导致什么问题?这就会导致这个activity永远释放不掉了,这是很严重的内存泄露。
所以建议的写法是:
1 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { 2 private final WeakReference<ImageView> imageViewReference; 3 private int data = 0; 4 5 public BitmapWorkerTask(ImageView imageView) { 6 // 用弱引用来关联这个imageview。大家一定要记住,弱引用是避免android 在各种callback回调里发生内存泄露的最佳方法! 7 //而软引用则是做缓存的最佳方法 两者不要搞混了! 8 imageViewReference = new WeakReference<ImageView>(imageView); 9 } 10 11 // Decode image in background. 12 @Override 13 protected Bitmap doInBackground(Integer... params) { 14 data = params[0]; 15 return decodeSampledBitmapFromResource(getResources(), data, 100, 100); 16 } 17 18 @Override 19 protected void onPostExecute(Bitmap bitmap) { 20 //当你background线程跑完以后 先看看imageview还在不在,不在 就什么也不做 等着系统回收他的资源 21 //在的话 再赋值 22 if (imageViewReference != null && bitmap != null) { 23 final ImageView imageView = imageViewReference.get(); 24 if (imageView != null) { 25 imageView.setImageBitmap(bitmap); 26 } 27 } 28 } 29 }
好 到这里看上去 已经比较完美了,但是在很早以前 那些开源控件出来之前,在显示一个以图片imageview 为主的listview或者gridview的时候 这种方法 会有很严重的问题。
因为这会导致 图片显示错乱。我们可以想象一种场景,假设你一屏 显示5个imageview对吧,按照我们刚才的方法就是5个task 在跑。跑完的时候 5个imageview 分别set
他们自己的bitmap。但是。很多时候会发生这样一种情况。当你这5个task 还在跑的时候,用户又滑动了,比如一开始是标号0-4的 5个imageview 在屏幕中。
然后你有5个task在跑。还没有跑完。此时用户滑动了。0这个imageview 出去了,新进来一个标号为5的imageview。假设我们标号为0的imageview是想显示图片a的,
标号为5的imageview是想显示图片B的。当你滑动的时候 0的task还没有跑完,5的imageview刚准备进来,注意啊,0滑出去的时候 这个imageview是没有被系统回收的
而是进入的listview的 回收站了,此时进来的5 实际上就是listview 回收站里的0. 当你标号为5的imageview 完全进入的时候,此时1开始标号为0的那个task跑完了。
那你5显示的图片就是a了。。虽然最终可能5的task跑完如果5还在界面上,最终还是会显示b,但是这样做的体验就太2了。而且一堆错误。
谷歌呢,也就顺势给了我们一种官方的解决方法,大家可以参考一下。我略做注释:
1 //在listview或者gridview的getview方法里 我們就可以直接調用這個方法了 2 public void loadBitmap(int resId, ImageView imageView) { 3 //如果取得的task为空 就代表这个iv是新的iv 不是从listview回收站里取的 就可以新建一个task 然后 4 //用这个task 去新建一个drawable。然后用这个新的imageview去set 这个drawable即可 5 if (cancelPotentialWork(resId, imageView)) { 6 final BitmapWorkerTask task = new BitmapWorkerTask(imageView); 7 final AsyncDrawable asyncDrawable = 8 new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); 9 imageView.setImageDrawable(asyncDrawable); 10 task.execute(resId); 11 } 12 } 13 14 //从imageview里取得他的drawable。然后从取得的drawable里取得他的task 15 //这里实际上就可以看出来imageview-drawable-task是1对1的关系了 16 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { 17 if (imageView != null) { 18 final Drawable drawable = imageView.getDrawable(); 19 if (drawable instanceof AsyncDrawable) { 20 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; 21 return asyncDrawable.getBitmapWorkerTask(); 22 } 23 } 24 return null; 25 } 26 27 public static boolean cancelPotentialWork(int data, ImageView imageView) { 28 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 29 //如果这个task 不为空 就代表这个iv已经有task了 那这种情况 30 if (bitmapWorkerTask != null) { 31 final int bitmapData = bitmapWorkerTask.data; 32 // 如果这个task还没有跑完 那就直接cancel这个task。因为没有跑完就肯定是iv 还没有设定值,所以直接cancel 33 //cancel以后就跳出这个括号 直接返回true了,等同于这个iv是一个新的iv 可以重新绑定新的task 34 if (bitmapData == 0 || bitmapData != data) { 35 // Cancel previous task 36 bitmapWorkerTask.cancel(true); 37 } else { 38 // 如果已经跑完了 那就别绑定了否则会错乱的。所以返回false把 这里返回false loadBitmap就什么都不做的。图形就从根本上 39 //不会错乱了。 40 return false; 41 } 42 } 43 // task为空的话就返回true了。 44 return true; 45 }
当然了 task 我们也要略微最终调整一下:
1 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { 2 ... 3 4 @Override 5 protected void onPostExecute(Bitmap bitmap) { 6 if (isCancelled()) { 7 bitmap = null; 8 } 9 10 if (imageViewReference != null && bitmap != null) { 11 final ImageView imageView = imageViewReference.get(); 12 final BitmapWorkerTask bitmapWorkerTask = 13 getBitmapWorkerTask(imageView); 14 if (this == bitmapWorkerTask && imageView != null) { 15 imageView.setImageBitmap(bitmap); 16 } 17 } 18 } 19 }
3.图片缓存 http://developer.android.com/intl/zh-cn/training/displaying-bitmaps/cache-bitmap.html 这个我就不细讲了,网上资料太多了。有兴趣的可以自己看一下,开源的那些框架使用的技术原理实际上也就是这个,大差不差。
脸书的fresco 比这个稍微高级一些。貌似是在native层进行内存管理的。
4.管理Bitmap的内存。
这个要分成2个部分来讲。 在3.0 以前的版本bitmap的内存 就是各自存放的,唯一的区别就是2.2的时候 bitmap 还存在native里,而2.3 就一起存放在java heap里了。我们那会释放bitmap内存的时候 都是调用recyle这个方法的。
但是很多时候 我们很多地方会复用一张图片,要知道 bitmap的创建和销毁 是要很多开销的。所以 我们实际上可以自定义一个drawable 到实在没有人用他的时候 我们在通过这个drawble来recyle掉 bitmap 的内存。
1 //其实这里代码思路很简单的,就是扩展了一下drawable而已。你每次指定他显示 或者暂时做缓存的时候 2 //就改动一下计数器,然后check他的状态,在归0的时候 就可以彻底recyle这个资源了 3 public class RecyclingBitmapDrawable extends BitmapDrawable { 4 5 static final String TAG = "CountingBitmapDrawable"; 6 7 private int mCacheRefCount = 0; 8 private int mDisplayRefCount = 0; 9 10 private boolean mHasBeenDisplayed; 11 12 public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { 13 super(res, bitmap); 14 } 15 16 17 public void setIsDisplayed(boolean isDisplayed) { 18 synchronized (this) { 19 if (isDisplayed) { 20 mDisplayRefCount++; 21 mHasBeenDisplayed = true; 22 } else { 23 mDisplayRefCount--; 24 } 25 } 26 27 checkState(); 28 } 29 30 31 32 public void setIsCached(boolean isCached) { 33 synchronized (this) { 34 if (isCached) { 35 mCacheRefCount++; 36 } else { 37 mCacheRefCount--; 38 } 39 } 40 41 checkState(); 42 } 43 44 private synchronized void checkState() { 45 46 if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed 47 && hasValidBitmap()) { 48 if (BuildConfig.DEBUG) { 49 Log.d(TAG, "No longer being used or cached so recycling. " 50 + toString()); 51 } 52 53 getBitmap().recycle(); 54 } 55 } 56 57 private synchronized boolean hasValidBitmap() { 58 Bitmap bitmap = getBitmap(); 59 return bitmap != null && !bitmap.isRecycled(); 60 } 61 62 }
那在3.0以后,因为bitmap 都在 java层的 heap中处理了,所以你要释放一个bitmap 只要将引用置为null 就行了 不需要如此麻烦,除此之外3.0以后的版本 还提供了一个很好用的参数 叫
options.inBitmap。
实际上总结起来就是,如果你使用了这个属性,那么使用这个属性的decode过程中 会直接参考 inBitmap 所引用的那块内存,,大家都知道 很多时候ui卡顿是因为gc 操作过多而造成的。使用这个属性 能避免大内存块的申请和释放。带来的好处就是gc 操作的数量减少。这样cpu会有更多的时间 做ui线程,界面会流畅很多,同时还能节省大量内存!
使用inBitmap属性以后:
1 final BitmapFactory.Options options = new BitmapFactory.Options(); 2 options.inSampleSize = 1; 3 options.inMutable = true;
注意第三行 一定要设置为true 这样返回的bitmap 才是mutable 也就是可重用的,否则是不能重用的。这个属性你以后设置了也没用的。
看如下代码:
1 final BitmapFactory.Options options = new BitmapFactory.Options(); 2 //size必须为1 否则是使用inBitmap属性会报异常 3 options.inSampleSize = 1; 4 //这个属性一定要在用在src Bitmap decode的时候 不然你再使用哪个inBitmap属性去decode时候会在c++层面报异常 5 //BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target. 6 options.inMutable = true; 7 inBitmap2 = BitmapFactory.decodeFile(path1,options); 8 iv.setImageBitmap(inBitmap2); 9 options.inBitmap = inBitmap2; 10 long start=System.currentTimeMillis(); 11 iv2.setImageBitmap(BitmapFactory.decodeFile(path2,options)); 12 iv3.setImageBitmap(BitmapFactory.decodeFile(path3,options)); 13 iv4.setImageBitmap(BitmapFactory.decodeFile(path4,options));
此时占用的内存大概是11mb
如果我们把第九行注释掉: 发现占用内存暴增到18mb。
此外就是版本号不同 inBitmap使用要注意的地方稍微也不同。英文原版如下:
Android 3.0 (API level 11) introduces the BitmapFactory.Options.inBitmap field. If this option is set, decode methods that take the Options object will attempt to reuse an existing bitmap when loading content. This means that the bitmap's memory is reused, resulting in improved performance, and removing both memory allocation and de-allocation. However, there are certain restrictions with how inBitmap can be used. In particular, before Android 4.4 (API level 19), only equal sized bitmaps are supported. For details, please see the inBitmap documentation.
简单来说 就是4.4 以前 你要使用这个属性 那图片大小必须一样,但是4.4 以后只要decode的图片 比inBitmap的图片要小 就可以使用这个属性了。
但是这个属性在使用的时候一定要当心:
如果你不同的imageview 使用的scaletype 不同,但是你这些不同的imageview的bitmap 在decode时候 如果都是引用的同一个inBitmap的话,
这些图片会相互影响,所以大家一定要注意,使用inBitmap这个属性的时候 一定要小心小心再小心。
最后如果谷歌的官方教程 DisplayBitmaps 这个demo 你如果能完全吃透的话,相信你对bitmap操作就完全没有问题了,如果有阅读源码困难的同学可以在留言里告诉我
人数多的话 我会再写一篇文章 帮助分析DisplayBitmaps 这个官方demo里的所有细节帮助大家理解。