从零开始仿写一个抖音App——app架构更新与网络层定制

本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,不然将追究版权责任。微信号:a1018998632,交流qq群:859640274java

连载文章

本项目的 github 地址:MyTikTok

国庆快结束了,国庆中有六天都在写文章看代码还有比我苦逼的吗(买个惨,哈哈)。这几天为项目新增了五个模块,顺便看了看 kotlin 的语法并在项目中简单的实践了一下。本文中会讲解其中的两个模块,剩下的一些会在不久后发布的下一篇文章中进行讲解。linux

  • 1.讨论——总结前两周评论中有意义的讨论并给予个人解答
  • 2.app架构更新——随着开发的进行,发现第二篇文章中的架构有一些问题,因此在这里更新一下
  • 3.网络层定制——基于 retrofit 和 okhttp3 定制一个网络请求层,中间会附加一些原理讲解

1、讨论

讨论1:zsh 对 bash 的支持并非彻底的,若是运行纯 bash 有时候会出问题建议不要在服务器上用。android

  • 1.这个读者的建议很是好,上篇文章中我写了一个 unbunt 环境的初始化脚本,看来这个脚本只能本身在 linux 下开发的时候使用了

讨论2:我觉得 aop 是经过 aspectjrt 来实现的 原来是和 Butterknife 相似来实现的git

  • 1.在我认知里面的 aop 能够简单的概括成:经过注解的信息在某些方法的先后添加代码。
  • 2.因此 aspectj 也是能够实现我在前篇文章中说的 aop 日志的。
  • 3.若是读者了解 aspectj 的原理的话就会发现:他也是经过 gradle 插件来将代码插到注解的方法先后的,只不过这一部分不须要开发者来是实现。
  • 4.而项目中本身实现一个这样的东西一个是为了可定制性,另外一个就是为了能了解一些技术的原理而不是单单只会用。

讨论3:建议以已完成某个功能模块或者某篇文章为版本,建立不一样的tag,这样利于食用。(github 上面的 issue)程序员

  • 1.这个读者的建议也很是好,我已经在每次更新文章的 commit 上面加上了 tag ,你们能够结合这个来看代码。

2、app架构更新

我想看过本系列第二篇文章的同窗都看过本项目的模块架构图。距离写下本项目的第一行代码到如今已经差很少三个月过去了,这个过程当中项目中增长了不少模块,我对大的项目的把握程度也加深了许多,因此这一节我更新一下 app 的架构。github

图1:app 架构图.png

我接下来就按照图1开始讲解,标了红色的小模块表示已经进行过开发的模块数据库

  • 1.首先从最底层开始,这里是一些二方库(本身开发的sdk)三方库(开源的sdk)。其余的全部模块都能依赖这里的库,固然都是单向依赖(A 依赖 B,可是 B 不能依赖 A)
  • 2.再向上一层,这里有两个大模块,generate-codeinternal-base
    • 1.generate-code:这里放着生成代码的几个模块,好比用 apt 生成代码的 annotation-progress,又如用 gradle 的 transform 配合 javassist 生成代码的 invoker。
    • 2.internal-base:这里放着 app 中的全部的底层模块,例如负责网络请求的 http 模块,例如负责图片加载的 image 模块,例如复制数据库的 database 模块等等。
    • 3.在这里 generate-code 与 internal-base 这两个大模块之间能够互相依赖(注意这里表示的不是相似 http 与 image 之间能够互相依赖,由于这样会产生循环依赖的错误)
    • 4.这两个大模块均可以被更上层的大模块所依赖,注意这里是单向依赖,是必须遵照的约定,由于没有代码层面的约束
  • 3.再向上看,左边是一个 external-base 大模块和一个 core 小模块组成的
    • 1.external-base:这个大模块里面目前尚未添加小模块,可是将来应该会添加进去,这里面装着的是外部侵入的代码的封装,好比 bugly 除了须要添加库的依赖还须要为其加一些另外的代码,又好比一些 android 厂商的 push 方案集成以后须要的适配代码
    • 2.core:这个是一个小模块,将其单独放在外面是由于其起一个承上启下的做用
      • 1.这里面装着更上层模块的公共代码,好比 app 进入时的初始化代码。
      • 2.解决一些底层小模块之间须要互相引用的问题,好比 http 须要和 image 之间互相引用,此时会形成循环引用的错误,此时就将这些代码放到 core 中进行处理。暂定,在写下面的时候我发现这个特性可能会形成本模块依赖过多的问题,后面应该还会继续拆分这个小模块
      • 3.沟通底层和上层的模块
    • 3.这里的两个模块能够被同层右边的 app-plugins 大模块所依赖,这里也是单向依赖
  • 4.再看同层的右边,这一个大模块名为 app-plugins,里面的每个小模块都能被编译成一个 app。而后其能够被最顶层的 app-variants 所依赖,最终构建出不一样功能的 app。
  • 5.最顶层就是 app-variants,这个大模块只能依赖 app-plugins,里面几乎不会有什么代码,有的就是一个个 gradle 配置,最终会生成不一样功能的 app。

3、网络层定制

如今 okhttp + retrofit,也许是一个新项目的标配了,可是不少人都只是在使用这两个库的最基本的功能,却不知这两个库能够经过定制来实现更多的功能。这一节我就来说讲如何基于这两个库来定制一个大项目的网络请求层。中间会穿插着一些原理的讲解。编程

1.网络层请求流程

图2:网络层定制图.png

接下来我会按照图2开始讲解 okhttp + retrofit 整个请求流程,待读者对整个流程有所了解以后再讲定制的代码,这样会事半功倍。后端

  • 1.图中红色的框是开始部分,咱们就从这里开始。这里默认你们都会使用这两个框架,多余的东西就再也不赘述了。
  • 2.首先咱们在须要请求一个接口的时候会使用 Retrofit 对象调用其 create 方法建立一个 XXXService。咱们看下图3的代码:
    • 1.能够看见这里就是简单的用了一下动态代理的方式将 XXXService 的每一个接口交给特定的 ServiceMethod 来实现。
    • 2.这里的 ServiceMethod 怎么来的呢?看36行的 loadServiceMethod 方法,这首先为了性能会去 serviceMethodCache 中看看是否有 XXXService 某个接口对应的 ServiceMethod,若是没有的话就用 Builder 模式建立一个。

图3:Retrofit#create.png

  • 3.回看图2,建立好了 XXXService 的实现类以后,咱们通常会结合 Rxjava 调用某个接口,让其返回一个 Observable 对象。由前面的介绍,咱们知道这里 Observable 实际上是调用 ServiceMethod.adapt(OkhttpCall) 返回的(能够看图3的21行),咱们进入这个方法。
    • 1.这个方法里会将调用交给 CallAdapter.adapt(OkhttpCall)
    • 2.有些同窗可能知道这个 CallAdapter 是在初始化 Retrofit 的时候被 Retrofit.Builder() 添加的 CallAdapterFactory 建立的。其有几个具体实现如图2。
    • 3.那么这里要选哪个呢?选择 CallAdapter 的具体逻辑在 ServiceMethod.build 里面他会调用 ServiceMethod.createCallAdapter 这里最终会交给 Retrofit.callAdapter 来寻找合适的 CallAdapter。
    • 4.那么3中的具体查找逻辑是什么呢?这里我总结一下:
      • 1.会对 CallAdapterFactory 进行循环查找,一旦返回一个 CallAdapter 不为 null 那么就使用这个。
      • 2.具体是否为 null 的逻辑交给具体的 CallAdapterFactory 去实现。
      • 3.由于是顺序查找,因此若是列表中有多个匹配项,这里只取最开始的一个。
  • 4.到这里咱们先不看图2,通常来讲匹配上的 CallAdapterFactory 会是 RxJava2CallAdapterFactory。咱们先研究一下他是怎么产生一个 Observable 的。
    • 1.先看一下图4,咱们直接看20行,这里解释了为何通常会匹配到 RxJava2CallAdapterFactory 由于咱们的 XXXService 定义接口的时候通常选择的返回值 都是 Observable 或者有关 Rxjava 的返回值。而后咱们直接看55行,这里返回了一个 RxJava2CallAdapter,这个就是生成 Observable 的对象。
    • 2.接着咱们看图5,还记得上面的3.1中咱们说的吗?Observable 就是 CallAdapter.adapt(OkhttpCall) 产生的。这里就是具体实现。
      • 1.能够看见18行根据接口调用是同步仍是异步会生成两种不一样的 Observable。
      • 2.而后后面都是根据一些 flag,为 Observable 添加一些操做符。

图4:RxJava2CallAdapterFactory#get.png

图5:RxJava2CallAdapter#adapt.png

  • 5.再回到图2,如今咱们已经有 Observable 了。这里咱们先跳过图2中的几个步骤,直接来到黄色的框,从这里开始咱们可让获得的 Observable 开始运行。对 Rxjava 熟悉的同窗应该知道,一个 Observable 会从操做符流的最顶部开始运行。因此这里会从咱们前面讲到的 RxJava2CallAdapter.adapt 中定义的第一个 Observable 的 subscribe 开始运行。咱们就默认此次接口调用是同步的这样简单点,因此会先进入 CallExecuteObservable 中。
    • 1.先看图6,第1行构造这个对象的时候会传入一个 Call 对象,其实现有不少咱们在这里能够默认其为 OkhttpCall。
    • 2.图6的第5行,是 Observable 开始运行的时候最早调用的方法(有兴趣的同窗能够看看 Rxjava 的源码解析)。这里咱们能够看见13行,其将调用交给了 Okhttp.execute。
    • 3.咱们能够看向图7的20行,这里调用了 createRawCall 建立了一个 okhttp3.Call 其具体实现是 RealCall(咱们直接使用 okhttp 的时候也是经过这个请求网络)。
    • 4.在回到图2中,如图2所示当调用 RealCall.execute 的时候,就会进入 okhttp 的请求链。okhttp 使用了责任链模式,将请求穿过图2中的一个个拦截器,每一个拦截器都负责一个功能。开发者能够在拦截器链的最开始插入本身的拦截器,以实现一些定制操做。
    • 5.再回到图7,okhttp 将数据请求完毕以后会返回一个 okhttp3.Response,这时候会在32行调用43行的 parseResponse 来将解析这个 Response。
    • 6.图7中后面有些代码看不见了,其实最终 Response 的解析会交给 ServiceMethod.toResponse。而其又会交给 Converter.coverter。这接口的实现类也不少,最多见的应该就是 GsonConverterFactory 提供的 GsonResponseBodyConverter 了。如图2,咱们通常也是在建立 Retrofit 的时候添加一些 Converter 以供这里使用。一样相似 CallAdapter,Converter 的选取也是同样的策略
    • 7.通过以上调用,咱们就有了一个retrofit2.Respons,其内部有一个解析了 body 以后的对象。

图6:CallExecuteObservable#subscribeActual.png

图7:OkHttpCall#execute.png

  • 6.CallExecuteObservable 中调用完毕以后,调用流程通常会交给 BodyObservable,这里面很简单,就是将 retrofit2.Respons 中的解析后的 body 交给下一个 Observable 操做符。就这样顺着操做符流最终咱们在 XXXService 中定义的接口的返回值 Observable 的泛型对象就会被传入到 subscribe 中供外部调用者使用。如图2中的粉色框。

2.网络层定制代码

所谓定制就是在网络请求流程的各个主要节点中添加本身的代码实现以达到特殊的需求。通过前面的讲解,我想读者应该对整个网络层的请求流程有了一个大体的了解。这时咱们能够再看看图2,能够看见其中有几处我绿色的框,这几个地方就是咱们能够添加定制代码的地方。接下来我就会按顺序讲解一下这几处的定制代码是如何实现的。bash

图8:RetrofitFactory.png

图9:DefaultRetrofitConfig.png

(1)retrofit2.Call的装饰

咱们按请求顺序能够在图2中首先看见的是 NewCall.execute 这个框,接下来我就来讲说这个能够怎么定制。

  • 1.按照咱们前面的讲解,你们应该知道,若是不作任何定制的话这里的 NewCall 就是 OkhttpCall,其会返回一个 retrofit.Response。最终会在开发者的 subscribe 里面返回一个解析了 body 以后的数据结构(这里就称为 ContentData)。有时候咱们会在 subscribe 里面须要更多的信息,好比在数据转化过程当中丢失的 head 的信息
  • 2.此时咱们就能够对 OkhttpCall 进行一个封装,首先咱们能够定义一个咱们本身的 DataContainer 对象,其用于封装 ContentData,而后其还能够装数据转化中丢失的数据。如图10。

图10:DataContainer.png

  • 3.那么咱们在定义 XXXService 的接口的返回值的时候就能这样定义:Observable<DataContainer<ContentData>>
  • 4.此时有人眼尖就会发现,不对啊这个 DataContainer 是被 Gson 反序列化过来的,里面的 okhttp3.Response 对象服务器又不知道是什么这样怎么序列化呢?
  • 5.答案就在图8,图9中。你们能够看图8的第7行,这里我添加了一个自定义的 CallAdapterFactory。
  • 6.在看图8的4四、4八、49行,根据前面咱们描述的请求流程,44行的 CallAdapter 会用来生成 Observable。再看48行,这里的 call 就是 OkhttpCall 了。咱们将其传入 buildCall 中返回了一个 NewCall,这里就是关键。
  • 7.buildCall 的实现代码在图9,能够看38行。这里的实现很是简单直接就是将 OkhttpCall 封装 返回了一个 ContainerCall,如图11。

图11:DataContainerCall.png

  • 8.DataContainerCall 里面的代码就不用我说了吧,就是给 DataContainer 传入一个 okhttp3.Response 对象。
  • 9.你们是否是以为就这样一个小东西很简单?其实我也以为很简单,可是只要你会用了这一个小东西,那么更多实用的功能都能被这样实现。

(2)OkhttpClient定制

按顺序下来,第二个定制的地方就是 OkhttpCall 调用 okhttp.RealCall 的地方了。

  • 1.咱们看图8的21行,这里给 Retrofit 添加了一个 OkhttpClient。以后的请求都是经过它来发送的。
  • 2.这里插一下,你们能够看看3行,这里传的是一个 RetrofitConfig,它实际上是一个接口,像图9的 DefaultRetrofitConfig 就是它的一个实现。固然咱们还能够有不一样的实现以实现不一样的定制方式。
  • 3.那么咱们仍是再看图9的6行,能够看见这个方法的返回值 Builder 中添加了一系列 Intercept。由咱们前面的讲解可知,这些是拦截器,而后会按添加的顺序拦截请求和响应。
  • 4.这里能够看见我实现了各类不一样的功能:打印网络请求日志(这个在上一篇文章中没实现,如今实现了)、过滤过于频繁的请求(防止ddos攻击)、SSL认证(固然如今没有后端还没实现)、超时拦截、添加自定义的参数等等。
  • 5.这里的定制比较简单,你们能够去看看各个拦截器中的实现。

(3)Converter定制

  • 1.其实这个也很简单,你们可能都用过,就是图8的五、6两行,添加的数据转换器。
  • 2.你们只要了解我前面讲解的 Converter 的执行策略就能够了。

(4)CallAdapter定制

  • 1.你们能够回看 (1)retrofit2.Call的装饰 这一节,咱们添加了一个 CustomAdapterFactory。
  • 2.由于 CustomAdapterFactory 比 RxJava2CallAdapterFactory 先添加,因此其优先级比较高。再看图8的40行,这里获取了一个 delegate,其实就是 RxJava2CallAdapterFactory。因此咱们能够在 RxJava2CallAdapter 返回的 Observable 上面添加一些统一的操做符。
  • 3.具体的代码在图8的49行,而后转到图9的42行。能够看见我就只添加了一些简单的操做符:计数请求成功和失败次数、配合 ThrottlingInterceptor 进行频繁请求过滤。

(5)网络层定制代码总结

上面就是在网络请求的四个主要节点进行定制的方式。其实总结起来比较简单:1是扩展 Retrofit 返回的结果、2是扩展 okhttp 请求和返回、3是解析 okhttp 返回给 Retrofit 的结果、4是加强对 Retrofit 返回结果的处理。

4、总结

不知不觉已经写了这么多了,原本觉得还能够写一节 Fresco 的定制,如今看来只能放在下一篇文章了。在这里预告一下:从零开始仿写一个抖音App这一系列的文章大概还有一到两篇 android 层面的文章,而且会在接下来的一周左右放出。

这一阶段结束以后个人文章和学习重心将会转向音视频这块。这几个月过来虽然有时候文章会 delay,但最终我也信守承诺没有弃坑。最后但愿你们能持续关注本系列,毕竟我都已经这么努力了不是:)

不贩卖焦虑,也不标题党。分享一些这个世界上有意思的事情。题材包括且不限于:科幻、科学、科技、互联网、程序员、计算机编程。下面是个人微信公众号:世界上有意思的事,干货多多等你来看。

世界上有意思的事
相关文章
相关标签/搜索