一个利用内存缓存和磁盘缓存图片的例子
public class BitmapCache { public static final String TAG = "debug"; private LruCachemBitmapCache; public BitmapCache() { // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache 使用的缓存值,使用系统分配给应用程序大小的 1/8 int maxSize = (int) (Runtime.getRuntime().maxMemory() / 8); mBitmapCache = new LruCache (maxSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount(); } }; } public void add(String key, Bitmap value) { mBitmapCache.put(key, value); } public void remove(String key) { mBitmapCache.remove(key); } public Bitmap get(String key) { return mBitmapCache.get(key); } public boolean containsKey(String key) { return mBitmapCache.get(key) != null; } public static long toMB(long byteOfSize) { return byteOfSize >> 20; }}
import java.io.File;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import android.util.Log;public class BitmapDiskCache { private static final String TAG = "debug"; private static final int DISK_CACHE_INDEX = 0; private static final int DISK_CACHE_COUNT = 1; private static final int VERSION = 1; private DiskLruCache mDiskLruCache; private DiskLruCache.Editor mEditor; public BitmapDiskCache(File diskCacheDir, long nDiskCacheSize) { if (!diskCacheDir.exists()) { diskCacheDir.mkdirs(); } try { mDiskLruCache = DiskLruCache.open(diskCacheDir, VERSION, DISK_CACHE_COUNT, nDiskCacheSize); } catch (IOException e) { Log.e(TAG, "", e); } } public InputStream getInputStream(String key) { if (null != mDiskLruCache) { try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (null != snapshot) { return snapshot.getInputStream(DISK_CACHE_INDEX); } } catch (IOException e) { Log.e(TAG, "", e); } } return null; } public OutputStream beginEdit(String key) throws IOException { if (null != mDiskLruCache) { mEditor = mDiskLruCache.edit(key); if (null != mEditor) { return mEditor.newOutputStream(DISK_CACHE_INDEX); } } return null; } public void endEdit(Boolean isSuccess) throws IOException { if (null != mEditor) { if (isSuccess) { mEditor.commit(); } else { mEditor.abort(); } mEditor = null; } } public void flush() { if (null != mDiskLruCache) { try { mDiskLruCache.flush(); } catch (IOException e) { Log.e(TAG, "", e); } } }}
import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.Closeable;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.lang.ref.SoftReference;import java.net.HttpURLConnection;import java.net.URL;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.util.concurrent.Executor;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadFactory;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;import android.content.Context;import android.graphics.Bitmap;import android.os.Build;import android.os.Build.VERSION_CODES;import android.os.Environment;import android.os.Handler;import android.os.Looper;import android.os.Message;import android.os.StatFs;import android.util.Log;import android.widget.ImageView;public class BitmapLoader { private static final String TAG = "debug"; private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; private static final int MAX_MUM_POOL_SIZE = CPU_COUNT * 2 + 1; private static final long KEEP_ALIVE_TIME = 10; private static final int IO_BUFFER_SIZE = 1024 * 8; private static final int MESSAGE_POST_RESULT = 1000; private BitmapCache mBitmapCache; private BitmapDiskCache mBitmapDiskCache; private static ThreadFactory sThreadFactory = new ThreadFactory() { private AtomicInteger mCounter = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { return new Thread(r, "BitmapLoader#" + mCounter.getAndIncrement()); } }; private static Executor sThreadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_MUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue(), sThreadFactory); class LoaderResult { SoftReference mImageViewRef; Bitmap mBitmap; String mUri; public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) { mImageViewRef = new SoftReference (imageView); mUri = uri; mBitmap = bitmap; } } private Handler mMainHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { LoaderResult result = (LoaderResult) msg.obj; ImageView imageView = result.mImageViewRef.get(); if (null != imageView) { imageView.setImageBitmap(result.mBitmap); String uri = (String) imageView.getTag(); if (uri.equals(result.mUri)) { imageView.setImageBitmap(result.mBitmap); } else { Log.w(TAG, "set image bitmap, but url has changed, ignored!"); } } else { Log.w(TAG, "set image bitmap, ImageView released, ignored!"); } }; }; public BitmapLoader(Context context) { mBitmapCache = new BitmapCache(); File diskCacheDir = new File(getDiskCacheDir(context, "bmp")); if (!diskCacheDir.exists()) { diskCacheDir.mkdirs(); } if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) { mBitmapDiskCache = new BitmapDiskCache(diskCacheDir, DISK_CACHE_SIZE); } } public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) { String key = hashKeyFormUrl(uri); Bitmap bitmap = mBitmapCache.get(key); if (null != bitmap) { Log.d(TAG, "load from memory cache on bind"); imageView.setImageBitmap(bitmap); return; } imageView.setTag(uri); Runnable loadBitmapTask = new Runnable() { @Override public void run() { Log.d(TAG, Thread.currentThread().getName() + " run"); Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight); if (bitmap != null) { LoaderResult result = new LoaderResult(imageView, uri, bitmap); mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget(); } } }; sThreadPoolExecutor.execute(loadBitmapTask); } public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) { String key = hashKeyFormUrl(uri); Bitmap bitmap = mBitmapCache.get(key); if (null != bitmap) { Log.d(TAG, "load from memory cache"); return bitmap; } try { bitmap = loadBitmapFromDiskCache(key, reqWidth, reqHeight); if (null != bitmap) { Log.d(TAG, "load from disk cache"); return bitmap; } bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight); if (null != bitmap) { Log.d(TAG, "load from http"); } } catch (IOException e) { Log.e(TAG, "", e); } if (null == bitmap) { bitmap = downloadBitmapFromUrl(uri, reqWidth, reqHeight); if (null != bitmap) { mBitmapCache.add(key, bitmap); Log.d(TAG, "load from url"); } } return bitmap; } private Bitmap loadBitmapFromDiskCache(String key, int reqWidth, int reqHeight) throws IOException { if (Looper.myLooper() == Looper.getMainLooper()) { Log.w(TAG, "load bitmap from UI Thread, it's not recommended!"); } if (mBitmapDiskCache == null) { return null; } Bitmap bitmap = null; InputStream is = mBitmapDiskCache.getInputStream(key); if (null != is) { bitmap = BitmapTools.decodeSampledBitmapFromInputStream(is, reqWidth, reqHeight); if (bitmap != null) { mBitmapCache.add(key, bitmap); } } return bitmap; } 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 (mBitmapDiskCache == null) { return null; } String key = hashKeyFormUrl(url); OutputStream os = mBitmapDiskCache.beginEdit(key); if (null != os) { if (downloadUrlToStream(url, os)) { mBitmapDiskCache.endEdit(true); } else { mBitmapDiskCache.endEdit(false); } mBitmapDiskCache.flush(); } return loadBitmapFromDiskCache(key, reqWidth, reqHeight); } private Bitmap downloadBitmapFromUrl(String urlString, int reqWidth, int reqHeight) { HttpURLConnection urlConnection = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); return BitmapTools.decodeSampledBitmapFromInputStream(in, reqWidth, reqHeight); } catch (IOException e) { Log.e(TAG, "Error in downloadBitmap: ", e); } finally { if (urlConnection != null) { urlConnection.disconnect(); } close(in); } return null; } 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(), 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(); } close(out); close(in); } return false; } private void close(Closeable obj) { if (null != obj) { try { obj.close(); } catch (IOException e) { Log.e(TAG, "", e); } } } private String hashKeyFormUrl(String url) { String cacheKey; try { final MessageDigest md5Digest = MessageDigest.getInstance("MD5"); md5Digest.update(url.getBytes()); cacheKey = bytesToHexString(md5Digest.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(); } private String getDiskCacheDir(Context context, String uniqueName) { boolean isAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); final String cachePath; if (isAvailable) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return (cachePath + File.separator + uniqueName); } private long getUsableSpace(File path) { if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) { return path.getUsableSpace(); } final StatFs stats = new StatFs(path.getPath()); return stats.getBlockSizeLong() * stats.getAvailableBlocksLong(); }}
/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package com.bitmap;import java.io.BufferedInputStream;import java.io.BufferedWriter;import java.io.Closeable;import java.io.EOFException;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.FileWriter;import java.io.FilterOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.io.OutputStreamWriter;import java.io.Reader;import java.io.StringWriter;import java.io.Writer;import java.lang.reflect.Array;import java.nio.charset.Charset;import java.util.ArrayList;import java.util.Arrays;import java.util.Iterator;import java.util.LinkedHashMap;import java.util.Map;import java.util.concurrent.Callable;import java.util.concurrent.ExecutorService;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;/** ****************************************************************************** * Taken from the JB source code, can be found in: * libcore/luni/src/main/java/libcore/io/DiskLruCache.java * or direct link: * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java ****************************************************************************** * * A cache that uses a bounded amount of space on a filesystem. Each cache * entry has a string key and a fixed number of values. Values are byte * sequences, accessible as streams or files. Each value must be between { @code * 0} and { @code Integer.MAX_VALUE} bytes in length. * *The cache stores its data in a directory on the filesystem. This * directory must be exclusive to the cache; the cache may delete or overwrite * files from its directory. It is an error for multiple processes to use the * same cache directory at the same time. * *
This cache limits the number of bytes that it will store on the * filesystem. When the number of stored bytes exceeds the limit, the cache will * remove entries in the background until the limit is satisfied. The limit is * not strict: the cache may temporarily exceed it while waiting for files to be * deleted. The limit does not include filesystem overhead or the cache * journal so space-sensitive applications should set a conservative limit. * *
Clients call {
@link #edit} to create or update the values of an entry. An * entry may have only one editor at one time; if a value is not available to be * edited then { @link #edit} will return null. *
- *
- When an entry is being created it is necessary to * supply a full set of values; the empty value should be used as a * placeholder if necessary. *
- When an entry is being edited, it is not necessary * to supply data for every value; values default to their previous * value. *
Clients call {
@link #get} to read a snapshot of an entry. The read will * observe the value at the time that { @link #get} was called. Updates and * removals after the call do not impact ongoing reads. * *This class is tolerant of some I/O errors. If files are missing from the * filesystem, the corresponding entries will be dropped from the cache. If * an error occurs while writing a cache value, the edit will fail silently. * Callers should handle other problems by catching {
@code IOException} and * responding appropriately. */public final class DiskLruCache implements Closeable { static final String JOURNAL_FILE = "journal"; static final String JOURNAL_FILE_TMP = "journal.tmp"; static final String MAGIC = "libcore.io.DiskLruCache"; static final String VERSION_1 = "1"; static final long ANY_SEQUENCE_NUMBER = -1; private static final String CLEAN = "CLEAN"; private static final String DIRTY = "DIRTY"; private static final String REMOVE = "REMOVE"; private static final String READ = "READ"; private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final int IO_BUFFER_SIZE = 8 * 1024; /* * This cache uses a journal file named "journal". A typical journal file * looks like this: * libcore.io.DiskLruCache * 1 * 100 * 2 * * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 * DIRTY 335c4c6028171cfddfbaae1a9c313c52 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 * REMOVE 335c4c6028171cfddfbaae1a9c313c52 * DIRTY 1ab96a171faeeee38496d8b330771a7a * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 * READ 335c4c6028171cfddfbaae1a9c313c52 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 * * The first five lines of the journal form its header. They are the * constant string "libcore.io.DiskLruCache", the disk cache's version, * the application's version, the value count, and a blank line. * * Each of the subsequent lines in the file is a record of the state of a * cache entry. Each line contains space-separated values: a state, a key, * and optional state-specific values. * o DIRTY lines track that an entry is actively being created or updated. * Every successful DIRTY action should be followed by a CLEAN or REMOVE * action. DIRTY lines without a matching CLEAN or REMOVE indicate that * temporary files may need to be deleted. * o CLEAN lines track a cache entry that has been successfully published * and may be read. A publish line is followed by the lengths of each of * its values. * o READ lines track accesses for LRU. * o REMOVE lines track entries that have been deleted. * * The journal file is appended to as cache operations occur. The journal may * occasionally be compacted by dropping redundant lines. A temporary file named * "journal.tmp" will be used during compaction; that file should be deleted if * it exists when the cache is opened. */ private final File directory; private final File journalFile; private final File journalFileTmp; private final int appVersion; private final long maxSize; private final int valueCount; private long size = 0; private Writer journalWriter; private final LinkedHashMap
import java.io.InputStream;import android.content.res.Resources;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.util.Log;public class BitmapTools { private static final String TAG = "debug"; public static class Size { public Size(int width, int height) { this.width = width; this.height = height; } public void resize(int width, int height) { this.width = width; this.height = height; } public int getWidth() { return width; } public int getHeight() { return height; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Size size = (Size) o; if (width != size.width) return false; return height == size.height; } @Override public int hashCode() { int result = width; result = 31 * result + height; return result; } @Override public String toString() { return "Size{" + "width=" + width + ", height=" + height + '}'; } private int width; private int height; } public static Bitmap decodeSampledBitmapFromPath(String pathName, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; BitmapFactory.decodeFile(pathName, opts); // Calculate inSampleSize opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); // Decode bitmap with inSampleSize set opts.inJustDecodeBounds = false; return BitmapFactory.decodeFile(pathName, opts); } public static Bitmap decodeSampledBitmapFromData(byte[] data, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, opts); // Calculate inSampleSize opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); // Decode bitmap with inSampleSize set opts.inJustDecodeBounds = false; return BitmapFactory.decodeByteArray(data, 0, data.length, opts); } public static Bitmap decodeSampledBitmapFromResource(Resources res, int id, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, id, opts); // Calculate inSampleSize opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); // Decode bitmap with inSampleSize set opts.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, id, opts); } public static Bitmap decodeSampledBitmapFromInputStream(InputStream is, int reqWidth, int reqHeight) { Bitmap bitmap = BitmapFactory.decodeStream(is); if (bitmap == null) return null; // Log.d(TAG, "req size: " + reqWidth + ", " + reqHeight); // Log.d(TAG, "bitmap size: " + bitmap.getWidth() + ", " + bitmap.getHeight()); if (bitmap.getWidth() > reqWidth || bitmap.getHeight() > reqHeight) { Size srcSize = new Size(bitmap.getWidth(), bitmap.getHeight()); Size reqSize = new Size(reqWidth, reqHeight); Size newSize = calculateNewSize(srcSize, reqSize); bitmap = Bitmap.createScaledBitmap(bitmap, newSize.getWidth(), newSize.getHeight(), false); } return bitmap; } public static Size calculateNewSize(Size srcSize, Size reqSize) { int newWidth; int newHeight; if (srcSize.getWidth() > srcSize.getHeight()) { newWidth = reqSize.getWidth(); newHeight = newWidth * srcSize.getHeight() / srcSize.getWidth(); } else { newHeight = reqSize.getHeight(); newWidth = newHeight * srcSize.getWidth() / srcSize.getHeight(); } return new Size(newWidth, newHeight); } public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final 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; } } Log.d(TAG, "inSampleSize=" + inSampleSize); return inSampleSize; }}