Bitmap的高效加載
核心思想:用 BitmapFactory.Options 來(lái)加載合適尺寸的圖片浓镜。通過(guò)BitmapFactory.Options 設(shè)置采樣率來(lái)壓縮圖片盒蟆。
拿一張1024 * 1024 像素的圖片來(lái)說(shuō)叽奥,假定采用ARGB8888格式存儲(chǔ),那么它占有的內(nèi)存為 1024 * 1024 * 4 Bytes,即4MB蝴乔,如果ImageView的尺寸為512 * 512四啰,那么就沒(méi)必要把全尺寸的圖片加載到ImageView中宁玫。BitmapFactory.Options 中有一個(gè)field叫 inSampleSize,即采樣率柑晒,默認(rèn)值是1撬统,如果設(shè)置該值小于1則無(wú)任何效果,如果設(shè)置為大于1的值敦迄,則縮放后圖片占用內(nèi)存為 1 / (inSampleSize ^ 2)恋追。還用剛才的例子,如果把 1024 * 1024的圖片設(shè)置到 512 * 512 的ImageView中罚屋,只需設(shè)置 inSampleSize = 2:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
return BitmapFactory.decodeFromResource(res, resId, options);
怎樣計(jì)算加載圖片占用內(nèi)存的大锌啻选?
圖片占用內(nèi)存的大小取決于3個(gè)變量:
- 寬
- 高
- 存儲(chǔ)格式
寬和高很好理解脾猛,就是圖片在橫向和縱向上的像素?cái)?shù)撕彤。存儲(chǔ)格式?jīng)Q定了每個(gè)像素占用多少內(nèi)存。常見(jiàn)的存儲(chǔ)格式為 ARGB8888猛拴,我們就以它為例羹铅。
ARGB8888的意思是 A(Alpha),R(Red)愉昆,G(Green)职员,B(Blue)四個(gè)通道,每個(gè)通道占用8bit跛溉,一共32bit焊切,也就是4byte
此外還有 RGB565,RGB888等等格式芳室,后面的數(shù)字就代表對(duì)應(yīng)的通道所占的bit數(shù)专肪,把所有數(shù)字加起來(lái)就是1個(gè)像素占有的bit數(shù),也就可以算出byte值堪侯。
怎樣計(jì)算采樣率嚎尤?
采樣率的計(jì)算也需要依賴 BitmapFactory.Options。當(dāng) BitmapFactory.Options的 inJustDecodeBounds 參數(shù)設(shè)置為true時(shí)芽死,BitmapFactory只會(huì)解析圖片的原始寬/高信息坪哄,并不會(huì)去真正加載圖片。有了圖片的寬高和ImageView的寬高,我們就可以計(jì)算出合適的采樣率:
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
// 把 inJustDecodeBounds設(shè)為true,BitmapFactory就不會(huì)把圖片加載到內(nèi)存,只會(huì)去計(jì)算圖片的尺寸
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqHeight) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
Android 中的緩存策略
LruCache
LruCache 是Android提供的緩存類锉矢,實(shí)現(xiàn)LRU算法缠俺,底層用LinkedHashMap來(lái)存儲(chǔ)需要緩存的對(duì)象偿警。這個(gè)類已經(jīng)納入到Android源碼當(dāng)中七嫌,一般被用來(lái)作為內(nèi)存中的緩存使用绍赛。
DiskLruCache
顧名思義腿倚,這個(gè)類實(shí)現(xiàn)的是磁盤(pán)緩存。雖然它得到了Android官方文檔的推薦蚯妇,但目前并不屬于Android SDK的一部分敷燎,所以我們發(fā)現(xiàn)像Glide和OkHttp這些開(kāi)源框架里都用到了這個(gè)類,但都針對(duì)自己的情況作了修改箩言。
ImageLoader的實(shí)現(xiàn)
如果要自己實(shí)現(xiàn)一個(gè)ImageLoader硬贯,那么一般要具備如下功能:
- 圖片的同步加載
- 圖片的異步加載
- 圖片壓縮 (計(jì)算采樣率)
- 內(nèi)存緩存
- 磁盤(pán)緩存
- 網(wǎng)絡(luò)拉取
完整代碼:
package com.anjiawei.httpdemo.bitmap.imageloader;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.StatFs;
import android.support.v4.util.LruCache;
import android.util.Log;
import android.widget.ImageView;
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.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 okhttp3.internal.cache.DiskLruCache;
public class ImageLoader {
private static final String TAG = "ImageLoader";
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 50;
private static final int IO_BUFFER_SIZE = 1024;
private static final int MSG_POST_RESULT = 0;
private static final int TAG_KEY_URL = 0;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAX_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), sThreadFactory);
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private Context mContext;
private boolean mIsDiskLruCacheCreated;
private ImageResizer mImageResizer;
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.mImageView;
String url = (String) imageView.getTag(TAG_KEY_URL);
if (url.equals(result.mUrl)) {
imageView.setImageBitmap(result.mBitmap);
} else {
Log.w(TAG, "set image bitmap, but url has changed, ignored");
}
}
};
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.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();
}
}
mImageResizer = new ImageResizer();
}
public static ImageLoader getInstance(Context context) {
return new ImageLoader(context);
}
/**
* load bitmap from memory cache or disk or network
* This is a synchronized method
*
* @param url
* @param reqWidth
* @param reqHeight
* @return
*/
public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromMemCache, url = " + url);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromDisk, url = " + url);
return bitmap;
}
bitmap = loadBitmapFromHttp(url, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp, url = " + url);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && !mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error, DiskLruCache is not created");
bitmap = downloadBitmapFromUrl(url);
}
return bitmap;
}
/**
*
* @param url
* @param imageView
* @param reqWidth
* @param reqHeight
*/
public void bindBitmap(final String url, final ImageView imageView, final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URL, url);
Bitmap bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, url, bitmap);
mMainHandler.obtainMessage(MSG_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
private Bitmap downloadBitmapFromUrl(String urlString) {
Bitmap bitmap = null;
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);
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;
}
public File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
String cachePath = null;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getPath();
} else {
context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return stats.getBlockSizeLong() * stats.getAvailableBlocksLong();
}
private Bitmap loadBitmapFromMemCache(String url) {
String key = hashKeyFromUrl(url);
return mMemoryCache.get(key);
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(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("cannot visit network from UI thread");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFromUrl(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()) {
Log.w(TAG, "load bitmap from UI Thread, it's not recommended");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
public 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();
}
MyUtils.close(in);
MyUtils.close(out);
}
return false;
}
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
final MessageDigest 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();
}
}