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 。这个参数其实很多人的理解是错误的,很多人以为这个参数 使用了以后 可以减少bitmap所占用的内存,但其实不是的,你使用这个参数 内存大小是不会变化的,

但是使用这个参数 对于bitmap的decode的提升会非常大。可以好好看一下这个属性:

http://developer.android.com/intl/zh-cn/reference/android/graphics/BitmapFactory.Options.html#inBitmap

实际上总结起来就是,如果你使用了这个属性,那么使用这个属性的decode过程中 会直接参考 inBitmap 所引用的那块内存,,大家都知道 很多时候ui卡顿是因为gc 操作过多而造成的。使用这个属性 能避免大内存块的申请和释放。带来的好处就是gc 操作的数量减少。这样cpu会有更多的时间 做ui线程,界面会流畅很多,。使用这个属性的时候主要就是

1  final BitmapFactory.Options options = new BitmapFactory.Options();
2         options.inSampleSize = 1;
3         options.inMutable = true;

注意第三行 一定要设置为true 这样返回的bitmap 才是mutable 也就是可重用的,否则是不能重用的。这个属性你以后设置了也没用的。

此外就是版本号不同 稍微也不同。英文原版如下:

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里的所有细节帮助大家理解。

时间: 2024-07-30 10:18:12

Android Bitmap实战技巧的相关文章

Android实战技巧之四十三:终止一个线程引起的

这是一道老牌面试题.通常面试官会问你对Java线程的了解,然后再问此问题. 从理论到实践,这是一条好路子. 线程是操作系统实现多任务的一种方式,可以理解为线程是一个任务的执行单元.比如Android系统中每个App都会有自己的主线程,同时还可以创建worker thread"并行"为我们工作. Java中创建新线程的方法 Java对线程(Thread)提供了语言级的支持(依托虚拟机吧).java.lang包下有Thread类和Runnable接口,都可以替你完成创建新线程的工作. 1.

Android实战技巧:深入解析AsyncTask

AsyncTask的介绍及基本使用方法 关于AsyncTask的介绍和基本使用方法可以参考官方文档和Android实战技巧:多线程AsyncTask这里就不重复. AsyncTask引发的一个问题 上周遇到了一个极其诡异的问题,一个小功能从网络上下载一个图片,然后放到ImageView中,是用AsyncTask来实现的,本身逻辑也很简单,仅是在doInBackground中用HTTP请求把图片的输入流取出,然后用BitmapFactory去解析,然后再把得到的Bitmap放到ImageView中

Android实战技巧:ViewStub的应用

Android实战技巧:ViewStub的应用

Android实战技巧之二十七:Maven编译开源二维码扫描项目zxing

拥有自己的手机软件工具箱是件非常有意义的事情.就目前国内Android的生态环境来说,混乱的不能再乱了.由于我们登录不了官网App商店,下软件就只好在国内五花八门的软件市场下载.由于这些市场的监管不力,什么样的软件都有,就拿二维码扫描软件来说,好多都带那种狗皮膏药一样的广告插件,真是特别讨厌. 在开源世界中有很多优秀的软件,其中zxing就是非常好的Android扫碼工具软件.我们可以拿来即用还可以学习内部机制,然后做些定制化个性化.既可以自己享用,又可以跟大家分享.真是不错. zxing在gi

Android实战技巧之十一:Android Studio和Gradle

经过两个多月的AS体验,我认为是时候将Android的开发环境迁移到AS上了.目前最新版本是1.0.2,除了UI控件拖拽偶尔崩溃的问题(Ubuntu),其他功能用来还是十分流畅和高效.打动我的有如下几个特色: 智能感知体验特好,堪比VS 布局预览,手写布局后预览页面即时显示,便于布局调整和优化 编辑速度飞快流畅,毫无eclipse的卡顿 布局或源码中有图标和颜色的预览,十分直观 调试时体验极佳 集成了Terminal,喜欢命令行操作的伙伴不用额外启动终端了. 总之一句话,就是用起来特别爽! An

Android开发实用技巧:Drawable和Bitmap之间不得不说的秘密

Bitmap - 称作位图,一般位图的文件格式后缀为bmp,当然编码器也有很多如RGB565.RGB888.作为一种逐像素的显示对象执行效率高,但是缺点也很明显存储效率低.我们理解为一种存储对象比较好. Drawable - 作为Android平下通用的图形对象,它可以装载常用格式的图像,比如GIF.PNG.JPG,当然也支持BMP,当然还提供一些高级的可视化对象,比如渐变.图形等. 一. Bitmap转Drawable Bitmap bm = xxx; //xxx根据你的情况获取 Bitmap

我为什么要写《OpenCV Android 开发实战》这本书

我为什么要写<OpenCV Android 开发实战>这本书 2015年我出版了个人第一本关于图像处理方面的书籍<Java图像处理-编程技巧与应用实践>,这本书主要是从理论与编码上面详细阐述了图像处理基础算法以及它们在编码实现上的技巧.一转眼已经三年过去了,在这三年的时光里我无时无刻都在关注图像处理与计算机视觉技术发展与未来,同时渐渐萌发了再写一本图像处理相关技术书籍的念头,主要是因为<Java图像处理-编程技巧与应用实践>一书主要不是针对工程应用场景,读者在学完之后很

Android Bitmap 开源图片框架分析(精华三)

主要介绍这三个框架,都挺有名的,其他的框架估计也差不多了 Android-Universal-Image-Loaderhttps://github.com/nostra13/Android-Universal-Image-Loader ImageLoaderhttps://github.com/novoda/ImageLoader Volley(综合框架,包含图片部分)https://github.com/mcxiaoke/android-volley 扯淡时间,可以跳过这段这些开源框架的源码还

Android Bitmap 开源图片框架分析(精华四)

disk缓存主要难点在于内存缓存,disk缓存其实比较简单,就是图片加载完成后把图片文件存到本地方便下次使用 同样,先贴一下官方主页的介绍(主页地址见文章最开始处)和内存缓存差不多,根据算法不同提供了几种类别,可以自行通过ImageLoaderConfiguration.discCache(..)设置<ignore_js_op> 硬盘缓存,保存是以文件的形式框架提供了4种类型,具体算法规则不同,看名字我们大概也能知道对应意思 UnlimitedDiscCache