使用OkHttp 缓存 API 调用提高Android应用性能
坦率地说,我们都遇到过这样的情况——焦急地刷新应用,看着加载图标不停地旋转,等待那个至关重要的 API 响应。这样的等待我们已经是炉火纯青了,是吧?手指有节奏地轻敲屏幕,微微转动着眼珠,嘴里不那么轻声地嘀咕着:“拜托,赶紧给我数据!”构建与外部 API 交互的应用时,缓存可谓是一项利器。API 调用往往速度缓慢、资源占用大,还受到 API 提供者的速率限制。但是,通过实施强大的缓存机制,你可以极大地减少 API 调用的次数、降低响应时间,并提供更加流畅的用户体验。解决重复 API 调用问题的方法之一就是在本地缓存/存储它们的响应。本文将介绍如何利用 OkHttp 库的 CacheControl 类来存储具有时间有效性的 API 响应。
实施缓存
定义缓存大小和实例
要能够将 API 调用的响应本地存储到缓存中,首先,我们需要定义缓存并通知客户端。在下面的代码片段中,我们使用 okhttp 库中的 Cache 类定义了缓存。我们将此缓存的最大大小设置为 5 MB。然后,在初始化 okhttpclient 参数时使用 cache()
函数。
//OkHttpClient.kt
// Defining a cache of 5 MB size
val cacheSize = (5 * 1024 * 1024).toLong()//Initializing instance of Cache class
val myCache = Cache(context.cacheDir, cacheSize)//defining okhttpclient instance
val okHttpClient = OkHttpClient.Builder().cache(myCache).build()
定义缓存规则
根据设备是否连接到互联网,让我们来定义一些基本的网络缓存规则:
- 如果设备连接到互联网:如果最后一次 API 响应是在不到 30 分钟之前检索的,则显示缓存的响应;否则,获取新的响应并将其存储在缓存中。
- 如果设备离线:使用最多 1 天前的 API 响应以保持应用程序功能。
你可以为你的项目定义不同且更为细致的规则来应对复杂的情况,但为了简单起见,我们将采用上述规则。
第一步是检查用户是否连接到互联网。为此,我们可以使用 ConnectivityManager
类收集数据并检查用户是否连接到互联网。让我们来定义一个名为 hasNetwork()
的函数,如下所示:
//CheckNetwork.kt
fun hasNetwork(context: Context): Boolean {val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManagerval nw = connectivityManager.activeNetwork ?: return falseval actNw = connectivityManager.getNetworkCapabilities(nw) ?: return falsereturn when {actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> trueactNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> trueactNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> trueactNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> trueelse -> false}
}
我们检查用户是否连接到WiFi、蜂窝移动网络或蓝牙网络,并根据情况返回true或false。
为了实现我们缓存规则的定义,我们将使用 OkHttp 库中的一个组件,叫做 Interceptor
。拦截器是一个强大的机制,允许您在应用程序发送或接收 HTTP 请求和响应之前拦截、处理甚至修改它们。它们在通信流程中起到中间人的作用,让您更加灵活地控制 OkHttp 处理网络请求的方式。
在拦截器中,我们将提供指导,告诉我们的 okHttpClient 实例何时使用缓存响应。
在构建客户端实例时,让我们使用 addInterceptor()
函数来添加拦截器。我们可以在这个函数中修改我们的 API 请求。在 lambda 函数中,我们可以获取请求对象,并在最终发送到服务器之前对其进行修改。为了添加缓存功能,我们使用 cacheControl()
函数,该函数接受 CacheControl
类的参数。
我们需要实现的第一条规则是,如果设备连接到互联网,则使用 30 分钟前的响应(如果有的话),否则获取新的响应。我们使用 maxAge() 函数来实现这一点。
我们需要实现的第二条规则是,如果设备断开连接,则使用最多 1 天前的响应。为此,我们使用 maxStale()
函数。
// Defining a cache of 5 MB size
val cacheSize = (5 * 1024 * 1024).toLong()//Initializing instance of Cache class
val myCache = Cache(context.cacheDir, cacheSize)//defining okhttpclient instance
val okHttpClient = OkHttpClient.Builder().cache(myCache).addInterceptor { chain ->var request = chain.request()request = if (hasNetwork(context))request.newBuilder().cacheControl(CacheControl.Builder().maxAge(30, TimeUnit.MINUTES).build()).build()elserequest.newBuilder().cacheControl(CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build()).build()chain.proceed(request)}.build()
maxAge() 与 maxStale() 的区别
max-age
和 max-stale
是 HTTP 缓存中用来控制响应新鲜度的指令。它们有着不同的含义:
-
max-age
:指定客户端认为缓存响应是新鲜的最大时间,以秒为单位。如果响应的年龄超过了max-age
,客户端将认为该响应已过期,并尝试从服务器获取新的响应。 -
max-stale
:告知客户端即使响应已过期,仍可接受,但仅在指定的最大过期时间内。同样以秒为单位。
以上是它们的区别。现在我们已经在 OkHttpClient 中实现了缓存系统,是时候为你的项目构建一个快速可靠的用户体验了。如果你使用 Retrofit 作为 OkHttp 的封装器,也可参考以下代码片段来实现缓存!
//RetrofitClient.kt
class RetrofitClient(private val context: Context) {val cacheSize = (5 * 1024 * 1024).toLong()val instance: Api by lazy {val myCache = Cache(context.cacheDir, cacheSize)val okHttpClient = OkHttpClient.Builder().cache(myCache).addInterceptor { chain ->var request = chain.request()request = if (hasNetwork(context))request.newBuilder().cacheControl(CacheControl.Builder().maxAge(30, TimeUnit.MINUTES).build()).build()elserequest.newBuilder().cacheControl(CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build()).build()chain.proceed(request)}.addInterceptor(HttpLoggingInterceptor().apply {this.level = HttpLoggingInterceptor.Level.BODY }).build()val retrofit = Retrofit.Builder().baseUrl("BASE_URL").addConverterFactory(GsonConverterFactory.create()).client(okHttpClient).build()retrofit.create(Api::class.java)}
}
现在,你可以开始在实际设备或模拟器上运行你的项目了。要检查缓存是否有效,请使用Android Studio中的App Inspection标签页,或者使用OkHttp的日志拦截器来记录所有网络调用。
结论
对于长时间不变的API响应,可以通过上述技术进行缓存,从而节省大量资源。软件开发始终涉及根据时间和空间做出一定的权衡。在这种情况下,我们为了节省时间而牺牲了一点空间(用于存储API响应)。