前言
在 Android 开发中, 网络请求是每个开发者的必备技能. 当前也有很多优秀, 开源的网络请求库. 例如:
- OkHttp https://github.com/square/okhttp
- Retrofit https://github.com/square/retrofit
- Android-async-http https://github.com/loopj/android-async-http
其中 Retrofit 是对 OkHttp 的封装, Android-async-http 是对 HttpClient 的封装, 利用这些网络库开发者可以极大提升编码效率. 但是, 即便有这么多优秀的网络库可以选择, 大多数团队依旧会自己封装网络库, 为什么呢? 所以本文首先就要讨论:
1 为什么要封装自己的网络库呢?
当知道封装网络库的必要性之后, 便摩拳擦掌准备去大干一番, 但很快便面对一个问题:
2 如何一步步封装自己的网络库呢?
终于封装好了自己的库, 但使用时肯定能发现不少 bug 和可以优化的地方, 那么:
3 应该用什么样的思想来指导改进网络库呢?
在回答上述问题之前, 先了解下网络请求的基本流程:
网络请求基本流程
网络请求的实质是去查看, 修改远程计算机 (包括服务器) 上的信息, 仅从客户端来看基本流程如下:
网络请求流程. PNG
如图示, 网络请求的基本流程就是如此简单, 和把大象放入冰箱一样, 都是三步. 接下来我们在代码级别来看看:
如何进行网络请求
以使用 OkHttp 框架访问百度首页为例子
- // 构造一个 HttpClient 相当于设置个人邮箱.
- OkHttpClient client=new OkHttpClient();
- // 创建 Request 对象, 相当于写信.
- Request request = new Request.Builder()
- .url("http://www.baidu.com")
- .build()
- // 将 Request 封装为 call, 相当于把信放进邮箱, 成为设置后待发送的信件
- Call call = client.newCall(request);
- // 放置到请求队列, 开始发送并等待回复, 相当于邮箱开始发动信件, 并等待对方回复.
- call.enqueue(new Callback() {
- @Override
- public void onFailure(Call call, IOException e) {
- // 当请求被取消, 连接中断, 找不到服务器等问题会调用这个接口
- }
- @Override
- public void onResponse(Call call, final Response response) throws IOException {
- // 远程服务器成功返回调用
- final String res = response.body().string();
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Log.e("TAG"," "+res);
- }
- });
- }
- });
- }
由上可以看到用 OkHttp 框架进行网络请求逻辑清晰简单, 跟我们用邮件跟朋友交流差不多. 首先是设置邮箱(如果没有什么特殊需求就用默认设置, 如上文)+ 写邮件内容, 然后把写好邮件后放到设置好的邮箱里面, 最后点击发送, 等待朋友的回复, 当朋友回复了就去查看处理.
为什么要封装网络库
知道如何用 OkHttp 后很兴奋, 于是用这一套开始了网络请求之旅, so easy!! 复制 - 粘贴 - 修改, 复制 - 粘贴 - 修改, 复制 - 粘贴 - 修改... 终于做了七八个网络请求, 一看任务量完成了六分之一, 啊 -- 累死宝宝了!!
不行了不行了, 要喝杯 luckin coffee 鼓舞下士气, 说走就走, 喝着咖啡想着回来就一口气加班搞定它. 突然想起 OOP 重要原则 - 代码复用原则, 一拍脑子我 TM 真是个憨货, 把这些请求的共同部分提取出来, 对外提供更简单的接口, 这样用着方便不容易出错, 以后有问题修改工作量也大大减少了, 这就是我们封装网络库第一个原因:
一, 近似业务模型的代码复用 -- 方便使用和修改;
说干就干, 撸起袖子正准备上场, 再次灵光一闪, 不对不对, 这次要好好思考下整个完美的, 半途而废啥的最浪费时间了! 首先看看网络上优秀的开源库都有哪些功能可以借鉴.
去 GitHub 上看了 star 比较多的一些 OkHttp 库封装, 整理了下功能列表:
一般的 get 请求
一般的 post 请求
基于 Http Post 的文件上传(类似表单)
文件下载 / 加载图片
上传下载的进度回调
支持取消某个请求
支持自定义 Callback
支持 HEAD,DELETE,PATCH,PUT
支持 session 的保持
支持自签名网站 https 的访问, 提供方法设置下证书就行
支持 RxJava
支持自定义缓存策略
这些网络库是很好参考借鉴, 但是, 它们跟我们的业务结合不紧密, 直接用还是会造成:
1 每个网络请求都要加上业务逻辑;
2 有不少多余功能, 导致网络库很大, 可能影响效率 ;
3 团队特殊要求达不到, 比如利用三方实现 DNS 防劫持.
所以需要仔细去分析业务, 梳理网络请求类型. 很快我们发现需要四种缓存策略: 立即请求, 缓存 10s, 缓存 1h, 缓存 24h, 所以封装网络库第二个原因也是功能点:
二, 全局, 全团队统一的缓存策略;
接下来需要去沉下心, 去谷歌搜索下网络请求问题, 针对业务场景去思考下一旦上线会遇到什么样的问题, 很快确定了第二个问题, DNS 劫持问题, 所以我们封装网路库第二个原因也是重要功能点:
三, 全局, 团队统一的 DNS 反劫持;
为了防止遗漏, 又去找团队成员, 上级老大聊天请教, 看有什么特殊要求. 这时候运营部门提了个需求, 统计 dns 劫持率, 老大说出现网络问题要能够快速定位. 所以封装网络库第四个重点:
四, 全局, 团队统一的网络请求统计和关键 log
明确了什么要封装自己的网络库, 以及必须具备哪些功能, 接下便开始正面遭遇问题:
如何一步步封装网络库呢
人类做事习惯上是顺序进行的, 这就决定了人类的可靠性思维 - 逻辑思维是线性的, 进而决定了人类的可靠性表达也是线性的! 越顺滑的思路, 越顺滑的表达, 越容易被人理解与接受.
所以当遭遇事件类相关任务, 又不知道怎么做的时候, 从事件整体业务流程进行分析是一个很好的切入点.
从整体业务流程出发, 使我们不至于迷失, 但到了每一个环节该如何做, 就需要一些指导与规范, 那必须就是:
SDK 设计原则: A 简洁易用 B 功能完备 C 扩展性好.
当然 "简单吗? 优美吗?" 也是每个软件工程师须时时反问的.
一 简洁易用 - 从用户使用与理解角度考虑对外接口与模块划分
现在假设网络库已经写好了, 用户该库发起网络请求, 所以遇到第一个问题节点就是网络库对外接口设计. 接口设计原则是简单! 简单! 简单! 不仅仅是代码看着的简单, 而是在于用户易于理解和使用的简单!
上文提到通过 OkHttp 网络请求大概分为四步:
1 构建, 设置 OkHttpClient-- 相当于设置电子邮箱;
2 构建 Request 请求内容 -- 相当于写信;
3 用 OkHttpClient 把 Request 转换成为待发送的 Request-Call-- 相当于把信件放入邮箱, 变为邮件;
4 发送请求, 等待回复, 并处理.
现在就设想最简单的网络请求是怎么样的: 额, 大概是这样的吧 ---- 客户端添加一个请求(由 Url 构建出来)- 发送出去 - 等待回复处理, 比如如下
- new HttpClient.HttpClientBuilder().build() // 设置邮箱
- .addRequest(new GetRequest("http://www.baidu.com")) // 写信并添加到邮箱或者叫写邮件
- .sendRequest(new DefaultCallback(){ // 发送邮件等待回复
- @Override
- public void onResponse(Call call, Response response) {
- super.onResponse(call, response);
- Log.e("JG","response="+response.toString()); // 服务器成功返回
- }
- @Override
- public void onFailure(Call call, IOException e) {
- super.onFailure(call, e);
- }
- });
从这个简单流程出发, 网络库可抽象出如下模块:
1 HttpClient 客户端模块 ;
2 Request 请求模块 ;
3 Callback 回复处理模.
据此我们可以把网络库模块划分为这三个大的模块. 同理, 在每一个大模块内部也要根据流程进行划分为更细的模块
这一小节主要探讨模块设计与划分, 核心思想是跳出代码逻辑, 从整体业务流程出发, 找到关键的处理节点, 从而对网路库进行模块设计与划分. 但到底这种设计是否行得通, 还要从每一个具体业务实现进行重新审核.
功能完备 - 从业务需求实现出发审查模块划分的合理性
接下来我们开始分析每一个业务需求, 验证刚才的模块划分是否合理.
1 get/post 请求
最初我们从 get/post 请求开始思考业务逻辑, 所以这个可以跟模块完美结合, get 与 post 的区别 https://juejin.im/entry/597ca6caf265da3e301e64db 在我们这里就是不同的 Request 封装.
2 DNS 防劫持
DNS-Domin Name System 域名解析系统, 将域名 (例如:"http://www.baidu.com/") 转换为 IP 地址(例如: 220.181.112.244), 这个解析过程涉及到本地缓存, 运营商缓存, 各级别域名服务器等, 它是 Http 协议的一部分.
DNS 劫持劫持又称域名劫持 https://juejin.im/post/59ba146c6fb9a00a4636d8b6 , 本质就是通过攻破 DNS 解析过程中某些环节与节点, 来给用户返回假网址 IP.
既然 DNS 劫持结果是返回错误的 IP, 那是否直接用 Ip 来访问就可以防止 DNS 劫持了? 这就是 DNS 反劫持的主要思想: 拿到域名 ->来通过 Http 请求 ->访问权威三方 (比如阿里的 HTTPDNS https://help.aliyun.com/product/30100.html ) 提供的 DNS 解析服务器 ->三方告诉你 IP, 便可通过该 IP 来进行网络请求了.
OkHttp 实现 DNS 反劫持: 由上文可知看到 DNS 反劫持关键在于, 用三方的 DNS 解析系统代替系统默认的 DNS 解析系统! 所以 OkHttp 提供了一个抽象的 DNS 类, 用户只用继承这个类, 便可以方便的接入自定义的 DNS 解析系统.
- public class HttpDns implements Dns {
- private static final String TAG = HttpDns.class.getSimpleName();
- @Override
- public List<InetAddress> lookup(String hostname) throws UnknownHostException {
- Log.v(TAG, "lookup:" + hostname);
- // 只需要在 lookup 方法中调用 HttpDns 的 SDK 去获取 IP
- // 如果获取到了就返回一个 List<InetAddress > 的值
- // 如果购买了阿里的 HttpDns 服务就可以用
- // 默认又返回系统的 DNS 解析, 这就叫 DNS 降级
- return SYSTEM.lookup(hostname);
- }
- }
然后通过 OkHttClient 设置此 DNS,HttpClient 模块主要就是封装 OkHttClient, 所以审核通过! 可行再次 + 1.
3 缓存设置
缓存设置指缓存的位置, 大小, 时间, OkHttp 通过两种方式可以实现缓存设置:
- //1 通过库 cache 接口
- new OkHttpClient.Builder()
- .cache(new Cache(file, cacheSize)) // 配置缓存
- // 2 通过拦截器
- new OkHttpClient.Builder()
- .cache(new CacheInterceptor(){
- @Override
- public Response intercept(Chain chain) throws IOException {
- Request request = chain.request();
- Response response = chain.proceed(request);
- return response;
- }
- });
具体做法请参照: okhttp 缓存实践 https://juejin.im/post/5afb89dcf265da0ba26727c7
它依旧可以通过 OkHttpClient 完成, 我们依旧只需要把这部分封装在 HttpClient 模块就好.
4 全局 log 统计
全局 log 统计依旧是使用拦截器完成, 上代码
- public class LogInterceptor implements Interceptor {
- private static final String TAG=LogInterceptor.class.getSimpleName();
- @Override
- public Response intercept(Chain chain) throws IOException {
- // 把请求 request 拦截下来
- Request request = chain.request();
- // 可以打印请求内容
- Log.v(TAG,"request method="+request.method()+",request url="+request.url());
- // 继续向下一个传递处理, 并拦截到处理结果 response
- Response response = chain.proceed(request);
- // 可以打印返回内容
- Log.v(TAG,"response="+response.toString());
- return response;
- }
- }
依旧是封装在 HttpClient 模块. 全部审核通过, 不过从最初设计也可以知道, 我们从 OkHttp 出借鉴思想, 肯定是可行的.
三, 扩展性好 - 从开闭原则进行模块间解耦操作
现在各个模块分工明确, 业务功能基本全部实现了, 但随着业务的发展, 我们可能会有新类型的请求, 新种类的返回处理等, 所以一开始我们就要考虑整个库的扩展性.
扩展性好的关键在于模块间耦合度低, 解耦的关键在于依赖抽象, 也就是实体模块 (比如一个实体类) 之间没有直接调用关系, 实体模块之间数据传递要通过中间层(抽象类或者接口).
再次回顾下我们最初设计的调用接口:
- new HttpClient.HttpClientBuilder().build() // 设置邮箱
- .addRequest(new GetRequest("http://www.baidu.com")) // 写信并添加到邮箱或者叫写邮件
- .sendRequest(new DefaultCallback(){ // 发送邮件等待回复
- @Override
- public void onResponse(Call call, Response response) {
- super.onResponse(call, response);
- Log.e("JG","response="+response.toString()); // 服务器成功返回
- }
- @Override
- public void onFailure(Call call, IOException e) {
- super.onFailure(call, e);
- }
- });
其中 addRequest
- public ReadyRequest addRequest(BaseRequest baseRequest){
- return new ReadyRequest(this,baseRequest);
- }
其中 GetRequest 是继承自 BaseRequest, 这样 HttpClient 这个实体类就没有直接和实体类 GetRequest 相关, 这就是通过依赖抽象进行了解耦合. 当我们需要一种新的 Request, 只需继承 BaseRequest 就可以方便的扩展使用.
如何不断优化网络库
现在我们做好了一个基本可以使用, 并且具备一定扩展性的网络库, 但是使用过程中肯定可以发现 bug 和可以优化地方, 那么如何一步步把我们的网络库从普通变为卓越呢?
那首先我还是会问一个问题, 对一个具体 App 来说怎么样才是一个顶级的网络库?
1 安全性高;
2 网络访问速度快, 性能优越;
3 用户使用方便.
看了一些优秀的网络库改进过程, 暂时总结出来点如下:
1 统计网络请求常见 bug 与风险, 增加预防处理;
2 统计业务流程想关性, 进行预加载或者缓存;
3 统计用户使用习惯和思维, 统一智能设置或者修改接口;
4 不断探讨学习其他优秀的软件设计思维与思维, 进行部分重构.
这些都是大数据与 AI 思维的延伸, 这部分还在继续思考中, 如果各位有什么想法, 欢迎在下面交流评论!!
总结
本文从简单的网络请求开始, 谈到了为了业务需求和方便使用来封装自己的网络库, 进而探讨了如何一步步封装一个网络库, 并不断优化, 完善. 其中重点是想重现了下封装, 优化 SDK 的一个思维过程:
1 跳出代码, 从整体业务流程进行初步模块划分;
2 从方便 (用户) 理解使用的角度设计调用接口;
3 从业务具体实现出发, 重新审视模块划分;
4 用软件设计思维与模式再次审视当前结构设计, 增加扩展性, 方便用户灵活扩展.
5 用大数据与 AI 进化思维进行不停优化;
同时, 也花了一两天时间, 手动封装了一个网络库来验证思维过程的可行性. GitHub 地址: https://github.com/kingkong-li/networklib
欢迎各位大神前来交流, 共同开发学习~~
来源: http://www.jianshu.com/p/c8262379c8ec