《Android开发艺术探索》之Bitmap的加载和Cache(十四)
Posted 公孙勤修
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Android开发艺术探索》之Bitmap的加载和Cache(十四)相关的知识,希望对你有一定的参考价值。
第12章 Bitmap的加载和Cache
本章的主题是Bitmap的加载和Cache,主要包含三个方面:
首先讲述如何有效的加载一个Bitmap,Bitmap由于特殊性以及android对单个应用所施加的内存限制,比如16MB,这导致加载Bitmap时很容易的出现内存溢出,高效加载Bitmap是一个问题;
接着介绍Android的缓存策略,缓存策略是通用的思想,在实际开发中经常需要用Bitmap缓存,通过缓存策略,我们不需要每次都从网络上请求图片或者从存储设备中加载图片,这样就极大的提高了图片的加载效率以及产品的用户体验,目前比较好的缓存策略是LrcCache(内存缓存,最近最少使用算法)和DiskLruCache(存储缓存)。
最后还会介绍一下如何优化卡顿现象,Listview和GridView由于加载大量子视图,当用户快速滑动时候会卡顿,进行优化。
还有一个示例程序:涉及了图片加载、缓存策略以及列表的滑动流畅性。
(1)Bitmap的高效加载
1.1.常见的图片加载方式:
第一种:
设置前景:android:src=”@drawable/xxx”
设置背景:android:background=”@drawable/xxx”
第二种:SetImageResource的参数是resId,必须是drawable目录下的资源,类似于刚才的src。
iv1 = (ImageView) findViewById(R.id.image_pc1);
iv1.setImageResource(R.mipmap.test);
第三种:setImageBitmap参数是Bitmap,可以解析不同来源的图片再进行设置。把Bitmap对象封装成Drawable对象,然后调用setImageDrawable来设置图片。
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 3;
iv1.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.test, options));
第四种:setImageDrawable参数是Drawable,也是可以接受不同来源的图片,方法中所做的事情就是更新ImageView的图片。相对来说高效。
// 将mipmap封装成drawable对象
Drawable drawable = ContextCompat.getDrawable(getApplicationContext(), R.mipmap.test);
iv1.setImageDrawable(drawable);
1.2.什么是Bitmap?如何加载一个Bitmap?
Bitmap在Android中指的是一张图片(jpg、png以及其他常见图片格式),BitmapFactory的decodeFile,decodeResource,decodeStream,decodeByteArray分别从文件系统,资源,输入流,以及字节数组中加载一个Bitmap对象,decodeFile,decodeResource间接调用decodeStream,这四类方法在底层用native实现。
1.3.如何高效加载Bitmap?
BitmapFactory.Options来加载所需尺寸的图片,相比于ImageView显示图片(ImageView并没有图片的原始尺寸那么大,这个时候把整个图片加载进来后再设给ImageView,ImageView也没法显示原始图片),通过BitmapFactory.Options可以按照一定的采样率来压缩图片,降低内存占用率并在一定程度上避免OOM(内存溢出),BitmapFactory提供的四种方法都支持BitmapFactory.Options参数,采样缩放后加载。
BitmapFactory.Options主要用了inSmapleSize(采样率)参数,当该参数为1时,采样图片大小为图片的原始大小;当采样率为2时,宽/高均为原图大小的1/2,像素值为原图的1/4,,内存也占原来的1/4。采样率同时作用宽高,导致缩放后图片的大小以采样率的2次方形式递减。即缩放比例为1/(inSampleSize的2次方)。
如何获取Bitmap的采样率并加载出图片,将ImageView设置为100*100像素?1.将BitmapFactory.Options的inJustDecodeBounds参数设置为true并且加载图片(为true时只会解析图片的原始宽/高,并不会真正的加载图片);2.从BitmapFactory.Options中取出图片的原始宽高;3.根据采用率的规则结合目标的View所需计算采用率;4.将BitmapFactory.Options的inJustDecodeBounds参数设置为false然后重新加载图片。
public class MainActivity extends AppCompatActivity {
private ImageView iv1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv1 = (ImageView) findViewById(R.id.image_pc1);
iv1.setImageBitmap(decodeSampleBitmapfromResorece(getResources(),R.mipmap.test,300,100));
}
private Bitmap decodeSampleBitmapfromResorece(Resources res, int resId, int reqwidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
//将inJustDecodeBounds参数设置为true;
options.inJustDecodeBounds = true;
//加载图片
BitmapFactory.decodeResource(res,resId,options);
//从BitmapFactory.Options中取出图片的原始宽高;
options.inSampleSize = CalculateInSampleSize(options,reqwidth,reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res,resId,options);
}
private 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;
while ((halfHeight/inSampleSize)>=reqHeight &&(halfwidth/inSampleSize)>=reqwidth){
inSampleSize*=2;
}
}
return inSampleSize;
}
}
(2)Android中的缓存策略
缓存策略在Android中有着广泛的使用场景,尤其在图片加载这个场景下,缓存策略变得更加重要。考虑一种场景:一批网络图片,需要下载后在用户界面给予显示,流量很贵,故而使用缓存,当程序第一次从网络加载图片(或者其他文件)后将其缓存至移动设备上,下次使用就不用从网络上获取了。有可能还在内存中缓存一份,当请求获取图片时,先在内存中获取->存储设备中获取->网络下载。
缓存策略包括添加、获取和删除。前两者简单,第三者采用LRU(最近最少使用算法)。它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象,采用LRU算法的缓存有两种:LruCache(用于内存缓存)和DiskLruCache(用于存储设备缓存),二者结合可以很方便的实现一个有价值的ImageLoader。
2.1.LruCache
2.1.1.相关定义
LruCache是一个缓存类,通过support-v4兼容包可以兼容到早期的Android版本。LruCache是一个泛型类,它内部采用了LinkedHashMap以强引用的方式存储外界的缓存对象,提供get和put的操作完成缓存的获取和添加,当缓存满时,移除较早使用的对象,并添加新的对象。LruCache是线程安全的。
强软弱引用的区别:强引用:直接的对象引用;软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。
LruCache定义:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
...
}
2.1.2.如何使用?
LruCache的典型初始化过程:
//获取当前进程的可用内存
int maxmemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.i(TAG, "onCreate: maxmemory"+maxmemory);
//缓存容量的大小为当前可用内存的1/8
int cachesize = maxmemory / 8;
Log.i(TAG, "onCreate: cachesize"+cachesize);
LruCache mMapCache = new LruCache<String, Bitmap>(cachesize) {
// sizeOf则完成了bitmap对象的大小计算,计算缓存对象的大小
@Override
protected int sizeOf(String key, Bitmap value) {
//除以1024也是为了将其 单位转换成KB
return value.getRowBytes() * value.getHeight() / 1024;
}
};
向Lrucache添加对象:
mMapCache.put(key,BitmapFactory.decodeResource(getResources(), R.mipmap.test, options));
从Lrucache获取对象:
mMapCache.get(key);
2.2.DiskLruCache
DiskLruCache用于实现存储设备缓存,即磁盘缓存,他通过缓存对象写入文件系统从而实现缓存的效果,下面从DiskLruCache的创建、缓存添加和缓存查找三个方面说明。
2.2.1.DiskLruCache的创建
不能使用构造方法创建,提供了open方法用于创建自身。open方法如下:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
//1. 第一个参数表示磁盘缓存在文件系统的路径,可选择SD卡的缓存目录,若删除应用会将其一并删除;如若希望保存缓存文件,就保存到SD卡的其他特定目录
//2.第二个参数表示应用版本号,一般设为1
//3.第三个参数表示单个节点所对应的数据个数,一般设为1即可
//4.第四个参数表示缓存的总大小,比如50MB,当缓存大小超过一个设定值,它会清除一些缓存来保证总大小不大于这个值:
典型的DiskLruCache的创建过程:
long DISK_CACHE_SIZE = 1024 * 1024 *50;
File diskCacheDir = getDiskCacheDir(this,"bitmap");
if(!diskCacheDir.exists()){
diskCacheDir.mkdirs();
}
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
2.2.2.缓存添加
步骤一:DiskLruCache的缓存添加操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象,以图片缓存为例,首先需要获取图片url所对应的key(一般采用url的值作为key),然后根据key就可以通过edit()来获取Editor对象,DiskLruCache不允许同时编辑一个缓存对象。
private String hashKeyFormUrl(String url){
String cacheKey;
try {
MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] digest) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < digest.length; i++) {
String hex = Integer.toHexString(0xFF&digest[i]);
if(hex.length() == 1){
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
步骤二:网络下载图片,通过文件输出流写入到文件系统上。
private boolean downloadUrlToStream(String urlString,OutputStream outputStream){
int IO_BUFFER_SIZE = 0;
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
int b ;
while ((b = in.read()) != -1){
out.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(urlConnection != null){
urlConnection.disconnect();
}
}
return false;
}
步骤三:将图片的url转为key之后,就可以获取Editor对象了,对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过它就可以得到一个文件输入流,通过Editor的commit完成提交。
int DISK_CACHE_INDEX = 0;
//由于前面的open设置了一个节点只能有一个数据,因此DISK_CACHE_SIZE = 0
String key = hashKeyFormUrl(url);
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null){
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
//执行下载
if(downloadUrlToStream(url,outputStream)){
//提交写入操作
editor.commit();
}else {
//下载异常,执行回退操作
editor.abort();
}
//更新操作
mDiskCache.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
2.2.3.缓存查找
缓存查找过程中也需要将url转换为key。然后通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象即可得到缓存的文件输入流,有了输入流,自然就可以得到Bitmap对象了。为了避免OOM,不建议加载原始图片,使用BitmapFactory.Options对象加载缩放后的图片,但那对应的是FileInputStream有序文件流;可使用BitmapFactory.decodeFileDespritor去加载缩放后的图片。
Bitmap bitmap = null;
String keys = hashKeyFormUrl(url);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(keys);
if(snapshot != null){
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap=mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
if(bitmap != null){
addBitmapToMemoryCache(keys,bitmap);
}
}
2.3.ImageLoader
一个优秀的ImageLoader应该具备:图片的同步加载、图片的异步加载、图片压缩、内存缓存、磁盘缓存、网络拉取。
(1)图片的同步加载是指能够同步的方式向调用者所提供加载的图片,可能是从内存缓存中读取,磁盘读取,网络拉取;(2)异步加载调用者不想在单独的线程里加载图片并将图片设置给需要的ImageView,ImageLoader在自己的线程中加载图片并将图片设置给所需的Imageview;(3)图片压缩可降低OOM概率,(4)内存缓存是LruCache,磁盘缓存是DiskLruCache,降低流量消耗,若两级缓存都不可用可从网络中拉取。
2.3.1.图片压缩功能实现
第一节已实现。
2.3.2.内存缓存和磁盘缓存的实现
二级缓存实现该功能
//1.ImageLoader的初始化,包括创建LruCache和DiskLruCache。
public ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
if(!diskCacheDir.exists()){
diskCacheDir.mkdirs();
}
//大于DISK_CACHE_SIZE的50MB,确保如果用户手机空间不足,不使用磁盘缓存
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
mIsDiskLruCacheCreate = true;
} catch (IOException e) {
e.printStackTrace();
}
}
//2.内存缓存的添加和读取功能
//内存缓存的添加功能
private void addBitmapToMemoryCache(String key,Bitmap bitmap){
if(getBitmapFromMemoryCache(key) == null){
mMemoryLruCache.put(key,bitmap);
}
}
//内存缓存的读取功能
private Bitmap getBitmapFromMemoryCache(String key){
return mMemoryLruCache.get(key);
}
//3.磁盘的读取和添加功能
private Bitmap loadBitmapFromHttp(String url,int reqWidth,int reqHeight) throws IOException {
if(Looper.myLooper() == Looper.getMainLooper()){
throw new RuntimeException("cat not visit network from UI thread");
}
if(mDiskLruCache == null){
return null;
}
String key = hashKeyFormUrl(url);
//磁盘缓存的添加需要Editor来完成,提供commit和abort方法来完成、撤销文写系统的写操作。
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null){
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if(downloadUrlToStream(url,outputStream)){
editor.commit();
}else {
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url,reqWidth,reqHeight);
}
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
if(Looper.myLooper() == Looper.getMainLooper()){
throw new RuntimeException("cat not visit network from UI thread");
}
if(mDiskLruCache == null){
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
//读取操作需要将url的md5转换为snapshot ,通过snapshot获取磁盘缓存对相对应的FileInputStream,FileInputStream无法便捷压缩,故而通过FileDescriptor来加载压缩后的图片,最后降价在后的的Bitmap加载进内存缓存中。
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot != null){
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap =ImageResizer.decodeSampleBitmapFromResource(fileDescriptor,reqWidth,reqHeight);
if(bitmap != null){
addBitmapToMemoryCache(key,bitmap);
}
}
return bitmap;
}
2.3.3.同步加载和异步加载接口的设计
同步加载接口需要外部在线程调用,这是因为同步加载很可能比较耗时。缓存中读取图片,磁盘中读取图片,网络中拉取图片。在子线程调用。
private Bitmap loadBitmap(String url,int reqWidth,int reqHeight){
//1.从内存缓存中读取图片
Bitmap bitmap = loadBitmapFromMemCache(url);
if(bitmap != null){
return bitmap;
}
try {
//2.从磁盘中读取图片
bitmap = loadBitmapFromDiskCache(url,reqWidth,reqHeight);
if(bitmap != null){
return bitmap;
}
//3.从网络中拉取图片。
bitmap = loadBitmapFromHttp(url,reqWidth,reqHeight);
//4.loadBitmapFromHttp会检查执行环境,如果是主线程就会抛出异常。
} catch (IOException e) {
e.printStackTrace();
}
if(bitmap == null && !mIsDiskLruCacheCreate){
bitmap = downloadBitmapFromUrl(url);
}
return bitmap;
}
异步加载接口的设计:
//异步加载接口的设计
private void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight){
imageView.setTag(TGA_KEY_URI,uri);
try {
//尝试从内存中读取图片
Bitmap bitmap = loadBitmapFromDiskCache(uri,reqWidth,reqHeight);
//读取成功返回结果
if(bitmap != null){
imageView.setImageBitmap(bitmap);
return;
}
} catch (IOException e) {
e.printStackTrace();
}
///若读取不成功,线程池调用loadBitmap方法
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri,reqWidth,reqHeight);
//加载成功后将图片图片uri和需要绑定的imageview封装成LoaderResult 对象
if(bitmap != null){
LoaderResult result = new LoaderResult(imageView,uri,bitmap);
// mMainHandler向主线程发送一个消息,这样就可以主线程中给imageview设置图片
mMainHandler.obtainMessage(MESSAGE_POST_RESULT,reqHeight);
sendToTarget();
}
}
};
// THREAD_POOL_EXECUTOR线程池执行该任务
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
// THREAD_POOL_EXECUTOR的实现,为什么要用线性池而非普通线程,列表下滑产生大量线程,不利于整体效率提升;不选择用AsyncTask,是因为一方面3.0以上AsynvTask无法实现并发的效果,改造AsynvTask来完成异步任务,不太自然。
private static final int CODE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SZIE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread( Runnable runnable) {
return new Thread(runnable,"ImageLoader#"+ mCount.getAndIncrement());
}
};
public static final Executors THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CODE_POOL_SIZE
,MAXIMUM_POOL_SZIE,KEEP_ALIVE, TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>(),sThreadFactory);
//Handler的实现,采用主线程的Looper来构造Handler对象,使得ImageLoader可以在费主线程中构造,为了解决View复用导致列表错位问题,在ImageView设置图片之前会检查它的Url有没有发生改变。
private Handler mMainHandler = new Handler(Looper.myLooper()){
@Override
public void handleMessage(Message msg) {
LoaderResult result = msg.obj;
ImageView imageView = result.imageview;
imageView.setImageBitmap(result.bitmap);
String uri = imageView.getTag(TAG_KEY_URI);
if(uri.equals(result.uri)){
imageView.setImageBitmap(result.bitmap);
}
}
};
记得android:hardwareAccelerated=”true”开启硬件加速解决莫名卡顿问题。
以上是关于《Android开发艺术探索》之Bitmap的加载和Cache(十四)的主要内容,如果未能解决你的问题,请参考以下文章
android开发艺术探索读书笔记之-------view的事件分发机制