做android应用少不了要和网络打交道,在我刚开始学android的时候总是处理不好网络图片的加载,尤其是图片乱跳的问题,后来发现了各种缓存图片的方法:本地缓存、软引用、LruCache....
我知道的这三种方法中,第一中和其他两种并不冲突,我们完全可以缓存到本地一份,在缓存到内存中一份。软引用这样方式,第一次使用软引用的时候,感觉做一个完美的内存缓存太容易了,可惜在android2.3以后android加强了对软引用的回收,这种方式基本上算是废了。
LruCache登场
软引用废了,难道就没有替代品了吗? 有,android sdk中google官方添加了LruCache类, 该类使用Lru算法实现内存缓存,关于LruCache的使用,可以google一下,很简单。
本地缓存方案
以前我的本地缓存方案都是自己写流实现的,这种方式完全可以,而且也不是很费劲,现在也可以使用这种方式。但是,有一个更好用的方式:DiskLruCache,广域DiskLruCache,还是google一下就ok。这里说明一下,DiskLruCache并没有在SDK中需要我们自己下载,可以使用我在github上fork的:https://github.com/qibin0506/DiskLruCache 直接将源码copy到项目中即可。
ImageLoader的思路
给定一个url,我们通过这个url,先去内存中取图片,如果内存中不存在,在去本地获取,本地存在的话,再添加到内存中,不存在就去网络中下载,下载完毕后,缓存到本地和内存中。
实现
思路很清晰,那么现在开始跟着思路实现一下代码吧。
public class ImageLoader { private static final int SUCCESS = 1; private static final int FAILED = 0; private static int sImageWidth; private static Context sContext; private static ImageLoader sInstance; private static ExecutorService sThreadPool = Executors.newCachedThreadPool(); private LruCache<String, Bitmap> mLruCache; private DiskLruCache mDiskCache; /** * 初始化ImageLoader * @param context * @param width 预想的图片宽度 */ public static void init(Context context, int width) { sContext = context; sImageWidth = width; } /** * 单例 获取ImageLoader的实例 * @return */ public synchronized static ImageLoader getInstance() { if(null == sInstance) { sInstance = new ImageLoader(); } return sInstance; } private ImageLoader() { // begin 初始化LruCache int maxMemory = (int) Runtime.getRuntime().maxMemory(); int maxSize = maxMemory / 8; mLruCache = new LruCache<String, Bitmap>(maxSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }; // end // begin 初始化DiskLruCache try { mDiskCache = DiskLruCache.open(getCacheDir(), getAppVersion(), 1, 10*1024*1024); } catch (Exception e) { e.printStackTrace(); } // end } public void load(final String url, final OnImageListener l) { final Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case SUCCESS: l.onResult((Bitmap) msg.obj); break; case FAILED: l.onResult(null); break; } } }; // 从memory中获取 Bitmap bmp = getFromLru(url); if(null != bmp) { System.out.println("--getFromLru"); Message msg = handler.obtainMessage(SUCCESS, bmp); msg.sendToTarget(); return; } // 如果memory中没有 // 从disk中获取 bmp = getFromDisk(url); if(null != bmp) { System.out.println("---getFromDisk"); Message msg = handler.obtainMessage(SUCCESS, bmp); msg.sendToTarget(); return; } // 如果disk中没有, 则从网络中下载 sThreadPool.execute(new Runnable() { @Override public void run() { System.out.println("----getFromNet"); DefaultHttpClient client = null; try { client = new DefaultHttpClient(); HttpGet get = new HttpGet(url); HttpResponse response = client.execute(get); if(200 == response.getStatusLine().getStatusCode()) { InputStream in = response.getEntity().getContent(); Bitmap bmp = BitmapFactory.decodeStream(in); bmp = scaleImage(bmp); // 缓存到本地 addToDisk(url, bmp); // 缓存到memory addToLru(url, bmp); Message msg = handler.obtainMessage(SUCCESS, bmp); msg.sendToTarget(); } } catch (Exception e) { e.printStackTrace(); handler.sendEmptyMessage(FAILED); } finally { if(null != client) { client.getConnectionManager().shutdown(); } } } }); } // 缩放图片 private Bitmap scaleImage(Bitmap bmp) { int sample = bmp.getWidth() / sImageWidth; int height = bmp.getHeight() / sample; return ThumbnailUtils.extractThumbnail(bmp, sImageWidth, height); } // 从memory中获取 private Bitmap getFromLru(String url) { return mLruCache.get(Encrypt.md5(url)); } // 添加到内存中 private void addToLru(String url, Bitmap bmp) { if(getFromLru(url) == null) { System.out.println("++addToLru"); mLruCache.put(Encrypt.md5(url), bmp); } } // 从本地缓存获取 private Bitmap getFromDisk(String url) { Bitmap bmp = null; try { DiskLruCache.Snapshot snapshot = mDiskCache.get(Encrypt.md5(url)); InputStream in = snapshot.getInputStream(0); bmp = BitmapFactory.decodeStream(in); addToLru(url, bmp); // 这里可以断言 内存中肯定没有 in.close(); } catch (Exception e) { } return bmp; } // 添加到本地缓存 private void addToDisk(String url, Bitmap bmp) throws Exception { System.out.println("+++addtoDisk"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); bmp.compress(CompressFormat.PNG, 100, baos); byte[] buf = baos.toByteArray(); DiskLruCache.Editor editor = mDiskCache.edit(Encrypt.md5(url)); OutputStream out = editor.newOutputStream(0); out.write(buf); out.flush(); editor.commit(); } // 获取缓存目录 private File getCacheDir() { File dir = null; if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { dir = new File(sContext.getExternalCacheDir().getPath() + File.separator + "images" + File.separator); }else { dir = new File(sContext.getCacheDir().getPath() + File.separator + "images" + File.separator); } return dir; } // 获取软件版本 private int getAppVersion() { PackageInfo pi; try { pi = sContext.getPackageManager().getPackageInfo(sContext.getPackageName(), 0); return pi.versionCode; } catch (NameNotFoundException e) { e.printStackTrace(); } return 0; } public interface OnImageListener { public void onResult(Bitmap bmp); } }
27~32行,获取ImageLoader的实例, 因为ImageLoader是单例的。
构造方法中,36~43行初始化LruCache,47~51行初始化DiskLruCache, DiskLruCache的静态方法open接收4个参数,分别是:缓存的路径,软件的版本号,一个key对应多少文件,最大缓存的大小。
load方法中,71~77行,从memory中尝试获取图片,如果获取到了,则发送消息,并停止执行。81~87行,如果从memory中获取不到,则尝试从本地缓存中获取,如果获取到了,则发送消息,并停止执行。94~110行,从网络下载图片,并添加到本地缓存和memory中。
scaleImage方法,是一个简单的缩放图片功能,使用了ThumbnailUtils.extractThumbnail,如果在低版本中,需要自己去实现。
131~141行,是从memory中获取和添加到memory中。
144~169行,是从本地缓存中获取和添加到本地缓存。
剩下的两个方法是获取缓存路径和软件版本号。
ImageLoader定义好了以后,怎么使用呢? 很简单,调用load方法就行。
首先自定义一个Adapter继承自BaseAdapter,
public class ImageAdapter extends BaseAdapter { private Context mContext; private String[] mData; public ImageAdapter(Context context, String[] data) { mContext = context; mData = data; initImageLoader(); } private void initImageLoader() { WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); ImageLoader.init(mContext, dm.widthPixels / 3); } @Override public int getCount() { return mData.length; } @Override public Object getItem(int position) { return mData[position]; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { final ViewHolder holder; if(null == convertView) { convertView = View.inflate(mContext, R.layout.image_item, null); holder = new ViewHolder(); holder.imageView = (ImageView) convertView.findViewById(R.id.iv); convertView.setTag(R.id.iv, holder); }else { holder = (ViewHolder) convertView.getTag(R.id.iv); } holder.imageView.setImageResource(R.drawable.ic_launcher); ImageLoader imageLoader = ImageLoader.getInstance(); imageLoader.load(mData[position], new ImageLoader.OnImageListener() { @Override public void onResult(Bitmap bmp) { holder.imageView.setImageBitmap(bmp); } }); return convertView; } class ViewHolder { ImageView imageView; } }标准的BaseAdapter,需要注意到是第45行,主要是为了解决图片跳动的问题。
最后看一下效果吧:
图片借用了郭神的, 实在是懒得自己去找了。 效果还不错,挺顺畅的。