Android Bitmap进阶

来源:互联网 发布:nginx ssl ciphers 编辑:程序博客网 时间:2024/06/06 21:06

1、前言

上一篇当中介绍了关于Bitmap的基础介绍,而这一篇我们打算从性能方面和Bitmap的矩阵对象变换这两个方面去重新的认识一下我们的Bitmap对象,为什么选择说性能呢,因为大家都知道并且在我的上一篇博客当中我也介绍过,Bitmap是把图片的数据直接储存在内存当中的,那我们可以试想一下,既然是加载在内存当中的,那么这个肯定会很危险,如果我们加载特别大的图片的话,我们的程序可能就会挂掉。

我用了一张思维导图来和大家分享一下今天打算和大家聊的话题,主要就是分为两个部分性能方面和Bitmap的矩阵对象变换,好了废话少说,我们之间开始吧。


2、正文

在Android系统当中我们的系统会特定的给每一个App分配一定的内存空间进行使用,这里的一定的内存空间是固定的,和手机自身的RAM没有任何的关系,不同的手机给App分配的内存空间是一样的,但都有一个最大值的限制,如果我们的App当前使用的内存和将要申请的内存大于系统给我们的App所分配的内存的时候就会出现OOM异常。

那我们现在就开始想这个特定的内存限制是多大呢,默认Google系统是16M,但是在各大手机厂商竞争的今天,他们对这个值进行了一定的修改,那就是华为mate7:192M 、小米4:128M、 红米:128M 、三星SM-N7508v:96M,并且在Android4.0以后我们可以在Application节点的后面加上一个属性是android:largeheap,它是一个Boolean的值,当为true是最大分配的内存会达到原来的两倍多一些。大家可以使用下面的方法查看手机分配给App的内存,

ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);int memorySize = activityManager.getMemoryClass();

我们的Bitmap究竟会占用多大的内存呢?

我们在上一讲当中我们说过一个Bitmap.Config类,而这个类当中用了四个枚举类型来表示了当前图像的质量,分别是

  • ALPHA_8 Alpha:由8位组成,1字节
  • ARGB_4444:由4个4位组成即16位,2字节
  • ARGB_8888:由4个8位组成即32位,4字节
  • RGB_565:R为5位,G为6位,B为5位共16位,2字节

我们假设一张图片的分辨率是1024*768,采用ARGB_8888,那么占用的空间就是1024*768*4=3MB,如果是一张内存也许还OK,那我们现在去读取相机里面的图片(相机里面的图片都比较大),3648*2736的一样照片,内存占用为3648*2736*4=33MB,想想呀,33M,一张,那我们再来算我们是一个ListView呢。同时去刷多张呢,那我们内存就肯定会挂掉,没的说。

读取大图片时,如何避免会出错误的系统方法,

出问题了,我们就得想办法解决我们的问题,但是在解决之前呢,我们首先来看看上一篇博客当中我们遗留下来的一个问题,那就是尽量不要使用ImageView.setImageResource或者BitmapFactory.decodeResource这两个方法去读取一张特别大的图片,为什么呢!因为在我们底层最终都是使用Java层的createBitmap,使用的创建Bitmap的参数都是默认的,没有去规避我们会出现OOM异常的风险所以我们不能这么使用。解决办法以下列出三种,

降低图片的大小,降低图片的分辨率

我们在上面的时候算过我们的Bitmap占用内存的大小和这个Bitmap的位图有这直接的关系,那么我们可以降低图片的大小,这样我们可以去规避OOM异常,下面我们来看一个例子。


我们把图片的分辨率改变会出现上述的效果,并且我们发现在比较后面的地方发现图片的改变不是很明显,这就说明了我们的眼睛所看不到的东西,所以我们可以提升速度,去牺牲一些用户发现不了的图片质量还是很值得,下面我们说一下实现思路,我们先不去加载图片,得到图片的具体信息,比如宽高,然后根据图片的宽高去设置图片的缩放比,最后加载图片。

补充:

  • BitmapFactory.Options的inJustDecodeBounds属性设置为true,decodeResource()方法就不会生成Bitmap对象,而仅仅是读取该图片的尺寸和类型信息
  • BitmapFactory.Options类的inSampleSize,该参数为int型,他的值指示了在解析图片为Bitmap时在长宽两个方向上像素缩小的倍数。inSampleSize的默认值和最小值为1(当小于1时,解码器将该值当做1来处理),且在大于1时,该值只能为2的幂(当不为2的幂时,解码器会取与该值最接近的2的幂)。例如,当inSampleSize为2时,一个2000*1000的图片,将被缩小为1000*500,相应地,它的像素数和内存占用都被缩小为了原来的1/4

下面我们直接看代码,

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:orientation="vertical"    android:layout_height="match_parent"    >    <ImageView        android:id="@+id/iv_content"        android:layout_margin="20dip"        android:background="#F00"        android:layout_width="match_parent"        android:layout_height="400dip" />    <SeekBar        android:id="@+id/seekBar"        android:layout_width="match_parent"        android:layout_alignParentBottom="true"        android:layout_height="wrap_content"        android:max="540"        android:layout_margin="20dip"/></RelativeLayout>

max使用540,是为了给大家看效果设置SeekBar使用的,真正加载图片时,SeekBar直接忽略掉

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        final ImageView mImageView = (ImageView) findViewById(R.id.iv_content);        SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar);        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {            @Override            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {                if(progress > 4){                    if(progress % 2 == 0){                        mImageView.setImageBitmap(getSampledBitmapFromResource(getResources(), R.drawable.index, progress, progress));                    }                }            }            @Override            public void onStartTrackingTouch(SeekBar seekBar) {}            @Override            public void onStopTrackingTouch(SeekBar seekBar) {}        });    }    /** 根据reqWidth,reqHeight计算缩放比*/    public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {        // 原始图片的宽高        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;            // 在保证解析出的bitmap宽高分别大于目标尺寸宽高的前提下,取可能的inSampleSize的最大值            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {                inSampleSize *= 2;            }        }        Log.i("suansuan", "inSampleSize = " + inSampleSize);        return inSampleSize;    }    /** 获取Bitmap的图片 */    public Bitmap getSampledBitmapFromResource(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);        // 计算 inSampleSize 的值        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);        // 根据计算出的 inSampleSize 来解码图片生成Bitmap        options.inJustDecodeBounds = false;        return BitmapFactory.decodeResource(res, resId, options);    }}
这就是第一种解决方式了

采用更节省内存的编码,例如ARGB_4444。

如果通过上述的方法,你还是感觉有卡顿,并且图片的质量在底一点的话,那我就推荐去降低图片的质量,而不是大小了

在上面分析过 Bitmap.Config * 分辨率 = Bitmap的大小,那么除了去减小分辨率,去降低图片的质量也是OK的,而我们的BitmapFactory.Options中,有一个inPreferredConfig属性,这个属性的值是一个Bitmap.Config,我们只需要给inPreferredConfig设置一个节省内存的编码就可以了,例如:options.inPreferredConfig=Bitmap.Config.ARGB_4444.这样就ok了。

使用缓存解决,加载大量的Bitmap图片

上面讨论的是加载一张大图,现在呢我们来试试一次显示很多图片。在很多情况下(例如使用 ListView, GridView 或者ViewPager控件),显示在屏幕上的图片以及即将显示在屏幕上的图片数量是非常大的(例如在图库中浏览大量图片)。

就以ListView举例子,我们的一个ListView每一个item条目都有Bitmap资源,一开始加载出来的时候就是一屏幕的数据,然后用户滚动,当item不可见的时候,系统自动的去复用item的View对象,那我们View当中的Bitmap对象就会被销毁掉,然后在新的条目出现的时候View是上次的复用的上次View,而我们又要重新的去设置Bitmap。为了保证UI的流畅性和载入图片的效率,我们需要避免重复的处理这些需要显示的图片,所以我们使用缓存去规避这些问题

所以我们想到了缓存,在这里使用缓存机制是最合适不过的,在我们的View复用的时候,我们图片没有销毁,而是在保存在了内存当中,当用户回滑的时候,我们不必再次为它重新分配资源,而是在缓存当中拿出来直接使用了。根据上面我们也知道,我的内存不是无限大的,那老给这里面添加图片的内存缓存,我们的应用程序早晚会挂掉。我们开始思考,能不能把最不常用的删除,常用的留下来呢,好的,就是这个思想,于是我们最有名的缓存算法出现了,

  • LruCache:一般使用它来做内存缓存,想生深入了解的朋友可以看看它的源码,LinkedHashMap。
  • DiskLruCache:使用它来做存储缓存。

如何使用LruCache进行内存的缓存呢,

    private LruCache<String, Bitmap> mMemoryCache;    public void init(){        int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);        int cacheSize = maxMemory / 8;        mMemoryCache = new LruCache<String, Bitmap>(cacheSize){            @Override            protected int sizeOf(String key, Bitmap value) {                return value.getRowBytes() * value.getHeight() / 1024;            }        };    }    .....   public void addBitmapToMemoryCache(String key, Bitmap bitmap) {        if (getBitmapFromMemCache(key) == null) {            mMemoryCache.put(key, bitmap);        }    }    public Bitmap getBitmapFromMemCache(String key) {        return mMemoryCache.get(key);    }
上述就是一个典型的LruCache的使用示例,在这个示例中,该程序的1/8内存都用来做缓存用了。在一个normal/hdpi设备中,这至少有4MB(32/8)内存。
在一个分辨率为 800×480的设备中,满屏的GridView全部填充上图片将会使用差不多1.5MB(800*480*4 bytes)
的内存,所以这样差不多在内存中缓存了2.5页的图片

然后在使用的时候,先检查LruCache 中是否存在。如果存在就使用缓存后的图片,如果不存在就启动后台线程去载入图片并缓存:

public void loadBitmap(int resId, ImageView imageView) {     final String imageKey = String.valueOf(resId);     final Bitmap bitmap = getBitmapFromMemCache(imageKey);     if (bitmap != null) {         mImageView.setImageBitmap(bitmap);     } else {         //下面一代码可以自行按照上述所说去裁剪图片大小。        mImageView.setImageResource(R.drawable.image_placeholder);         BitmapWorkerTask task = new BitmapWorkerTask(mImageView);         task.execute(resId);     } }
最后我是通过BitmapWorkerTask这个类继承AsyncTask去载入图片,加入缓存

class BitmapWorkerTask extends AsyncTask<integer, void,="" bitmap=""> {     ...     // Decode image in background.     @Override     protected Bitmap doInBackground(Integer... params) {         final Bitmap bitmap = decodeSampledBitmapFromResource(                 getResources(), params[0], 100, 100));         addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);         return bitmap;     }     ... }

如何使用DiskLruCache进行磁盘缓存

private DiskLruCache mDiskCache; private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;// 10MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override protected void onCreate(Bundle savedInstanceState) {     ...     // 在这里做内存缓存的初始化操作    ...     File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);     mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);     ... }public void addBitmapToCache(String key, Bitmap bitmap) {     // 添加内存缓存    if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap);     }                                    // 添加磁盘缓存    if (!mDiskCache.containsKey(key)) { mDiskCache.put(key, bitmap);     } }  /** 得到磁盘缓存的文件 */public static File getCacheDir(Context context, String uniqueName) {       final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED || !Environment.isExternalStorageRemovable() ? context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();     return new File(cachePath + File.separator + uniqueName); }/** 从磁盘缓存当中获取Bitmap */public Bitmap getBitmapFromDiskCache(String key) {     return mDiskCache.get(key); }  

最后我是通过BitmapWorkerTask这个类继承AsyncTask去载入图片,加入缓存。

 class BitmapWorkerTask extends AsyncTask<integer, void,="" bitmap=""> {     ...     // 在子线程当中decode图片    @Override     protected Bitmap doInBackground(Integer... params) {         final String imageKey = String.valueOf(params[0]);         // 磁盘缓存在子线程当中         Bitmap bitmap = getBitmapFromDiskCache(imageKey);         if (bitmap == null) { // 在磁盘缓存中没有找到             final Bitmap bitmap = decodeSampledBitmapFromResource(                     getResources(), params[0], 100, 100));            //添加缓存到            addBitmapToCache(String.valueOf(imageKey, bitmap);         }        return bitmap;     }     ... }

关于OOM的内容介绍到这里就差不多,下面我们来看看Bitmap和图片矩阵(Matrix)之间的关系。

首先我们需要理解一下Matrix是什么东西,Matrix是一个3*3图片矩阵,在这个矩阵当中储存了图片的四种变换的数值,我们来看看图片矩阵当中储存那四种变化的数值,

  • Translate:-------->>>平移变换
  • Rotate:-------->>>旋转变换
  • Scale:-------->>>缩放变换
  • Skew:-------->>>错切变换

常用API:

Matrix提供了一些方法来控制图片变换:
setTranslate(float dx,float dy):控制Matrix进行位移。
setSkew(float kx,float ky):控制Matrix进行倾斜,kx、ky为X、Y方向上的比例。
setSkew(float kx,float ky,float px,float py):控制Matrix以px、py为轴心进行倾斜,kx、ky为X、Y方向上的倾斜比例。
setRotate(float degrees):控制Matrix进行depress角度的旋转,轴心为(0,0)。
setRotate(float degrees,float px,float py):控制Matrix进行depress角度的旋转,轴心为(px,py)。
setScale(float sx,float sy):设置Matrix进行缩放,sx、sy为X、Y方向上的缩放比例。
setScale(float sx,float sy,float px,float py):设置Matrix以(px,py)为轴心进行缩放,sx、sy为X、Y方向上的缩放比例。


注意:以上的set方法,均有对应的post和pre方法,Matrix调用一系列set,pre,post方法时,可视为将这些方法插入到一个队列.当然,按照队列中从头至尾的顺序调用执行.其中pre表示在队头插入一个方法,post表示在队尾插入一个方法.而set表示把当前队列清空,并且总是位于队列的最中间位置.当执行了一次set后:pre方法总是插入到set前部的队列的最前面,post方法总是插入到set后部的队列的最后面

1 0
原创粉丝点击