Android数据缓存框架 - 网络请求模块完全解析

### 序言

我们知道,要做数据的缓存肯定要有数据,而数据一般从后端接口获取。那么,就少不了网络请求。作为一个完善的数据缓存框架,肯定也要对网络请求层做一些基础的封装。我们Android开发中常用的网络框架有哪些?HttpURLConnection、OkHttp、Retrofit、NoHttp、Volley等。dcache-android框架基于了OkHttp和Retrofit,并改进了一些写法,使得开发起来更加优雅、得心应手。移动端的网络请求主要是GET和POST请求两种。在协议层,dcache-android框架并没有做相关的改动,而是采用了Retrofit+OkHttp的写法。所以使用之前,你应该先掌握这两个框架的使用。为了扫清知识障碍,我还是简单介绍下它们的基本用法。OkHttp作为网络库的始祖,主要是对网络协议层进行了封装。而Retrofit则是基于OkHttp在写法和调用上做了一些封装,它的一些注解和model数据的自动转换让代码更加简洁和优雅。

### 回顾OkHttp的使用

**依赖库**

```groovy
implementation("com.squareup.okhttp3:okhttp:4.8.0")
```

**创建OkHttpClient的对象**

```java
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.build();
```

**设置超时时间**

```java
builder.connectTimeout(3, TimeUnit.SECONDS);
builder.readTimeout(10, TimeUnit.SECONDS);
```

**添加拦截器**

```java
builder.addInterceptor(new RequestHeaderInterceptor());
builder.addNetworkInterceptor(new FormatLogInterceptor());
```
示例:请求头拦截器拦截token
```java
public class RequestHeaderInterceptor implements Interceptor {
    @NonNull
    @Override
    public Response intercept(@NonNull Chain chain) throws IOException {
        Request request = chain.request();
        Request.Builder requestBuilder = request.newBuilder();
        Headers headers = request.headers();
        if (headers != null) {
            Set<String> names = headers.names();
            for (String name : names) {
                String value = headers.get(name);
                // 将旧的请求头设置到新请求中
                requestBuilder.header(name, value);
            }
        }
        String token = SPUtils.readString(GlobalContext.get(), Constants.PREFS_TOKEN);
        if (token != null) {
            requestBuilder.header("Authorization", "Bearer " + token);
        }
        return chain.proceed(requestBuilder.build());
    }
}
```

**SSL证书相关**

```java
final TrustManager[] trustAllCerts = new TrustManager[] {
    new X509TrustManager() {
        @Override
        public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return new java.security.cert.X509Certificate[]{};
        }
    }
};        
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
// Create an ssl socket factory with our all-trusting manager
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
builder.hostnameVerifier((hostname, sslSession) -> true);
```

**发请求**

```java
Request request = new Request.Builder().url("http://localhost:8080/demo/").build();
try  {
    Response response = okHttpClient.newCall(request).execute()
    if (response.isSuccessful()) {
        ResponseBody body = response.body();
    } else {
        // 请求失败
    }
}
// 或者是以下写法
okHttpClient.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(@NonNull Call call, @NonNull IOException e) {
    }

    @Override
    public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
    }
});
```

### 回顾Retrofit的使用

**依赖库**

```groovy
implementation("com.squareup.retrofit2:retrofit:2.8.1")
implementation("com.squareup.retrofit2:converter-gson:2.8.1")
```

**创建Retrofit对象**

```java
Retrofit retrofit = new Retrofit.Builder()
// 设置网络请求的BaseUrl地址
.baseUrl("https://api.demo.com/") 
// 设置数据解析器 
.addConverterFactory(GsonConverterFactory.create()) 
.build();
```

**定义Get请求接口**

```java
public interface GetDemoService  {

    @GET("exampleGetRequest")
    Call<XxxResponse> exampleGetRequest(@Query("param1") String param1,
    @Query("param2") int param2);
}
```

**定义Form的Post请求接口**

```java
public interface PostDemoService  {

    @POST("examplePostRequest")
    @FormUrlEncoded
    Call<XxxResponse> examplePostRequest(@Field("param1") String param1,
    @Field("param2") int param2);
}
```

**定义Payload的Post请求接口**

```java
public interface PostDemoService  {

    @POST("examplePostRequest")
    Call<XxxResponse> examplePostRequest(@Body RequestBody body);
}
// model转body,也可以直接传model,把Content-Type添加到RequestHeaderInterceptor
RequestBody body = RequestBody.create(
        MediaType.parse("application/json; charset=utf-8"),
        new Gson().toJson(model)
);
```

**创建Retrofit的API接口实例,并请求数据**

```java
PostDemoService service = retrofit.create(PostDemoService.class);
service.examplePostRequest("倩倩", 18).enqueue(new Callback<XxxResponse>() {
    @Override
    public void onResponse(Call<XxxResponse> call, Response<XxxResponse> response) {
    }
);
```

### dcache框架的api请求

**依赖库**

```kotlin
implementation 'com.github.dora4:dcache-android:1.7.6'
```

**初识ApiService和RetrofitManager**

使用dcache框架发起api请求,跟Retrofit一样,你也要定义一个API接口类,和Retrofit的不同之处在于,你还要让接口继承ApiService接口,以告诉框架,这是一个dcache的Retrofit API接口,方便RetrofitManager进行管理。下面就牵扯出我们的另一个主角,RetrofitManager。RetrofitManager要先初始化配置才能使用。

kotlin的写法

```kotlin
RetrofitManager.init {
    okhttp {
        // add方法的返回值是boolean,所以调用了networkInterceptors还需要返回this
        networkInterceptors().add(FormatLogInterceptor())
        this
    }
    mappingBaseUrl(TestService::class.java, "http://api.k780.com")
    mappingBaseUrl(AccountService::class.java, "http://github.com/dora4")
}
```

java的写法

```java
// Java通过getConfig进行配置
builder.addNetworkInterceptor(new FormatLogInterceptor());
OkHttpClient okhttpClient = builder.build();
RetrofitManager.getConfig()
    .setClient(okhttpClient)
    .mappingBaseUrl(TestService.class, "http://api.k780.com")
    .mappingBaseUrl(AccountService.class, "http://github.com/dora4");
```

RetrofitManager的相关API

有以下几个重要的方法,重点看mappingBaseUrl,它是将dcache的Retrofit API服务跟Base URL建立映射关系的关键方法,重复映射给相同的服务会被覆盖,所以也可以通过它更换请求的服务器地址。

| RetrofitManager | 描述                                          |
| --------------- | ------------------------------------------- |
| checkService    | 检测一个API服务是否可用,如果不可用,则通过mappingBaseUrl()进行注册 |
| getService      | 获取API服务对象                                   |
| removeService   | 移除API服务对象                                   |
| mappingBaseUrl  | 给API服务绑定Base URL                            |

**发起请求**

kotlin的写法

```kotlin
RetrofitManager.getService(AccountService::class.java).getAccount()
    .enqueue(object : DoraCallback<Account>() {
      
    override fun onFailure(code: Int, msg: String?) {
    }
      
    override fun onSuccess(account: Account) {
    }
})
```

java的写法

```java
RetrofitManager.INSTANCE.getService(AccountService.class).getAccount()
    .enqueue(new DoraCallback<Account>() {
      
    @Overide  
    public void onFailure(int code, String msg) {
    }
      
    @Overide  
    public void onSuccess(Account account) {
    }
})
```

**dcache的kotlin协程请求**

dcache的协程请求方式。

```kotlin
net {
    val testRequest = try {
        api { RetrofitManager.getService(TestService::class.java).testRequest() }
    } catch (e: DoraHttpException) {
        Toast.makeText(this, e.toString(), Toast.LENGTH_SHORT).show()
    }
    val testRequest2 = api {
        RetrofitManager.getService(TestService::class.java).testRequest()
    }
    val testRequest3 = result {
        RetrofitManager.getService(TestService::class.java).testRequest()
    }
    request {
        // 你自己的网络请求
        var isSuccess = true
        if (isSuccess) {
            // 成功的回调里面要释放锁
            it.releaseLock()
        } else {
            // 失败的回调里面也要释放锁
            it.releaseLock()
        }      
        // 释放了锁后,request函数的代码执行就结束了,无论后面还有没有代码
        Log.e("这行代码不会被执行,你也可以释放锁来跳出循环,直接结束函数调用")
    }
    Toast.makeText(this, "$testRequest--$testRequest2--$testRequest3", Toast.LENGTH_SHORT).show()
}
```

首先在Activity或Fragment中创建一个net作用域,net作用域下有以下几个方法可以使用,api、rxApi、result、rxResult、request。这些方法在net作用域下会被按顺序执行,比如,你按顺序写了request、rxApi和result方法,那么先执行request,等锁释放后执行rxApi里面的代码,rxApi执行完自动释放锁,最后执行result方法,result也是自动释放锁。这种同步的方法相比传统的异步方式有其特有的优点,比如在启动页的时候可以不用估计跳转时间,写死,其实是最慢的。因为你要保险通常会多设置一点时间。它的第二个好处就是方便合并多个接口的数据了。当然也有一些弊端,比如只是设置数据的请求,同步的方式会比异步要慢。所以,你应该结合业务综合考量。它们都有哪些区别呢?rx开头的顾名思义,就是用在rxjava上的,详细点说就是用在Retrofit API返回数据为Observable时的。此处的api通常用在要捕获请求代码里面的异常时。而result则是在失败后直接返回null,简单粗暴,可以用于爬虫程序抓别人的数据资源,哈哈。涉及的相关法律问题跟本框架无关,后果自负。而request则是自己发起异步的网络请求,如HttpURLConnection等。需要注意的是,request并不会自动释放锁,如果忘记释放锁,则会导致net作用域下后面的方法无法被执行。request高阶函数会传入一个NetLock,通过调用it.releaseLock来释放锁。在releaseLock方法中可以指定返回的数据,直接可以在request块的外面被接收到。这样你可以很方便的在子线程请求,得到结果后调用releaseLock函数来将结果传回net作用域下的主线程。dcache已经自动帮你处理好了线程切换。主要是通过拦截Continuation。

```kotlin
package dora.http.coroutine

import android.app.Activity
import kotlin.Result
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext

/**
 * 线程切换。
 */
class ThreadSwitchContinuation<T>(private val activity: Activity, private val continuation : Continuation<T>) : Continuation<T> {

    override val context: CoroutineContext = continuation.context

    override fun resumeWith(result: Result<T>) {
        activity.runOnUiThread {
            continuation.resumeWith(result)
        }
    }
}
```

可以看到我们在退出那5种挂起函数之前,是可以随意在子线程中做一些操作,然后调用resumeWith()函数退出挂起函数后,也就是net作用域的代码也就自动回到了主线程,以便于直接拿到数据后,刷新UI界面。

**结合dcache的缓存进行网络请求,边请求边缓存**

```kotlin
package com.example.dcache.biz.weather

import android.content.Context
import android.content.res.AssetManager
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.dcache.R
import dora.cache.data.fetcher.OnLoadStateListener
import dora.http.DoraHttp.net
import dora.http.DoraHttp.request
import dora.http.log.FormatLogInterceptor
import dora.http.retrofit.RetrofitManager
import java.io.*

/**
 * 综合案例,演示一个天气预报功能,即使断网后也能再加载历史数据。
 */
class WeatherActivity : AppCompatActivity() {

    lateinit var repository: TemperatureRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_weather)
        repository = TemperatureRepository(this)
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = GridLayoutManager(this, 5)
        val adapter = TemperatureAdapter()
        recyclerView.adapter = adapter
        RetrofitManager.initConfig {
            okhttp {
                // 添加一个格式化日志输出的拦截器
                interceptors().add(FormatLogInterceptor())
                build()
            }
            // mappingBaseUrl取代了过时的registerBaseUrl,方便更换域名
            mappingBaseUrl(WeatherService::class.java,"https://api.caiyunapp.com/v2.5/Pezyxsyn6yccBaZd/")
        }
        repository.getListLiveData().observe(this, Observer {
            adapter.setTemperatures(it)
        })
        // 开辟一个net作用域来执行协程
        net {
            readAssetsText(this, "adcode_test.csv")
        }
     }

    @Throws(IOException::class)
    suspend fun readAssetsText(context: Context, fileName: String) {
        val assetManager: AssetManager = context.assets
        val bf = BufferedReader(InputStreamReader(assetManager.open(fileName)))
        var line: String? = ""
        while (bf.readLine().also { line = it } != null) {
            val value = line?.split(",")
            value?.let { data ->
                request {
                    Thread(Runnable {
                        repository.latlng = "${data[1]},${data[2]}"
                        repository.addr = data[6]
                        // 边加载和显示数据,边缓存数据到数据库,它的数据在onCreate的以下中代码被观察
                        // repository.getListLiveData().observe(this, Observer {
                        //    adapter.setTemperatures(it)
                        // })
                        repository.fetchListData("天气预报", object : OnLoadStateListener {
                            override fun onLoad(state: Int) {
                                Log.d("WeatherActivity", "数据是否加载成功:${state==0}")
                                // 无论成功与失败,3秒后播报下一个城市
                                SystemClock.sleep(3000)
                                // 解除阻塞
                                it.releaseLock()
                            })
                        }
                    ).start()        
                }
            }
        }
    }
}
```

我们可以看到,这里使用了Repository相关的东西。Repository通过调用fetchData或fetchListData请求网络接口数据并缓存起来,一个Repository对应的是一个model数据,通常情况下也仅对应一个接口。

### 开源项目地址

说了半天,连个Github的地址都不给,那等于说了个寂寞。所以在最后贴上开源库的地址,便于仔细研究源码。dcache缓存库地址:<https://github.com/dora4/dcache-android>。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
回答: Android中有多个数据缓存框架可供选择,其中一个常用的框架Android Glide。Glide是一个开源的图片加载和缓存处理的第三方框架,它内部已经实现了缓存策略,使得开发者可以更方便地加载和显示图片。使用Glide,开发者只需要几行简单明晰的代码,就可以完成大多数图片从网络或本地加载并显示的功能需求。Glide的缓存数据的操作是通过DiskLruCache.Editor这个类完成的,开发者可以通过调用DiskLruCache的edit()方法来获取Editor实例,然后进行缓存数据的操作。\[1\]\[2\]在Android布局文件中,可以使用ImageView来显示图片,如下所示的布局文件代码片段所示:\[3\] #### 引用[.reference_title] - *1* *3* [Android图片加载与缓存开源框架Android Glide](https://blog.csdn.net/zhangphil/article/details/45535693)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Android缓存机制&一个缓存框架推荐](https://blog.csdn.net/mazhidong/article/details/69676830)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dora丶Android

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值