所謂三級緩存指的是內(nèi)存、磁盤緩存、網(wǎng)絡(luò)加載 烫扼。bitmap獲取存取邏輯大致如下:
1.獲取 url 哀澈,請求網(wǎng)絡(luò)加載牌借。
2.把加載成功的流存入到磁盤(DiskLruCache)
3.流對象轉(zhuǎn)成bitmap對象,并壓縮存入到內(nèi)存中(LruCache)
4.設(shè)置壓縮后的bitmap對象給ImageView
此時圖片已成功顯示并實現(xiàn)存儲割按,再次加載時可依次從內(nèi)存膨报、磁盤中取出bitmap,當(dāng)都為空去請求網(wǎng)絡(luò)并存儲,以此重復(fù)哲虾。
其中涉及到LruCache和DiskLruCache(LRU算法)丙躏,線程池使用,圖片壓縮束凑,Handler解決異步問題晒旅,通過setTag來標(biāo)識ImageView。
關(guān)于:
LruCache
LRU:(Least Recently Used)是近期最少使用算法汪诉,內(nèi)部采用一個LinkedHashMap以強(qiáng)引用的方式存儲外界的緩存對象废恋,核心思想是當(dāng)緩存滿時優(yōu)先淘汰近期最少使用的緩存對象(JakeWharton/DiskLruCache* 下載*)。
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.getWidth() / 1024;
}
};
只需要提供緩存的總?cè)萘看笮〔⒅貙憇izeOf方法扒寄,sizeOf方法的作用是計算緩存對象的大小鱼鼓。特殊情況還需重寫entryRemoved方法,移除舊緩存時會調(diào)用此方法该编,因此可以其中做些資源回收工作(如需要)迄本。
ThreadPoolExecutor
線程池:包括 corePoolSize 核心線程數(shù)、maximumPoolSize 最大線程數(shù)课竣、keepAliveTime 非核心線閑置時的超時時長 嘉赎、unit 時長單位、workQueue 任務(wù)隊列于樟、threadFactory 線程工廠公条,其中線程工廠為線程池提供創(chuàng)建新線程的功能,只有 Thread new Thread(Runnabler)方法迂曲。
執(zhí)行任務(wù)時大致遵循如下規(guī)則:
1)如果線程中的線程數(shù)量 未達(dá)到 核心線程的數(shù)量 靶橱,那么會直接啟動一個核心線程來執(zhí)行任務(wù)。
2)如果 線程中的線程數(shù)量 已經(jīng)達(dá)到或者超過核心線程的數(shù)量 ,那么任務(wù)會被插入到任務(wù)隊列中排除等待執(zhí)行关霸。
3)如果 在步驟2中無法將任務(wù)插入到任務(wù)隊列传黄,這往往是任務(wù)隊列 已滿,這個時候 如果線程數(shù)量未達(dá)到線程池規(guī)定的最大值 谒拴,那么會立刻啟動一個非核心線程來執(zhí)行任務(wù)尝江。
4)如果步驟3中線程數(shù)量已經(jīng)達(dá)到 線程池規(guī)定的最大值 ,那么就拒絕執(zhí)行此任務(wù)英上,
ThreadPoolExecutor 會調(diào)用 RejectedExecutionHandler 的 rejectedExecution 方法來通知調(diào)用者炭序。
線程池的優(yōu)點:
1)重用線程池中的線程,避免因為線程的創(chuàng)建和銷毀所帶來的性能 開銷苍日。
2)能有效控制 線程池的最大并發(fā)數(shù)據(jù)惭聂,避免大量的線程之前 因互相搶占系統(tǒng)資源而導(dǎo)致阻塞的現(xiàn)象。
3)能夠?qū)€程進(jìn)行簡單的管理相恃,并提供定時執(zhí)行以及指定間隔循環(huán)執(zhí)行等功能辜纲。
其實Android封裝了四類線程池,其中FixedThreadPoo只有核心線程并且核心線程不會被回收拦耐,沒有超時機(jī)制耕腾,另外隊列也沒有大小限制 ,所以能更加快速地響應(yīng)外界的請求杀糯;CachedThreadPool適合執(zhí)行大量的耗時較少的任務(wù)扫俺;ScheduledThreadPool適合處理定時和具有固定重復(fù)任務(wù);SingleThreadExcuor內(nèi)部只有一個核心線程固翰,它確保 所有的任務(wù)都 在同一個線程中按順序執(zhí)行狼纬,所以這些任務(wù)之間不需要處理線程同步的問題÷罴剩可以根據(jù)需求選擇合適的線程池疗琉,原理其實就在創(chuàng)建線程池時設(shè)置不同的參數(shù),此處不做詳解歉铝。
圖片壓縮
想要壓縮盈简,我們第一步應(yīng)該是獲得imageview想要顯示的大小,沒大小肯定沒辦法壓縮太示?
那么如何獲得imageview想要顯示的大小呢柠贤?(該模塊轉(zhuǎn)自HongYang)
/**
* http://blog.csdn.net/lmj623565791/article/details/41874561
*
* @author zhy
*/
public class ImageSizeUtil {
/**
* 根據(jù)需求的寬和高以及圖片實際的寬和高計算SampleSize
*
* @param options
* @return
*/
public static int caculateInSampleSize(Options options, int reqWidth,
int reqHeight) {
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
while (width > reqWidth || height > reqHeight) {
width /= 2;
height /= 2;
inSampleSize++;
}
return inSampleSize;
}
/**
* 根據(jù)ImageView獲適當(dāng)?shù)膲嚎s的寬和高
*
* @param imageView
* @return
*/
public static ImageSize getImageViewSize(ImageView imageView) {
ImageSize imageSize = new ImageSize();
DisplayMetrics displayMetrics = imageView.getContext().getResources()
.getDisplayMetrics();
LayoutParams lp = imageView.getLayoutParams();
int width = imageView.getWidth();// 獲取imageview的實際寬度
if (width <= 0) {
width = lp.width;// 獲取imageview在layout中聲明的寬度
}
if (width <= 0) {
//width = imageView.getMaxWidth();// 檢查最大值
width = getImageViewFieldValue(imageView, "mMaxWidth");
}
if (width <= 0) {
width = displayMetrics.widthPixels;
}
int height = imageView.getHeight();// 獲取imageview的實際高度
if (height <= 0) {
height = lp.height;// 獲取imageview在layout中聲明的寬度
}
if (height <= 0) {
height = getImageViewFieldValue(imageView, "mMaxHeight");// 檢查最大值
}
if (height <= 0) {
height = displayMetrics.heightPixels;
}
imageSize.width = width;
imageSize.height = height;
return imageSize;
}
public static class ImageSize {
int width;
int height;
}
/**
* 通過反射獲取imageview的某個屬性值
*
* @param object
* @param fieldName
* @return
*/
public static int getImageViewFieldValue(Object object, String fieldName) {
int value = 0;
try {
Field field = ImageView.class.getDeclaredField(fieldName);
field.setAccessible(true);
int fieldValue = field.getInt(object);
if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {
value = fieldValue;
}
} catch (Exception e) {
}
return value;
}
}
可以看到,我們拿到imageview以后:
首先企圖通過getWidth獲取顯示的寬先匪;有些時候,這個getWidth返回的是0弃衍;
那么我們再去看看它有沒有在布局文件中書寫寬呀非;
如果布局文件中也沒有精確值,那么我們再去看看它有沒有設(shè)置最大值;
如果最大值也沒設(shè)置岸裙,那么我們只有拿出我們的終極方案猖败,使用我們的屏幕寬度;
總之降允,不能讓它任性恩闻,我們一定要拿到一個合適的顯示值。
可以看到這里或者最大寬度剧董,我們用的反射幢尚,而不是getMaxWidth();因為getMaxWidth竟然要API 16翅楼;為了兼容性尉剩,我們采用反射的方案。
得到尺寸大小后可通過caculateInSampleSize()方法設(shè)置合適的inSampleSize毅臊。
好吧理茎,代碼給了很詳盡的注釋,具體實現(xiàn)可根據(jù)代碼看更加清晰管嬉。
完整代碼如下 :
/**
* Created by zhanFeng on 2017/3/22.
*/
public class ImageLoader {
private static ImageLoader sImageLoader;
public static ImageLoader getInstance(Context context) {
if (sImageLoader == null) {
synchronized (ImageLoader.class) {
if (sImageLoader == null) {
sImageLoader = new ImageLoader(context);
}
}
}
return sImageLoader;
}
private static final String TAG = "ImageLoader";
private Context mContext;
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
//磁盤緩存空間 20M
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 20;
private boolean mIsDiskLruCacheCreated = false;
private final static int TAG_KEY_URI = R.id.imageLoader_uri;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXMUN_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final ThreadFactory sThreadFactor = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
//創(chuàng)建線程池
private static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXMUN_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactor);
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult loaderResult = (LoaderResult) msg.obj;
ImageView imageView = loaderResult.mImageView;
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(loaderResult.uri)) {
System.out.println("Handler:" + uri);
imageView.setImageBitmap(loaderResult.bitmap);
} else {
Log.e(TAG, "set image bitmap,but url has changed,ignored!");
}
}
};
private ImageLoader(Context context) {
//單例中防止上下文引起的內(nèi)存泄漏
mContext = context.getApplicationContext();
//獲取應(yīng)用內(nèi)存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//內(nèi)存緩存空間為應(yīng)用內(nèi)存的1/8
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getWidth() / 1024;
}
};
//創(chuàng)建保存bitmap的文件
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
//獲取手機(jī)存儲空間大小,當(dāng)空間允許時創(chuàng)建本地緩存對象
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
//創(chuàng)建成功時設(shè)置標(biāo)記
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 獲取手機(jī)存儲空間大小
*
* @param path
* @return
*/
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs statFs = new StatFs(path.getPath());
return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong();
}
/**
* 創(chuàng)建保存bitmap的文件
* true 有SD卡且未移除時創(chuàng)建在SD卡創(chuàng)建 false 創(chuàng)建在內(nèi)置存儲卡上
*
* @param context
* @param bitmapName
* @return
*/
private File getDiskCacheDir(Context context, String bitmapName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + bitmapName);
}
/**
* 從內(nèi)存中獲取bitmap
*
* @param url
* @return
*/
private Bitmap loadBitmapFromMemCache(String url) {
String key = hashKeyFormUrl(url);
return mMemoryCache.get(key);
}
/**
* 添加bitmap到內(nèi)存中
*
* @param url
* @param bitmap
*/
private void addBitmapToMemoryCache(String url, Bitmap bitmap) {
if (loadBitmapFromMemCache(url) == null) {
if (bitmap != null) {
mMemoryCache.put(url, bitmap);
}
}
}
public void displayImage(@Nullable final String url, final ImageView imageView) {
//setTag 與 Handler中g(shù)etTag可對imageView進(jìn)行識別皂林,避免異步造成圖片顯示錯位
imageView.setTag(TAG_KEY_URI, url);
Bitmap bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
//創(chuàng)建任務(wù)
Runnable runnableTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(url, imageView);
if (bitmap != null) {
LoaderResult loaderResult = new LoaderResult(imageView, url, bitmap);
//Handler發(fā)送消息
mMainHandler.obtainMessage(0, loaderResult).sendToTarget();
}
}
};
//將任務(wù)放入線程池執(zhí)行
THREAD_POOL_EXECUTOR.execute(runnableTask);
}
/**
* @param url
* @param imageView
* @return
*/
private Bitmap loadBitmap(String url, ImageView imageView) {
Bitmap bitmap = null;
bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(url, imageView);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromHttp(url, imageView);
} catch (IOException e) {
e.printStackTrace();
}
//存儲空間不夠大時未創(chuàng)建磁盤緩存,直接從網(wǎng)絡(luò)加載
if (bitmap == null && !mIsDiskLruCacheCreated) {
bitmap = downloadBitmapFromUrl(url);
}
return bitmap;
}
/**
* 從磁盤中加載bitmap
*
* @param url
* @param imageView
* @return bitmap
* @throws IOException
*/
private Bitmap loadBitmapFromDiskCache(String url, ImageView imageView) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread,it's not recommend!");
}
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(0);
FileDescriptor fileDescriptor = fileInputStream.getFD();
ImageSizeUtil.ImageSize imageViewSize = ImageSizeUtil.getImageViewSize(imageView);
//將文件描述符壓縮并轉(zhuǎn)換成bitmap
bitmap = decodeSampledBitmapFromPath(fileDescriptor, imageViewSize.width, imageViewSize.height);
if (null != bitmap) {
//保存到內(nèi)存中
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
private static class LoaderResult {
private ImageView mImageView;
private String uri;
private Bitmap bitmap;
public LoaderResult(ImageView imageView, String url, Bitmap bitmap) {
this.mImageView = imageView;
this.uri = url;
this.bitmap = bitmap;
}
}
public static final int IO_BUFFER_SIZE = 1024 * 8;
private Bitmap downloadBitmapFromUrl(String uri) {
Bitmap bitmap = null;
HttpURLConnection httpURLConnection = null;
BufferedInputStream is = null;
try {
URL url = new URL(uri);
try {
httpURLConnection = (HttpURLConnection) url.openConnection();
is = new BufferedInputStream(httpURLConnection.getInputStream(), IO_BUFFER_SIZE);
//此處可把bitmap對象進(jìn)行壓縮蚯撩,可自行實現(xiàn)础倍,很簡單的
bitmap = BitmapFactory.decodeStream(is);
} catch (IOException e) {
Log.e(TAG, "Error in downloadBitmap:" + e);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != httpURLConnection) {
try {
httpURLConnection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return bitmap;
}
/**
* 從網(wǎng)絡(luò)下載圖片
*
* @param url
* @param imageView
* @return
*/
private Bitmap loadBitmapFromHttp(String url, ImageView imageView) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network form UI Thread");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
try {
DiskLruCache.Editor edit = mDiskLruCache.edit(key);
if (null != edit) {
OutputStream outputStream = edit.newOutputStream(0);
if (downloadUrlToStream(url, outputStream)) {
edit.commit();
} else {
edit.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, imageView);
} catch (IOException e) {
return null;
}
}
/**
* 從網(wǎng)絡(luò)加載圖片并保存到磁盤中
*
* @param urlString
* @param outputStream
* @return
*/
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(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* md5加密,避免url存在特殊字符
*
* @param url
* @return
*/
private String hashKeyFormUrl(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[] digest) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < digest.length; i++) {
String hex = Integer.toHexString(0xFF & digest[i]);
if (hex.length() == 1) {
stringBuilder.append('0');
}
stringBuilder.append(hex);
}
return stringBuilder.toString();
}
/**
* 根據(jù)圖片需要顯示的寬和高對圖片進(jìn)行壓縮
*
* @param fileDescriptor
* @param width
* @param height
* @return
*/
protected Bitmap decodeSampledBitmapFromPath(FileDescriptor fileDescriptor, int width, int height) {
// 獲得圖片的寬和高求厕,并不把圖片加載到內(nèi)存中
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
//根據(jù)需求的寬和高以及圖片實際的寬和高計算SampleSize
options.inSampleSize = ImageSizeUtil.caculateInSampleSize(options,
width, height);
// 使用獲得到的InSampleSize再次解析圖片
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
return bitmap;
}
}
好吧著隆,終于自己實現(xiàn)了圖片加載,設(shè)置屬性可通過builder模式來進(jìn)行封裝呀癣。當(dāng)然美浦,其中遇到一個坑就是MD5加密時,相應(yīng)位置替換成mDigest.digest().length和mDigest.digest().[i]會出現(xiàn)不同的url轉(zhuǎn)化的key相同的問題项栏,此處還得對mDigest.digest()轉(zhuǎn)化成一個數(shù)組對象再進(jìn)行調(diào)用浦辨!
參考:
《Abdroid開發(fā)藝術(shù)探索》
Android 框架練成 教你打造高效的圖片加載框架
Android照片墻完整版,完美結(jié)合LruCache和DiskLruCache