当前位置: 首页 > news >正文

网络请求优化:用 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 刷新流程

  1. 检测 401(未授权)错误。

  2. 调用刷新 token 的接口获取新 token。

  3. 保存新 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 结合协程会更香,代码更简洁,异步处理更直观!

http://www.dtcms.com/a/356269.html

相关文章:

  • React前端开发_Day4
  • 华为HCIP数通学习与认证解析!
  • 基于STM32设计的智能宠物喂养系统(华为云IOT)_273
  • STM32F103C8T6的智能实验室危化品管理系统设计与华为云实现
  • Java 获取淘宝关键词搜索(item_search)API 接口实战指南
  • vue3+antd实现华为云OBS文件拖拽上传详解
  • 华为云CCE的Request和Limit
  • AI+云,双擎驱动——华为云让智能触手可及
  • Django Admin 管理工具
  • Java中协变逆变的实现与Kotlin中的区别
  • 如何用 Kotlin 在 Android 手机开发一个应用程序获取国家或地区信息
  • echo、seq、{}、date、bc命令
  • 如何用 Kotlin 在 Android 手机开发一个应用程序获取网络时间
  • OpenCV之霍夫变换
  • 在C++11中实现函数式编程的组合子
  • AI推介-大语言模型LLMs论文速览(arXiv方向):2025.04.25-2025.04.30
  • React Native 初体验
  • rabbitmq学习笔记 ----- 多级消息延迟始终为 20s 问题排查
  • OpenCV 图像预处理核心技术:阈值处理与滤波去噪
  • LubanCat-RK3568 UART串口通信,以及遇到bug笔记
  • CRYPT32!CryptMsgUpdate函数分析和asn.1 editor nt5inf.cat 的总览信息
  • 第八篇 永磁同步电机控制-MTPA、MTPV
  • 深入解析Qt节点编辑器框架:数据流转与扩展机制(三)
  • 实时音视频延迟优化指南:从原理到实践
  • 零知开源——基于STM32F407VET6和ADXL345三轴加速度计的精准运动姿态检测系统
  • Blender模拟结构光3D Scanner(三)获取相机观测点云的真值
  • OpenCV 基础知识总结
  • 无懈可击的 TCP AIMD
  • 亚马逊季节性产品运营策略:从传统到智能化的演进
  • kimi浏览器助手-月之暗面推出的智能浏览器扩展