自己写Android图片缓存框架之二级disk缓存

       上一节中已经运用Lru算法实现了内存缓存,在从桌面回到前台时可以快速的从内存中进行加载图片,但是如果应用被系统回收或人为的主动清除这样还是会从网络加载,所以我们不仅需要缓存在内存中,还要在磁盘中进行缓存,这样如果内存没有就从磁盘中进行读取数据。

      这里我们使用google提供的DiskLruCache来实现disk缓存,由于源码过长就不贴了,所有的代码包括图片加载的demo已经上传到github上。我们将DiskLruCache直接拷贝到项目代码中,并将原来的包名libcore.io更改为我们的包名。

      DiskLruCache提供了一系列方法,先依次分析一下

    private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
        this.directory = directory;
        this.appVersion = appVersion;
        this.journalFile = new File(directory, JOURNAL_FILE);
        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
        this.valueCount = valueCount;
        this.maxSize = maxSize;
    }
可以看到将DiskLruCache设为了private,这样我们就不能直接new出对象了,而是直接提供了一个open方法来返回对象

    /**
     * 根据地址打开缓存空间,如果不存在就创建一个
     *
     * @param directory 数据缓存地址
     * @param appVersion 当前app版本
     * @param valueCount 一个key值对应多少个缓存文件 一般传1
     * @param maxSize 最多可以缓存多少字节的数据 一般为10M
     * @throws java.io.IOException if reading or writing the cache directory fails
     */
    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");
        }
        //调用构造函数得到一个cache对象
        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) {
                cache.delete();
            }
        }

        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }
DiskLruCache提供了一个flush方法, 为了防止频繁的写数据,建议在Activity的onPause时调用一次flush()方法,除此之外我们需要对url进行MD5加密,防止url出现字符不合法的情况,MD5算法在大数据的hash碰撞上性能也比较良好,编码后每个字符都在0到F之间,符合要求。

	/**
	 * 使用MD5算法对传入的key进行加密,以免出现url不合法
	 * @param key
	 * @return
	 */
	public String hashKeyForDisk(String key){
		String cacheKey;
		try {
			MessageDigest digest = MessageDigest.getInstance("MD5");
			digest.update(key.getBytes());
			cacheKey = bytesToHexString(digest.digest());
		} catch (NoSuchAlgorithmException e) {
			cacheKey = String.valueOf(key.hashCode());
			e.printStackTrace();
		}
		return cacheKey;
	}
	
	private String bytesToHexString(byte[] bytes){
		StringBuilder builder = new StringBuilder();
		for (int i = 0; i < bytes.length; i++) {
			String hex = Integer.toHexString(0xFF & bytes[i]);
			if (hex.length() == 1) {
				builder.append("0");
			}
			builder.append(hex);
		}
		return builder.toString();
	}


磁盘的缓存路径一般是在内存卡中,但是现在的一些手机可能并没有内存卡,所以我们需要动态的加载存储路径,

	/**
	 * 根据传入的unique返回硬盘缓存地址
	 * 
	 * @param context
	 * @param uniqueName
	 * @return
	 */
	public File getDiskCacheDir(Context context, String uniqueName) {
		String cachePath;
		if (Environment.MEDIA_MOUNTED.equals(Environment
				.getExternalStorageState())
				|| !Environment.isExternalStorageRemovable()) {
			//当有SD卡时
			cachePath = context.getExternalCacheDir().getPath();
		} else {
			//当没有SD卡或SD卡被移除
			cachePath = context.getCacheDir().getPath();
		}

		return new File(cachePath + File.separator + uniqueName);
	}
另外有些图片可能会过大比如远超要显示的大小,这样不仅因为图片过大在下载时比较慢,而且容易造成OOM,所以我们需要对图片进行压缩,让其显示匹配手机显示的大小

	/**
	 * 计算目标图片缩放比例
	 * @param options
	 * @param reqWidth
	 * @param reqHeight
	 * @return
	 */
	public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
		int height = options.outHeight;
		int width = options.outWidth;
		int inSampleSize = 1;
		
		if (height > reqHeight || width > reqWidth) {
			//计算高度和宽度对目标高宽的比例
			int heightRadio = Math.round(height/reqHeight);
			int widthRadio = Math.round(width/reqWidth);
			//选择高宽中较小的一个作为压缩比例,保证图片比目标尺寸大
			inSampleSize = heightRadio < widthRadio ? heightRadio : widthRadio;
		}
		return inSampleSize;
	}
	
	public static Bitmap decodeSampledBitmap(FileDescriptor descriptor,int reqWidth,int reqHeight){
		
		BitmapFactory.Options options = new BitmapFactory.Options();
		//加载时将injustdecodebounds设置为true,获取图片大小
		options.inJustDecodeBounds = true;
		BitmapFactory.decodeFileDescriptor(descriptor, null, options);
		//计算压缩比例
		options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 
		options.inJustDecodeBounds = false;
		
		return BitmapFactory.decodeFileDescriptor(descriptor, null, options);
	}
在将图片写入disk缓存中时,我们需要获取一个DiskLruCache.Editor的editor来将流写入缓存中

	@Override
	protected Bitmap doInBackground(String... params) {
		imageUrl = params[0];
		FileDescriptor descriptor = null;
		FileInputStream inputStream = null;
		
		Snapshot snapshot = null;
		
		try {
			//生成图片对应的key
			String key = imageLoader.hashKeyForDisk(imageUrl);
			snapshot = imageLoader.diskCache.get(key);
			
			if (snapshot == null) {
				//在磁盘中没有找到缓存文件,去网络下载,并写入到缓存中
				DiskLruCache.Editor editor = imageLoader.diskCache.edit(key);
				if (editor != null) {
					OutputStream outputStream = editor.newOutputStream(0); //因为是1对1,直接设为0,就是取第一个
					if (downloadUrlToStream(imageUrl, outputStream)) {
						editor.commit();
					}else {
						editor.abort();  //取消本次写入操作
					}
				}
				snapshot = imageLoader.diskCache.get(key);
			}
			if (snapshot != null) {
				inputStream = (FileInputStream) snapshot.getInputStream(0);
				descriptor = inputStream.getFD();
			}
			//将缓存数据解析成bitmap对象
			Bitmap bitmap = null;
			if (descriptor != null) {
				bitmap = BitmapUtil.decodeSampledBitmap(descriptor, reqWidth, reqHeight);
			}
			if (bitmap != null) {
				//将bitmap加入到内存缓存中
				imageLoader.addBitmapToMemoryCache(params[0], bitmap);
			}
			return bitmap;
		} catch (IOException e) {
			e.printStackTrace();
		}
		// 通过url下载图片
		Bitmap bitmap = downloadBitmap(params[0]);
		if (bitmap != null) {
			// 将图片放入内存缓存中
			imageLoader.addBitmapToMemoryCache(params[0], bitmap);
		}
		return bitmap;
	}
	/**
	 * 根据url从网上获取流,并写入到output流中
	 * @param imageUrl
	 * @param outputStream
	 * @return
	 */
	private boolean downloadUrlToStream(String imageUrl,OutputStream outputStream){
		HttpURLConnection conn = null;
		BufferedOutputStream out = null;
		BufferedInputStream  in = null;
		
		try {
			URL url = new URL(imageUrl);
			conn = (HttpURLConnection) url.openConnection();
			in = new BufferedInputStream(conn.getInputStream(), 8*1024);
			out = new BufferedOutputStream(outputStream, 8*1024);
			
			int b;
			while((b = in.read())!= -1){
				out.write(b);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			if (conn != null) {
				conn.disconnect();
			}
			try {
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				in.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return false;
	}
大概步骤就是这样,中间一些细微的非核心代码没有放上来,可以直接去github上进行下载,包含示例demo

地址https://github.com/sheepm/Cache




你可能感兴趣的:(教程系列)