高效显示Bitmap+listview冲突解决+图片内存缓存+硬盘缓存
-
- Android高效加载大图
- 非UI线程处理Bitmap
- 处理并发问题
- 下面是缓存技术
- 总结
Android高效加载大图
BitmapFactory提供了一些解码(decode)的方法(decodeByteArray(), decodeFile(), decodeResource()等),用来从不同
的资源中创建一个Bitmap。 我们应该根据图片的数据源来选择合适的解码方法。 这些方法在构造位图的时候会尝试分配内
存,因此会容易导致 OutOfMemory 的异常。每一种解码方法都可以通过BitmapFactory.Options设置一些附加的标记,以此来
指定解码选项。设置 i**nJustDecodeBounds 属性为 true 可以在解码的时候避免内存的分配,它会返回一个 null 的Bitmap,
但是可以获取到 outWidth, outHeight 与 outMimeType**。该技术可以允许你在构造Bitmap之前优先读图片的尺寸与类型。
BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeResource(getResources(), R.id.myimage, options);int imageHeight = options.outHeight;int imageWidth = options.outWidth;String imageType = options.outMimeType;
为了避免 java.lang.OutOfMemory 的异常,我们需要在真正解析图片之前检查它的尺寸(除非你能确定这个数据源提供了准确
无误的图片且不会导致占用过多的内存)。
通过上面的步骤我们已经获取到了图片的尺寸,这些数据可以用来帮助我们决定应该加载整个图片到内存中还是加载一个缩小的版本。有下面一些因素需要考虑:
- 评估加载完整图片所需要耗费的内存。
- 程序在加载这张图片时可能涉及到的其他内存需求。
- 呈现这张图片的控件的尺寸大小。
- 屏幕大小与当前设备的屏幕密度。
例如,如果把一个大小为1024x768像素的图片显示到大小为128x96像素的ImageView上吗,就没有必要把整张原图都加载到内存中。
为了告诉解码器去加载一个缩小版本的图片到内存中,需要在BitmapFactory.Options 中设置 inSampleSize 的值。例如, 一个分辨率为2048x1536的图片,如果设置 inSampleSize 为4,那么会产出一个大约512x384大小的Bitmap。加载这张缩小的图片仅仅使用大概0.75MB的内存,如果是加载完整尺寸的图片,那么大概需要花费12MB(前提都是Bitmap的配置是ARGB_8888)。
下面有一段根据目标图片大小来计算Sample图片大小的代码示例:
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {// Raw height and width of imagefinal 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;// Calculate the largest inSampleSize value that is a power of 2 and keeps both// height and width larger than the requested height and width.while ((halfHeight / inSampleSize) > reqHeight&& (halfWidth / inSampleSize) > reqWidth) {inSampleSize *= 2;}}return inSampleSize;}
Note: 设置inSampleSize为2的幂是因为解码器最终还是会对非2的幂的数进行向下处理,获取到最靠近2的幂的数.这是官方给的示例代码,我们可以进行如下改造:
public static 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 heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 // 一定都会大于等于目标的宽和高。 inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize;}
为了使用该方法,首先需要设置 inJustDecodeBounds 为 true , 把options的值传递过来,然后设置 inSampleSize 的值并设置 inJustDecodeBounds 为 false ,之后重新调用相关的解码方法。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
使用上面这个方法可以简单地加载一张任意大小的图片。如下面的代码样例显示了一个接近 100x100像素的缩略图:
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
非UI线程处理Bitmap
使用AsyncTask:
AsyncTask 类提供了一个在后台线程执行一些操作的简单方法,它还可以把后台的执行结果呈现到UI线程中。下面是一个加
载大图的示例:
class BitmapWorkerTask extends AsyncTask { private final WeakReference imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) { // Use a WeakReference to ensure the ImageView can be garbage // collected imageViewReference = new WeakReference(imageView); } // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { data = params[0]; return decodeSampledBitmapFromResource(getResources(), data, 100, 100)); } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } } }
为ImageView使用WeakReference确保了AsyncTask所引用的资源可以被垃圾回收器回收。由于当任务结束时不能确保
ImageView仍然存在,因此我们必须在 onPostExecute() 里面对引用进行检查。该ImageView在有些情况下可能已经不存在
了,例如,在任务结束之前用户使用了回退操作,或者是配置发生了改变(如旋转屏幕等)。
开始异步加载位图,只需要创建一个新的任务并执行它即可:
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId);}
处理并发问题
通常类似ListView与GridView等视图控件在使用上面演示的AsyncTask 方法时,会同时带来并发的问题。首先为了更高的效
率,ListView与GridView的子Item视图会在用户滑动屏幕时被循环使用。如果每一个子视图都触发一个AsyncTask,那么就无
法确保关联的视图在结束任务时,分配的视图已经进入循环队列中,给另外一个子视图进行重用。而且, 无法确保所有的异
步任务的完成顺序和他们本身的启动顺序保持一致。
Multithreading for Performance 这篇博文更进一步的讨论了如何处理并发问题,并且提供了一种解决方法:ImageView保存
最近使用的AsyncTask的引用,这个引用可以在任务完成的时候再次读取检查。使用这种方式, 就可以对前面提到的AsyncTask进行扩展。
创建一个专用的Drawable的子类来储存任务的引用。在这种情况下,我们使用了一个BitmapDrawable,在任务执行的过程
中,一个占位图片会显示在ImageView中:
static class AsyncDrawable extends BitmapDrawable { private final WeakReference bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } }
在执行BitmapWorkerTask 之前,你需要创建一个AsyncDrawable并且将它绑定到目标控件ImageView中:
public void loadBitmap(int resId, ImageView imageView) { if (cancelPotentialWork(resId, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable( getResources(), mPlaceHolderBitmap, task); imageView.setImageDrawable(asyncDrawable); task.execute(resId); } }
在上面的代码示例中, cancelPotentialWork 方法检查是否有另一个正在执行的任务与该ImageView关联了起来,如果的确是
这样,它通过执行 cancel() 方法来取消另一个任务。在少数情况下, 新创建的任务数据可能会与已经存在的任务相吻合,这
样的话就不需要进行下一步动作了。下面是 cancelPotentialWork 方法的实现:
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; if (bitmapData == 0 || bitmapData != data) { // Cancel previous task bitmapWorkerTask.cancel(true); } else { // The same work is already in progress return false; } } // No task associated with the ImageView, or an existing task was // cancelled return true; }
在上面的代码中有一个辅助方法: getBitmapWorkerTask() ,它被用作检索AsyncTask是否已经被分配到指定的ImageView:
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; }
最后一步是在BitmapWorkerTask的 onPostExecute() 方法里面做更新操作:
class BitmapWorkerTask extends AsyncTask { ~~~~~~~ @Override protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); } } } }
下面是缓存技术:
参考:
- http://blog.csdn.net/fangchao3652/article/details/49703217
- http://blog.csdn.net/guolin_blog/article/details/34093441
总结:
大图片压缩处理只要是计算inSampleSize 并重写decode…()方法
然后就是非UI线程下载图片了,为了解决Listview图片乱序的问题:
我们有几种解决方法:(参考http://blog.csdn.net/guolin_blog/article/details/45586553)
- 使用findviewByTag(url) //当然之前在getview中要为Imageview setTag(url)
- 使用若引用即上面 讲解的这种方法
- 第三方图片异步加载库 如 volley的NetworkImageView 、Android-Universal-Image-Loader 还有FaceBook的Fresco等
缓存就用LruCache和DiskCache即可,当然第三方的类库有的早已经集成好了。
下面贴一个从网络下载图片+LruCache+DiskCache+findviewByTag的listview Adapter的代码:
package com.lnu.fang.lru.adapter;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileDescriptor;import java.io.FileInputStream;import java.io.IOException;import java.io.OutputStream;import java.net.HttpURLConnection;import java.net.URL;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.util.HashSet;import java.util.Set;import libcore.io.DiskLruCache;import libcore.io.DiskLruCache.Snapshot;import android.content.Context;import android.content.pm.PackageInfo;import android.content.pm.PackageManager.NameNotFoundException;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.os.AsyncTask;import android.os.Environment;import android.util.LruCache;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.ArrayAdapter;import android.widget.GridView;import android.widget.ImageView;import com.lnu.fang.lru.lactivity.R;/** * GridView的适配器,负责异步从网络上下载图片展示在照片墙上。 */public class PhotoWallAdapter extends ArrayAdapter<String> { /** * 记录所有正在下载或等待下载的任务。 */ private Set<BitmapWorkerTask> taskCollection; /** * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。 */ private LruCache<String, Bitmap> mMemoryCache; /** * 图片硬盘缓存核心类。 */ private DiskLruCache mDiskLruCache; /** * GridView的实例 */ private GridView mPhotoWall; /** * 记录每个子项的高度。 */ private int mItemHeight = 0; public PhotoWallAdapter(Context context, int textViewResourceId, String[] objects, GridView photoWall) { super(context, textViewResourceId, objects); mPhotoWall = photoWall; taskCollection = new HashSet<BitmapWorkerTask>(); // 获取应用程序最大可用内存 int maxMemory = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxMemory / 8; // 设置图片缓存大小为程序最大可用内存的1/8 mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount(); } }; try { // 获取图片缓存路径 File cacheDir = getDiskCacheDir(context, "thumb"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } // 创建DiskLruCache实例,初始化缓存数据 mDiskLruCache = DiskLruCache .open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); } } @Override public View getView(int position, View convertView, ViewGroup parent) { final String url = getItem(position); View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null); } else { view = convertView; } final ImageView imageView = (ImageView) view.findViewById(R.id.photo); if (imageView.getLayoutParams().height != mItemHeight) { imageView.getLayoutParams().height = mItemHeight; } // 给ImageView设置一个Tag,保证异步加载图片时不会乱序 等会通过findviewByTag寻找 imageView.setTag(url); imageView.setImageResource(R.drawable.empty_photo); loadBitmaps(imageView, url); return view; } /** * 将一张图片存储到LruCache中。 * * @param key * LruCache的键,这里传入图片的URL地址。 * @param bitmap * LruCache的键,这里传入从网络上下载的Bitmap对象。 */ public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemoryCache(key) == null) { mMemoryCache.put(key, bitmap); } } /** * 从LruCache中获取一张图片,如果不存在就返回null。 * * @param key * LruCache的键,这里传入图片的URL地址。 * @return 对应传入键的Bitmap对象,或者null。 */ public Bitmap getBitmapFromMemoryCache(String key) { return mMemoryCache.get(key); } /** * 加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageView的Bitmap对象, * 如果发现任何一个ImageView的Bitmap对象不在缓存中,就会开启异步线程去下载图片。 */ public void loadBitmaps(ImageView imageView, String imageUrl) { try { Bitmap bitmap = getBitmapFromMemoryCache(imageUrl); if (bitmap == null) { BitmapWorkerTask task = new BitmapWorkerTask(); taskCollection.add(task); task.execute(imageUrl); } else { if (imageView != null && bitmap != null) { imageView.setImageBitmap(bitmap); } } } catch (Exception e) { e.printStackTrace(); } } /** * 取消所有正在下载或等待下载的任务。 */ public void cancelAllTasks() { if (taskCollection != null) { for (BitmapWorkerTask task : taskCollection) { task.cancel(false); } } } /** * 根据传入的uniqueName获取硬盘缓存的路径地址。 */ public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } /** * 获取当前应用程序的版本号。 */ public int getAppVersion(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { e.printStackTrace(); } return 1; } /** * 设置item子项的高度。 */ public void setItemHeight(int height) { if (height == mItemHeight) { return; } mItemHeight = height; notifyDataSetChanged(); } /** * 使用MD5算法对传入的key进行加密并返回。 */ public String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.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(); } /** * 将缓存记录同步到journal文件中。 */ public void fluchCache() { if (mDiskLruCache != null) { try { mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } } /** * 异步下载图片的任务。 * * @author guolin */ class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> { /** * 图片的URL地址 */ private String imageUrl; @Override protected Bitmap doInBackground(String... params) { imageUrl = params[0]; FileDescriptor fileDescriptor = null; FileInputStream fileInputStream = null; Snapshot snapShot = null; try { // 生成图片URL对应的key final String key = hashKeyForDisk(imageUrl); // 查找key对应的缓存 snapShot = mDiskLruCache.get(key); if (snapShot == null) { // 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存 DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } // 缓存被写入后,再次查找key对应的缓存 snapShot = mDiskLruCache.get(key); } if (snapShot != null) { fileInputStream = (FileInputStream) snapShot.getInputStream(0); fileDescriptor = fileInputStream.getFD(); } // 将缓存数据解析成Bitmap对象 Bitmap bitmap = null; if (fileDescriptor != null) { bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor); } if (bitmap != null) { // 将Bitmap对象添加到内存缓存当中 addBitmapToMemoryCache(params[0], bitmap); } return bitmap; } catch (IOException e) { e.printStackTrace(); } finally { if (fileDescriptor == null && fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { } } } return null; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); // 根据Tag找到相应的ImageView控件,将下载好的图片显示出来。 ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl); if (imageView != null && bitmap != null) { imageView.setImageBitmap(bitmap); } taskCollection.remove(this); } /** * 建立HTTP请求,并获取Bitmap对象。 * * @param imageUrl * 图片的URL地址 * @return 解析后的Bitmap对象 */ private 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(), 8 * 1024); out = new BufferedOutputStream(outputStream, 8 * 1024); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (final IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) { e.printStackTrace(); } } return false; } }}
更多相关文章
- android:GLSurfaceView绘制bitmap图片及glViewport调整的效果
- Android中hybrid开发的基础知识
- Android实现照片选择显示-从相册选择或拍照选择
- android中显示网络图片
- Android(安卓)Studio集成百度云推送
- Android常用控件以及用法
- [置顶] android 优化那些事之图片缓存设计/如何高效的展示图片(
- 怎样在android中添加背景图片?
- Android--Adapter深入理解及ListView优化