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后部的队列的最后面
- Android Bitmap进阶
- Android进阶练习 - 高效显示Bitmap(管理Bitmap内存)
- Android进阶练习 - 高效显示Bitmap(管理Bitmap内存)
- Android进阶练习 - 高效显示Bitmap(管理Bitmap内存)
- Android进阶2之Bitmap、Drawable、byte[]转换
- Android进阶练习 - 高效显示Bitmap(简介)
- Android进阶练习 - 高效显示Bitmap(缓存 Bitmaps)
- Android进阶练习 - 高效显示Bitmap(缓存 Bitmaps)
- Android进阶2之Bitmap、Drawable、byte[]转换
- Android进阶练习 - 高效显示Bitmap(在UI主线程外处理Bitmap)
- android Bitmap
- android Bitmap
- android bitmap
- android Bitmap
- android bitmap
- android Bitmap
- Android Bitmap
- Android--Bitmap
- 第12周项目4-利用遍历思想求解图问题
- JavaScript判断字符串非空的严格写法
- 浅谈MySql的存储引擎(表类型)
- 相机白平衡测试
- 第十三周项目5-拓扑排序算法的验证
- Android Bitmap进阶
- 使用超链接将页面镶嵌在easyUI的框架里面
- 第十二周项目2—操作用邻接表储存的图
- 【第十三周 项目5-拓扑排序算法验证】
- eclipse 快捷键 debug调试
- Jmete-JDBC Request与BeanShell PostProcessor的结合使用
- Windows运行命令
- java开发中的23种设计模式讲解
- 第十三周 项目2 Kruskal算法的验证