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 提高程序的可维护性