1、背景android
在Android开发中,任何一个APP都离不开图片的加载和显示问题。这里的图片来源分为三种:项目图片资源文件(通常为res/drawable目录下的图片文件)、手机本地图片文件、网络图片资源等。图片的显示咱们通常采用ImageView做为载体,经过ImageView的相应API便可设置其显示的图片内容。c++
咱们知道:若是是须要展现项目中的图片资源文件,咱们只须要调用ImageView的setImageResource(int id)方法并传入该图片资源的id(通常为R.drawable.xxx)便可。可是若是是须要展现手机本地的某张图片或者网络上的某个图片资源,又该怎么办呢?——问题Agit
为了回答问题A,咱们先思考一个更深的问题B:Android中是如何将某一张图片的内容加载到内存中继而由ImageView显示的呢?github
咱们知道:若是咱们想经过TextView展现一个本地txt文件的内容,咱们只须要由该文件建立并包装一个输入流对象。经过该输入流对象便可获得一个表明该文件内容的字符串对象,再将该字符串对象交由TextView展现便可。换句话说,这个txt文件的内容在内存中的表达形式就是这个字符串对象。web
类推一下,虽然图片文件也是文件,可是咱们显然不可能对图片文件也采用这种方式:即经过该图片创建并包装一个输入流对象再获取一个字符串对象。毕竟不管如何咱们都没法将某个图片的内容表示为一个字符串对象(细想一下就知道了,你能经过一段话100%准确地描述一张图片吗?显然不现实)。那么,这就引入了问题C:既然字符串对象不行,那么咱们该以哪一种对象来在内存中表示某个图片的内容呢?答案就是:Bitmap对象!算法
2、基本概述缓存
Bitmap,即位图。它本质上就是一张图片的内容在内存中的表达形式。那么,Bitmap是经过什么方式表示一张图片的内容呢?安全
Bitmap原理:从纯数学的角度,任何一个面都由无数个点组成。可是对于图片而言,咱们不必用无数个点来表示这个图片,毕竟单独一个微小的点人类肉眼是看不清的。换句话说,因为人类肉眼的能力有限,咱们只须要将一张图片表示为 有限但足够多的点便可。点的数量不能无限,由于无限的点信息量太大没法存储;可是点的数量也必须足够多,不然视觉上没法造成连贯性。这里的点就是像素。好比说,某个1080*640的图片,这里的像素总数即为1080X640个。性能优化
将图片内容表示为有限但足够多的像素的集合,这个“无限→有限”的思想极其迷人。因此,咱们只须要将每一个像素的信息存储起来,就意味着将整个图片的内容进行了表达。微信
像素信息:每一个像素的信息,无非就是ARGB四个通道的值。其中,A表明透明度,RGB表明红绿蓝三种颜色通道值。每一个通道的值范围在0~255之间,即有256个值,恰好能够经过一个字节(8bit)进行表示。因此,每一个通道值由一个字节表示,四个字节表示一个像素信息,这彷佛是最好的像素信息表示方案。
可是这里忽略了两个现实的需求问题:
①在实际需求中,咱们真的须要这么多数量的颜色吗?上述方案是256X256X256种。有的时候,咱们并不须要这么丰富的颜色数量,因此能够适当减小表示每一个颜色通道的bit位数。这么作的好处是节省空间。也就是说,每一个颜色通道都采用8bit来表示是表明所有颜色值的集合;而咱们能够采用少于8bit的表示方式,尽管这会缺失一部分颜色值,可是只要颜色够用便可,而且这还能够节省内存空间。
②咱们真的须要透明度值吗?若是咱们须要某个图片做为背景或者图标,这个图片透明度A通道值是必要的。可是若是咱们只是普通的图片展现,好比拍摄的照片,透明度值毫无心义。细想一下,你但愿你手机自拍的照片透明或者半透明吗?hell no! 所以,透明度这个通道值是否有必要表示也是根据需求自由变化的。
具体每一个像素点存储ARGB值的方案介绍,后面会详细介绍。
总结:Bitmap对象本质是一张图片的内容在内存中的表达形式。它将图片的内容看作是由存储数据的有限个像素点组成;每一个像素点存储该像素点位置的ARGB值。每一个像素点的ARGB值肯定下来,这张图片的内容就相应地肯定下来了。
如今回答一下问题A和问题B:Android就是将全部的图片资源(不管是何种来源)的内容以Bitmap对象的形式加载到内存中,再经过ImageView的setImageBitmap(Bitmap b)方法便可展现该Bitmap对象所表示的图片内容。
3、详细介绍
一、Bitmap.Config
Config是Bitmap的一个枚举内部类,它表示的就是每一个像素点对ARGB通道值的存储方案。取值有如下四种:
ARGB_8888:这种方案就是上面所说的每一个通道值采8bit来表示,每一个像素点须要4字节的内存空间来存储数据。该方案图片质量是最高的,可是占用的内存也是最大的
ARGB_4444:这种方案每一个通道都是4位,每一个像素占用2个字节,图片的失真比较严重。通常不用这种方案。
RGB_565:这种方案RGB通道值分别占五、六、5位,可是没有存储A通道值,因此不支持透明度。每一个像素点占用2字节,是ARGB_8888方案的一半。
ALPHA_8:这种方案不支持颜色值,只存储透明度A通道值,使用场景特殊,好比设置遮盖效果等。
比较分析:通常咱们在ARGB_8888方式和RGB_565方式中进行选取:不须要设置透明度时,好比拍摄的照片等,RGB_565是个节省内存空间的不错的选择;既要设置透明度,对图片质量要求又高,就用ARGB_8888。
二、Bitmap的压缩存储
Bitmap是图片内容在内存中的表示形式,那么若是想要将Bitmap对象进行持久化存储为一张本地图片,须要对Bitmap对象表示的内容进行压缩存储。根据不一样的压缩算法能够获得不一样的图片压缩格式(简称为图片格式),好比GIF、JPEG、BMP、PNG和WebP等。这些图片的(压缩)格式能够经过图片文件的后缀名看出。
换句话说:Bitmap是图片在内存中的表示,GIF、JPEG、BMP、PNG和WebP等格式图片是持久化存储后的图片。内存中的Bitmap到磁盘上的GIF、JPEG、BMP、PNG和WebP等格式图片通过了”压缩”过程,磁盘上的GIF、JPEG、BMP、PNG和WebP等格式图片到内存中的Bitmap通过了“解压缩”的过程。
那么,为何不直接将Bitmap对象进行持久化存储而是要对Bitmap对象进行压缩存储呢?这么作依据的思想是:当图片持久化保存在磁盘上时,咱们应该尽量以最小的体积来保存同一张图片的内容,这样有利于节省磁盘空间;而当图片加载到内存中以显示的时候,应该将磁盘上压缩存储的图片内容完整地展开。前者即为压缩过程,目的是节省磁盘空间;后者即为解压缩过程,目的是在内存中展现图片的完整内容。
三、有损压缩和无损压缩
Bitmap压缩存储时的算法有不少种,可是总体可分为两类:有损压缩和无损压缩。
compress(Bitmap.CompressFormat format, int quality, OutputStream stream)
介绍一下比较很差理解的属性:
①inJustDecodeBounds:这个属性表示是否只扫描轮廓,默认为false。若是该属性为true,decodeXXXX方法不会返回一个Bitmap对象(即不会为Bitmap分配内存)而是返回null。那若是decodeXXXX方法再也不分配内存以建立一个Bitmap对象,那么还有什么用呢?答案就是:扫描轮廓。
BitmapFactory.Options对象的outWidth和outHeight属性分别表明Bitmap对象的宽和高,可是这两个属性在Bitmap对象未建立以前显然默认为0,默认只有在Bitmap对象建立后才能被赋予正确的值。而当inJustDecodeBounds属性为true,虽然不会分配内存建立Bitmap对象,可是会扫描轮廓来给outWidth和outHeight属性赋值,就至关于绕过了Bitmap对象建立的这一步提早获取到Bitmap对象的宽高值。那这个属性到底有啥用呢?具体用处体如今Bitmap的采样率计算中,后面会详细介绍。
②inSample:这个表示Bitmap的采样率,默认为1。好比说有一张图片是2048像素X1024像素,那么默认状况下该图片加载到内存中的Bitmap对象尺寸也是2048像素X1024像素。若是采用的是ARGB_8888方式,那么该Bitmap对象加载所消耗的内存为2048X1024X4/1024/1024=8M。这只是一张图片消耗的内存,若是当前活动须要加载几张甚至几十张图片,那么会致使严重的OOM错误。
OOM错误:尽管Android设备内存大小可能达到好几个G(好比4G),可是Andorid中每一个应用其运行内存都有一个阈值,超过这个阈值就会引起out of memory即OOM错误(内存溢出错误)。由于如今市场上流行的手机设备其操做系统都是在Andori原生操做系统基础上的拓展,因此不一样的设备环境中这个内存阈值不同。能够经过如下方法获取到当前应用所分配的内存阈值大小,单位为字节: Runtime.getRuntime().maxMemory();
尽管咱们确实能够经过设置来修改这个阈值大小以提升应用的最大分配内存(具体方式是在在Manifest中设置android.largeHeap="true"),可是须要注意的是:内存是一种很宝贵的资源,不加考虑地无脑给每一个应用提升最大分配内存是一个糟糕的选择。由于手机总内存相比较每一个应用默认的最大分配内存虽然高不少,可是手机中的应用数量是很是多的,每一个应用都修改其运行内存阈值为几百MB甚至一个G,这很严重影响手机性能!另外,若是应用的最大分配内存很高,这意味着其垃圾回收工做也会变得更加耗时,这也会影响应用和手机的性能。因此,这个方案须要慎重考虑不能滥用。
关于这个方案的理解能够参考一位大神的解释:“在一些特殊的情景下,你能够经过在manifest的application标签下添加largeHeap=true的属性来为应用声明一个更大的heap空间。而后,你能够经过getLargeMemoryClass()来获取到这个更大的heap size阈值。然而,声明获得更大Heap阈值的本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的由于你须要使用更多的内存而去请求一个大的Heap Size。只有当你清楚的知道哪里会使用大量的内存而且知道为何这些内存必须被保留时才去使用large heap。所以请谨慎使用large heap属性。使用额外的内存空间会影响系统总体的用户体验,而且会使得每次gc的运行时间更长。在任务切换时,系统的性能会大打折扣。另外, large heap并不必定可以获取到更大的heap。在某些有严格限制的机器上,large heap的大小和一般的heap size是同样的。所以即便你申请了large heap,你仍是应该经过执行getMemoryClass()来检查实际获取到的heap大小。”
综上,咱们已经知道了Bitmap的加载是一个很耗内存的操做,特别是在大位图的状况下。这很容易引起OOM错误,而咱们又不能轻易地经过修改或提供应用的内存阈值来避免这个错误。那么咱们该怎么作呢?答案就是:利用这里所说的采样率属性来建立一个原Bitmap的子采样版本。这也是官方推荐的对于大位图加载的OOM问题的解决方案。其具体思想为:好比仍是那张尺寸为2048像素X1024像素图片,在inSample值默认为1的状况下,咱们如今已经知道它加载到内存中默认是一个2048像素X1024像素大位图了。咱们能够将inSample设置为2,那么该图片加载到内存中的位图宽高都会变成原宽高的1/2,即1024像素X512像素。进一步,若是inSample值设置为4,那么位图尺寸会变成512像素X256像素,这个时候该位图所消耗的内存(假设仍是ARGB_8888方式)为512X256X4/1024/1024=0.5M,能够看出从8M到0.5M,这极大的节省了内存资源从而避免了OOM错误。
切记:官方对于inSample值的要求是,必须为2的幂,好比二、四、8...等整数值。
这里会有两个疑问:第一:经过设置inSample属性值来建立一个原大位图的子采样版本的方式来下降内存消耗,听不上确实很不错。可是这不会致使图片严重失真吗?毕竟你丢失了那么多像素点,这意味着你丢失了不少颜色信息。对这个疑问的解释是:尽管在采样的过程确实会丢失不少像素点,可是原位图的尺寸也在减少,其像素密度是不变的。好比说若是inSample值为2,那么子采样版本的像素点数量是原来的1/4,可是子采样版本的显示尺寸(区域面积)也会变成原来的1/4,这样的话像素密码是不变的所以图片不用担忧严重失真问题。第二:inSample值如何选取才是最佳?这其实取决于ImageView的尺寸,具体采样率的计算方式后面会详细介绍。
③inPreferredConfig:该属性指定Bitmap的色深值,该属性类型为Bitmap.Config值。
例如你能够指定某图片加载为Bitmap对象的色深模式为ARGB_8888,即:options.inPreferredConfig=Bitmap.Config.ARGB_8888;
④isMutable:该属性表示经过decodeXXXX方法建立的Bitmap对象其表明的图片内容是否容许被外部修改,好比利用Canvas从新绘制其内容等。默认为false,即不容许被外部操做修改。
利用这些属性定制BitmapFactory.Options对象,从而灵活地按照本身的需求配置建立的Bitmap对象。
5、Bitmap的进阶使用
一、高效地加载大位图
上面刚说了大位图加载时的OOM问题,解决方式是经过inSample属性建立一个原位图的子采样版本以减低内存。那么这里的采样率inSample值如何选取最好呢?这里咱们利用官方推荐的采样率最佳计算方式:基本步骤就是:①获取位图原尺寸 ②获取ImageView即最终图片显示的尺寸 ③依据两种尺寸计算采样率(或缩放比例)。
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; //当要显示的目标大小和图像的实际大小比较接近时,会产生不必的采样,先除以2再判断以防止过分采样 while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
依据上面的最佳采样率计算方法,进一步能够封装出利用最佳采样率建立子采样版本再建立位图对象的方法,这里以从项目图片资源文件加载Bitmap对象为例:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; //由于inJustDecodeBounds为true,因此不会建立Bitmap对象只会扫描轮廓从而给options对象的宽高属性赋值 BitmapFactory.decodeResource(res, resId, options); // 计算最佳采样率 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 记得将inJustDecodeBounds属性设置回false值 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
二、Bitmap加载时的异步问题
因为图片的来源有三种,若是是项目图片资源文件的加载,通常采起了子采样版本加载方案后不会致使ANR问题,毕竟每张图加载消耗的内存不会很大了。可是对于本地图片文件和网络图片资源,因为分别涉及到文件读取和网络请求,因此属于耗时操做。为了不ANR的产生,必须将图片加载为Bitmap对象的过程放入工做线程中;获取到Bitmap对象后再回到UI线程设置ImageView的显示。举个例子,若是采用AsyncTask做为咱们的异步处理方案,那么代码以下:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { private final ImageView iv; private int id = 0; public BitmapWorkerTask(ImageView imageView) { iv = imageView; } // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { id = params[0]; //假设ImageView尺寸为500X500,为了方便仍是以项目资源文件的加载方式为例,由于这能够复用上面封装的方法 return decodeSampledBitmapFromResource(getResources(), id, 500, 500); } @Override protected void onPostExecute(Bitmap bitmap) { iv.setImageBitmap(bitmap); } }
该方案中,doInBackground方法执行在子线程,用来处理 ”图片文件读取操做+Bitmap对象的高效加载操做” 或 ”网络请求图片资源操做+Bimap对象的高效加载操做”等两种情形下的耗时操做。onPostExecute方法执行在UI线程,用于设置ImageView的显示内容。看上去这个方案很完美,可是有一个很隐晦的严重问题:
由当前活动启动了BitmapWorkerTask任务后:当咱们退出当前活动时,因为异步任务只依赖于UI线程因此BitmapWorkerTask任务会继续执行。正常的操做是遍历当前活动实例的对象图来释放各对象的内存以销毁该活动,可是因为当前活动实例的ImageView引用被BitmapWorkerTask对象持有,并且仍是强引用关系。这会致使Activity实例没法被销毁,引起内存泄露问题。内存泄露问题会进一步致使内存溢出错误。
为了解决这个问题,咱们只须要让BitmapWorkerTask类持有ImageView的弱引用便可。这样当活动退出时,BitmapWorkerTask对象因为持有的是ImageView的弱引用,因此ImageView对象会被回收,继而Activity实例获得销毁,从而避免了内存泄露问题。具体修改后的代码以下:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) { // 用弱引用来关联这个imageview!弱引用是避免android 在各类callback回调里发生内存泄露的最佳方法! //而软引用则是作缓存的最佳方法 二者不要搞混了! imageViewReference = new WeakReference<ImageView>(imageView); } // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { data = params[0]; return decodeSampledBitmapFromResource(getResources(), data, 100, 100); } @Override protected void onPostExecute(Bitmap bitmap) { //当后台线程结束后 先看看ImageView对象是否被回收:若是被回收就什么也不作,等着系统回收他的资源 //若是ImageView对象没被回收的话,设置其显示内容便可 if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } } }
拓展:①WeakReference是弱引用,其中保存的对象实例能够被GC回收掉。这个类一般用于在某处保存对象引用,而又不干扰该对象被GC回收,能够用于避免内存泄露。②SoftReference是软引用,它保存的对象实例,不会被GC轻易回收,除非JVM即将OutOfMemory,不然不会被GC回收。这个特性使得它很是适合用于设计Cache缓存。缓存能够省去重复加载的操做,并且缓存属于内存所以读取数据很是快,因此咱们天然不但愿缓存内容被GC轻易地回收掉;可是由于缓存本质上就是一种内存资源,因此在内存紧张时咱们须要能释放一部分缓存空间来避免OOM错误。综上,软引用很是适合用于设计缓存Cache。可是,这只是早些时候的缓存设计思想,好比在Android2.3版本以前。在Android2.3版本以后,JVM的垃圾收集器开始更积极地回收软引用对象,这使得本来的缓存设计思想失效了。由于若是使用软引用来实现缓存,那么动不动缓存对象就被GC回收掉实在是没法接受。因此,Android2.3以后对于缓存的设计使用的是强引用关系(也就是普通对象引用关系)。不少人会问这样不会因为强引用的缓存对象没法被回收从而致使OOM错误吗?确实会这样,可是咱们只须要给缓存设置一个合理的阈值就行了。将缓存大小控制在这个阈值范围内,就不会引起OOM错误了。
三、列表加载Bitmap时的图片显示错乱问题
咱们已经知道了如何高效地加载位图以免OOM错误,还知道了如何合理地利用异步机制来避免Bitmap加载时的ANR问题和内存泄露问题。如今考虑另外一种常见的Bitmap加载问题:当咱们使用列表,如ListView、GridView和RecyclerView等来加载多个Bitmap时,可能会产生图片显示错乱的问题。先看一下该问题产生的缘由。以ListView为例:
①ListView为了提升列表展现内容在滚动时的流畅性,使用了一种item复用机制,即:在屏幕中显示的每一个ListView的item对应的布局只有在第一次的时候被加载,而后缓存在convertView里面,以后滑动改变ListView时调用的getView就会复用缓存在converView中的布局和控件,因此可使得ListView变得流畅(由于不用重复加载布局)。
②每一个Item中的ImageView加载图片时每每都是异步操做,好比在子线程中进行图片资源的网络请求再加载为一个Bitmap对象最后回到UI线程设置该item的ImageView的显示内容。
③ 听上去①是一种很是合理有效的提升列表展现流畅性的机制,②看起来也是图片加载时很常见的一个异步操做啊。其实①和②自己都没有问题,可是①+②+用户滑动列表=图片显示错乱!具体而言:当咱们在其中一个itemA加载图片A的时候,因为加载过程是异步操做须要耗费必定的时间,那么有可能图片A未被加载完该itemA就“滚出去了”,这个itemA可能被当作缓存应用到另外一个列表项itemB中,这个时候恰好图片A加载完成显示在itemB中(由于ImageView对象在缓存中被复用了),本来itemB该显示图片B,如今显示图片A。这只是最简单的一种状况,当滑动频繁时这种图片显示错乱问题会越发严重,甚至让人毫无头绪。
那么如何解决这种图片显示错乱问题呢?解决思路其实很是简单:在图片A被加载到ImageView以前作一个判断,判断该ImageView对象是否仍是对应的是itemA,若是是则将图片加载到ImageView当中;若是不是则放弃加载(由于itemB已经启动了图片B的加载,因此不用担忧控件出现空白的状况)。
那么新的问题出现了,如何判断ImageView对象对应的item已经改变了?咱们能够采起下面的方式:
①在每次getView的复用布局控件时,对会被复用的控件设置一个标签(在这里就是对ImageView设置标签)。标签内容必须能够标识不一样的item!这里使用图片的url做为标签内容,而后再异步加载图片。
②在图片下载完成后要加载到ImageView以前作判断,判断该ImageView的标签内容是否和图片的url同样:若是同样说明ImageView没有被复用,能够将图片加载到ImageView当中;若是不同,说明ListView发生了滑动,致使其余item调用了getView从而将该ImageView的标签改变,此时放弃图片的加载(尽管图片已经被下载成功了)。
总结:解决ListView异步加载Bitmap时的图片错乱问题的方式是:为被复用的控件对象(即ImageView对象)设置标签来标识item,异步任务结束后要将图片加载到ImageView时取出标签值进行比对是否一致:若是一致意味着没有发生滑动,正常加载图片;若是不同意味着发生了滑动,取消加载。
四、Android中的Bitmap缓存策略
若是只是加载若干张图片,上述的Bitmap使用方式已经绝对够用了;可是若是在应用中须要频繁地加载大量的图片,特别是有些图片会被重复加载时,这个时候利用缓存策略能够很好地提升图片的加载速度。好比说有几张图片被重复加载的频率很高,那么能够在缓存中保留这几张图片的Bitmap对象;后续若是须要加载这些图片,则不须要花费不少时间去从新在网络上获取并加载这些图片的Bitmap对象,只须要直接向缓存中获取以前保留下来的Bitmap对象便可。
Android中对Bitmap的缓存策略分为两种:
在实际使用中,咱们不须要强行二选一,能够两者都使用,毕竟各有优点。因此Android中完整的图片缓存策略为:先尝试在内存缓存中查找Bitmap对象,若是有直接加载使用;若是没有,再尝试在磁盘缓存中查找图片文件是否存在,若是有将其加载至内存使用;若是仍是没有,则老老实实发送网络请求获取图片资源并加载使用。须要注意的是,后面两种状况下的操做都必须使用异步机制以免ANR的发生。
Android中经过LruCache实现内存缓存,经过DiskLruCache实现磁盘缓存,它们采用的都是LRU(Least Recently Used)最近最少使用算法来移除缓存中的最近不常访问的内容(变相地保留了最近常常访问的内容)。
①内存缓存LruCache
LruCache原理:LruCache底层是使用LinkedHashMap来实现的,因此LruCache也是一个泛型类。在图片缓存中,其键类型是字符串,值类型为Bitmap。利用LinkedHashMap的accessOrder属性能够实现LRU算法。accessOrder属性决定了LinkedHashMap的链表顺序:accessOrder为true则以访问顺序维护链表,即被访问过的元素会安排到链表的尾部;accessorder为false则以插入的顺序维护链表。
而LruCache利用的正是accessOrder为true的LinkedHashMap来实现LRU算法的。具体表现为:
1° put:经过LinkedHashMap的put方法来实现元素的插入,插入的过程仍是要先寻找有没有相同的key的数据,若是有则替换掉旧值,而且将该节点移到链表的尾部。这能够保证最近常常访问的内容集中保存在链表尾部,最近不常访问的内存集中保存在链表头部位置。在插入后若是缓存大小超过了设定的最大缓存大小(阈值),则将LinkedHashMap头部的节点(最近不常访问的内容)删除,直到size小于maxSize。
2° get:经过LinkedHashMap的get方法来实现元素的访问,因为accessOrder为true,所以被访问到的元素会被调整到链表的尾部,所以不常被访问的元素就会留到链表的头部,当触发清理缓存时不常被访问的元素就会被删除,这里是实现LRU最关键的地方。
3° remove:经过LinkedHashMap的remove方法来实现元素的移除。
3° size:LruCache中很重要的两个成员变量size和maxSize,由于清理缓存的是在size>maxSize时触发的,所以在初始化的时候要传入maxSize定义缓存的大小,而后重写sizeOf方法,由于LruCache是经过sizeOf方法来计算每一个元素的大小。这里咱们是使用LruCache来缓存图片,因此sizeOf方法须要计算Bitmap的大小并返回。
LruCache对其缓存对象采用的是强引用关系,采用maxSize来控制缓存空间大小以免OOM错误。并且LruCache类在Android SDK中已经提供了,在实际使用中咱们只须要完成如下几步便可:
具体代码参考以下:
//初始化LruCache对象 public void initLruCache() { //获取当前进程的可用内存,转换成KB单位 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); //分配缓存的大小 int maxSize = maxMemory / 8; //建立LruCache对象并重写sizeOf方法 lruCache = new LruCache<String, Bitmap>(maxSize) { @Override protected int sizeOf(String key, Bitmap value) { // TODO Auto-generated method stub return value.getWidth() * value.getHeight() / 1024; } }; } /** * 封装将图片存入缓存的方法 * @param key 图片的url转化成的key * @param bitmap对象 */ private void addBitmapToMemoryCache(String key, Bitmap bitmap) { if(getBitmapFromMemoryCache(key) == null) { mLruCache.put(key, bitmap); } } //封装从LruCache中访问数据的方法 private Bitmap getBitmapFromMemoryCache(String key) { return mLruCache.get(key); } /** * 由于外界通常获取到的是url而不是key,所以为了方便再作一层封装 * @param url http url * @return bitmap */ private Bitmap loadBitmapFromMemoryCache(String url) { final String key = hashKeyFromUrl(url); return getBitmapFromMemoryCache(key); }
②磁盘缓存DiskLruCache
因为DiskLruCache并不属于Android SDK的一部分,须要自行设计。与LruCache实现LRU算法的思路基本上是一致的,可是有不少不同的地方:LruCache是内存缓存,其键对应的值类型直接为Bitmap;而DiskLruCache是磁盘缓存,因此其键对应的值类型应该是一个表明图片文件的类。其次,前者访问或添加元素时,查找成功能够直接使用该Bitmap对象;后者访问或添加元素时,查找到指定图片文件后还须要经过文件的读取和Bitmap的加载过程才能使用。另外,前者是在内存中的数据读写操做因此不须要异步;后者涉及到文件操做必须开启子线程实现异步处理。
具体DiskLruCache的设计方案和使用方式能够参考这篇博客:https://www.jianshu.com/p/765640fe474a
有了LruCache类和DiskLruCache类,能够实现完整的Android图片二级缓存策略:在具体的图片加载时:先尝试在LruCache中查找Bitmap对象,若是有直接拿来使用。若是没有再尝试在DiskLruCache中查找图片文件,若是有将其加载为Bitmap对象再使用,并将其添加至LruCache中;若是没有查找到指定的图片文件,则发送网络请求获取图片资源并加载为Bitmap对象再使用,并将其添加DiskLruCache中。
五、Bitmap内存管理
Android设备的内存包括本机Native内存和Dalvik(相似于JVM虚拟机)堆内存两部分。在Android 2.3.3(API级别10)及更低版本中,位图的支持像素数据存储在Native内存中。它与位图自己是分开的,Bitmap对象自己存储在Dalvik堆中。Native内存中的像素数据不会以可预测的方式释放,可能致使应用程序短暂超出其内存限制并崩溃。从Android 3.0(API级别11)到Android 7.1(API级别25),像素数据与相关Bitmap对象一块儿存储在Dalvik堆上,一块儿交由Dalvik虚拟机的垃圾收集器来进行回收,所以比较安全。
①在Android2.3.3版本以前:
在Bitmap对象再也不使用并但愿将其销毁时,Bitmap对象自身因为保存在Dalvik堆中,因此其自身会由GC自动回收;可是因为Bitmap的像素数据保存在native内存中,因此必须由开发者手动调用Bitmap的recycle()方法来回收这些像素数据占用的内存空间。
②在Android2.3.3版本以后:
因为Bitmap对象和其像素数据一块儿保存在Dalvik堆上,因此在其须要回收时只要将Bitmap引用置为null 就好了,不须要如此麻烦的手动释放内存操做。
固然,通常咱们在实际开发中每每向下兼容到Android4.0版本,因此你懂得。
③在Android3.0之后的版本,还提供了一个很好用的参数,叫options.inBitmap。若是你使用了这个属性,那么在调用decodeXXXX方法时会直接复用 inBitmap 所引用的那块内存。你们都知道,不少时候ui卡顿是由于gc 操做过多而形成的。使用这个属性能避免频繁的内存的申请和释放。带来的好处就是gc操做的数量减小,这样cpu会有更多的时间执行ui线程,界面会流畅不少,同时还能节省大量内存。简单地说,就是内存空间被各个Bitmap对象复用以免频繁的内存申请和释放操做。
须要注意的是,若是要使用这个属性,必须将BitmapFactory.Options的isMutable属性值设置为true,不然没法使用这个属性。
具体使用方式参考以下代码:
final BitmapFactory.Options options = new BitmapFactory.Options(); //size必须为1 不然是使用inBitmap属性会报异常 options.inSampleSize = 1; //这个属性必定要在用在src Bitmap decode的时候 否则你再使用哪一个inBitmap属性去decode时候会在c++层面报异常 //BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target. options.inMutable = true; inBitmap2 = BitmapFactory.decodeFile(path1,options); iv.setImageBitmap(inBitmap2); //将inBitmap属性表明的引用指向inBitmap2对象所在的内存空间,便可复用这块内存区域 options.inBitmap = inBitmap2; //因为启用了inBitmap属性,因此后续的Bitmap加载不会申请新的内存空间而是直接复用inBitmap属性值指向的内存空间 iv2.setImageBitmap(BitmapFactory.decodeFile(path2,options)); iv3.setImageBitmap(BitmapFactory.decodeFile(path3,options)); iv4.setImageBitmap(BitmapFactory.decodeFile(path4,options));
补充:Android4.4之前,你要使用这个属性,那么要求复用内存空间的Bitmap对象大小必须同样;可是Android4.4 之后只要求后续复用内存空间的Bitmap对象大小比inBitmap指向的内存空间要小就可使用这个属性了。另外,若是你不一样的imageview 使用的scaletype 不一样,可是你这些不一样的imageview的bitmap在加载是若是都是引用的同一个inBitmap的话,
这些图片会相互影响。综上,使用inBitmap这个属性的时候 必定要当心当心再当心。
6、开源框架
咱们如今已经知道了,Android图片加载的知识点和注意事项实在太多了:单个的位图加载咱们要考虑Bitmap加载的OOM问题、异步处理问题和内存泄露问题;列表加载位图要考虑显示错乱问题;频繁大量的位图加载时咱们要考虑二级缓存策略;咱们还有考虑不一样版本下的Bitmap内存管理问题,在这部分最后咱们介绍了Bitmap内存复用方式,咱们须要当心使用这种方式。
那么,能不能有一种方式让咱们省去这么多繁琐的细节,方便咱们对图片进行加载呢?答案就是:利用已有的成熟的图片加载和缓存开源框架!好比square公司的Picasso框架、Google公司的Glide框架和Facebook公司的Fresco框架等。特别是Fresco框架,提供了三级缓存策略,很是的专业。根据APP对图片显示和缓存的需求从低到高排序,咱们能够采用的方案依次为:Bitmapfun、Picasso、Android-Universal-Image-Loader、Glide、Fresco。
这些框架能够方便咱们实现对网络图片的加载和缓存操做。具体再也不赘述。