最近一直在學習男神+大神任玉剛老師的Android開發藝術探索,我個人非常推薦這本書,寫的真tm好,不好意思,有點激動。大家有空可以去看看。看完總覺得要寫點什麼吧,就將其中Bitmap載入和Cache的內容分享給還沒有來得及看過的朋友們。
快取策略在Android中有著廣泛的。。。算了,先輕鬆一下,來張美女鎮樓
老司機要發車了,來不及解釋了,大家快上車啊。
言歸正傳(畫風一轉)快取策略在Android中有著廣泛的使用場景,尤其在圖片載入這個場景下,快取策略變得更為重要。今天就帶大家自己動手設計圖片快取工具。
提到快取,不得不提目前常用的一種快取演算法-LRU(Least Recently Used),LRU是近期最少使用演算法。採用這種演算法思想的快取有兩種:LruCache和DiskLruCache,LruCache用於實現記憶體快取,而DiskLruCache則充當了儲存裝置快取,通過兩者完美結合,就可以輕鬆實現我們的ImageLoader。
下面我們就正式開始了。。。
1.首先我們來看下兩個核心的東西:LruCache和DiskLruCache。
1.1LruCache
LruCache的實現比較簡單,大家可以參考它的原始碼。LruCache主要用來實現記憶體快取,我們拿圖片快取來舉例子,反手就是一段程式碼:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
複製程式碼
可以看出,我們只需要提供快取的總容量大小並重寫sizeOf方法即可。
除了LruCache的建立以外,還有快取的獲取和新增,這也很簡單,從LruCache中獲取一個快取物件,如下所示。
mMemoryCache.get(key)
複製程式碼
向LruCache中新增一個快取物件,如下所示。
mMemoryCache.put(key,bitmap)
複製程式碼
從Android3.1開始,LruCache就已經是Android原始碼的一部分了。
1.2DiskLruCache
DiskCahe用於實現儲存裝置快取,已經得到了Android官方的推薦,但不屬於SDK的一部分,我們要匯入引用一下:
compile 'com.jakewharton:disklrucache:2.0.2'
1.2.1先來看下DiskLruCache的建立
DiskLruCache提供open方法用於建立自身,如下所示。
pulic static DiskLruCache open(File directory,int appVersion,int valueCount,long maxSize)
複製程式碼
這裡有四個引數,稍微解釋下,有興趣的朋友,下去可以深入瞭解下:
directory:磁碟快取的儲存路徑。
appVersion:版本號,一般設為1即可。
valueCount:單個節點所對應的資料的個數,一般也設為1即可。
maxSize:快取的總大小。
整個建立過程,反手又是一段程式碼:
private static final int DISK_CACHE_SIZE = 50*1024*1024;
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
複製程式碼
1.2.2DiskLruCache的快取新增
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor!=null){
OutputStream output Stream = editor.newOutputStream(DISK_CACHE_INDEX);
}
複製程式碼
這裡的hashKeyFormUrl()方法主要是將url轉成key,之所以這麼做,是因為url中很可能有特殊字元,會影響url在Android中的直接使用。
有了檔案輸出流,接下來就是講網路下載的圖片時的檔案流寫入到檔案系統上了:
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
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 (IOException e) {
e.printStackTrace();
}finally {
if(urlConnection!=null){
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
複製程式碼
還有不要忘記了,editor要提交寫入操作。
editor.commit();
複製程式碼
1.2.3DiskLruCache快取的查詢
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight)
throws IOException{
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("load bitmap from UI Thread,it's not recommended");
}
if(mDiskLruCache ==null){
return null;
}
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.decodeSampleFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
if(bitmap !=null){
addBitmapToMemoryCache(key,bitmap);
}
}
return bitmap;
}
複製程式碼
程式碼的意思應該好理解,將url轉成key,通過get方法得到一個Snapshot物件,通過這個物件即可得到快取的輸入流,有了輸入流,還怕拿不到bitmap物件嗎。
好了,介紹完兩個核心的知識點,喝口雪碧壓壓驚。
接下來就是重頭戲了,就是ImageLoader的實現。
2.ImageLoader的實現
要實現整個完整的ImageLoader功能,要怎麼做呢?先捋下思想,應該具備以下幾個功能:
-圖片的同步載入
-圖片的非同步載入
-圖片壓縮
-記憶體快取
-磁碟快取
-網路拉取
接下來,我們一步一步的實現。
2.1圖片壓縮功能的實現
將壓縮圖片功能抽出來單獨形成一個類,就叫ImageResizer吧,看下實現:
public class ImageResizer {
private static final String TAG = "ImageResizer";
public Bitmap decodeSampleFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res,resId,options);
}
public Bitmap decodeSampleFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
if (width < reqWidth || height < reqHeight) {
return inSampleSize;
}
inSampleSize *= 2;
while (width /inSampleSize>=reqWidth&&height/inSampleSize>=reqHeight){
inSampleSize *=2;
}
Log.e(TAG, "inSampleSize: "+inSampleSize);
return inSampleSize;
}
}
複製程式碼
2.2記憶體快取和磁碟快取的實現
之前介紹過了,這裡放下具體實現:
初始化:
public ImageLoader(Context context) {
mImageResizer = new ImageResizer();
mContext = context;
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = 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();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製程式碼
記憶體快取新增和獲取
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
Log.e(TAG, "addBitmapToMemoryCache: ");
if (getBitmapFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
複製程式碼
磁碟快取新增和獲取
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);
}
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight)
throws IOException{
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("load bitmap from UI Thread,it's not recommended");
}
if(mDiskLruCache ==null){
return null;
}
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.decodeSampleFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
if(bitmap !=null){
addBitmapToMemoryCache(key,bitmap);
}
}
return bitmap;
}
複製程式碼
2.3同步載入和非同步載入介面的設計
看下同步載入:
public Bitmap loadBitmap(String uri,int reqWidth,int reqHeight){
Bitmap bitmap = loadBitmapFromMemCache(uri);
if(bitmap!=null){
Log.e(TAG, "loadBitmapFromMemCache,url "+uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri,reqWidth,reqHeight);
if(bitmap!=null){
Log.e(TAG, "loadBitmapFromDiskCache,url "+uri );
return bitmap;
}
bitmap = loadBitmapFromHttp(uri,reqWidth,reqHeight);
Log.e(TAG, "loadBitmapFromHttp,url "+uri);
} catch (IOException e) {
e.printStackTrace();
}
if(bitmap==null&&!mIsDiskLruCacheCreated){
Log.e(TAG, "encounter error,DiskLruCache is not created.");
bitmap = downLoadBitmapFromUrl(uri);
}
return bitmap;
}
複製程式碼
再看下非同步載入
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);
Message message = mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result);
message.sendToTarget();
// sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
複製程式碼
仔細的同學發現了,這裡面好像用到了執行緒池和Handler,關於執行緒池的內容這裡就不扯了,大家自己去度娘。
這裡直接把擼好的程式碼放上來:
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.d(TAG, "set image bitmap,but url has changed,ignored!");
}
}
};
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r,"ImageLoader#"+mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS
,new LinkedBlockingDeque<Runnable>(),sThreadFactory);
複製程式碼
哇哈哈哈哈哈哈哈哈。。。。。
終於要接近尾聲了,提取的不好,大家多見諒(有興趣的同學一定要去看看任玉剛老師的書)。下面還是給下ImageLoader的完整程式碼。
public class ImageLoader {
private static final String TAG = "ImageLoader";
private Context mContext;
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private Bitmap bitmap;
private static final int DISK_CACHE_SIZE = 50*1024*1024;
private boolean mIsDiskLruCacheCreated;
private static final int DISK_CACHE_INDEX = 0;
private ImageResizer mImageResizer;
private String cachePath;
private static final int IO_BUFFER_SIZE = 8*1024;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final int CORE_POOL_SIZE = CPU_COUNT+1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT*2+1;
private static final long KEEP_ALIVE = 10L;
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.d(TAG, "set image bitmap,but url has changed,ignored!");
}
}
};
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r,"ImageLoader#"+mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS
,new LinkedBlockingDeque<Runnable>(),sThreadFactory);
private static final int MESSAGE_POST_RESULT = 1;
public ImageLoader(Context context) {
mImageResizer = new ImageResizer();
mContext = context;
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = 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();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if(Build.VERSION.SDK_INT> Build.VERSION_CODES.GINGERBREAD){
return path.getUsableSpace();
}
StatFs statFs = new StatFs(path.getPath());
return statFs.getBlockSize()*statFs.getAvailableBlocks();
}
private File getDiskCacheDir(Context mContext, String uniqueName) {
boolean externalStorageAvailable = Environment
.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
if(externalStorageAvailable){
cachePath = mContext.getExternalCacheDir().getPath();
}else {
cachePath = mContext.getCacheDir().getPath();
}
return new File(cachePath+File.separator+uniqueName);
}
/**
* 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.e(TAG, "loadBitmapFromMemCache,url "+uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri,reqWidth,reqHeight);
if(bitmap!=null){
Log.e(TAG, "loadBitmapFromDiskCache,url "+uri );
return bitmap;
}
bitmap = loadBitmapFromHttp(uri,reqWidth,reqHeight);
Log.e(TAG, "loadBitmapFromHttp,url "+uri);
} catch (IOException e) {
e.printStackTrace();
}
if(bitmap==null&&!mIsDiskLruCacheCreated){
Log.e(TAG, "encounter error,DiskLruCache is not created.");
bitmap = downLoadBitmapFromUrl(uri);
}
return bitmap;
}
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);
Message message = mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result);
message.sendToTarget();
// sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
private Bitmap downLoadBitmapFromUrl(String uri) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try {
URL url = new URL(uri);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (IOException e) {
Log.e(TAG, "Error in downloadBitmap:"+e);
}finally {
if(urlConnection!=null){
urlConnection.disconnect();
}
MyUtils.close(in);
}
return bitmap;
}
private Bitmap loadBitmapFromMemCache(String url) {
String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemoryCache(key);
return bitmap;
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
Log.e(TAG, "addBitmapToMemoryCache: ");
if (getBitmapFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
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);
}
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight)
throws IOException{
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("load bitmap from UI Thread,it's not recommended");
}
if(mDiskLruCache ==null){
return null;
}
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.decodeSampleFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
if(bitmap !=null){
addBitmapToMemoryCache(key,bitmap);
}
}
return bitmap;
}
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
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 (IOException e) {
e.printStackTrace();
}finally {
if(urlConnection!=null){
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
private String hashKeyFormUrl(String url) {
String cacheKey;
MessageDigest mDigest;
try {
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();
}
}
複製程式碼
自己模仿著寫了個專案分享到github上了。
Github地址