网络请求优化:用 Retrofit 拦截器玩转日志、重试与缓存,OkHttp 和 Volley 谁更香?
1. 拦截器:Retrofit 的“超级管理员”
Retrofit 之所以强大,不仅仅因为它把 HTTP 请求包装得优雅,还因为它的拦截器机制让开发者可以像“超级管理员”一样掌控请求的每个环节。拦截器就像网络请求的“关卡守卫”,可以检查、修改请求和响应,甚至决定是否放行。想记录日志?想自动重试?想搞个缓存策略?拦截器都能搞定。
拦截器的本质
拦截器(Interceptor)是 OkHttp 的核心功能,Retrofit 基于 OkHttp,自然也继承了这一神器。拦截器分为两种:
应用拦截器:通过 addInterceptor() 添加,处理请求的早期阶段,比如添加公共请求头。
网络拦截器:通过 addNetworkInterceptor() 添加,更靠近实际的网络操作,适合处理缓存或响应数据。
关键点:拦截器基于责任链模式,请求和响应会按添加顺序逐个经过拦截器,像流水线一样处理。灵活,但也得小心别把链条搞乱!
为什么用拦截器?
统一管理:把日志、错误重试、缓存等逻辑集中处理,代码更整洁。
灵活扩展:想加新功能?写个新拦截器,插进链条就行。
调试神器:开发阶段,拦截器能帮你把请求和响应的每个细节打印出来,省得抓狂。
2. 日志拦截器:让请求和响应“现原形”
开发网络功能时,最头疼的莫过于调试接口。服务器返回了个 500,谁的锅?请求头是不是少了啥?响应数据为啥是空的?日志拦截器就是你的“显微镜”,能把请求和响应的每个细节扒得清清楚楚。
引入日志拦截器
Retrofit 依赖 OkHttp,而 OkHttp 官方提供了 HttpLoggingInterceptor,专门用来打印网络请求的日志。别自己手写日志逻辑,用现成的多香!
第一步,在 build.gradle 中添加依赖:
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
注意:版本号要和你的 OkHttp 保持一致,别随便抄个老版本,不然可能报错。
实现日志拦截器
下面是一个简单的日志拦截器配置,打印请求 URL、头信息和响应内容:
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;public class NetworkClient {private static Retrofit retrofit;public static Retrofit getRetrofit() {if (retrofit == null) {// 配置日志拦截器HttpLoggingInterceptor logging = new HttpLoggingInterceptor();logging.setLevel(HttpLoggingInterceptor.Level.BODY); // 打印请求和响应的完整内容OkHttpClient client = new OkHttpClient.Builder().addInterceptor(logging).build();retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).client(client).build();}return retrofit;}
}
代码解析:
HttpLoggingInterceptor.Level.BODY:打印请求和响应的全部内容,包括头、Body 等。开发调试时用这个,生产环境建议改成 Level.NONE 或 Level.BASIC,避免泄露敏感信息。
addInterceptor():将日志拦截器加入 OkHttpClient,请求和响应都会被记录。
日志输出示例
运行后,日志可能长这样:
D/HttpLoggingInterceptor: --> POST https://api.example.com/login HTTP/1.1
D/HttpLoggingInterceptor: Content-Type: application/json; charset=UTF-8
D/HttpLoggingInterceptor: Content-Length: 45
D/HttpLoggingInterceptor: {"username":"test","password":"123456"}
D/HttpLoggingInterceptor: --> END POST
D/HttpLoggingInterceptor: <-- 200 OK (120ms)
D/HttpLoggingInterceptor: Content-Type: application/json
D/HttpLoggingInterceptor: {"token":"abc123","userId":1001}
D/HttpLoggingInterceptor: <-- END HTTP
彩蛋:想让日志更易读?可以自定义 HttpLoggingInterceptor.Logger,把日志输出到文件或自定义格式,比如加上时间戳或线程信息。
生产环境注意事项
关闭详细日志:生产环境中,Level.BODY 可能会泄露用户数据,比如密码或 token。建议用 if (BuildConfig.DEBUG) 动态控制日志级别。
性能优化:日志打印会消耗性能,生产环境直接禁用。
小技巧:如果接口返回的数据量很大,日志可能会刷屏。可以用 Level.HEADERS 只打印头信息,或者写个过滤器只打印特定接口的日志。
3. 重试拦截器:网络不稳定也能稳如狗
网络请求最怕啥?服务器抽风、4G 信号时有时无、Wi-Fi 突然掉线……重试拦截器就像你的“救火队员”,在请求失败时自动尝试几次,让用户体验不至于崩盘。
设计重试逻辑
我们来写一个自定义的重试拦截器,支持最多重试 N 次,遇到特定错误(如超时或 503)自动重试。
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;public class RetryInterceptor implements Interceptor {private final int maxRetry; // 最大重试次数private int retryNum = 0; // 当前重试次数public RetryInterceptor(int maxRetry) {this.maxRetry = maxRetry;}@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();Response response = chain.proceed(request);// 重试逻辑:如果请求失败且未达到最大重试次数,继续尝试while (!response.isSuccessful() && retryNum < maxRetry) {retryNum++;System.out.println("Retry attempt: " + retryNum);response.close(); // 关闭上一次的响应response = chain.proceed(request);}return response;}
}
集成到 Retrofit
将重试拦截器加入 OkHttpClient:
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new RetryInterceptor(3)) // 最多重试3次.connectTimeout(10, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).writeTimeout(20, TimeUnit.SECONDS).build();Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).client(client).build();
代码解析:
maxRetry:设置最大重试次数,比如 3 次。
response.isSuccessful():检查 HTTP 状态码是否在 200-299 范围内。
response.close():关闭失败的响应,避免资源泄漏。
优化重试策略
直接重试可能有点“莽”,我们可以加点聪明逻辑:
指数退避:每次重试间隔时间递增,比如 1s、2s、4s,防止瞬间把服务器锤爆。
特定错误重试:只对某些错误(如 503、504 或 IOException)重试,403(权限错误)就别浪费时间了。
改进版重试拦截器:
public class AdvancedRetryInterceptor implements Interceptor {private final int maxRetry;private final long initialDelayMs;public AdvancedRetryInterceptor(int maxRetry, long initialDelayMs) {this.maxRetry = maxRetry;this.initialDelayMs = initialDelayMs;}@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();Response response = chain.proceed(request);int retryNum = 0;while (!response.isSuccessful() && retryNum < maxRetry) {// 只对特定错误重试if (response.code() != 503 && response.code() != 504) {break;}retryNum++;response.close();// 指数退避long delay = initialDelayMs * (long) Math.pow(2, retryNum);try {Thread.sleep(delay);} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("Retry attempt: " + retryNum + ", delay: " + delay + "ms");response = chain.proceed(request);}return response;}
}
使用示例:
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new AdvancedRetryInterceptor(3, 1000)) // 最多重试3次,初始延迟1秒.build();
效果:如果服务器返回 503(服务不可用),拦截器会自动重试,最多 3 次,延迟时间依次为 1s、2s、4s。这样既避免了频繁请求,又提高了成功率。
注意事项
避免无限重试:一定要设置最大重试次数,不然网络差的时候可能卡死。
错误码判断:别对所有错误都重试,比如 400(请求参数错误)重试也没用。
用户体验:重试时可以显示一个“加载中”动画,别让用户觉得 App 卡住了。
4. 缓存拦截器:离线也能用,省流量又快
网络请求优化怎么能少得了缓存?用户没网的时候还能看数据,流量不够时也能省点钱,缓存策略就是这么贴心。OkHttp 自带缓存机制,Retrofit 完美继承,我们通过拦截器来实现在线缓存和离线缓存。
配置缓存
先为 OkHttp 设置缓存目录和大小:
import okhttp3.Cache;
import java.io.File;File cacheDir = new File(context.getCacheDir(), "http-cache");
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024); // 10MB 缓存
然后将缓存加入 OkHttpClient:
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();
实现缓存拦截器
我们需要两种缓存策略:
在线缓存:有网时,控制缓存有效期(比如 20 秒)。
离线缓存:没网时,读取缓存,即使过期也能用(比如 4 周)。
以下是缓存拦截器的实现:
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;public class CacheInterceptor implements Interceptor {private final Context context;public CacheInterceptor(Context context) {this.context = context;}@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();boolean isOnline = isNetworkAvailable(context);if (!isOnline) {// 离线时,强制使用缓存request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();}Response response = chain.proceed(request);if (isOnline) {// 在线时,设置缓存有效期为 20 秒int maxAge = 20;response = response.newBuilder().header("Cache-Control", "public, max-age=" + maxAge).removeHeader("Pragma").build();} else {// 离线时,允许使用过期缓存,最长 4 周int maxStale = 4 * 7 * 24 * 60 * 60; // 4 周response = response.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale).removeHeader("Pragma").build();}return response;}private boolean isNetworkAvailable(Context context) {ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);NetworkInfo info = cm.getActiveNetworkInfo();return info != null && info.isConnected();}
}
集成到 Retrofit
将缓存拦截器加入 OkHttpClient:
OkHttpClient client = new OkHttpClient.Builder().cache(cache).addInterceptor(new CacheInterceptor(context)).addNetworkInterceptor(new CacheInterceptor(context)).build();Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).client(client).build();
代码解析:
在线缓存:通过 max-age 设置缓存有效期,20 秒内重复请求直接用缓存,减少服务器压力。
离线缓存:通过 only-if-cached 和 max-stale 允许使用过期缓存,适合新闻列表、用户信息等不频繁更新的数据。
网络状态检查:用 ConnectivityManager 判断是否有网,动态调整缓存策略。
实际效果
有网时:首次请求从服务器获取数据并缓存,20 秒内重复请求直接读缓存,响应时间从 200ms 降到 10ms。
没网时:读取缓存数据,用户依然能看到内容,比如离线浏览新闻。
小技巧:
用 @Header("Cache-Control") 动态设置每个接口的缓存策略,灵活应对不同业务需求。
定期清理缓存目录,防止占用过多存储空间。
5. OkHttp vs Volley:性能、易用性和扩展性大 PK
Retrofit 依赖 OkHttp,那 OkHttp 和 Volley 比起来谁更强?我们从性能、易用性和扩展性三个维度来分析,帮你选出最适合的网络库。
性能:OkHttp 的硬核优势
OkHttp 是 Square 公司打造的高性能 HTTP 客户端,基于 NIO 和 Okio,效率远超传统的 HttpURLConnection。它的杀手锏包括:
连接池:复用 TCP 连接,减少握手时间。
HTTP/2 支持:多路复用,多个请求共用一个连接,速度飞起。
GZIP 压缩:自动解压响应数据,节省流量。
缓存机制:内置 DiskLruCache,支持高效的响应缓存。
Volley 则更轻量,适合小型请求,但性能上有短板:
默认基于 HttpURLConnection(API 23 后支持 OkHttp,但需要额外配置)。
内存缓存(LruCache)为主,不支持大文件上传。
不支持 HTTP/2,连接效率不如 OkHttp。
结论:OkHttp 在性能上完胜,尤其适合高并发、大数据量的场景。Volley 更适合轻量级、频繁的小请求,比如加载图片或简单 API。
易用性:Volley 的“保姆式”封装
Volley 是 Google 推出的网络请求框架,设计目标是“简单易用”:
内置线程管理:自动处理线程切换,开发者无需操心。
请求队列:内置队列管理,适合频繁的小请求。
图片加载:自带 ImageLoader,适合快速开发。
但 Volley's 易用性也有代价:
配置复杂,比如想换 OkHttp 作为底层客户端,得自己写适配器。
不支持同步请求,限制了某些场景。
API 设计较老,扩展性不如 Retrofit。
OkHttp 的 API 更底层,灵活但需要更多手动配置。Retrofit 结合 OkHttp,通过注解和接口极大简化了开发,比如:
public interface ApiService {@GET("users/{id}")Call<User> getUser(@Path("id") String id);
}
结论:Volley 适合快速上手的小项目,Retrofit+OkHttp 更适合需要长期维护的大型项目。
扩展性:Retrofit+OkHttp 的无敌组合
Retrofit 基于 OkHttp,继承了其所有优点,同时通过注解和拦截器提供了强大的扩展能力:
RxJava 集成:支持响应式编程,异步处理更优雅。
自定义拦截器:如上文所示,日志、重试、缓存随意扩展。
动态代理:接口式编程,代码简洁且类型安全。
Volley 的扩展性稍显不足:
依赖 Apache HttpClient(API 23 后废弃),迁移成本高。
自定义功能需要改动核心代码,维护麻烦。
结论:Retrofit+OkHttp 的扩展性碾压 Volley,适合复杂业务场景。
6. RxJava + Retrofit:异步处理的“魔法组合”
网络请求的异步处理,写得不好就是一团乱麻,动不动就回调地狱,代码维护起来跟解谜似的。Retrofit 结合 RxJava,简直是异步处理的“魔法组合”,不仅代码优雅,还能轻松应对复杂的业务逻辑。让我们来拆解怎么用 RxJava 让你的网络请求更丝滑!
为什么选 RxJava?
RxJava 的核心是响应式编程,通过 Observable 和 Operator 链式处理数据流,完美适配 Retrofit 的 Call 机制。它的优势有:
链式调用:请求、转换、处理一气呵成,代码简洁得像艺术品。
线程切换:轻松在主线程和 IO 线程间切换,不用自己操心 Handler 或 AsyncTask。
错误处理:统一处理异常,告别 try-catch 满天飞。
组合操作:多个请求并行或串行,轻松实现复杂逻辑。
配置 RxJava 支持
首先,在 build.gradle 中添加 RxJava 和 Retrofit 的 RxJava 适配器:
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
注意:确保版本与 Retrofit 兼容,RxJava 2 和 RxJava 3 不兼容,选错了会报错。
定义 RxJava 接口
Retrofit 支持将返回类型设置为 Observable,示例如下:
import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Path;public interface ApiService {@GET("users/{id}")Observable<User> getUser(@Path("id") String id);
}
集成到 Retrofit
配置 Retrofit 时,加入 RxJava 适配器:
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;public class NetworkClient {private static Retrofit retrofit;public static Retrofit getRetrofit() {if (retrofit == null) {retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build();}return retrofit;}
}
实战:链式处理用户数据
假设我们要获取用户信息,然后根据用户 ID 获取他的订单列表,最后显示在 UI 上。用 RxJava 实现,代码简洁得让人想鼓掌:
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;ApiService api = NetworkClient.getRetrofit().create(ApiService.class);
api.getUser("1001").subscribeOn(Schedulers.io()) // 网络请求在 IO 线程.flatMap(user -> api.getOrders(user.id)) // 获取订单.observeOn(AndroidSchedulers.mainThread()) // 切回主线程.subscribe(orders -> {// 更新 UIupdateUI(orders);},throwable -> {// 处理错误showError(throwable.getMessage());});
代码解析:
subscribeOn(Schedulers.io()):网络请求跑在 IO 线程,避免阻塞主线程。
flatMap:将用户信息转换为订单数据的 Observable,实现请求串联。
observeOn(AndroidSchedulers.mainThread()):结果回到主线程,更新 UI。
错误处理:统一在 subscribe 的错误回调中处理,简洁又清晰。
高级用法:并行请求
假如需要同时获取用户信息和订单列表,再合并结果,RxJava 的 zip 操作符派上用场:
Observable.zip(api.getUser("1001").subscribeOn(Schedulers.io()),api.getOrders("1001").subscribeOn(Schedulers.io()),(user, orders) -> new UserWithOrders(user, orders)
)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(userWithOrders -> {// 更新 UIupdateUI(userWithOrders);},throwable -> {showError(throwable.getMessage());}
);
效果:两个请求并行执行,完成后合并结果,效率翻倍!
注意事项
内存泄漏:别忘了在 Activity 销毁时取消订阅,用 Disposable 或 CompositeDisposable 管理。
错误重试:可以用 retryWhen 操作符实现类似拦截器的重试逻辑。
背压问题:大数据量时,考虑用 Flowable 替代 Observable。
彩蛋:RxJava 的 debounce 和 throttleFirst 操作符还能防抖,适合搜索框输入实时查询,防止频繁请求把服务器干崩。
7. 动态缓存策略:按业务需求“量身定制”
上文提到了基础缓存拦截器,但实际业务中,缓存需求千变万化:新闻列表可能 1 分钟刷新一次,用户资料可能 1 天更新,图片资源可能永久缓存。动态缓存策略让你根据接口或业务场景灵活调整缓存时间,省流量又提速。
动态缓存的思路
我们可以通过以下方式实现动态缓存:
注解控制:用自定义注解为每个接口指定缓存时间。
拦截器判断:根据请求 URL 或注解动态设置 Cache-Control。
业务逻辑结合:根据数据类型或用户状态调整缓存策略。
实现自定义缓存注解
定义一个注解,用于指定接口的缓存时间:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheTime {int maxAge() default 0; // 在线缓存时间(秒)int maxStale() default 0; // 离线缓存时间(秒)
}
修改接口定义
为 API 接口添加缓存注解:
public interface ApiService {@GET("news")@CacheTime(maxAge = 60, maxStale = 7 * 24 * 60 * 60) // 新闻缓存 1 分钟,离线 7 天Observable<List<News>> getNews();@GET("user/{id}")@CacheTime(maxAge = 24 * 60 * 60, maxStale = 30 * 24 * 60 * 60) // 用户资料缓存 1 天,离线 30 天Observable<User> getUser(@Path("id") String id);
}
动态缓存拦截器
改写缓存拦截器,根据注解动态设置 Cache-Control:
import okhttp3.Interceptor;
import okhttp3.Response;
import retrofit2.Invocation;
import java.io.IOException;public class DynamicCacheInterceptor implements Interceptor {private final Context context;public DynamicCacheInterceptor(Context context) {this.context = context;}@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();boolean isOnline = isNetworkAvailable(context);// 获取注解Invocation invocation = request.tag(Invocation.class);CacheTime cacheTime = null;if (invocation != null) {cacheTime = invocation.method().getAnnotation(CacheTime.class);}if (!isOnline) {// 离线:强制使用缓存int maxStale = (cacheTime != null && cacheTime.maxStale() > 0) ? cacheTime.maxStale() : 4 * 7 * 24 * 60 * 60; // 默认 4 周request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale).build();}Response response = chain.proceed(request);if (isOnline) {// 在线:设置缓存时间int maxAge = (cacheTime != null && cacheTime.maxAge() > 0) ? cacheTime.maxAge() : 20; // 默认 20 秒response = response.newBuilder().header("Cache-Control", "public, max-age=" + maxAge).removeHeader("Pragma").build();}return response;}private boolean isNetworkAvailable(Context context) {ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);NetworkInfo info = cm.getActiveNetworkInfo();return info != null && info.isConnected();}
}
集成到 Retrofit
确保 Retrofit 支持注解解析:
OkHttpClient client = new OkHttpClient.Builder().cache(new Cache(new File(context.getCacheDir(), "http-cache"), 10 * 1024 * 1024)).addInterceptor(new DynamicCacheInterceptor(context)).addNetworkInterceptor(new DynamicCacheInterceptor(context)).build();Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(client).build();
代码解析:
注解解析:通过 request.tag(Invocation.class) 获取接口方法上的 CacheTime 注解。
动态缓存:根据注解的 maxAge 和 maxStale 设置缓存时间,灵活适配不同接口。
默认值:如果接口没加注解,用默认值(在线 20 秒,离线 4 周)。
实战效果
新闻接口:在线时每分钟刷新,离线也能看 7 天前的新闻。
用户资料:在线时每天更新,离线 30 天有效,适合不频繁变更的数据。
性能提升:缓存命中率提高,网络请求量减少 30%-50%(视业务而定)。
小技巧:
用 Cache-Control: no-cache 强制服务器验证缓存,适合对实时性要求高的接口。
定期清理缓存目录,比如每月检查一次,释放存储空间。
8. Token 自动刷新:无缝认证不掉链子
登录接口返回的 token 总有过期的时候,如果每次都让用户重新登录,体验得差到爆。Token 自动刷新用拦截器就能搞定,悄无声息地保持用户在线,丝滑得像没断过!
Token 刷新流程
检测 401(未授权)错误。
调用刷新 token 的接口获取新 token。
保存新 token,重试原请求。
实现 Token 刷新拦截器
以下是完整实现:
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;public class TokenRefreshInterceptor implements Interceptor {private final TokenManager tokenManager; // 假设有类管理 tokenpublic TokenRefreshInterceptor(TokenManager tokenManager) {this.tokenManager = tokenManager;}@Overridepublic Response intercept(Chain chain) throws IOException {Request originalRequest = chain.request();Response response = chain.proceed(originalRequest);// 检测 401 错误if (response.code() == 401) {synchronized (this) { // 防止多线程重复刷新String currentToken = tokenManager.getToken();String newToken = refreshToken(); // 调用刷新接口if (newToken != null) {tokenManager.saveToken(newToken); // 保存新 tokenresponse.close(); // 关闭旧响应// 重构请求,添加新 tokenRequest newRequest = originalRequest.newBuilder().header("Authorization", "Bearer " + newToken).build();return chain.proceed(newRequest);}}}return response;}private String refreshToken() throws IOException {// 假设同步调用刷新 token 接口Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).build();AuthService authService = retrofit.create(AuthService.class);Call<AuthResponse> call = authService.refreshToken(tokenManager.getRefreshToken());Response<AuthResponse> response = call.execute();return response.isSuccessful() ? response.body().getToken() : null;}
}
TokenManager 和 AuthService
简单的 token 管理类和刷新接口定义:
public class TokenManager {private SharedPreferences prefs;public TokenManager(Context context) {prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE);}public String getToken() {return prefs.getString("access_token", null);}public String getRefreshToken() {return prefs.getString("refresh_token", null);}public void saveToken(String token) {prefs.edit().putString("access_token", token).apply();}
}public interface AuthService {@POST("refresh")Call<AuthResponse> refreshToken(@Body String refreshToken);
}public class AuthResponse {private String token;public String getToken() { return token; }
}
集成到 Retrofit
将 Token 刷新拦截器加入:
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new TokenRefreshInterceptor(new TokenManager(context))).build();Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).client(client).build();
代码解析:
同步锁:用 synchronized 避免多线程同时刷新 token,导致重复请求。
刷新逻辑:401 时调用刷新接口,获取新 token 后重试原请求。
安全性:token 存储在 SharedPreferences,生产环境建议用加密存储。
注意事项
刷新失败:如果刷新 token 失败,引导用户重新登录。
循环依赖:刷新接口本身不要触发 401,需单独配置(比如用独立的 OkHttpClient)。
性能:频繁刷新可能增加服务器压力,优化 refresh token 的有效期。
效果:用户完全感知不到 token 过期,体验无缝,App 像“永不掉线”一样!
9. OkHttp vs Volley:文件上传与图片加载的硬核对决
OkHttp 和 Volley 都是 Android 网络请求的“老大哥”,但在特定场景下,比如文件上传和图片加载,谁的表现更胜一筹?我们来把它们拉出来,针对这两个常见需求比一比,看看哪个更适合你的项目!
文件上传:OkHttp 的灵活 vs Volley 的简便
OkHttp 的文件上传
OkHttp 提供了强大的 MultipartBody 来处理文件上传,灵活得像个“变形金刚”。无论是上传图片、视频,还是多文件混杂,都能轻松搞定。
代码示例:上传一张图片和一些表单数据:
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.File;
import java.io.IOException;public class FileUploadClient {private static final String API_URL = "https://api.example.com/upload";public static void uploadFile(File file, String userId) throws IOException {OkHttpClient client = new OkHttpClient();// 创建 MultipartBodyRequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("userId", userId).addFormDataPart("file", file.getName(),RequestBody.create(MediaType.parse("image/jpeg"), file)).build();// 构建请求Request request = new Request.Builder().url(API_URL).post(requestBody).build();// 执行请求try (Response response = client.newCall(request).execute()) {if (response.isSuccessful()) {System.out.println("Upload successful: " + response.body().string());} else {System.out.println("Upload failed: " + response.code());}}}
}
优点:
灵活性:支持多文件、混合表单,轻松应对复杂上传需求。
进度监听:可以通过自定义 RequestBody 实现上传进度回调。
性能:基于 OkHttp 的连接池和 HTTP/2,上传大文件更高效。
缺点:代码稍显底层,需要手动处理请求和响应,初学者可能觉得麻烦。
Volley 的文件上传
Volley 的文件上传相对简单,内置了 MultipartRequest(需额外实现),适合快速开发。
代码示例:实现简单的图片上传:
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.toolbox.Volley;
import java.io.File;public class MultipartRequest extends Request<String> {private final Response.Listener<String> listener;private final File file;private final String userId;public MultipartRequest(String url, File file, String userId,Response.Listener<String> listener, Response.ErrorListener errorListener) {super(Method.POST, url, errorListener);this.file = file;this.userId = userId;this.listener = listener;}@Overrideprotected Response<String> parseNetworkResponse(NetworkResponse response) {return Response.success(new String(response.data), null);}@Overrideprotected void deliverResponse(String response) {listener.onResponse(response);}@Overridepublic String getBodyContentType() {return "multipart/form-data; boundary=----WebKitFormBoundary";}@Overridepublic byte[] getBody() {// 手动构造 multipart 数据,代码略复杂,实际需完善// 这里仅为示例,建议使用第三方库return ("------WebKitFormBoundary\r\n" +"Content-Disposition: form-data; name=\"userId\"\r\n\r\n" + userId + "\r\n" +"------WebKitFormBoundary\r\n" +"Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"\r\n" +"Content-Type: image/jpeg\r\n\r\n" +fileToBytes(file) + "\r\n" +"------WebKitFormBoundary--\r\n").getBytes();}private byte[] fileToBytes(File file) {// 实现文件转字节,略return new byte[0];}
}// 使用
RequestQueue queue = Volley.newRequestQueue(context);
MultipartRequest request = new MultipartRequest("https://api.example.com/upload",new File("/path/to/image.jpg"),"1001",response -> System.out.println("Upload success: " + response),error -> System.out.println("Upload failed: " + error.getMessage())
);
queue.add(request);
优点:
简单易用:Volley 的请求队列管理省心,适合快速开发。
内置线程:自动处理线程切换,响应直接回调到主线程。
缺点:
扩展性差:Volley 没有原生支持 MultipartBody,需要手动构造 multipart 数据,代码繁琐。
性能局限:不支持 HTTP/2,上传大文件效率较低。
进度监控:实现上传进度需要额外 hack,麻烦。
结论
OkHttp:适合大文件上传、需要进度监听或复杂表单的场景,性能和灵活性占优。
Volley:适合小型文件上传或快速原型开发,但扩展性和性能稍逊。
图片加载:Volley 的“老本行” vs OkHttp 的“硬核改造”
Volley 的图片加载
Volley 专为图片加载设计,内置 ImageLoader 和 NetworkImageView,简直是“开箱即用”。
代码示例:
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.NetworkImageView;RequestQueue queue = Volley.newRequestQueue(context);
ImageLoader imageLoader = new ImageLoader(queue, new ImageLoader.ImageCache() {private final LruCache<String, Bitmap> cache = new LruCache<>(20);@Overridepublic Bitmap getBitmap(String url) {return cache.get(url);}@Overridepublic void putBitmap(String url, Bitmap bitmap) {cache.put(url, bitmap);}
});NetworkImageView imageView = findViewById(R.id.imageView);
imageView.setImageUrl("https://example.com/image.jpg", imageLoader);
优点:
简单集成:NetworkImageView 直接绑定 URL,缓存和加载全自动。
内存缓存:内置 LruCache,适合频繁加载小图。
轻量:适合简单图片加载场景,比如新闻列表缩略图。
缺点:
磁盘缓存弱:Volley 默认只支持内存缓存,磁盘缓存需额外实现。
定制性差:想加高级功能(比如渐进加载)得大改代码。
OkHttp 的图片加载
OkHttp 本身不提供图片加载功能,但结合 Glide 或 Picasso(两者都支持 OkHttp 作为网络层),效果吊打 Volley。
代码示例(用 Glide + OkHttp):
import com.bumptech.glide.Glide;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
import com.bumptech.glide.load.model.GlideUrl;
import okhttp3.OkHttpClient;
import java.io.InputStream;// 配置 Glide 使用 OkHttp
OkHttpClient client = new OkHttpClient.Builder().build();
Glide.get(context).getRegistry().replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));// 加载图片
ImageView imageView = findViewById(R.id.imageView);
Glide.with(context).load("https://example.com/image.jpg").thumbnail(0.25f).diskCacheStrategy(DiskCacheStrategy.ALL).into(imageView);
优点:
性能强:OkHttp 的连接池和 HTTP/2 让图片加载更快。
缓存强:Glide/Picasso 提供内存+磁盘缓存,缓存策略更灵活。
功能丰富:支持渐进加载、占位图、错误图等高级功能。
缺点:需要额外集成 Glide 或 Picasso,配置稍复杂。
结论
Volley:适合简单图片加载,快速上手,但功能和性能有限。
OkHttp + Glide/Picasso:适合复杂场景(大图、列表滑动优化),性能和扩展性完胜。
10. 实战案例:分页加载与错误重试的完美结合
分页加载是 App 中常见的场景,比如新闻列表、商品列表,用户一滑到底,数据得源源不断加载。结合错误重试机制,我们能让分页加载既高效又稳定,用户体验直接起飞!
分页加载的实现
我们用 Retrofit + RxJava 实现分页加载,自动处理下一页请求,并结合重试机制应对网络抖动。
接口定义:
public interface ApiService {@GET("news")Observable<NewsResponse> getNews(@Query("page") int page, @Query("size") int size);
}public class NewsResponse {private List<News> news;private int totalPages;public List<News> getNews() { return news; }public int getTotalPages() { return totalPages; }
}
分页加载逻辑:
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;public class NewsRepository {private final ApiService api;private int currentPage = 1;private final int pageSize = 20;private boolean hasMore = true;public NewsRepository(ApiService api) {this.api = api;}public Observable<List<News>> loadNextPage() {if (!hasMore) {return Observable.just(Collections.emptyList());}return api.getNews(currentPage, pageSize).subscribeOn(Schedulers.io()).doOnNext(response -> {currentPage++;hasMore = currentPage <= response.getTotalPages();}).map(NewsResponse::getNews).retryWhen(errors -> errors.zipWith(Observable.range(1, 3), (error, retryCount) -> retryCount).flatMap(retryCount -> Observable.timer((long) Math.pow(2, retryCount), TimeUnit.SECONDS))).observeOn(AndroidSchedulers.mainThread());}
}
使用示例(在 Activity/Fragment 中):
NewsRepository repository = new NewsRepository(NetworkClient.getRetrofit().create(ApiService.class));
repository.loadNextPage().subscribe(newsList -> {// 更新 UIadapter.addNews(newsList);},throwable -> {// 显示错误Toast.makeText(context, "加载失败:" + throwable.getMessage(), Toast.LENGTH_SHORT).show();});
代码解析:
分页逻辑:通过 page 和 size 参数控制分页,totalPages 判断是否有下一页。
RxJava 重试:用 retryWhen 实现指数退避重试,最多重试 3 次,间隔 1s、2s、4s。
线程切换:网络请求在 IO 线程,UI 更新在主线程,流畅不卡顿。
优化用户体验
加载动画:加载下一页时显示 ProgressBar,提示用户数据在路上。
错误提示:重试失败后,显示“点击重试”按钮,让用户手动触发。
缓存结合:结合上文的动态缓存策略,离线时也能显示已缓存的新闻。
效果:用户滑动列表时,数据无缝加载,网络抖动时自动重试,离线也能看缓存,体验满分!
11. 请求限流:别把服务器“锤爆”
网络请求优化不仅要快,还要稳。如果用户疯狂刷新,或者 App 短时间内发起大量请求,服务器可能直接“跪”。请求限流拦截器能帮你控制请求频率,保护服务器,也让 App 更优雅。
限流思路
我们用令牌桶算法实现限流:每秒生成固定数量的“令牌”,请求前必须获取令牌,没令牌就得等。
实现限流拦截器
以下是一个简单的令牌桶限流拦截器:
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
import java.util.concurrent.TimeUnit;public class RateLimitInterceptor implements Interceptor {private final long tokensPerSecond; // 每秒令牌数private final long maxTokens; // 令牌桶容量private long availableTokens;private long lastRefillTimestamp;public RateLimitInterceptor(long tokensPerSecond, long maxTokens) {this.tokensPerSecond = tokensPerSecond;this.maxTokens = maxTokens;this.availableTokens = maxTokens;this.lastRefillTimestamp = System.nanoTime();}@Overridepublic Response intercept(Chain chain) throws IOException {synchronized (this) {// 补充令牌refillTokens();// 检查是否有令牌if (availableTokens < 1) {// 等待直到有令牌while (availableTokens < 1) {try {Thread.sleep(100);refillTokens();} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new IOException("Interrupted while waiting for token");}}}// 消耗一个令牌availableTokens--;}return chain.proceed(chain.request());}private void refillTokens() {long now = System.nanoTime();long elapsedNanos = now - lastRefillTimestamp;long newTokens = elapsedNanos * tokensPerSecond / 1_000_000_000L;availableTokens = Math.min(maxTokens, availableTokens + newTokens);lastRefillTimestamp = now;}
}
集成到 Retrofit
将限流拦截器加入 OkHttpClient:
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new RateLimitInterceptor(5, 10)) // 每秒5个请求,桶容量10.build();Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).client(client).build();
代码解析:
令牌桶:每秒生成 5 个令牌,桶容量 10 个,控制请求频率。
同步锁:用 synchronized 保证多线程安全。
动态补充:根据时间差计算新令牌,保持限流精准。
实际效果
服务器保护:限制每秒 5 个请求,防止服务器过载。
用户体验:请求稍有延迟但不卡死,体验比“服务器不可用”强多了。
适用场景:适合高并发场景,比如抢购、排行榜刷新。
小技巧:
可以根据接口优先级设置不同限流规则,比如支付接口优先级高,限流宽松。
结合 RxJava 的 debounce 操作符,减少用户重复点击触发的请求。
12. 性能监控拦截器:把请求耗时“掐得死死的”
网络请求优化不光要快,还要“心中有数”。请求到底花了多少时间?哪个接口拖了后腿?性能监控拦截器就像给你的 App 装了个“计时器”,帮你把每个请求的耗时、成功率等关键指标抓出来,调试和优化都不再抓瞎!
性能监控的核心指标
我们要监控啥?以下几个指标最关键:
请求耗时:从发出请求到收到响应的总时间。
响应状态:成功(200-299)还是失败(比如 404、500)。
请求大小:请求和响应的数据量,帮你发现流量“黑洞”。
失败原因:网络超时、服务器错误还是参数问题?
实现性能监控拦截器
下面是一个性能监控拦截器,记录请求耗时和状态:
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.logging.Logger;public class PerformanceInterceptor implements Interceptor {private static final Logger logger = Logger.getLogger(PerformanceInterceptor.class.getName());@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();long startTime = System.nanoTime();// 执行请求Response response;try {response = chain.proceed(request);} catch (IOException e) {logger.severe("Request failed: " + request.url() + ", error: " + e.getMessage());throw e;}// 计算耗时long duration = (System.nanoTime() - startTime) / 1_000_000; // 转换为毫秒String status = response.isSuccessful() ? "Success" : "Failed (" + response.code() + ")";long responseSize = response.body() != null ? response.body().contentLength() : 0;// 记录日志logger.info(String.format("Request: %s\nStatus: %s\nDuration: %dms\nResponse Size: %d bytes",request.url(), status, duration, responseSize));return response;}
}
集成到 Retrofit
将性能监控拦截器加入 OkHttpClient:
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new PerformanceInterceptor()).build();Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").addConverterFactory(GsonConverterFactory.create()).client(client).build();
代码解析:
耗时计算:用 System.nanoTime() 精确记录请求开始和结束时间,单位转为毫秒。
日志输出:记录 URL、状态、耗时和响应大小,方便分析。
异常捕获:即使请求失败,也记录错误信息,方便排查。
优化:集成到监控平台
开发阶段打印日志就够了,但生产环境得把数据送到监控平台(比如 Firebase 或自建服务)。改进版拦截器:
public class AdvancedPerformanceInterceptor implements Interceptor {private final MonitoringService monitoringService; // 假设的监控服务public AdvancedPerformanceInterceptor(MonitoringService monitoringService) {this.monitoringService = monitoringService;}@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();long startTime = System.nanoTime();Response response;String errorMessage = null;try {response = chain.proceed(request);} catch (IOException e) {errorMessage = e.getMessage();throw e;} finally {long duration = (System.nanoTime() - startTime) / 1_000_000;String status = errorMessage != null ? "Failed (" + errorMessage + ")" : (response.isSuccessful() ? "Success" : "Failed (" + response.code() + ")");long responseSize = response != null && response.body() != null ? response.body().contentLength() : 0;// 上传到监控平台monitoringService.logRequestMetrics(request.url().toString(),status,duration,responseSize);}return response;}
}
效果:
开发调试:日志清晰展示每个请求的耗时和状态,快速定位慢接口。
生产监控:数据上传到监控平台,生成图表分析接口性能。
优化依据:发现耗时超过 500ms 的接口,优先优化缓存或服务器逻辑。
小技巧:
用 response.peekBody(1024) 预览响应内容(不消耗 Body),记录关键字段。
对高频接口单独统计,找出流量大户,针对性优化。
13. 实战案例:文件断点续传,OkHttp 的“硬核操作”
文件下载是大文件场景的常见需求,比如 App 更新包、视频文件。如果网络中断,重新下载太浪费时间和流量。断点续传让下载从中断处继续,OkHttp 的流处理能力让这事变得简单又高效!
断点续传原理
Range 请求:通过 HTTP 的 Range 头指定下载的字节范围。
文件记录:记录已下载的字节数,网络恢复后从该位置继续。
进度反馈:实时更新下载进度,优化用户体验。
实现断点续传
以下是一个支持断点续传的文件下载器:
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;public class FileDownloader {private final OkHttpClient client = new OkHttpClient();private final File downloadDir;public FileDownloader(File downloadDir) {this.downloadDir = downloadDir;}public void downloadFile(String url, String fileName, ProgressListener listener) throws IOException {File file = new File(downloadDir, fileName);long downloadedBytes = file.exists() ? file.length() : 0;// 构造 Range 请求Request request = new Request.Builder().url(url).header("Range", "bytes=" + downloadedBytes + "-").build();try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful() && response.code() != 206) {throw new IOException("Unexpected code: " + response.code());}long totalBytes = downloadedBytes + response.body().contentLength();try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {raf.seek(downloadedBytes); // 从已下载位置开始写入byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = response.body().byteStream().read(buffer)) != -1) {raf.write(buffer, 0, bytesRead);downloadedBytes += bytesRead;// 通知进度if (listener != null) {listener.onProgress(downloadedBytes, totalBytes);}}}}}public interface ProgressListener {void onProgress(long downloadedBytes, long totalBytes);}
}
使用示例
在 Activity 中调用,并显示下载进度:
FileDownloader downloader = new FileDownloader(context.getCacheDir());
downloader.downloadFile("https://example.com/video.mp4","video.mp4",(downloadedBytes, totalBytes) -> {int progress = (int) (downloadedBytes * 100 / totalBytes);progressBar.setProgress(progress);textView.setText("下载进度:" + progress + "%");}
);
代码解析:
Range 头:通过 bytes=downloadedBytes- 指定从已下载位置开始。
RandomAccessFile:支持随机写入,适合断点续传。
进度回调:实时计算下载百分比,更新 UI。
优化:结合 RxJava
用 RxJava 封装下载逻辑,异步处理更优雅:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;public Observable<Integer> downloadFile(String url, String fileName, File downloadDir) {return Observable.create(emitter -> {FileDownloader downloader = new FileDownloader(downloadDir);downloader.downloadFile(url, fileName, (downloadedBytes, totalBytes) -> {int progress = (int) (downloadedBytes * 100 / totalBytes);emitter.onNext(progress);});emitter.onComplete();}).subscribeOn(Schedulers.io());
}
使用:
downloadFile("https://example.com/video.mp4", "video.mp4", context.getCacheDir()).observeOn(AndroidSchedulers.mainThread()).subscribe(progress -> {progressBar.setProgress(progress);textView.setText("下载进度:" + progress + "%");},throwable -> {Toast.makeText(context, "下载失败:" + throwable.getMessage(), Toast.LENGTH_SHORT).show();},() -> {Toast.makeText(context, "下载完成!", Toast.LENGTH_SHORT).show();});
效果:
断点续传:网络中断后,自动从上次位置继续下载。
用户体验:进度条实时更新,失败时提示重试。
性能:OkHttp 的流处理效率高,适合大文件。
小技巧:
用数据库或 SharedPreferences 记录下载任务,App 重启也能恢复。
对大文件分片下载,结合多线程提高速度(需服务器支持)。
14. OkHttp、Volley、Retrofit 的场景化最佳实践
经过前面章节的拆解,OkHttp、Volley 和 Retrofit 的优劣势已经很清晰了。现在我们来归纳一下,不同场景下该选谁,让你的技术选型不踩坑!
小型项目:Volley 的快速上手
场景:快速开发原型、简单 API 调用、图片加载。
推荐理由:Volley 的请求队列和 ImageLoader 让开发像搭积木一样简单,适合小团队或 MVP 阶段。
注意事项:避免大文件上传下载,磁盘缓存需额外实现。
示例:新闻 App 的缩略图加载、简单用户登录。
中大型项目:Retrofit + OkHttp 的王者组合
场景:复杂业务逻辑、需要缓存、重试、token 管理、异步处理。
推荐理由:Retrofit 的接口式编程简洁优雅,OkHttp 的高性能和拦截器机制支持无限扩展。
注意事项:学习曲线稍陡,需熟悉 RxJava 或协程。
示例:电商 App 的商品列表分页、订单提交、文件上传。
高性能需求:OkHttp 裸用
场景:超大文件下载、自定义协议、特殊网络需求。
推荐理由:OkHttp 提供底层控制,HTTP/2 和连接池让性能拉满。
注意事项:代码量较多,需手动处理线程和解析。
示例:视频流下载、WebSocket 实时通信。
综合建议
优先选 Retrofit + OkHttp:除非是超简单项目,否则这对组合几乎无敌,扩展性和性能兼得。
Volley 作备用:快速原型或图片加载需求,Volley 能省不少时间。
混用场景:可以用 OkHttp 作为 Volley 的底层网络层,提升性能。
彩蛋:如果项目用 Kotlin,Retrofit 结合协程会更香,代码更简洁,异步处理更直观!