12.2.3 ImageLoader的實現
在本章的前面先后介紹了Bitmap的高效加載方式、LruCache以及DiskLruCache,現在我們來著手實現一個優秀的ImageLoader。
一般來說,一個優秀的ImageLoader應該具備如下功能:
· 圖片的同步加載;
· 圖片的異步加載;
· 圖片壓縮;
· 內存緩存;
· 磁盤緩存;
· 網絡拉取。
圖片的同步加載是指能夠以同步的方式向調用者提供所加載的圖片,這個圖片可能是從內存緩存中讀取的,也可能是從磁盤緩存中讀取的,還可能是從網絡拉取的。圖片的異步加載是一個很有用的功能,很多時候調用者不想在單獨的線程中以同步的方式來獲取圖片,這個時候ImageLoader內部需要自己在線程中加載圖片并將圖片設置給所需的ImageView。圖片壓縮的作用更毋庸置疑了,這是降低OOM概率的有效手段,ImageLoader必須合適地處理圖片的壓縮問題。
內存緩存和磁盤緩存是ImageLoader的核心,也是ImageLoader的意義之所在,通過這兩級緩存極大地提高了程序的效率并且有效地降低了對用戶所造成的流量消耗,只有當這兩級緩存都不可用時才需要從網絡中拉取圖片。
除此之外,ImageLoader還需要處理一些特殊的情況,比如在ListView或者GridView中,View復用既是它們的優點也是它們的缺點,優點想必讀者都很清楚了,那缺點可能還不太清楚。考慮一種情況,在ListView或者GridView中,假設一個item A正在從網絡加載圖片,它對應的ImageView為A,這個時候用戶快速向下滑動列表,很可能item B復用了ImageView A,然后等了一會之前的圖片下載完畢了。如果直接給ImageView A設置圖片,由于這個時候ImageView A被item B所復用,但是item B要顯示的圖片顯然不是item A剛剛下載好的圖片,這個時候就會出現item B中顯示了item A的圖片,這就是常見的列表的錯位問題,ImageLoader需要正確地處理這些特殊情況。
上面對ImageLoader的功能做了一個全面的分析,下面就可以一步步實現一個ImageLoader了,這里主要分為如下幾步。
1.圖片壓縮功能的實現
圖片壓縮在第12.1節中已經做了介紹,這里就不再多說了,為了有良好的設計風格,這里單獨抽象了一個類用于完成圖片的壓縮功能,這個類叫ImageResizer,它的實現如下所示。
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
public Bitmap decodeSampledBitmapFromResource(Resources res,
int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w=" + width + " h=" + height);
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, "sampleSize:" + inSampleSize);
return inSampleSize;
}
}
2.內存緩存和磁盤緩存的實現
這里選擇LruCache和DiskLruCache來分別完成內存緩存和磁盤緩存的工作。在ImageLoader初始化時,會創建LruCache和DiskLruCache,如下所示。
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
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();
}
}
}
在創建磁盤緩存時,這里做了一個判斷,即有可能磁盤剩余空間小于磁盤緩存所需的大小,一般是指用戶的手機空間已經不足了,因此沒有辦法創建磁盤緩存,這個時候磁盤緩存就會失效。在上面的代碼實現中,ImageLoader的內存緩存的容量為當前進程可用內存的1/8,磁盤緩存的容量為50MB。
內存緩存和磁盤緩存創建完畢后,還需要提供方法來完成緩存的添加和獲取功能。首先看內存緩存,它的添加和讀取過程比較簡單,如下所示。
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
而磁盤緩存的添加和讀取功能稍微復雜一些,具體內容已經在12.2.2節中進行了詳細的介紹,這里再簡單說明一下。磁盤緩存的添加需要通過Editor來完成,Editor提供了commit和abort方法來提交和撤銷對文件系統的寫操作,具體實現請參看下面的loadBitmap-FromHttp方法。磁盤緩存的讀取需要通過Snapshot來完成,通過Snapshot可以得到磁盤緩存對象對應的FileInputStream,但是FileInputStream無法便捷地進行壓縮,所以通過FileDescriptor來加載壓縮后的圖片,最后將加載后的Bitmap添加到內存緩存中,具體實現請參看下面的loadBitmapFromDiskCache方法。
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()) {
Log.w(TAG, "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.decodeSampledBitmapFromFileDescriptor
(fileDescriptor,
reqWidth, reqHeight);
if (bitmap ! = null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
3.同步加載和異步加載接口的設計
首先看同步加載,同步加載接口需要外部在線程中調用,這是因為同步加載很可能比較耗時,它的實現如下所示。
/**
* 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.d(TAG, "loadBitmapFromMemCache, url:" + uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap ! = null) {
Log.d(TAG, "loadBitmapFromDisk, url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp, url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && ! mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
從loadBitmap的實現可以看出,其工作過程遵循如下幾步:首先嘗試從內存緩存中讀取圖片,接著嘗試從磁盤緩存中讀取圖片,最后才從網絡中拉取圖片。另外,這個方法不能在主線程中調用,否則就拋出異常。這個執行環境的檢查是在loadBitmapFromHttp中實現的,通過檢查當前線程的Looper是否為主線程的Looper來判斷當前線程是否是主線程,如果不是主線程就直接拋出異常中止程序,如下所示。
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
接著看異步加載接口的設計,如下所示。
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);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).
sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
從bindBitmap的實現來看,bindBitmap方法會嘗試從內存緩存中讀取圖片,如果讀取成功就直接返回結果,否則會在線程池中去調用loadBitmap方法,當圖片加載成功后再將圖片、圖片的地址以及需要綁定的imageView封裝成一個LoaderResult對象,然后再通過mMainHandler向主線程發送一個消息,這樣就可以在主線程中給imageView設置圖片了,之所以通過Handler來中轉是因為子線程無法訪問UI。
bindBitmap中用到了線程池和Handler,這里看一下它們的實現,首先看線程池THREAD_POOL_EXECUTOR的實現,如下所示。可以看出它的核心線程數為當前設備的CPU核心數加1,最大容量為CPU核心數的2倍加1,線程閑置超時時長為10秒,關于線程池的詳細介紹可以參看第11章的有關內容。
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 static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(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 LinkedBlockingQueue<Runnable>(), sThreadFactory);
之所以采用線程池是有原因的,首先肯定不能采用普通的線程去做這個事,線程池的好處在第11章已經做了詳細的說明。如果直接采用普通的線程去加載圖片,隨著列表的滑動這可能會產生大量的線程,這樣并不利于整體效率的提升。另外一點,這里也沒有選擇采用AsyncTask, AsyncTask封裝了線程池和Handler,按道理它應該適合ImageLoader的場景。從第11章中對AsyncTask的分析可以知道,AsyncTask在3.0的低版本和高版本上具有不同的表現,在3.0以上的版本AsyncTask無法實現并發的效果,這顯然是不能接受的,因為ImageLoader就是需要并發特性,雖然可以通過改造AsyncTask或者使用AsyncTask的executeOnExecutor方法的形式來執行異步任務,但是這終歸是不太自然的實現方式。鑒于以上兩點原因,這里選擇線程池和Handler來提供ImageLoader的并發能力和訪問UI的能力。
分析完線程池的選擇,下面看一下Handler的實現,如下所示。ImageLoader直接采用主線程的Looper來構造Handler對象,這就使得ImageLoader可以在非主線程中構造了。另外為了解決由于View復用所導致的列表錯位這一問題,在給ImageView設置圖片之前都會檢查它的url有沒有發生改變,如果發生改變就不再給它設置圖片,這樣就解決了列表錯位的問題。
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.w(TAG, "set image bitmap, but url has changed, ignored! ");
}
};
};
到此為止,ImageLoader的細節都已經做了全面的分析,下面是ImageLoader的完整的代碼。
public class ImageLoader {
private static final String TAG = "ImageLoader";
public static final int MESSAGE_POST_RESULT = 1;
private static final int CPU_COUNT = Runtime.getRuntime().available-
Processors();
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 static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private boolean mIsDiskLruCacheCreated = false;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPool-
Executor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
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.w(TAG, "set image bitmap, but url has changed, ignored! ");
}
};
};
private Context mContext;
private ImageResizer mImageResizer = new ImageResizer();
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
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();
}
}
}
/**
* build a new instance of ImageLoader
* @param context
* @return a new instance of ImageLoader
*/
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
/**
* load bitmap from memory cache or disk cache or network async, then bind
imageView and bitmap.
* NOTE THAT: should run in UI Thread
* @param uri http url
* @param imageView bitmap's bind object
*/
public void bindBitmap(final String uri, final ImageView imageView) {
bindBitmap(uri, imageView, 0, 0);
}
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);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).
sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
/**
* 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.d(TAG, "loadBitmapFromMemCache, url:" + uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap ! = null) {
Log.d(TAG, "loadBitmapFromDisk, url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp, url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && ! mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
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 (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()) {
Log.w(TAG, "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.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(out);
MyUtils.close(in);
}
return false;
}
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 (final IOException e) {
Log.e(TAG, "Error in downloadBitmap: " + e);
} finally {
if (urlConnection ! = null) {
urlConnection.disconnect();
}
MyUtils.close(in);
}
return bitmap;
}
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[] 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();
}
public File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment
.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
@TargetApi(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
private static class LoaderResult {
public ImageView imageView;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
}
- 前言
- 第1章 Activity的生命周期和啟動模式
- 1.1 Activity的生命周期全面分析
- 1.1.1 典型情況下的生命周期分析
- 1.1.2 異常情況下的生命周期分析
- 1.2 Activity的啟動模式
- 1.2.1 Activity的LaunchMode
- 1.2.2 Activity的Flags
- 1.3 IntentFilter的匹配規則
- 第2章 IPC機制
- 2.1 Android IPC簡介
- 2.2 Android中的多進程模式
- 2.2.1 開啟多進程模式
- 2.2.2 多進程模式的運行機制
- 2.3 IPC基礎概念介紹
- 2.3.1 Serializable接口
- 2.3.2 Parcelable接口
- 2.3.3 Binder
- 2.4 Android中的IPC方式
- 2.4.1 使用Bundle
- 2.4.2 使用文件共享
- 2.4.3 使用Messenger
- 2.4.4 使用AIDL
- 2.4.5 使用ContentProvider
- 2.4.6 使用Socket
- 2.5 Binder連接池
- 2.6 選用合適的IPC方式
- 第3章 View的事件體系
- 3.1 View基礎知識
- 3.1.1 什么是View
- 3.1.2 View的位置參數
- 3.1.3 MotionEvent和TouchSlop
- 3.1.4 VelocityTracker、GestureDetector和Scroller
- 3.2 View的滑動
- 3.2.1 使用scrollTo/scrollBy
- 3.2.2 使用動畫
- 3.2.3 改變布局參數
- 3.2.4 各種滑動方式的對比
- 3.3 彈性滑動
- 3.3.1 使用Scroller7
- 3.3.2 通過動畫
- 3.3.3 使用延時策略
- 3.4 View的事件分發機制
- 3.4.1 點擊事件的傳遞規則
- 3.4.2 事件分發的源碼解析
- 3.5 View的滑動沖突
- 3.5.1 常見的滑動沖突場景
- 3.5.2 滑動沖突的處理規則
- 3.5.3 滑動沖突的解決方式
- 第4章 View的工作原理
- 4.1 初識ViewRoot和DecorView
- 4.2 理解MeasureSpec
- 4.2.1 MeasureSpec
- 4.2.2 MeasureSpec和LayoutParams的對應關系
- 4.3 View的工作流程
- 4.3.1 measure過程
- 4.3.2 layout過程
- 4.3.3 draw過程
- 4.4 自定義View
- 4.4.1 自定義View的分類
- 4.4.2 自定義View須知
- 4.4.3 自定義View示例
- 4.4.4 自定義View的思想
- 第5章 理解RemoteViews
- 5.1 RemoteViews的應用
- 5.1.1 RemoteViews在通知欄上的應用
- 5.1.2 RemoteViews在桌面小部件上的應用
- 5.1.3 PendingIntent概述
- 5.2 RemoteViews的內部機制
- 5.3 RemoteViews的意義
- 第6章 Android的Drawable
- 6.1 Drawable簡介
- 6.2 Drawable的分類
- 6.2.1 BitmapDrawable2
- 6.2.2 ShapeDrawable
- 6.2.3 LayerDrawable
- 6.2.4 StateListDrawable
- 6.2.5 LevelListDrawable
- 6.2.6 TransitionDrawable
- 6.2.7 InsetDrawable
- 6.2.8 ScaleDrawable
- 6.2.9 ClipDrawable
- 6.3 自定義Drawable
- 第7章 Android動畫深入分析
- 7.1 View動畫
- 7.1.1 View動畫的種類
- 7.1.2 自定義View動畫
- 7.1.3 幀動畫
- 7.2 View動畫的特殊使用場景
- 7.2.1 LayoutAnimation
- 7.2.2 Activity的切換效果
- 7.3 屬性動畫
- 7.3.1 使用屬性動畫
- 7.3.2 理解插值器和估值器 /
- 7.3.3 屬性動畫的監聽器
- 7.3.4 對任意屬性做動畫
- 7.3.5 屬性動畫的工作原理
- 7.4 使用動畫的注意事項
- 第8章 理解Window和WindowManager
- 8.1 Window和WindowManager
- 8.2 Window的內部機制
- 8.2.1 Window的添加過程
- 8.2.2 Window的刪除過程
- 8.2.3 Window的更新過程
- 8.3 Window的創建過程
- 8.3.1 Activity的Window創建過程
- 8.3.2 Dialog的Window創建過程
- 8.3.3 Toast的Window創建過程
- 第9章 四大組件的工作過程
- 9.1 四大組件的運行狀態
- 9.2 Activity的工作過程
- 9.3 Service的工作過程
- 9.3.1 Service的啟動過程
- 9.3.2 Service的綁定過程
- 9.4 BroadcastReceiver的工作過程
- 9.4.1 廣播的注冊過程
- 9.4.2 廣播的發送和接收過程
- 9.5 ContentProvider的工作過程
- 第10章 Android的消息機制
- 10.1 Android的消息機制概述
- 10.2 Android的消息機制分析
- 10.2.1 ThreadLocal的工作原理
- 10.2.2 消息隊列的工作原理
- 10.2.3 Looper的工作原理
- 10.2.4 Handler的工作原理
- 10.3 主線程的消息循環
- 第11章 Android的線程和線程池
- 11.1 主線程和子線程
- 11.2 Android中的線程形態
- 11.2.1 AsyncTask
- 11.2.2 AsyncTask的工作原理
- 11.2.3 HandlerThread
- 11.2.4 IntentService
- 11.3 Android中的線程池
- 11.3.1 ThreadPoolExecutor
- 11.3.2 線程池的分類
- 第12章 Bitmap的加載和Cache
- 12.1 Bitmap的高效加載
- 12.2 Android中的緩存策略
- 12.2.1 LruCache
- 12.2.2 DiskLruCache
- 12.2.3 ImageLoader的實現446
- 12.3 ImageLoader的使用
- 12.3.1 照片墻效果
- 12.3.2 優化列表的卡頓現象
- 第13章 綜合技術
- 13.1 使用CrashHandler來獲取應用的crash信息
- 13.2 使用multidex來解決方法數越界
- 13.3 Android的動態加載技術
- 13.4 反編譯初步
- 13.4.1 使用dex2jar和jd-gui反編譯apk
- 13.4.2 使用apktool對apk進行二次打包
- 第14章 JNI和NDK編程
- 14.1 JNI的開發流程
- 14.2 NDK的開發流程
- 14.3 JNI的數據類型和類型簽名
- 14.4 JNI調用Java方法的流程
- 第15章 Android性能優化
- 15.1 Android的性能優化方法
- 15.1.1 布局優化
- 15.1.2 繪制優化
- 15.1.3 內存泄露優化
- 15.1.4 響應速度優化和ANR日志分析
- 15.1.5 ListView和Bitmap優化
- 15.1.6 線程優化
- 15.1.7 一些性能優化建議
- 15.2 內存泄露分析之MAT工具
- 15.3 提高程序的可維護性