4. 网络缓存实现:HTTP请求封装、OkHttp/URLConnection选用、异步下载与回调设计
网络缓存是图片三级缓存模型中的最后一级屏障。当内存缓存与磁盘缓存均未命中时,网络层负责从远端服务器拉取原始图片数据,并回填至下级缓存。本章聚焦网络层的工程化封装:HTTP请求统一抽象、底层引擎选型以及异步回调模型,确保下载过程可取消、可重试、可回抛至UI线程。
4.1 网络缓存层的职责边界
- 职责单一:只处理HTTP请求的发起与响应解析,不参与缓存策略决策。
- 异步非阻塞:所有网络操作必须脱离主线程,通过回调或监听器通知结果。
- 可配置:超时、重试次数、User-Agent、缓存头等参数应支持外部注入。
- 可取消:当图片被复用或页面销毁时,能够中断正在进行的网络请求。
架构提示:网络层只返回原始字节流(byte[]),Bitmap的转换与缩放由上层缓存层或展示层完成,避免网络层耦合UI逻辑。
4.2 HTTP请求封装设计
封装的核心目标是让调用方不关心底层是 OkHttp 还是 URLConnection,只通过统一的 ImageRequest 对象描述请求参数。
4.2.1 请求对象定义
public class ImageRequest {
private final String url;
private final Map<String, String> headers;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final int retryCount;
private ImageRequest(Builder builder) {
this.url = builder.url;
this.headers = Collections.unmodifiableMap(builder.headers);
this.connectTimeoutMs = builder.connectTimeoutMs;
this.readTimeoutMs = builder.readTimeoutMs;
this.retryCount = builder.retryCount;
}
// Getter 省略
public static class Builder {
private String url;
private Map<String, String> headers = new HashMap<>();
private int connectTimeoutMs = 15_000;
private int readTimeoutMs = 30_000;
private int retryCount = 2;
public Builder url(String url) {
this.url = url;
return this;
}
public Builder addHeader(String key, String value) {
headers.put(key, value);
return this;
}
public Builder connectTimeout(int ms) {
this.connectTimeoutMs = ms;
return this;
}
public Builder readTimeout(int ms) {
this.readTimeoutMs = ms;
return this;
}
public Builder retryCount(int count) {
this.retryCount = count;
return this;
}
public ImageRequest build() {
if (url == null || url.isEmpty())
throw new IllegalStateException("URL must not be null or empty");
return new ImageRequest(this);
}
}
}
4.2.2 响应对象定义
public class ImageResponse {
private final int statusCode;
private final byte[] data;
private final String contentType;
private final long contentLength;
private final Map<String, List<String>> headers;
public ImageResponse(int statusCode, byte[] data, String contentType,
long contentLength, Map<String, List<String>> headers) {
this.statusCode = statusCode;
this.data = data;
this.contentType = contentType;
this.contentLength = contentLength;
this.headers = headers;
}
public boolean isSuccessful() {
return statusCode >= 200 && statusCode < 300;
}
// Getter 省略
}
4.3 OkHttp 与 URLConnection 选型对比
| 对比维度 | OkHttp | HttpURLConnection |
|---|---|---|
| 连接池复用 | 内置连接池,支持 HTTP/2 多路复用 | 无连接池,每次请求新建连接 |
| 自动重定向 | 默认跟随,可配置 | 默认跟随,需手动设置 |
| 超时控制 | connect / read / write 独立超时 | 仅支持 connect 与 read 超时 |
| 拦截器链 | 支持自定义 Interceptor(日志/重试/缓存) | 无原生拦截器机制 |
| 取消请求 | 通过 Call.cancel() 优雅取消 | 通过 disconnect() 强制中断 |
| 低版本兼容 | API 21+(Android) | API 1+ 原生可用 |
| 包体积 | 约 0.5 MB(含依赖) | 0(系统内置) |
选用建议:新项目优先选用 OkHttp,其连接池、拦截器与取消机制能显著提升网络层稳定性;若项目对包体积极度敏感或需兼容 API 19 以下设备,可降级为 HttpURLConnection,但需自行封装重试与连接复用。
4.4 异步下载与回调设计
异步下载是网络缓存层的核心模式。统一采用 回调接口 将结果从工作线程抛回调用方,避免调用方感知线程切换细节。
4.4.1 回调接口定义
public interface DownloadCallback {
void onSuccess(ImageResponse response);
void onFailed(IOException e, int retryRemaining);
void onProgress(int bytesRead, int totalBytes);
}
4.4.2 异步下载流程
下图展示了从请求封装到回调通知的完整生命周期:
设计要点:回调分发器内部使用
Handler (Android) 或 Executor 将 onSuccess/onFailed 切换到主线程;onProgress 应限制回调频率(如每 200ms 回调一次),避免高频刷新导致界面卡顿。
4.5 核心代码实现
4.5.1 OkHttp 下载器实现
public class OkHttpDownloader {
private final OkHttpClient client;
private final ExecutorService executor;
public OkHttpDownloader(OkHttpClient client, ExecutorService executor) {
this.client = client;
this.executor = executor;
}
public void download(ImageRequest request, DownloadCallback callback) {
executor.submit(() -> {
int retry = request.getRetryCount();
do {
try {
Request.Builder builder = new Request.Builder()
.url(request.getUrl())
.get();
for (Map.Entry<String, String> h : request.getHeaders().entrySet()) {
builder.addHeader(h.getKey(), h.getValue());
}
Call call = client.newCall(builder.build());
Response response = call.execute();
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response.code());
}
byte[] data = response.body().bytes();
ImageResponse imgResp = new ImageResponse(
response.code(),
data,
response.header("Content-Type"),
response.body().contentLength(),
response.headers().toMultimap()
);
callback.onSuccess(imgResp);
return;
} catch (IOException e) {
retry--;
if (retry < 0) {
callback.onFailed(e, 0);
return;
}
callback.onFailed(e, retry);
}
} while (retry >= 0);
});
}
}
4.5.2 URLConnection 下载器实现
public class UrlConnectionDownloader {
private final ExecutorService executor;
public UrlConnectionDownloader(ExecutorService executor) {
this.executor = executor;
}
public void download(ImageRequest request, DownloadCallback callback) {
executor.submit(() -> {
int retry = request.getRetryCount();
do {
HttpURLConnection conn = null;
try {
URL url = new URL(request.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(request.getConnectTimeoutMs());
conn.setReadTimeout(request.getReadTimeoutMs());
conn.setInstanceFollowRedirects(true);
for (Map.Entry<String, String> h : request.getHeaders().entrySet()) {
conn.setRequestProperty(h.getKey(), h.getValue());
}
conn.connect();
int code = conn.getResponseCode();
if (code < 200 || code >= 300) {
throw new IOException("HTTP " + code);
}
byte[] data = readFully(conn.getInputStream());
ImageResponse imgResp = new ImageResponse(
code,
data,
conn.getContentType(),
conn.getContentLengthLong(),
conn.getHeaderFields()
);
callback.onSuccess(imgResp);
return;
} catch (IOException e) {
retry--;
if (retry < 0) {
callback.onFailed(e, 0);
return;
}
callback.onFailed(e, retry);
} finally {
if (conn != null) {
conn.disconnect();
}
}
} while (retry >= 0);
});
}
private byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] tmp = new byte[4096];
int n;
while ((n = in.read(tmp)) != -1) {
buffer.write(tmp, 0, n);
}
return buffer.toByteArray();
}
}
4.5.3 取消请求的封装
通过 Future 或 Call 的取消能力实现请求中断:
public class DownloadTask {
private Future<?> future;
private Call okHttpCall;
public void cancel() {
if (okHttpCall != null) {
okHttpCall.cancel(); // OkHttp 优雅取消
}
if (future != null) {
future.cancel(true); // 强制中断线程
}
}
public void bind(Future<?> future, Call call) {
this.future = future;
this.okHttpCall = call;
}
}
4.6 调试与验证
网络缓存层调试主要验证 超时生效、重试次数、回调时序 三个维度。
| 测试场景 | 预期行为 | 验证方法 |
|---|---|---|
| 服务器 200 返回图片 | onSuccess 被调用,data 非空 | Log.d 输出 data.length |
| 服务器 404 | onFailed 被调用,retryRemaining 递减 | Log.e 输出 status code |
| 连接超时 | onFailed 收到 SocketTimeoutException | 设置 1ms 超时触发 |
| 请求被取消 | 回调不被执行,Future.isCancelled() == true | 在 cancel() 后断言 |
| 重试耗尽 | 最终 onFailed 收到异常,retryRemaining == 0 | 拦截器日志计数 |
注意:测试网络层时务必使用 MockWebServer (OkHttp) 或 WireMock 模拟服务端,避免依赖真实网络导致测试不稳定。单元测试中所有回调需使用
CountDownLatch 同步等待结果。
4.7 常见错误与避坑
- 回调未切回主线程:直接在工作线程更新 UI 导致崩溃。务必通过 Handler / LiveData / RxJava 切换到主线程。
- 流未关闭导致内存泄漏:HttpURLConnection 的 InputStream 必须在 finally 块中关闭;OkHttp 的 Response.body() 使用 try-with-resources 自动关闭。
- 重试风暴:重试逻辑未加指数退避,服务器异常时客户端疯狂重试。建议用
Thread.sleep(1000 * retryCount)或 OkHttp 的拦截器实现退避。 - 取消后仍收到回调:Future.cancel(true) 中断线程后,IO 操作可能抛出 InterruptedException,需在回调入口检查线程中断状态。
- OkHttp 连接池未释放:全局只创建一个 OkHttpClient 实例,避免反复创建连接池耗尽 Socket。
4.8 性能与稳定性优化
- 连接池调优:OkHttp 默认最大空闲连接 5 个,对图片密集场景可提升至 10~15。
- 超时差异化:WiFi 环境下 connectTimeout 可缩短至 8s,移动网络保留 20s。
- 缓存头利用:解析
Cache-Control与ETag,配合磁盘缓存实现条件请求(304 Not Modified)。 - 并发控制:使用
Semaphore限制最大并发下载数(如 4~6),避免线程池被打满。 - 字节流直接写入磁盘:对于大图(> 2MB),从网络流直接写入临时文件,避免全部加载到堆内存。
// 示例:OkHttp 连接池与超时配置
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(12, 5, TimeUnit.MINUTES))
.addInterceptor(new RetryInterceptor(2, 500)) // 重试2次,间隔500ms
.build();
工程交付清单:本章完成后应当产出
ImageRequest、ImageResponse、DownloadCallback、OkHttpDownloader、UrlConnectionDownloader、DownloadTask 六个核心类,并通过 MockWebServer 单元测试覆盖成功/超时/重试/取消四条路径。