根据很多网友及项目需要,我们针对Retrofit做了一层简单封装,包含了很多可插拔的功能,能够适应大多数项目的需要,这一期我们来分析以下如何设计一款安全高效的Android网络库——FlexNet
1. 网络框架模块
在设计网络之前,需要首先考虑网络库的模块划分:
上图是通用网络库需要考虑的模块,下面结合我们的通用封装来筛选并设计所需模块。
1.1 底层网络请求库
在选择底层网络请求库时,考虑到目前市场上已经存在多种成熟的开源库,如OkHttp、Retrofit和Volley,大可不必自研,选择一个可靠且高效的底层网络内核是构建网络库的基础。目前,Retrofit和OkHttp是最常用的选择:
- Retrofit 提供了便捷的API定义方式和数据解析能力。
- OkHttp 则作为底层HTTP客户端,负责实际的网络通信。
结合各个库的优缺点以及使用情况,FlexNet当前基于Retrofit来开发。库选择好之后还要设计封装方式,为了确保灵活性,便于将来平滑快速切换到其他底层库,需要考虑以下问题:
- 设计统一封装接口:将底层网络请求库的关键功能封装在一个统一的接口中,以便于后续更换底层库时只需要修改这个接口的实现即可。这样做可以减少对其他部分代码的影响,提高切换的便捷性;
- 提供通用的配置参数:确保接口设计时能够接受和传递通用的配置参数,例如超时时间、缓存策略、拦截器等。这样即使切换到其他底层库,也能够充分利用这些配置参数,减少对代码的修改;
- 保持一致的数据返回格式:确保底层库的接口设计在数据返回格式上保持一致,这样在切换底层库时,上层代码对返回数据的处理方式可以保持不变;
- 及时更新文档和示例代码:为了方便后续开发人员接入或者切换底层库,需要编写一份详细的使用文档,并提供示例代码来演示如何使用FlexNet的接口和配置。这样可以加快新开发人员上手的速度,减少学习和使用的难度。
1.2 请求/响应格式
在1.1小节提到过,请求和响应的统一是解耦的必要步骤。请求和响应的格式也是网络库设计中的重要部分。
1. Request
网络返回的数据需要进行一定的处理,以便处理错误和正常情况。这些处理包括日志打印、埋点上报,或者将数据返回给业务方便于问题定位。为了满足这些需求,我们对Response进行了封装:
data class SongListReq(
val pageNum: Int = 1, val pageSize: Int = 20, val sourceType: String = SOURCE_QQ
) {
companion object {
const val SOURCE_QQ = "QQ"
}
}
2. Response
网络返回的数据需要针对错误、正常做一些处理,比如日志打印、埋点上报,或者返回给业务方便于定位问题,所以这里对Response做了一层封装:
sealed class ResponseEntity<T>(val body: T?, val code: Int, val msg: String)
可以看到,ResponseEntity
是一个sealed
类型。首先,我们定义了Error
和Success
两个子类,然后根据不同的错误和成功类型,派生出了不同的响应类,业务方拿到Response之后可以根据类型来做相应处理。
为了增强扩展性与向下兼容性,也可以直接使用Raw数据对象,或者JsonObject对象来处理,这样就需要自行处理成功和失败,及区分失败类型。
1.3 错误处理
良好的错误处理机制能够提高开发体验和应用稳定性:
- 统一错误处理: 捕获网络请求中的所有异常,并将其转换为友好的错误信息。
- 错误类型分类: 根据HTTP状态码、网络异常和业务逻辑错误进行详细分类。
- 回调和事件总线: 提供错误处理回调或通过事件总线(如EventBus)分发错误信息。
成功的方式千篇一律,失败的原因千差万别。在拿到Http Response之后,我们需要对响应做一些包装,方便区分成功和失败的原因,并给出提示。首先定义了包含失败和错误的返回码:
CODE_SUCCESS | 10000 | 请求成功,数据来源网络 |
---|---|---|
CODE_SUCCESS_CACHE | 10001 | 返回成功,数据来源于本地缓存 |
CODE_SUCCESS_BODY_NULL | 10002 | 请求成功,但消息体为空 |
CODE_ERROR_UNKNOWN | -200 | 未知错误 |
CODE_ERROR_UNKNOWN_HOST | -201 | host解析失败,无网络也属于其中 |
CODE_ERROR_NO_NETWORK | -202 | 无网络 |
Http的错误直接透传 |
针对每一种错误都定义了一个ResponseEntity
类型,可以通过类型或者code
判断错误类型,通过msg
获取错误信息
1.4 请求方法
网络库需要全面支持各种HTTP请求方法:
- GET: 获取资源。
- POST: 提交数据并创建新资源。
- PUT/PATCH: 更新现有资源。
- DELETE: 删除资源。
- HEAD/OPTIONS: 获取资源元数据和支持的方法。
确保库能够方便地配置和使用这些方法,并提供灵活的参数传递方式。
FlexNet沿用Retrofit的方式,通过注解完成请求方法的声明:
/** 获取音乐分类列表 */
@POST("api/cloudxcar/atmos/v1/category/page/list")
suspend fun getSongList(@Body body: SongListReq): ResponseEntity<MusicResponse<SongListRsp>>
为了方便后续扩展,需要对业务方做一定的限制,接口类必须实现IServerApi
接口
1.5 SSL证书验证
证书认证确保网络通信的安全性:
- 双向认证: 配置客户端和服务器之间的双向SSL认证,以防止中间人攻击。
- 证书锁定: 实现证书锁定(Pinning),只信任指定的证书或公钥。
- 自签名证书: 支持自签名证书的配置和使用。
为了方便App使用,我们的网络库集成了“ssllib”双向认证,大家可以自行在项目中添加公司或者个人的双向认证功能。在实现双向认证时,我们需要考虑以下两个方面:
- 提供双向认证开关:我们为用户提供了一个总开关,以及每个接口单独的开关。这样,用户可以根据实际需求选择是否开启双向认证。
- 处理启动过早导致的验证失败:有些App启动时机太早可能导致验证失败。为了解决这个问题,我们还提供了一个主动验证的接口,使得用户可以在合适的时机手动触发验证操作。
关于双向认证的描述及使用参考:双向认证
1.6 文件操作
网络还需要考虑文件的上传和下载功能:
- 文件上传: 实现多部件表单数据上传,支持大文件分块上传。
- 文件下载: 提供断点续传功能,确保下载过程的稳定性和可靠性。
- 进度显示: 提供上传和下载进度的实时反馈。
下载最基本的接口:
- onProgressUpdated: 下载进度回调,回传当前已下载大小和文件总大小
- onDownloadFailed: 下载失败回调,回传失败原因
- onDownloadCompleted: 下载完成回调,回传下载成功的File对象
1.7 数据缓存
- 数据缓存策略
- 缓存容量
- 持久化方式
- 更新策略
- 失效处理
- 删除数据缓存
1.8 拦截器
Interceptor可用于拦截并处理网络请求和响应,利用它可以在发送请求之前和接收响应之后对其进行自定义操作和处理。Interceptor可用于添加、修改、删除请求头信息、记录日志、进行权限验证、缓存响应等功能。
目前FlexNet默认实现了3种拦截器,也是在网络库中常用的拦截器:
- 请求拦截器: 添加公共请求头、参数签名等。
- 响应拦截器: 处理通用的响应逻辑,如统一解析包装的响应数据。
- 日志拦截器: 打印详细的请求和响应日志,方便调试和问题定位。
1.9 扩展功能
为了增强网络库的灵活性和可扩展性,可以提供一些扩展功能:
- 插件机制: 允许开发者通过插件机制扩展库的功能,而无需修改核心代码。
- 灵活配置: 提供丰富的配置选项,以满足不同项目的需求,如超时时间设置、重试策略等。
- 模块化设计: 将核心功能与扩展功能模块化,开发者可以按需引入所需模块。
2. App网络接口设计
2.1 Data Access Layer
这一层通常会设计一个DAO
DAO即Data-Access-Object,直译过来就是数据访问对象。很多人对DAO的理解是负责链接数据库,实际上并不一定。它更应该被理解为一个接口,一个DAL的实现,可能连接数据库,也可能连接到Redis,或者文件
通常一个Model 对应一个DAO。比如Account –> AccountDAO; User–> UserDAO
比如常见的DAO是这样:
public interface MusicDAO {
Account get(String songName);
void create(MusicInfo info);
void update(MusicInfo info);
void delete(String songName);
}
内部的实现可对接不同的数据源
2.2 Repository
Repository用于封装数据访问和 持久化 的细节。Repository层提供了一种统一的接口,用于获取和存储数据,并隐藏了底层数据访问技术的具体实现细节。它负责从数据源(如数据库、网络等)中获取数据,并将数据转化为应用程序可以直接使用的形式。Repository层的设计目标是对上层提供简单、统一的数据访问方式,同时可以灵活地切换底层数据源的具体实现。
Repository相对DAO层级更高,更抽象。一个Repository对应一个Domain,作为对外的唯一接口。
如:
object WeatherRepo : BaseRepo<WeatherApi>() {
suspend fun getWeather(): JsonObject {
return mRepo.getWeather()
}
override val baseUrl = "https://v0.yiketianqi.com/"
override val mutualAuthSwitch = false
}
3. 接入方式
网络库接入方式:FlexNet 网络库接入文档
4. 后续工作
-
目前还有一些和okHttp、Retrofit的耦合,后续需要彻底解耦
-
Java接口适配
-
完善胶水层