Android开发艺术探索(十二)
来源:互联网 发布:魔兽世界3.22数据库 编辑:程序博客网 时间:2024/06/05 10:36
Bitmap的加载和cache
Bitmap的高效加载
1、如何加载Bitmap?
Bitmap在Android中是指一张图片。Android中BitmapFactory提供了四类方法:
1、decodeFile:从文件中加载
2、decodeResource:从资源中加载
3、decodeStream:输入流中加载
4、decodeByteArray:字节数组
2、如何高效加载Bitmap?
核心思想是采用BitmapFactory.Options来加载所需尺寸的图片。
.
BitmapFactory.Options缩放图片,需要一个采样率参数,主要用到了InSampleSize参数。
当InSampleSize为1时,采样后的图片为原始大小;当InSampleSize大于1时,例如2,采样后的图片的宽、高均为原来的1/2,像素则为原来的1/4,占有的内存大小也为原来的1/4;
注意:只有InSampleSize大于1时,图片才会有缩放效果。
3、获取图片的采样率?
1、将BitmapFactory.Options的inJustDecodeBounds设置为true,并加载图片。
2、从BitmapFactory.Options中读取图片的原始宽高信息,他们对应outWidth和outHeight参数。
3、根据采样率的规则并结合目标View的所需大小计算出采样率InSampleSize
44、将BitmapFactory.Options的inJustDecodeBounds设置为false,重新加载图片
编写工具类代码如下:
public class BitmapUtils { private static final String TAG="BitmapUtils"; /** * 用于返回压缩尺寸后的图片 * * @param path 图片文件路径 * @param reqWidth 用于显示图片的目标ImageView宽度 * @param reqHeight 用于显示图片的目标ImageView高度 * @return */ public static Bitmap decodeSampledBitmap(String path, int reqWidth, int reqHeight){ final BitmapFactory.Options options=new BitmapFactory.Options(); //设置inJustDecodeBounds为true,表示只加载图片的边框信息 options.inJustDecodeBounds=true; //加载图片 BitmapFactory.decodeFile(path,options); Log.e(TAG,"start "+options.inSampleSize); //计算图片的采样率 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); Log.e(TAG,"end "+options.inSampleSize); //设置inJustDecodeBounds为false,重新加载图片 return BitmapFactory.decodeFile(path,options); } /** * 计算图片的采样率 * * @param options * @param reqWidth * @param reqHeight * @return */ private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height=options.outHeight; final int width=options.outWidth; //原始图片采样率,设置为1 int inSampleSize=1; if (height!=0&&width!=0){ //如果options输出的高度和宽度大于显示图片的ImageView的高度和宽度 if (height>reqHeight||width>reqHeight){ final int heightRatio=Math.round((float) height/(float) reqHeight); final int widthRatio=Math.round((float)width/(float)reqWidth); //计算出options输出的宽高和显示图片的imageView宽高比 inSampleSize=heightRatio<widthRatio?heightRatio:widthRatio; } } return inSampleSize; }}
这个时候,我们如果要在尺寸为100*100像素的ImageView显示一张图片,如下使用:
//path为图片的路径 iv.setImageBitmap(BitmapUtils.decodeSampledBitmap(path,100,100));
Android中的缓存策略
缓存策略:
内存存储、设备存储、网络获取。
当请求一张图片时,首先从内存中获取;如果没有则从设备中获取;如果设备中也没有在从网络中下载。
一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。
目前常用的一种缓存算法是LRU,LRU是近期最少使用算法。核心思想史,当缓存满时,会优先淘汰那些最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存。
LruCache
LruCache是一个泛型类,它内部采用了一个LinkedHashMap以强引用的方式存储外接的缓存对象,并且提供了get和put方法来完成缓存的获取和添加操作。
先说一下引用类型:
强引用:直接的对象引用;
软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。
弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。
LruCache是线程安全的:
public class LruCache<K, V> { private final LinkedHashMap<K, V> map; //初始化 public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap<K, V>(0, 0.75f, true); } ....}
上面代码中LruCache初始化时,我们需要给一个最大容量。一般设置为当前进程可用内存的1/8,单位是kb。
/**获取当前进程可用最大内存*/ int maxMemory= (int) (Runtime.getRuntime().maxMemory()/1024); LruCache<String,Bitmap> lruCache=new LruCache<String, Bitmap>(maxMemory/8){ /**重写sizeOf方法计算缓存的大小*/ @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes()*value.getHeight()/1024; } /**移除缓存对象需要调用次方法*/ @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { super.entryRemoved(evicted, key, oldValue, newValue); } }; /**获取缓存对象*/ lruCache.get(key); /**存入缓存对象*/ lruCache.put(key,bitmap); /**移除缓存对象*/ lruCache.remove(key);
DiskLruCache
DiskLruCache用于实现存储设备缓存。DiskLruCache并不是AndroidSDK的一部分。
下载地址
1、DiskLruCache的创建
//directory:表示磁盘缓存在文件系统中的存储路径。 //appVersion:应用的版本号 //valueCount:表示单个节点对应数据的个数,一般设置为1 //maxSize:表示缓存的总大小,比如50MB public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } if (valueCount <= 0) { throw new IllegalArgumentException("valueCount <= 0"); } // prefer to pick up where we left off DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); if (cache.journalFile.exists()) { try { cache.readJournal(); cache.processJournal(); cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), IO_BUFFER_SIZE); return cache; } catch (IOException journalIsCorrupt) {// System.logW("DiskLruCache " + directory + " is corrupt: "// + journalIsCorrupt.getMessage() + ", removing"); cache.delete(); } } // create a new empty cache directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache.rebuildJournal(); return cache; }
2、DiskLruCache的缓存添加
DiskLruCache添加的操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。首先获取到图片url所对应的key。
private String hashKeyFormUrl(String url) { String cacheKey; try { /**使用url的MD5值作为key*/ final 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[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); }
然后将图片的url转为key之后,就可以获取到Editor对象了。然后通过此Editor获取到一个文件输出流,需要注意的是:前面的DiskLruCache的open方法中设置了一个节点只能有一个数据,因此下面的DISK_CACHE_INDEX常量设置为0即可。
String key = hashKeyFormUrl(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); }
有了输出流之后,当网络下载图片时,就可以通过这个文件输出流写入到文件系统上。
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final 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 (IOException e) { Log.e(TAG, "downloadBitmap failed." + e); } finally { if (urlConnection != null) { urlConnection.disconnect(); } MyUtils.close(out); MyUtils.close(in); } return false; }
然后呢,通过Editor.commit()来提交写入操作,如果图片下载发生异常,还可以通过Editor的abort()方法来回退整个操作。
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); if (downloadUrlToStream(url, outputStream)) { editor.commit(); } else { editor.abort(); } mDiskLruCache.flush();
3、DiskLruCache的缓存查找
和缓存的添加过程类似,缓存查找过程也需要将url转换为key,然后通过DiskLruCache的get方法分得到一个Snapshot对象,接着再通过Snapshot对象可以的到缓存的文件输出流,通过文件输出流就可以获取到bitmap对象。
Bitmap bitmap = null; /**获取到键值*/ String key = hashKeyFormUrl(url); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); 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(key, bitmap); } }
自定义图片加载框架的实现
图片加载框架应该具备以下功能:
图片异步加载
图片的同步加载
图片压缩
内存缓存
磁盘缓存
网络获取
1、内存缓存和磁盘缓存的实现
private LruCache<String, Bitmap> mMemoryCache; private DiskLruCache mDiskLruCache; private ImageLoader(Context context) { mContext = context.getApplicationContext(); int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); int cacheSize = maxMemory / 8; /**设置内存缓存大小为当前进程可用最大内存的1/8*/ mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; File diskCacheDir = getDiskCacheDir(mContext, "bitmap"); if (!diskCacheDir.exists()) { diskCacheDir.mkdirs(); } //判断磁盘可用空间是否大于所需储存空间 if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) { try { /**设置硬盘缓存为50MB*/ mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE); mIsDiskLruCacheCreated = true; } catch (IOException e) { e.printStackTrace(); } } }
然后完成缓存的添加和获取:
/** * 添加bitmap到缓存中 * @param key * @param bitmap */ private void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } /** * 根据key值获取缓存对象 * @param key * @return */ private Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }
磁盘缓存的读取西药通过Snapshot来完成,通过Snap可以得到磁盘缓存对象对应的FileInputStream,但是FileInputStream无法便捷的进行压缩,所以通过FileDescriptor来加载压缩后的图片,最后将加载后的Bitmap添加到内存缓存中。
/** * 从网络获取图片 * @param url * @param reqWidth * @param reqHeight * @return * @throws IOException */ private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException { if (Looper.myLooper() == Looper.getMainLooper()) { throw new RuntimeException("can not visit network from UI Thread."); } if (mDiskLruCache == null) { return null; } String key = hashKeyFormUrl(url); 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); } /** * 从磁盘中读取缓存内容 * @param url * @param reqWidth * @param reqHeight * @return * @throws IOException */ private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException { if (Looper.myLooper() == Looper.getMainLooper()) { Log.w(TAG, "load bitmap from UI Thread, it's not recommended!"); } if (mDiskLruCache == null) { return null; } Bitmap bitmap = null; /**获取到key值*/ String key = hashKeyFormUrl(url); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); 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(key, bitmap); } } return bitmap; }
2、同步加载和异步加载接口的设计
同步加载接口需要外部在线程中调用,同步加载比较耗费时间。
同步加载实现:
/** * load bitmap from memory cache or disk cache or network. * @param uri http url * @param reqWidth the width ImageView desired * @param reqHeight the height ImageView desired * @return bitmap, maybe null. */ public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) { /**在内存中查找*/ Bitmap bitmap = loadBitmapFromMemCache(uri); if (bitmap != null) { Log.d(TAG, "loadBitmapFromMemCache,url:" + uri); return bitmap; } /**在硬盘中查找*/ try { bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight); if (bitmap != null) { Log.d(TAG, "loadBitmapFromDisk,url:" + uri); return bitmap; } bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight); Log.d(TAG, "loadBitmapFromHttp,url:" + uri); } catch (IOException e) { e.printStackTrace(); } /**网络加载*/ if (bitmap == null && !mIsDiskLruCacheCreated) { Log.w(TAG, "encounter error, DiskLruCache is not created."); bitmap = downloadBitmapFromUrl(uri); } return bitmap; }
需要注意的是:上面这个方法不能再主线程中使用,否则会抛出异常。
if (Looper.myLooper() == Looper.getMainLooper()) { throw new RuntimeException("can not visit network from UI Thread."); }
在这类进行了判断,判断当前的Looper是否为主线程的Looper,如果是主线程,就抛出异常。
异步加载实现:
public void bindBitmap(final String uri, final ImageView imageView) { bindBitmap(uri, imageView, 0, 0); } public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) { imageView.setTag(TAG_KEY_URI, uri); /**内存读取*/ Bitmap bitmap = loadBitmapFromMemCache(uri); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } Runnable loadBitmapTask = new Runnable() { @Override public void run() { /**使用线程池加载图片*/ Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight); if (bitmap != null) { LoaderResult result = new LoaderResult(imageView, uri, bitmap); //发送消息,通过Handler中转 mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget(); } } }; THREAD_POOL_EXECUTOR.execute(loadBitmapTask); }
bindBitmap中使用到了线程池和Handler。
/**线程池内,线程数量*/ private static final int CORE_POOL_SIZE = CPU_COUNT + 1; //CPU核心数的2倍+1 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; //线程闲置超时时长为10秒 private static final long KEEP_ALIVE = 10L; private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(r, "ImageLoader#" + mCount.getAndIncrement()); } }; /**THREAD_POOL_EXECUTOR 实现*/ public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), sThreadFactory);
为什么要采用线程池呢?
1、如果我们加载一张单个的图片,直接使用普通的线程加载就可以。但是如果列表中有大量的图片需要加载,随着列表滑动会产生大量的线程,整体效率会低下。
2、没有采用AsyncTask,AsyncTask在3.0以上无法实现并发效果,可以通过改造AsyncTask或者使用AsyncTask的executeOnexecutor方法的形式来执行异步任务。
这里选择线程池和Handler来提供ImageLoader的并发能力和访问UI的能力。
Handler的实现?
ImageLoader直接采用了主线程的Looper来构造Handler对象,这就使得ImageLoader可以在非主线程中构造了。
private Handler mMainHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { LoaderResult result = (LoaderResult) msg.obj; ImageView imageView = result.imageView; imageView.setImageBitmap(result.bitmap); String uri = (String) imageView.getTag(TAG_KEY_URI); if (uri.equals(result.uri)) { imageView.setImageBitmap(result.bitmap); } else { Log.w(TAG, "set image bitmap,but url has changed, ignored!"); } }; };
在以上的代码中,为了解决View复用造成的列表错位问题,给ImageView设置图片之前,都会检查它的url有没有发生改变,如果发生改变就不在给它设置图片。
ImageLoader的使用
加载图片时使用方式:
/**首先给控件设置相应的tag标签*/ imageView.setTag(uri); /**从左到右参数依次为图片的url、显示目标控件ImageView、显示的宽度、显示的高度*/ mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
- Android开发艺术探索(十二)
- Android开发艺术探索
- Android 开发艺术探索读书笔记
- 《Android开发艺术探索》读书笔记
- 【读书笔记】Android开发艺术探索
- 《Android开发艺术探索》读书笔记
- Android开发艺术探索1
- Android开发艺术探索2
- Android开发艺术探索3
- Android开发艺术探索(九)
- Android开发艺术探索(十)
- Android开发艺术探索(十一)
- 《Android 开发艺术探索》读书笔记
- 《Android开发艺术探索》笔记
- Android 开发艺术探索 读书笔记
- 《Android开发艺术探索》笔记
- Android 开发艺术探索 第一章
- Android开发艺术探索-Drawable
- iOS开发之duplicate symbols for architecture x86_64错误
- 阅读郭林《第一行代码》的笔记——第6章 数据存储全方案,详解持久化技术
- Beehives
- Cygwin下vim的配置
- 关于最小生成树的Prim算法和Kruskal算法
- Android开发艺术探索(十二)
- Intellij IDEA 快捷键整理(TonyCody)
- 实验楼Linux学习笔记(一)之基本概念及操作
- ubuntu16.04安装N卡驱动,cuda toolkit7.5,opencv 2.4.13 with module gpu
- 新的征程
- JDBC应用流程
- LINQ 通过Dictionary的Value查找Key
- java 数据库类型 报错
- Spring使用总结(一):缓存