2. Android 图片加载核心流程
在 Android 平台上加载一张网络图片并显示到 ImageView 中,涉及五个关键阶段:Bitmap 解码、内存缓存、磁盘缓存、网络请求、线程调度。理解每一阶段的职责与协作关系,是实现高性能异步加载的基础。
2.1 五阶段流程总览
下表梳理了每个阶段的核心组件与主要作用:
| 阶段 | 核心组件 / API | 主要作用 |
|---|---|---|
| Bitmap 解码 | BitmapFactory、BitmapFactory.Options |
将字节流转换为 Bitmap,支持降采样(inSampleSize) |
| 内存缓存 | LruCache、LinkedHashMap |
快速存取已解码的 Bitmap,避免重复解码 |
| 磁盘缓存 | DiskLruCache、文件系统 |
持久化原始网络数据,减少网络请求 |
| 网络请求 | OkHttp、HttpURLConnection |
从远程服务器下载图片字节流 |
| 线程调度 | ThreadPoolExecutor、Handler |
将耗时操作切到子线程,结果回调主线程 |
2.2 各阶段深度解析
2.2.1 Bitmap 解码
BitmapFactory 提供 decodeFile()、decodeStream()、decodeByteArray() 等静态方法。关键优化是通过 BitmapFactory.Options 设置 inJustDecodeBounds 先读取尺寸,再计算 inSampleSize 进行降采样,避免一次性加载大图导致 OOM。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
// 根据目标尺寸计算 inSampleSize
options.inSampleSize = calculateInSampleSize(imageWidth, imageHeight, targetWidth, targetHeight);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
2.2.2 内存缓存
Android 官方推荐使用 LruCache,基于 LRU 算法淘汰最近最少使用的 Bitmap。缓存大小通常取应用可用内存的 1/8。注意:LruCache 的 sizeOf() 应返回 bitmap.getAllocationByteCount()(API 19+)以保证准确计数。
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
LruCache memoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getAllocationByteCount() / 1024;
}
};
2.2.3 磁盘缓存
DiskLruCache 将图片的字节数据以文件形式存储在应用缓存目录,支持自动淘汰。每个缓存条目对应一个 key(通常为 URL 的 MD5),value 为文件流。读取时先查内存缓存,未命中则从磁盘读取并解码,同时回写到内存缓存。
Editor 提交/回滚;不要在主线程中读写磁盘。
2.2.4 网络请求
推荐使用 OkHttp 进行 HTTP 请求,支持连接池、响应缓存(配合 Cache 类实现磁盘层)、重试机制。图片下载时需设置 ResponseBody.byteStream() 为 BufferedSource 以便边下载边解码。
OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(cacheDir, 10 * 1024 * 1024)) // 10MB 磁盘缓存
.build();
Request request = new Request.Builder().url(url).build();
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
// 传递 inputStream 给解码或磁盘写入
}
2.2.5 线程调度
使用 ThreadPoolExecutor 管理后台线程池,避免无限创建线程。将图片加载任务封装为 Runnable 提交,执行完成后通过 Handler 或 View.post() 切回主线程更新 UI。推荐配置:核心线程数 = CPU 核数 + 1,最大线程数 = CPU 核数 × 2。
Executor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS, new LinkedBlockingQueue(128));
Handler mainHandler = new Handler(Looper.getMainLooper());
executor.execute(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = loadBitmap(url);
mainHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
});
2.3 核心流程框架图
下图展示了从“图片请求”到“显示到 View”的完整执行路径,以及缓存与线程调度的协作关系:
2.4 常见错误与性能陷阱
- 主线解码 OOM:未设置
inSampleSize或inPreferredConfig为ARGB_8888导致内存暴涨。应在子线程读取边界后计算采样。 - 线程阻塞 UI:在
ListView/RecyclerView滚动时直接在主线程加载图片会导致卡顿。务必使用ThreadPoolExecutor+Handler模式。 - 内存缓存穿透:缓存 key 设计不当(如未使用 URL + 尺寸作为复合 key)导致不停请求网络。
- 磁盘缓存未同步:多线程同时写入同一磁盘缓存条目可能造成文件损坏。应使用
DiskLruCache的edit()和commit()原子操作。
onLowMemory() 中清空内存缓存;通过 StrictMode 检测主线程磁盘/网络访问。