查看原文
其他

从零开始仿写一个抖音App

何时夕 开发者技术前线 2019-05-23

点击上方开发者技术前线”,选择“置顶或者星标”

你关注的就是我关心的!


本篇文章是从零开始仿写一个抖音App系列文章的第四篇,作者是何时夕,本文已获得原创授权发表。



一、写在前面

关于目的:笔者目前在抖音的竞品里面做android端的视频拍摄和编辑这块。大公司大家也知道,各个业务都是分层的,所以我们平时的业务都是在音视频架构组封装的sdk之上进行的。所以一旦时间长久了自身的竞争力就会减弱,毕竟没有掌握“核心科技”。好在sdk的源码是内部开放的,所以我可以读读源码去了解“核心科技”。正好读了源码之后可以练练手,自己去实现一遍,这就是本项目诞生的初衷。


关于代码:首先由于保密协议,我们公司的源码是绝对不能开源出去的,因此本项目的全部代码都将会是我自己根据读源码获取到的思想最后实现的。所以本项目最终在性能、兼容性、代码可读性都是不如原本的代码的,所以如果有读者要集成入商业项目的话请慎重。


关于项目:本项目预计会持续1-2年的时间,除非我中途离职了,否则一定会坚持更新。目前的预期是每两周更新一篇博客,与此同时更新一版feature。此外本项目虽然说是写一个抖音App,但其实最终项目中的实现只会是抖音App中各种特效的实现集合,至于和服务器交互还有界面的交互方面我并不会花费很长时间去写。当然除了抖音App中的特效,我有一个爱好是深度学习,所以我将会制作一些基于深度学习的特效集成到项目中,所以有这方面爱好的同学也可以和我多多交流。


飨读者:本项目虽然是我自己的练手项目,但是也有部分目的是希望让一部分不甘于现状想深入学习android的同学和我共同进步。所以大家有项目上面的问题和github上的issue都欢迎和我交流。


二、项目概述

这一节我主要是想对未来项目做一个概述吧,说一下项目的技术栈,这样也好让大家对项目有一个概念。


MVP:这是项目的架构方式,熟悉架构的同学应该知道现在android中有三种架构方式:MVC、MVP、MVVM。为何选择MVP想必大家也都清楚,首先MVC非常老旧也有一堆缺点所以第一个排除。然后是MVVM虽然这种架构已经被“吹”了很久了,但是到现在为止也没有一个成熟完整的解决方案,虽然我之前几个自己写的项目都是使用MVVM(databinding为基础),但是那都是小打小闹。据我所知的“大厂”中没有使用MVVM来当做真正的解决方案的。所以如果大家对这种MVVM有兴趣的话,可以去看看我之前写的几篇博客和项目。另外说一句,我会自己从零开始封装一个MVP的框架,也算对自己一个挑战吧。


okhttp+retrofit:这两个框架想必大家都很熟悉了,我就不多说了。只是我会在项目的过程中对这两个框架进行深度的定制以实现一些有意思的东西,所以这一方面还是有点看头的。



fresco:这个也是广为人知的框架了,可以说这是最强大和性能最好的图片框架了吧。不是我“吹”这个框架,虽然他的缺点有一些比如:侵入性强、框架比较重。但是一个像抖音这样的音视频app,使用fresco是非常适合的,其他图片框架像glide、picasso等等,都有功能不全的问题。此外因为我完整的读过fresco的源码,所以我也可以对fresco进行比较深度的定制。对fresco源码感兴趣的同学也可以去翻翻我之前的博客。


google新MVVM组件:这是google最近发布的组件套装ViewModel+Room+Lifecycles+LiveData,虽然我们用的是MVP框架,但是google推荐的组件使用起来还是用好处的。


首先Room就可以抛弃了,他是数据库的组件,我们有更好的选


ViewModel:第一个功能可以使ViewModel以及ViewModel 中的数据在屏幕旋转或配置更改引起的Activity重建时存活下来,重建后数据可继续使用,第二个功能可以帮助开发者轻易实现Fragment与Fragment之间,Activity与Fragment之间的通讯以及共享数据,所以还是很有必要使用的,可以集成在我们的MVP框架中。

.Lifecycles:这个就不用说了,生命周期组件是Android官方架构组件中的核心组件,它可以使各种实例作为观察者与Activity和Fragment等具有生命周期特性的组件绑定在一起,LiveData和ViewModel,都是基于此组件,简而言之就是,你将需要绑定生命周期的实例注册给该组件,该组件就会在你指定的某个生命周期方法执行时通知这个实例。

LiveData:这个组件其实做的和Rxjava的事情类似,第一个功能是观察者模式,在Value发生变化时通知之前注册的所有观察者,第二功能是基于生命周期组件与Activity和Fragment等具有生命周期特性的组件绑定在一起,在生命周期发生改变时停止或恢复之前的事件。了解Rxjava的同学都知道这些功能在其拓展包中都是有的,所以这个组件我们可以不使用。


插件化+组件化+热修复:这几个东西算是类似的吧,我还是一个个说


插件化:几个优点分别是,多成员负责不同的模块的时候加快编译速度、各个模块解耦、减小发版的包大小按需加载。使用的框架我倾向于自己写一个,但是不知道有没有时间,最后可能会在tinker和360RePlugin中选择一个吧

组件化:其实和插件化类似,主要用于解耦模块,用到的技术是路由组件和gradle分模块依赖技术,倾向于自己写一个


热修复:主要用于应对线上bug,应该会在andfix和Robust里面选一个,毕竟我们的项目对这个要求不。


AOP技术:这个技术用处很多,比如日志记录、自动埋点等等,目前候选的框架有:AspectJ、APT和ASM这些到时候看实现日志和埋点框架的时候再选吧。


Rxjava:不用说Rxjava的生态已经很成熟了,他有RxCache,RxLifecycle,RxAndroid,RxPermission,Retrofit-Adapter等大量并且强大的衍生库,在写项目的过程中我也会对使用到的Rxjava的功能进行分析。


JNA:可能有些同学不了解这个框架,其实这就是一个代替jni的对java开发者更加友好的调用native方法的框架,因为我前面说了我们这个项目主要目的是实现抖音中的各种特效,所以必不可少的会用到jni,因此我用了JNA这个更加方便的库。


9.opencv:目前我也没有开始了解我司到底是使用了哪种技术来实现视频中的各种特效,所以暂定是opencv,以后随着我深入核心代码,可能框架会改变。再次声明,本项目中的代码并不会有任何我司的核心代码,所以大家最好别将项目代码用于商业项目


10.深度学习框架:我之前也使用过tensorflow lite,但是这个框架太简陋了,所以现在的有一个新选择是腾讯的ncnn,这个问题将会在到了该使用深度学习的时候再选择。另外如果对tensorflow lite有兴趣的同学可以去看看我之前写的图片处理APP的和一个使用demo。


11.数据库:初步预想是使用GreenDao,使用起来方便一些,而且我们对于数据库使用程度并不强,我司就是使用这个框架,所以还是可靠的。


12.WebView:最近hybird app非常流行,所以项目里先把这个技术占一个坑吧,到时候可能会实现一个简陋的与js互相调用的WebView容器吧,到时候再看。

日志和埋点以及后端初步架构

一、讨论


讨论1:项目会不会使用 kotlin?

  • 1.目前我的计划是在基础模块上面使用 java ,在业务模块中看情况选择几个模块使用 kotlin。


讨论2:本系列文章是标题党,蹭抖音的热度

  • 1.首先明确一点为什么我要以抖音为例子,原因就是我的公司就是开发短视频的,技术上有类似的地方,而本公司的产品是不可能作为例子开发的,所以我就以抖音为例希望能过一遍大公司的项目开发流程和架构,不仅仅是给读者带来好处,对我来说也是一个很好的提升。

  • 2.当然不可否认的是抖音这个 title 给我带来了一定的流量,也吸引了一部分人的眼球,但是我问心无愧。因为每一篇文章的内容都是我花费两周以上的业余时间撰写的,内容的质量上我敢说比一般的文章要好上不少。

  • 3.有句话说得好:人红是非多,放在文章上也是一样。我不希望打无谓的口水仗所以:以后如果文章中有与技术和文章无关的攻击或者诋毁的评论我会直接删除,并且不做回复。


二、日志和埋点


日志在一个项目中起着非常重要的辅助作用,它可以让开发人员方便的定位 bug。它可以在系统上线之后让后台监控 app 的性能以及稳定性。他还可以收集用户的行为数据以方便对用户的需求进行分析。在这一节中我会分析5种不同的日志,并讲解其中几种日志的实现方式。


首先我先列举一下五种不同的 log 吧。


  • 1.debug 日志:用于开发人员本地 debug

  • 2.aop debug 日志:用于开发人员本地 debug, 使用了 aop 可以通过简单的注释,对方法和类进行切片打日志。用于打一些需要统一执行的日志。

  • 3.网络请求 日志:用于开发人员在本地对网络请求 debug

  • 4.本地文件 日志:用于记录在 app 上线之后出现的bug,将日志打到文件中,可以通过一个入口让用户手动点击上传日志。

  • 5.埋点 日志:用于记录用户使用 app 的数据、app 性能等等的埋点日志,数据结构由前后端协商定义,最后会存入后端的数据库以便进行一些数据分析。埋点的方式可以是手动的,可以是自动的。


1、debug日志



  • 1.debug 日志比较简单,如图一就是将 android 自身提供的 Log 类进行一些封装,添加一些自己需要的特性和扩展,这里就不多赘述了具体实现可以看项目中的代码。


2、aop日志


  • 1.很多人在写一些重复性的日志的时候就会想到 aop,这种技术可以在注解的方法前后注入需要的模板代码。我在上一篇文章中讲到了这个技术,有兴趣的同学可以去看看,这里我就简单说一下。

  • 2.首先我们得先定义一个注解类,其可以用于注解类或者方法。注解类中可以被填入一些信息,比如是否需要打印方法的初入参等等。

  • 3.在注解类使用了之后,我们需要用到 gradle transform。这种技术可以让我们在编译期间扫描所有的类,从而找被注解类所注解的方法和类。

  • 4.最后我们可以用上javassist来给找到的方法前后注入我们需要的代码。注意这里的日志可以是本地的 debug日志,也可以是本地文件日志,还可以是埋点日志。可以说 aop 日志只是一种对另外几种日志的自动化封装。


3、网络请求日志



  • 1.我们在调试网络请求的时候,除了抓包还会打印出网络请求。这个时候就如果有一种统一的形式来打印日志的话就会方便许多。


  • 2.现在绝大部分的厂商使用的网络请求库都是 okhttp ,所以我就直接在其上面进行日志的定制就行了。因为项目的 http 模块还没有进行开发,所以还没有实现代码,这里就讲一讲大致方案。之后在开发 http 模块的时候会顺便讲解具体实现。


  • 3.在讲解方案之前我们需要知道,okhttp 的工作方式。如图3中所示,在一个 okhttp 请求的过程中会经过一个个拦截器,从本地向网络请求的时候会经过一次,网络请求回来的时候又会经过一次。


  • 4.所以我们就可以添加一个日志拦截器在两次经过拦截器的时候打印请求的 head 和按需打印 请求的 body。注意,这里打印可以是向 debug 日志、本地文件日志、埋点日志这三个地方打印。分别用于本地 debug、线上 debug和网络性能监控。


4、本地文件日志


  • 1.当我们在线上遇见 bug 的时候咋办呢?有些 crash 的日志可以通过 bugly 这种平台来进行回捞。但是有些奇葩的 bug 只在某些机型甚至某些用户的手机上发生。这个时候本地文件日志就派上用场了。

  • 2.我们可以在开发的时候在一些关键的功能上手动添加上本地文件日志。当某个用户报了 bug 之后我们就可以让其通过一个入口将文件日志发送到后台,最后由开发人员进行日志分析找到问题。

  • 3.接下来我就来通过代码结合上面的图4来讲解本地文件日志的实现方式。

  • 4.我们先来看看图4:

     

    初始化和绑定LocalFileLoggerService(这是一个 service,可以通过 binder 来与外部交互)

    通过 binder 将外部的添加日志的请求交给LocalFileLoggerService

  •   a.LocalFileLogger负责提供本模块对外的 api,主要功能有两个:

      b.LocalFileLoggerService中会初始化一个 HandlerThread,本 Service 会通过   Handler 向其不断的抛入经过高性能拼接的日志的添加请求。


    FileLogger是负责将日志写入本地的类,其也初始化了一个 HandlerThread,并且自定义了一个 LoggerHandler。这个 Handler 会将 LocalFileLoggerService 抛过来的一条条日志进行累积,当积累到了一定量的时候。发出写入日志的请求交给 HandlerThread执行。



  • 5.再来看看代码,我们跟着代码走一遍流程:

  1. 首先在图5中我们可以看见在 addLog 中经过一系列的调用,最终交给了 sLogInterface.log 这个对象是一个 Binder 对象,用于操作 LocalFileLoggerService 。

  2. 进入到图6,可以看见 Service 初始化了一个 HandlerThread 然后定义了一个 Handler 用于向其中抛送请求。然后在看 mBinder 的实现就是通过 Handler 向 HandlerThread 中抛送 FileLogger.addLog 的执行请求

  3. 进入到图7,可以看见在 FileLogger 初始化的时候也初始化了一个 HandlerThread ,然后定义了一个 LoggerHandler 来向其中抛日志写入请求。FileLogger.addLog 方法中是直接发送一个请求。

  4. 再看图8,LoggerHandler.add 中并不会立即向本地写入日志,而是会有一 LOG_CACHE_COUNT 阈值,只有超过了这个阈值才会向文件系统中写入日志。


5、埋点日志



  • 1.埋点日志其实和文件日志类似,我这里就结合图9简单说一下,具体的代码大家可以去翻看项目


  • 2.首先还是有一个 UploadLogManager 用于给外部提供 api 以及初始化 LocalFileLoggerService。这里比文件系统复杂的地方就在于多了一个 UploadLogConfiguration 用于装配一些设置。


  • 3.有了 LocalFileLoggerService 之后这里分两个不同的埋点日志添加方式。


  • 1.实时埋点日志添加:外部需要立即将当前的埋点日志上报,此时就直接将请求发送给 UploadLogHandler 然后交给 HandlerThread 执行,最终 通过 LogSender执行网络上报。


    2.非实时埋点日志添加:这种方式是每隔一定的时间,LocalFileLoggerService 会从 UploadLogStorage 中取出一定量的日志,合并之后再像1中一样上报埋点。


  • 4.目前因为 Http 模块和 数据库模块都没有开始写,所以 UploadLogStorage 和 LogSender 都还只是接口,但是并不影响代码逻辑。


三、后端架构的初步设想


虽然本项目的着重点是仿抖音 android 端 app 的开发,但是后台方面也会有所涉及。接下来笔者会介绍一下本项目在后端方面的目标和预期达到的效果。


1、RPC

可能会有客户端的同学对 RPC(远程过程调用) 这个词不怎么了解,我这里就先简单介绍一下。

拿 Java 来说:比如我们有两个服务 A、B 在两个服务器上,此时我们要在 A 上调用 B 的服务获取其上的数据 Foo。那么在 A 中可以写成 Foo f = b.XXXService();。在这里 Foo 是 A、B 两个服务所定义的数据传输结构,b 是 B 服务所抽象出来的对象,其内部实现可以是各种网络数据交互协议,比如说 http 协议。简单来说:RPC就是要像调用本地的函数一样去调远程函数。

现存的 RPC 框架有很多,各个大厂也都开源了自己框架,我这里就介绍和比较一下几个框架,最后结合本项目的需求选择适合的框架。


  • 1.Dubbo:这个是阿里开源的一个框架,后来阿里因为种种原因把他废弃了,最后被当当网维护扩展出了一个 Dubbox。这里就讲一讲他的优劣势吧:

    • 1.跨平台能力差,原生的 Dubbo 基本上没有跨平台能力,后面的话集成了 thrift 作为扩展的话就有了,不过我总感觉集成之后用起来不方便。

    • 2.以 java 作为主开发语言的话,不能快速迭代。我们项目的时间主要是要向 android 客户端倾斜,所以需要一个能快速迭代的语言。

    • 3.序列化和反序列化的速度与其他 RPC 框架相比都不是很拔尖。

    • 4.性能较其他几个框架差。

    • 1.Dubbo 是用 java 写的,对于 android 客户端的开发者来说比较友好。

    • 2.Dubbo 的生态目前来说还是比较好的,笔者去年在有赞实习 java 开发的时候,用过半年的 Dubbo,感觉各种坑都有人踩过,各种库也都比较完善。

    • 3.对于服务治理支持的比较到位。

    • 1.优势:

    • 2.劣势:


  • 2.Thrift:这个是 FaceBook 开源的一个框架,2007年由facebook贡献到apache基金,是apache下的顶级项目。

    • 1.跨平台的语言协议写起来比较麻烦。

    • 2.不支持服务治理

    • 1.跨平台能力强,支持几乎所有的主流语言。

    • 2.性能比较好

    • 1.优势:

    • 2.劣势:


  • 3.Grpc:由 Google 开源的框架,我司目前后端也在使用这个框架

    • 1.不支持服务治理

    • 1.跨平台能力强、支持大部分主流开发语言

    • 2.跨平台语言协议用的是 ProtoBuf,与我们客户端的技术栈一致。

    • 3.性能比较好

    • 4.有我司的技术支持,当然不是官方的,不过我可以了解我司在这方面的技术,最后反哺到我们的项目中。

    • 1.优势:

    • 2.劣势:


看了上面的比较我想大家心里已经有了答案,没错我决定使用 grpc 做为本项目后端的 rpc 框架。然后开发的语言是 python 为主,java 为辅助,后面如果有时间的话可能会用 go 实现一个小的服务也说不定。使用这些语言的原因有下面几点:

  • 1.首先 python 目前后台的生态也比较成熟,用起来也比较方便快速。

  • 2.其次我们到了后面会使用 tensorflow 来训练各种深度学习的模型,这样的话熟练使用 python 是必须的。

  • 3.有人会问你为什么要用几种不同的语言来实现后端的服务呢?这不是多此一举吗。的确,从正常开发的角度来讲是挺多余的,但是多语言的环境在大一些的厂来讲是再正常不过的事情,我的一部分目的也是为了模拟这种场景。除此之外,这种多语言的环境在我看来还是比较有意思的,想试试玩玩看。




2.微服务与服务治理


其实本来在这里我有很多东西想说的,但是发现自己现在的能力并不能完全说好这两个东西,怕最后会误导大家,所以我这里就列一下最后本项目需要完成的与这两个目标相关的东西。

  • 1.在未来笔者预期的是会有10台服务机器,两台为一组提供一类服务,一共会有五个大类的服务。

  • 2.所以第一个要实现的功能就是:服务发现注册功能。这个功能主要是和注册中心进行交互。

    • 1.服务提供者启动,向注册中心注册自己提供的服务

    • 2.消费者启动,向注册中心订阅自己需要的服务

    • 3.注册中心返回服务提供者的列表给消费者

    • 4.消费者从服务提供者列表中,按照软负载均衡算法,选择一台发起请求

  • 3.为了了解和监控各个服务的情况,第二个要实现的功能就是:服务监控,即累计计算随着时间推移各个服务被调用的次数。

  • 4.为了区分内外网,以及统一鉴权。需要实现的第三个功能就是:服务网关,所有外部请求都会经过这个网关,网关会将请求分发给内部的机器,内部机器调用完成之后会将结果通过网关返回给外部。


四、ubuntu环境初始化


不知道在我的读者中有多少个人用的是 mac。因为我本人就是 mac 和 win 的双系统用户所以我深知。mac 在开发方面的好处。这一节就轻松一点,我演示一下如何将本地 mac 命令行环境初始化到云上的 ubuntu 中。


1、oh my zsh



  1. 首先需要在XX云中买一个机器。我买的是阿里云,最开始的系统模板选择 ubuntu16,然后什么都不要装。然后在本地用 ssh 登录云主机。



  2. 在本地电脑上 clone 一下我的这个库,接下来要用到里面的两个脚本文件。ubuntu初始化



  3. 用 scp 命令将2中的两个文件上传到服务器上分别是:ubuntu_init.sh 和 ubuntu_init_oh-my-zsh.sh。例如:scp a.jpg root@47.106.145.211:/root/a.jpg,将本地本目录的 a.jpg 文件上传为云服务器上的/root/a.jpg文件。


  4. 运行ubuntu_init.sh,中间会让你输入密码,最后会重启服务器。


  5. 等4中重启服务器之后,登录服务器然后运行ubuntu_init_oh-my-zsh.sh。如此就大功告成了。最终效果如图10,这个终端比 ubuntu 原生的好用多了,而且还支持各种定制的插件。


  6. 忘了说了这个命令行是一个开源项目:oh my zsh,英语比较好的同学可以看原项目,来拓展自己的配置。


2、vim 配置



  1. 接下来就是 vim 的配置,其实我到现在也没完全成功的把配置完全成功的把配置完成成功的转移到 ubuntu 上面,所以大家看看就好。

  2. ubuntu初始化,这个仓库里 .vimrc 是 vim 的配置文件。vim 插件管理,这个仓库里是 vim 插件库。

  3. 这里其实就是为了 show 一下我的成果,对于初学者来说能学习的方面不多,对于老鸟来说也看不上我的配置。


3、docker配置


这两周我也抽空学习了一下 docker,我的理解上 docker 就是一个方便打包重用超轻量虚拟机。所以我们后端也会用上这个技术以方便运维。我也是刚学这东西,所以我就贴几个我学习的网址吧!


  • 1.docker初始学习

  • 2.docker python 学习


五、写在最后


本篇文章是从零开始仿写一个抖音App 系列文章的开篇和三篇综合篇,篇幅比较长能看到这里的同学非常感谢你们对我的认可。后面文章可继续关注。


作者:何时夕
链接:https://juejin.im/post/5b9e9bf1e51d450e6b0dba92

关注开发者技术前线,获取最真实的程序员经历,最新的前沿技术资讯和观点。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存