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 为文件流。读取时先查内存缓存,未命中则从磁盘读取并解码,同时回写到内存缓存。

注意: DiskLruCache 在 Android 4.0+ 中已内置,但需要手动管理文件锁和 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”的完整执行路径,以及缓存与线程调度的协作关系:

Android 图片加载核心流程 用户请求 URL 线程调度 内存缓存 (LruCache) 命中? 是 Bitmap 解码 否 磁盘缓存 命中? 是 Bitmap 解码 否 网络请求 写入磁盘 输入流 Bitmap 解码 主线程更新 UI 关键优化点 降采样 → 避免 OOM 三级缓存 → 减少网络请求

2.4 常见错误与性能陷阱

  • 主线解码 OOM:未设置 inSampleSize 或 inPreferredConfig 为 ARGB_8888 导致内存暴涨。应在子线程读取边界后计算采样。
  • 线程阻塞 UI:在 ListView / RecyclerView 滚动时直接在主线程加载图片会导致卡顿。务必使用 ThreadPoolExecutor + Handler 模式。
  • 内存缓存穿透:缓存 key 设计不当(如未使用 URL + 尺寸作为复合 key)导致不停请求网络。
  • 磁盘缓存未同步:多线程同时写入同一磁盘缓存条目可能造成文件损坏。应使用 DiskLruCache 的 edit() 和 commit() 原子操作。
调试建议: 使用 Android Studio 的 Profile 观察内存抖动与线程活动;在 onLowMemory() 中清空内存缓存;通过 StrictMode 检测主线程磁盘/网络访问。