是时候开始重构Android Http网络层框架了(一个现代化的Http网络框架源码分析)

横观历史

image.png

一点感概

记得当年刚入行Android,让我记忆犹新的框架android-async-http,当时用的不亦乐乎,随着时间的变迁,官方的新宠Volley诞生,不久的不久官方宣布自己放弃,坑爹,Android 4.4后,HttpURLConnection底层实现改为OkHttp,随即OkHttp是各个大牛封装的根基,Retrofit最为知名,可以说几乎没有人没用过,后来不知道谁刮起了RxJava大风,变成了Retrofit+RxJava+OkHttp,到目前为止我都很反感RxJava,框架固然很好,但我们不合适,不爱就是不爱,没必要牵强。自从有了Coroutines协程,算是找到了最优解,为什么这么说呢?我们先来分析下这几种实现方式

android-async-http

基于Apache的HttpClient库构建的Android异步网络框架,大致用法如下:

 private AsyncHttpClient asyncHttpClient = new AsyncHttpClient() {

        @Override
        protected AsyncHttpRequest newAsyncHttpRequest(DefaultHttpClient client, HttpContext httpContext, HttpUriRequest uriRequest, String contentType, ResponseHandlerInterface responseHandler, Context context) {
            AsyncHttpRequest httpRequest = getHttpRequest(client, httpContext, uriRequest, contentType, responseHandler, context);
            return httpRequest == null
                    ? super.newAsyncHttpRequest(client, httpContext, uriRequest, contentType, responseHandler, context)
                    : httpRequest;
        }
    };

  @Override
    public RequestHandle executeSample(AsyncHttpClient client, String URL, Header[] headers, HttpEntity entity, ResponseHandlerInterface responseHandler) {
        return client.get(this, URL, headers, null, responseHandler);
    }

    @Override
    public String getDefaultURL() {
        return "https://httpbin.org/get";
    }

 @Override
    public ResponseHandlerInterface getResponseHandler() {
        return new AsyncHttpResponseHandler() {

            @Override
            public void onStart() {
                clearOutputs();
            }

            @Override
            public void onSuccess(int statusCode, Header[] headers, byte[] response) {
                debugHeaders(LOG_TAG, headers);
                debugStatusCode(LOG_TAG, statusCode);
                debugResponse(LOG_TAG, new String(response));
            }

            @Override
            public void onFailure(int statusCode, Header[] headers, byte[] errorResponse, Throwable e) {
                debugHeaders(LOG_TAG, headers);
                debugStatusCode(LOG_TAG, statusCode);
                debugThrowable(LOG_TAG, e);
                if (errorResponse != null) {
                    debugResponse(LOG_TAG, new String(errorResponse));
                }
            }

            @Override
            public void onRetry(int retryNo) {
                Toast.makeText(GetSample.this,
                        String.format(Locale.US, "Request is retried, retry no. %d", retryNo),
                        Toast.LENGTH_SHORT)
                        .show();
            }
        };
    }

我们暂且不提现在官方现在已经不用HttpClient,框架本身有很多可以借鉴的优秀设计,放在当初可谓是功能丰富,非常稳定,且bug极少。我少啰嗦几句,直接看下一个实现,最终我们再宏观的看看,到底网络框架的前世今生是什么走向。

VolleyPlus

VolleyPlus库对Volley进行的项目改进以及完整的图像缓存,涉及使用RequestQueue,RequestTickle和Request

RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());

StringRequest stringRequest = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        ....
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        ....
    }
});

mRequestQueue.add(stringRequest);

比起AsyncHttpClient,加入了请求队列(AsyncHttpClient也有线程池),线程调度,缓存DiskLruCache,支持的缓存类型:

  • 网络缓存
  • 资源缓存
  • 文件缓存
  • 视频缓存
  • 内容URI缓存

等等吧,也是给我们网络层提供了不少的遍历,接下来看看Retrofit+RxJava

Retrofit+RxJava

public interface ApiService {
    @GET("demo")
    Observable<Demo> getDemo(@Query("start") int start, @Query("count") int count);
}
        // 使用例子
        apiService.getDemo(0, 10)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Demo>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "onSubscribe: ");
                    }

                    @Override
                    public void onNext(Demo demo) {
                        Log.d(TAG, "onNext: " + demo.getTitle());
                        List<Subjects> list = demo.getSubjects();
                        for (Subjects sub : list) {
                            Log.d(TAG, "onNext: " + sub.getId() + "," + sub.getYear() + "," + sub.getTitle());
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(TAG, "onError: " + e.getMessage());
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "onComplete: Over!");
                    }
                });

这种似乎是很多很多App目前的使用方式,毋庸置疑,它解决了开发中网络层的很多问题,那为什么它会这么火呢?它背后的本质是什么?我这里啰嗦几句哈,大家应该都听说过后端的开发框架Spring,说到Spring,你肯定会想到它设计的两个核心概念,IOC控制反转,AOP面向切面编程,Retrofit本质上用IOC控制反转,你只需要定义接口,对象由框架本身负责管理,接口有个特点就是不变,利用IOC使得你不得不定义接口,这样间接提高代码的稳定性,是不是很佩服大佬的思想,在这感谢这些大佬们的努力,让我们慢慢养成一个好的编程习惯,当然我们也更应该关注他们设计的思想,这样我们在自己做框架的时候,是不是可以借鉴(copy)一下呢。好了,今天的主角上场了,来看下最新的实现

Net(okhttp+coroutines+lifecycle)

Get

        scopeNetLife {
            // 该请求是错误的路径会在控制台打印出错误信息
            Get<String>("error").await()
        }

Post

        scopeDialog {
            tv_fragment.text = Post<String>("dialog") {
                param("u_name", "drake")
                param("pwd", "123456")
            }.await()
        }

这么简单的实现蕴含了什么?

  1. 生命周期监控
  2. 加载中显示框
  3. 自动序列化
  4. 异常处理
  5. 协程同步
  6. 非线程阻塞

优势很明显,代码的确很简洁,让他们阅读代码不吃力,功能更完善,无需切换线程,消除类似Rxjava的observeOn,subscribe匿名内部类的模板代码,让人更加注重业务逻辑的编写。想必这就是我认为的最优解的一些理由吧。你肯定也会说Rxjava也有这些功能(如:lifecycle),是的,你想怎么用是你的权利,我不能左右。我只想说

,你饿了吗?哈哈
50555D97-51B2-4722-B334-55B80055E84D-6692-000003D06E6D4B5B.jpg

小结

经过几个版本的代码对比,你是不是已经察觉到,网络的前世今生,让我们一起总结下进化的特点

  1. 简单化
  2. 生命感知力
  3. 更轻量级的调度
  4. 自我管理
  5. 高内聚(隐藏实现细节)
  6. 高可定制

这就是我们网络框架的今生,你不重构一下吗?亲。

老师说经常教我们知其然知其所以然,那么我就带你走进Net框架实现的底层,让我们看看作者都做了些什么的封装。

源码分析

首先来看下项目的目录结构

image.png

项目主要分为两个Lib

Kalle的源码我们后期再分析,虽然已经一年零两个月未维护了,但毕竟大佬做的,有很多可以学习的地方,这期我们针对强哥的框架Net做深入的分析,由于本人最近两年一直在做Rom相关,对Jetpack相关的使用也不是特别的熟悉,有什么不对的还请各位手下留情,强哥说他还对Kalle的源码做了变更,所以才拿到项目里维护,后期还会升级一下okhttp的版本,因为YanZhenjie没有升级最新的okhttp,当然这不是我们的重点,下面我们来开始分析源码

再来看下强哥总结的框架特性

  • 协程
  • 并发网络请求
  • 串行网络请求
  • 切换线程
  • DSL编程
  • 方便的缓存处理
  • 自动错误信息吐司
  • 自动异常捕获
  • 自动日志打印异常
  • 自动JSON解析
  • 自动处理下拉刷新和上拉加载
  • 自动处理分页加载
  • 自动缺省页
  • 自动处理生命周期
  • 自动处理加载对话框
  • 协程作用域支持错误和结束回调
  • 支持先强制读取缓存后网络请求二次刷新
  • 附带超强轮循器

最近强哥还加了个优先队列,同时发起10个请求,优先取第一个回来的结果,而且代码极其简洁好用。

Net源码目录

image.png

22个文件,不多不少,刚刚好,整体看目录,根本不用深入看代码,我们就能清晰的知道这是干什么的,这就是一种好的目录结构,值得学习

  • connect 真实的链接对象
  • convert 序列化
  • error 异常定义
  • scope 作用域
  • tag 日志tag
  • time 时间相关(结合它的特性,轮训应该在这里有相关实现)
  • utils 工具类
  • Net.kt 网络请求的核心实现
  • NetConfig.kt 全局配置文件

类图

一张清晰可见的类图,是分析一个源码有利的手段,随我将其源码类图搞个明白,请看下图

Net类图.jpg

通过类图,我们可以清晰看到,该框架源码的类结构,我大致分以下模块,好统一分析

  • kalle扩展支持部分:ConnectFactory、Converter、NetException
  • 作用域部分:CoroutineScope结合Android Lifecycle的扩展,以及Sate、Page、Dialog对NetCoroutineScope的实现
  • 动态扩展部分,这是框架的核心:Net,组合协程、Kalle实现新的网络框架
  • 配置、工具部分:NetConfig、Utils(省略未画)、Interval

接下来,详细看下各个模块的实现

kalle扩展支持部分

ConnectFactory只有一个功能:就是创建一个Connection对象,参数是Request。这里为什么这样做呢?其实是为了能兼容不同的URLConnection,强哥使用的okhttp,自然使用HttpClient

image.png

Converter就是大家熟悉的Response处理,这里有个巧妙的设计,将接口返回的数据,通过传入的code作为key,默认是code,取到业务定义的code码,与传入的success参数对比,如果一样就是成功,然后parsebody,如果返回其他,则抛出失败的结果。

image.png

NetException 异常对于一个框架来说几乎不可避免,自定义的异常信息,有利于问题的归类和追踪。上图中就是作者自定义的RequestParamsException请求参数异常、ServerResponseException服务器异常,这个代码就不用看了,大家肯定是明白了,下面来看作用域部分

作用域部分

作用域都继承自CoroutineScope,CoroutineScope持有CoroutineContext上下文,其实不难理解,就像Android的Context,分别有Applicaition Context和Activity Context,其实就是标示出不同的生命周期,那么顺着这个理解就好说了,作者抽象AndroidScope,其实就是为了监听Lifecycle的生命周期,然后在默认的ON_DESTROY函数回调中cancel掉协程,如下图代码所示

image.png
image.png

看到了吧,继承CoroutineScope需要实现CoroutineContext对吧,那这里的context是啥呢?

image.png

协程上下文包含当前协程scope的信息,如CoroutineDispatcher,CoroutineExceptionHandler,Job,其实这三个都是继承实现CoroutineContext.Element,而这个+号不是我们以为的加号,点击后看到如下源码,这里巧妙的运用操作符重载,不理解概念可以点击该链接:https://www.kotlincn.net/docs/reference/operator-overloading.html 学习。

image.png

我们先不研究这样做的用意哈,这里你要知道的是,该协程上下文承载了三个对象

  • Dispatchers.Main 主线程
  • exceptionHandler 异常捕获
  • SupervisorJob 需要执行的协程

SupervisorJob 它类似于常规的 Job,唯一的不同是:SupervisorJob 的取消只会向下传播。看下官方的例子

import kotlinx.coroutines.*

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // 启动第一个子作业——这个示例将会忽略它的异常(不要在实践中这么做!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("The first child is failing")
            throw AssertionError("The first child is cancelled")
        }
        // 启动第二个子作业
        val secondChild = launch {
            firstChild.join()
            // 取消了第一个子作业且没有传播给第二个子作业
            println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // 但是取消了监督的传播
                println("The second child is cancelled because the supervisor was cancelled")
            }
        }
        // 等待直到第一个子作业失败且执行完成
        firstChild.join()
        println("Cancelling the supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}

这段代码的输出如下:

The first child is failing
The first child is cancelled: true, but the second one is still active
Cancelling the supervisor
The second child is cancelled because

其实我没怎么理解向下传播,哪位大佬能解释解释,我们接着往下分析。​

image.png

作者抽象这些高级函数,用于实现dsl的效果如:

image.png
image.png

catch用于捕获协程异常信息处理,finally是在invokeOnCompletion里触发,invokeOnCompletion在协程进入完成状态时触发,包括异常和正常完成。

image.png

好滴,AndroidScope大致就分析完了,我们来简单总结一下

  • AndroidScope 在lifecycle ON_DESTROY时 自动cancel
  • 可以监听异常catch,可以实现finally
  • 主线程执行,作用域内可以刷新UI,不用切换线程

就这些,再来看下NetCoroutineScope,它是网络框架的扩展重心,NetCoroutineScope同样的具有自动取消的逻辑,如:

image.png

看下面的变量,我们其实能知道,NetCoroutineScope主要是扩展了网络的缓存策略,需不需要缓存,是否缓存成功等等,还有个animate,这里看作者注释,是控制是否在缓存成功后依然显示加载动画

image.png

然后覆盖launch函数,将逻辑插入到里面,其实这里违背了里氏替换原则,子类尽量不要重写父类的方法,继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

image.png

然后就是cache配置的实现

image.png

用例:

用起来简单的一批

image.png

接下来就是StateCoroutineScope、PageCoroutineScope、DialogCoroutineScope,这三个我就不一一解读了,同样的套路,对实际业务中的高发场景做的高度抽象。如:StateCoroutineScope是对StateLayout的扩展处理,在StateLayout onViewDetachedFromWindow时 ​自动cancel()掉协程,等等吧。

Net的核心部分,动态扩展

不啰嗦,直接看代码哦

image.png
  • 扩展CoroutineScope,添加get函数
  • 内连函数,reified修饰M真泛型,真泛型不理解的可以看大佬的掘金介绍:https://juejin.im/post/6844904030662033422
  • 执行完后对uid做cancel或者remove处理,释放缓存
  • 结合NetConfig的host+path,形成完整的请求路径
  • SimpleUrlRequest创建新的请求
  • async(Dispatchers.IO) 子线程的网络请求
image.png

Post同理,其实差别就在请求参数,其他Head、Options 就不用分析喽

分析到这里,我们再总结一发

  • 作用域部分继承扩展自CoroutineScope
  • Net部分动态扩展自CoroutineScope

两部分好像有关联,其实没有任何的代码耦合,这也是非常值得学习的地方。这样做的好处其实就是限制了请求在作用域之外,造成代码的不规范。如图,在作用域外根本请求不了

image.png

好了,这部分基本就结束了,接下来看下源码中最后一部分

配置、工具部分

NetConfig对象缓存网络的配置,如域名,app上下文,弹窗,错误,StateLayout的全局缺省页

image.png
image.png
  • initNet 初始化
  • onError 全局错误信息处理
  • onStateError 缺省页处理

用法:

image.png

在Application onCreate函数中初始化就行了,就这样就完了,没没没,还有,来看个例子

image.png

为什么有个ScopeNetLife,不应该是NetCoroutineScope吗?对的其实就是它,实现在这里,我们一起来看下

image.png

这个是Utils中的ScopeUtils类,同样是熟悉的动态扩展,简单的扩展,实现DSL风格就是这里的结果。

好了,基本上都讲完了吧,经过一系列的分析,你是不是已经按耐不住自己要去体验了呢?

源码地址

强哥首页:https://github.com/liangjingkanji

Net源码:https://github.com/liangjingkanji/Net

欢迎关注骚扰哦,听说强哥最近有大动作,未来Net会更加的好用,也希望你能喜欢这次讲解,辛苦你点个赞呗,感谢。

作者

i校长

简书 https://www.jianshu.com/u/77699cd41b28

掘金 https://juejin.im/user/131597127135687

个人网站 http://jetpack.net.cnhttp://ibaozi.cn

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343