第四届字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~
第四节:数据存储 & 网络通信
课程概述
-
网络通信
- 网络库框架对比
- Retrofit使用&原理介绍
- TTNet介绍,了解字节跳动网络请求框架
-
数据存储
- Android数据存储方式对比,了解不同场景该使用的工具
- 数据库框架对比
- Room数据库使用与原理介绍
课前
实践准备 (必须)
- 使用Android Studio 创建好一个空的Activity,并运行起来
- Retrofit和Room用到了很多注解,可以提前了解下Android的注解相关的知识
\
课中
-
网络请求
1.1 简介
- 客户端向服务端发起请求,服务端返回数据给到客户端
- 由于网络请求在大型App里使用非常频繁,为了更好的支持业务迭代,一般会进行网络请求的封装
1.2网络框架对比
1.2.1 说明:
- Volley的Request和Response都是把数据方法放到byte[]数组里,不支持输入输出流,把数据放到数组中,如果大文件多了,数组就会非常大且多,消耗内存
- 行业内,目前基本上都是Retrofit 和 OkHttp组合的这种方式来进行网络请求
- IO 和 NIO这两个都是Java中的概念,如果我从硬盘读取数据,第一种方式就是程序一直等,数据读完后才能继续操作这种是最简单的也叫阻塞式IO,还有一种是你读你的,程序接着往下执行,等数据处理完你再来通知我,然后再处理回调。而第二种就是 NIO 的方式,非阻塞式, 所以NIO当然要比IO的性能要好了,而 Okio是 Square 公司基于IO和NIO基础上做的一个更简单、高效处理数据流的一个库。
1.2.2 总结:
- 目前Retrofit和OkHttp的组合,功能更加全面,封装更加彻底,当下最为流行的网络请求方式,我们本文也会重点来关注Retrofit的使用和原理的介绍。
1.3 Retrofit的使用介绍
Retrofit其实是对OkHttp的一个封装,也是当前最为流行的一种网络请求组合方式。
1.3.1 使用举例
场景假设:客户端知道了一个用户的uid,想通过服务端查下这个用户的姓名,通过Retrofit如何实现呢? 接口:www.bytedance.com/users/{uid}… 其中{uid}要替换为实际的uid,例如1123,最终请求为www.bytedance.com/users/1123/…
类型:GET请求 接口返回:
{
"message": "success",
"data": {
"uid":"1123",
"first_name":"张",
"last_name":"三丰"
}
}
1.3.2 使用介绍
-
添加Retrofit库的依赖
- 在需要用到Retrofit接口的module中,新增依赖(最新的版本可看GitHub )
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
//...其他依赖
}
- 创建 用于描述网络请求 的接口
//接口类名:可自定义,尽量和这类请求的含义相关
interface IUserInfoService {
@GET("users/{uid}/name")
fun getUserName(@Path("uid") uid: Int): Call<ResponseBody>
//@GET("users/{name}/uid")
//fun getRequest(@Path("name" name:String)) Call<User>
//后续可以增加其他的接口,一个接口对应一个api请求
}
//函数名:可自定义,需要能识别出该接口的作用,该interface里可以增加多个不同的函数
//@GET 注解:用于指定该接口的相对路径,并采用Get方法发起请求
//@Path 注解:需要外部调用时,传入一个uid,该uid会替换@GET注解里相对路径的{uid}
//返回值Call<ResponseBody>,这里用ResponseBody,我们可以直接拿到请求的String内容
//如果要自动转为Model类,例如User,这里直接替换为User就好。
- 发起网络请求
fun getUserName(view: View) {
//创建Retrofit实例
val retrofit = Retrofit.Builder()
.baseUrl("https://www.bytedance.com/")
.build()
//创建iUserInfoService实例
val iUserInfoService = retrofit.create(IUserInfoService::class.java)
//创建网络请求Call对象
val call = iUserInfoService.getUserName(1123)
//发起异步请求
call.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>,
response: Response<ResponseBody>) {
//请求成功时回调
request_result_tv.text = "请求成功:" + response.body()!!.string()
}
override fun onFailure(call: Call<ResponseBody>, e: Throwable) {
//请求失败时候的回调
request_result_tv.text = "请求失败:" + e.message
}
})
}
1.3.3 总结
- 引入依赖库
- 创建 用于描述网络请求 的接口
-
发起网络请求
- 创建
Retrofit实例 - 创建
iUserInfoService实例 - 创建网络请求
Call对象 - 使用
Call对象发起异步请求
- 创建
- 其他更多的用法,更多的注解,可以看Retrofit官网square.github.io/retrofit/
\
1.4 TTNet
TTNet是字节跳动通用的网络请求封装框架,用来向服务端发起请求。
目前Android端上的头条,抖音,番茄小说都在使用该库作为网络请求封装框架,作为字节内部的基础库,提供了一整套Android客户端网络请求解决方案。
- 它有哪些突出的优点:
-
- 基于Retrofit改造,具备了Retrofit所具有的优点
- 支持多个Http网络库的动态切换(okhttp和cronet)
- 支持网络拦截配置:添加公共参数,动态切换协议及Host,动态选路等
- 支持流解析,json序列化
- ......
1.4.1 为什么要做TTNet?
Retrofit作为网络封装框架,已经足够优秀了,那么我们为什么还要基于Retrofit改造做我们自己的TTNet网络封装框架呢?
Retrofit是开源的网络框架,我们只能被动依赖,无法主动修改以适应我们的业务发展。如:Retrofit只支持OkHttp网络库,不支持其他网络库。
那么,问题来了。我们有更优秀的网络库cronet,我们想使用它,但是我们又迷恋Retrofit的优秀的封装特性和接口调用的简易特性?
答案:打造自己维护的网络框架 TTNet !
暂时无法在飞书文档外展示此内容
1.4.2 TTNet与Retrofit的使用对比
| Retrofit | TTNet |
|---|---|
1.4.3 Retrofit使用OkHttp的流程介绍
1.4.3.1 Retrofit的主流程
先上个图,结果先行方便大家理解主流程调用关系。
暂时无法在飞书文档外展示此内容
\
1.4.3.2 Retrofit调用底层OkHttp的方式
暂时无法在飞书文档外展示此内容
在主流程图中,okhttp网络库的api,在Retrofit中是何时发起请求的,对应api已列出,相同颜色即意味着调用关系。
如:
绿色代码:标记1处,创建Retrofit对象时,对应创建了OkHttpClient对象;
红色代码:标记4处,使用Retrofit的Call对象发起请求时,对应创建OkHttp网络库中Request对象和Call对象并发起请求。
\
1.4.3.3 Retrofit里的OkHttpClient的调用时机
public void getRequest() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://www.bytedance.com/")
//.addCallAdapterFactory(okhttp3.Call.Factory)
.build() ; // 标记(1)
}
// Retrofit$Builder
public Retrofit build() {
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient() ; // 标记(2)
}
List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
// platform为Android的实例
// 集合里的实例为ExecutorCallAdapterFactory的实例。这里要留意下
adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
callbackExecutor, validateEagerly);
}
static class Android extends Platform {
@Override public Executor defaultCallbackExecutor() {
return new MainThreadExecutor();
}
@Override CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
return new ExecutorCallAdapterFactory(callbackExecutor);
}
}
// Retrofit
public final class Retrofit {
private final okhttp3.Call.Factory callFactory;
Retrofit(okhttp3.Call.Factory callFactory, HttpUrl baseUrl,
List<Converter.Factory> converterFactories, List<CallAdapter.Factory> adapterFactories,
Executor callbackExecutor, boolean validateEagerly) {
this .callFactory = callFactory; // 标记(3)
}
}
OkHttpClient对象的创建,我们已经看到了。那么创建好之后,在何处完成调用呢? 我们往下走,答案就在下面的流程梳理里面。
不过,我们先留意下,adapterFactories集合里的对象。里面有一个默认的CallAdapter.Factory实例,它就是子类ExecutorCallAdapterFactory的实例。我们在后续解析请求方法时,会用到它。
1.4.3.4 OkHttpCall的创建
当我们通过代理对象调用我们的接口方法IUserNameService#getUserName时,会触发InvocationHandler#invoke方法。请求物料的封装细节较多,单独拉了一篇文档。可以参考:13.2请求物料的封装
public void getRequest(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://www.bytedance.com/")
.build();
IUserNameService iUserNameService = retrofit.create(IUserNameService.class);
// 标记1
Call<ResponseBody> call = iUserNameService.getUserName( 1123 );
}
// Retrofit
public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
// 标记(2)
@Override public Object invoke(Object proxy, Method method, Object... args) {
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
我们需要重点关注invoke方法中OkHttpCall对象的创建,serviceMethod.callAdapter.adapt(okHttpCall)中方法调用的实现,serviceMethod.callAdapter.adapt(okHttpCall)的返回值。一会儿会用到
1.4.3.5 okhttp.Request对象和okhttp.Call对象的创建及请求发起
public void getRequest(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://www.bytedance.com/")
.build();
IUserNameService iUserNameService = retrofit.create(IUserNameService.class);
// call对象的取值为InvocationHandler#invoke()方法的返回值,默认为ExecutorCallbackCall对象
Call<ResponseBody> call = iUserNameService.getUserName(1123);
// 标记1: 由请求物料的封装过程,我们知道 call为ExecutorCallbackCall的实例
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
// Retrofit
public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
// 标记(2)
@Override public Object invoke(Object proxy, Method method, Object... args) {
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
// ExecutorCallbackCall
@Override
public void enqueue(final Callback<T> callback) {
// ExecutorCallbackCall实例所拥有的delegate为OkHttpCall对象。
delegate.enqueue(new Callback<T>() {
@Override
public void onResponse(Call<T> call, final Response<T> response) {
// ...
}
});
}
//OkHttpCall 这里的内容是不是似曾相识,没错它就是OkHttp完成请求的过程。我们上面已贴出
public void enqueue(final Callback<T> callback) {
// 这里完成的了Request的创建
Request request = serviceMethod.toRequest(args);
// 这里完成了Call对象的创建
okhttp3.Call call = serviceMethod.callFactory.newCall(request);
// 通过call对象发起异步请求
call.enqueue( new okhttp3.Callback() {
@Override
public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
throws IOException {
Response<T> response = parseResponse(rawResponse);
callSuccess(response);
}
@Override
public void onFailure ( okhttp3.Call call, IOException e ) {
callback.onFailure(OkHttpCall.this, e);
}
});
}
// ServiceMethod
Request toRequest(Object... args) throws IOException {
RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
contentType, hasBody, isFormEncoded, isMultipart);
ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;
for (int p = 0; p < argumentCount; p++) {
handlers[p].apply(requestBuilder, args[p]);
}
return requestBuilder.build();
}
在OkHttpCall#enqueue方法里,通过ServiceMethod#toRequest方法,完成了Request的创建。因为ServiceMethod拥有接口方法IUserNameService#getUserName的全部请求物料信息,传入请求实参后,便可直接构造request对象。
构建完request对象后,serviceMethod.callFactory为OkHttpClient实例。通过OkHttpClient#newCall方法构建Call对象。
构建Call对象完成后,通过Call对象发起异步请求。
请求完毕后,返回响应对象Response。到此OkHttp请求调用过程完毕。
\
到这里Retrofit只支持OkHttp网络库就得到了解答:
通过代理对象对方法接口进行调用时,会在InvocationHandler#invoke方法回调里为serviceMethod.callAdapter.adapt()设置okHttpCall对象,进而发起请求时,委托OkHttpCall对象发起网络请求。而OkHttpCall#enqueue 调用okHttp网络库发起请求。
所以serviceMethod.callAdapter.adapt()设置死了为okHttpCall对象。Retrofit发起请求只能对OkHttp网络库进行支持了。
public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
// 标记(2)
@Override public Object invoke(Object proxy, Method method, Object... args) {
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
所以我们要支持多个网络库的改造,那么就在**OkHttpCall** 的改造上做文章就好。
\
总结
- 关于命名的大多数规范核心在于考虑上下文
- 人们在阅读理解代码的时候也可以看成是计算机运行程序,好的命名能让人把关注点留在主流程上,清晰地理解程序的功能,避免频繁切换到分支细节,增加理解成本
\
1.5 TTNet实现原理
TTNet基于Retrofit的进行二次开发,核心主要是替换Retrofit其中的2点
- 替换底层用到的
OKHttpClient
- 替换底层用到的
OKHttpCall
1.5.1 TTNet主流程
1.5.2 更多TTNet相关的解析,可以看TTNet 源码分析
\
1.5 网络请求总结
\
-
数据存储
2.1 简介
- 数据存储的方式有很多种,其使用场景也不一样,本文主要介绍其中4种最为常见的数据存储方式和使用场景
- 数据库是4种常见数据存储方式中较为复杂的能力,本文也会重点介绍
2.2 存储方式对比
持久性的本地数据存储是Android中常见的能力,可以在应用被杀死的情况下,而保持数据不会被清除。我们可以根据不同场景的诉求,可以选用不同的存储方式,常见的数据存储主要有以下4种。
2.3 数据库开源框架对比
数据库 相对来说,其使用会比较复杂些,我们单独进行探索,下面是几个主流的数据库框架对比
不同的产品,对功能的诉求不太应用,头条因为也有用到LiveData,同时考虑到是Google出品,其流行度和稳定性都有较好的保障,所以更倾向于使用Room数据库,下面会重点介绍下Room数据库。
\
2.4 Room数据库的使用
Room是 Google Jetpack 家族里的一员,Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库
主要的3个组件
- 数据库类(
Database),用于保存数据库并作为应用持久性数据底层连接的主要访问点。
- 数据实体(
Entity),用于表示应用的数据库中的表。
- 数据访问对象(
DAO),提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法。
2.4.1 Room接入
Gradle目录的build.gradle文件里添加如下:
2.4.2 数据表设计
下面假设设计一个表,表名为user,数据表包含uid、first_name、 last_name 3个字段
2.4.3 新建Entity
定义一个User数据实体,User的每个实例都代表App数据库中的user表的一行
2.4.4 新增DAO
定义一个名为UserDao的DAO。用来对User表的增删改查
2.4.5 新建数据库类
进行数据库配置,并需满足以下几个条件:
- 新增一个
RoomDatabase的abstract子类
- 子类需加注解
@Database(entities = [xxx], version = n),entities包含数据实体,将会在这个数据库中创建对应的表,version是数据的版本号
- 对于与数据库关联的每个DAO类,数据库类必须定义一个无参的抽象方法,并返回DAO类实例
2.4.6 获取dao对象
可进行数据库的增删改查操作
2.5 Room原理介绍
核心
- 编译期,通过kapt处理
@``Dao、@Database注解,动态生成对应的实现类
- 底层使用Android提供的
SupportSQLiteOpenHelper实现数据库的增删改查等操作
\
2.5.1 kapt注解处理
Room在编译期,通过kapt处理@Dao和@Database注解,生成DAO和Database的实现类 AppDatabase --> AppDatabase_Impl UserDao-->UserDao_Impl
kapt生成的代码在build/generated/source/kapt/
2.5.2 实现类利用Android SQLite进行数据库操作
AppDatabase_Impl:数据库实例的具体实现,自动生成,主要有以下几个方法
createOpenHelper(): Room.databaseBuilder().build()创建Database时,会调用实现类的 createOpenHelper()创建SupportSQLiteOpenHelper,此Helper用来创建DB以及管理版本 userDao():创建UserDao_Impl
UserDao_Impl: UserDao的具体实现,自动生成,主要有以下3个属性以及UserDao里定义的接口
__db:RoomDatabase的实例 __insertionAdapterOfUser :EntityInsertionAdapterd实例,用于数据insert __deletionAdapterOfUser:EntityDeletionOrUpdateAdapter实例,用于数据的update/delete
以下几个是UserDao里我们自己定义的接口
insertAll(): 使用__db开启事务,使用__insertionAdapterOfUser执行插入操作
delete():使用__db开启事务,使用__deletionAdapterOfUser执行删除操作 getAll():使用Cursor循环读取数据库的每条记录,并将结果保存在List<User>中返回 loadAllByIds():和getAll()类似,查询语句不同 findByNames():和getAll()类似,查询语句不同
\
2.6 数据库总结
课后
- 进一步了解
Retrofit的其他使用方式 square.github.io/retrofit/
- 进一步了解
OkHttp的用法和原理 square.github.io/okhttp/
- 进一步了解
Room数据库的用法和原理 developer.android.com/training/da…
- 自己尝试使用
Retrofit和Room数据库,进行简单demo的编写
参考资料
- 使用
Room将数据保存到本地数据库developer.android.com/training/da…
Room原理详解:juejin.cn/post/692378…
第五节:探索多媒体直播技术
课程概述
-
视频的基本元素
- 分辨率
- 帧率
- 码率
- 视频轨道
-
视频压缩
- I、P、B 帧的特点
- GOP 的概念
- H264、H265 编码的特点
-
直播推拉流协议
- 对比不同视频场景的技术方案选取
- 对比 FLV 和 MP4 的封装特点
- 介绍 RTMP / HTTP-FLV / HLS 的特点和选择
-
客户端****工作及总结
- 开播:图像采集、图像处理、编码、推流
- 看播:拉流、解码、图像处理、渲染
\
课前准备
-
下载并安装 ffmpeg:ffmpeg.org/download.ht…
-
安装 ffmpeg 后里面会包含 ffprobe,使用 ffprobe 查看视频文件的信息
-
\
-
下载并安装剪映:lv.ulikecam.com/
-
导入导出素材,选择不同的“格式”,对比导出后的视频大小
-
是不是 HEVC 格式的视频文件会更小呢?
\
课中
视频的基本元素
像素
是指在由一个数字序列表示的图像中的一个最小单位,称为像素,通常表现为一个小方格。每个像素有自己的颜色值,一般为RGB 三原色来表示。
RGB 三原色
每种颜色都可用三个变量来表示-红色绿色以及蓝色的强度。记录及显示彩色图像时,RGB是最常见的一种方案。
它们的取值,R、G、B分别从0到255,一共256个等级。
同学们可以在 www.sessions.edu/color-calcu… 上控制游标感觉颜色的变化
\
分辨率
指的是纵横方向的像素数量,一般表示为:宽高 (or 长宽),720*1080等
\
码率 / 比特率
表示单位时间内传送 bit 的数目,单位bps,表示是单位时间播放连续的媒体如压缩后的音视频的bit数量,也称为码流。
这种bps的单位电信领域上表示速度,就是我们常说的网速了。100Mbps
\
越高的比特率,需要更高的带宽来支撑,否则会带来卡顿、成本等问题
太低的比特率,可能会导致视频画面过度压缩,模糊不清
\
通过改变码率可以让视频文件在保持帧率、分辨率的情况下减少文件体积
\
帧 & 帧率****
帧(Frame)
理解帧就是视频或者动画中的每一张画面,而视频和动画特效就是由无数张画面组合而成,每一张画面都是一帧。
帧率 (Frame Rate)
每秒传输帧数通俗来讲就是指动画或视频的画面数、帧率。每秒钟帧数越多,所显示的动作就会越流畅。
FPS 也可以理解为我们常说的“刷新率。当刷新率太低时我们肉眼都能感觉到屏幕的闪烁、不连贯,
对图像显示效果和视觉感观产生不好的影响。
\
封装
对于任何一部视频来说,只有图像,没有声音,肯定是不行的。所以,视频编码后,加上音频编码,要一起进行封装。
简单来说,就是将已经编码压缩好的视频轨和音频轨按照一定的格式放到一个文件中。再通俗点,视频轨相当于饭,而音频轨相当于菜,封装格式就是一个饭盒,用来盛放饭菜的容器。 目前主要的视频容器有如下: MPG、VOB、MP4、3GP、ASF、RMVB、WMV、MOV、Divx、MKV、FLV、TS/PS等。 封装之后的视频,就可以传输了,
你也可以通过视频播放器进行解码观看。
视频压缩
视频播放的本质:将视频画面从封装文件中进行解码、渲染
\
GIF VS MP4
GIF 图与 MP4 有点相似,都是包含了一组会动态图像的文件
但是相同内容的 GIF 和 MP4 文件体积相差非常大,GIF 会比 MP4 大上好几倍,主要原因是 GIF 缺少帧间编码,无法有效地利用图像间的相关信息
\
帧内压缩 / 空间压缩
帧内(Intraframe)压缩也称为空间压缩(Spatial compression)。当压缩一帧图像时,
仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。
帧内一般采用有损压缩算法,达不到很高的压缩比。
帧间压缩 / 时间压缩
帧间压缩(Interframe compression)也称为时间压缩(Temporal_compression),是基于许多视频或动画的
连续前后两帧具有很大的相关性(即连续的视频其相邻帧之间具有冗余信息)的特点来实现的。
通过比较时间轴上不同帧之间的数据实施压缩,进一步提高压缩比.一般是无损压缩。
I-frame:Intra-frame 帧内帧 P-frame:Predicted Frame 前向预测帧
B-frame:Bi-Directional frame
由于B帧可以参考和插入在它之前和之后发生的两个(或更多)帧(在时间维度上),所以它可以显著降低帧的大小,同时保持视频质量。B帧能够利用空间冗余和时间冗余(未来的帧和过去的帧),这使得它在视频压缩中非常有用。
各种帧类型对比
不同类型的帧组合成 GOP 的概念
视频改变播放进度后,如果不在同一个 GOP 中,需要从新位置所在的 GOP 的 I 帧开始解码
拖动后起播所需的耗时取决于位置在 GOP 中的位置,越靠前能越快响应
\
不同场景对 GOP 的设置也不一样
视频点播:节省带宽,高压缩率,会使用 B 帧
直播:低延迟,不使用 B 帧
视频编辑:提高响应,个别会使用全 I 帧
\
视频编码格式 H264 VS H265
H265 压缩比更高,需要更多的算力
H264 : AVC 更普及
H265 : HEVC 更小体积
\
直播推拉流协议
CDN:Content Delivery Network
建立并覆盖在Internet 之上,由分布在不同区域的边缘节点服务器群组成的分布式网络。通过智能调度将用户请求到最接近用户的服务节点,降低用户访问延迟,提升可用性。
\
边缘节点:指在靠近用户的网络边缘侧构建的业务平台,提供存储、计算、网络等资源,将部分关键业务应用下沉到接入网络边缘,以减少网络传输和多级转发带来的宽度和时延损耗
不同场景的视频通信方案
封装格式:MP4
Moov:Movie Box,存储 mp4 的 metadata,一般位于mp4文件的开头。
mvhd:Movie Header Box,mp4文件的整体信息,比如创建时间、文件时长等
trak:Track Box,一个mp4可以包含一个或多个轨道(比如视频轨道、音频轨道)
\
Stbl:Sample Table Box
包含了这些媒体数据的索引以及时间信息
封装格式:FLV
FLV是一个二进制文件,由文件头(FLV header)和很多tag组成。
tag又可以分成三类:audio,video,script,分别代表音频流,视频流,脚本流(关键字或者文件信息之类)
\
推拉流协议
RTMP : Real-Time Messaging Protocol
HTTP-FLV:HTTP + FLV
HLS : HTTP Live Streaming
\
推流协议
实时消息协议(英语:Real-Time Messaging Protocol,缩写RTMP)
也称实时消息传输协议,是最初由Macromedia为通过互联网在Flash播放器与一个服务器之间传输流媒体音频、视频和数据而开发的一个专有协议。后被 Adobe公司收购。
\
优势:
•基于 tcp 协议
•技术成熟,Ffmpeg 项目中有 rtmp 库
•低延迟
劣势
•停止更新
•规范上没有支持 H265
•使用 1935 端口,会被防火墙阻碍
\
HTTP-FLV Flash Video(简称FLV),是一种网络视频格式,用作流媒体格式。 协议友好,格式简单,便于分发
不转码的情况下直接转发即可,延迟较低
\
HTTP Live Streaming
缩写为HLS是由苹果公司提出基于HTTP的流媒体网络传输协议。
是苹果公司QuickTime X和iPhone软件系统的一部分。它的工作原理是把整个流分成一个个小的基于HTTP的文件
来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的
资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的扩展
M3U (m3u8) 播放列表文件,用于寻找可用的媒体流。
HLS只请求基本的HTTP报文,与实时传输协议(RTP)不同,HLS可以穿过任何允许HTTP数据通过的防火墙或者
M3u8 一级请求返回
相对地址
自适应码率 根据不同的带宽决定使用合适的源
\
M3u8 二级请求返回
视频文件数据
\
各方案对比
\
客户端工作
\
推流端
采集方式:摄像头、屏幕、图像采集卡等
图像处理的场景:美颜、绿幕、头饰
\
拉流端
\
课后作业
-
Ffmpeg 提取轨道数据(参考:使用FFmpeg添加、删除、替换和提取视频中的音频 - 掘金)
- 尝试使用 ffmpeg 提取 mp4 文件中的视频数据并封装到新的 mp4 文件
- 尝试使用 ffmpeg 提取 mp4 文件中的音频数据并封装到新的 mp3 文件
- 使用 ffprobe 查看一下题目 1.a 中提取出来的视频文件信息,看一下这个文件还是否有音轨?
- 《FFmpeg的安装和基本使用》 - 掘金 参考文中转 gif 格式的方案,尝试将一个短的视频转成 gif 格式,对比一下生成后的文件体积,结合课程中的编码知识,感受一下时间编码的重要性
\
推荐阅读:
【Android 音视频开发打怪升级:音视频硬解码篇】一、音视频基础知识 - 掘金
第六节:端智能技术演进与实践
课程概述
-
什么是端智能技术
- 端智能是什么 (what)
- 为什么要做端智能 (why)
- 端智能的发展历程
-
端智能技术实践案例 (how)
- 手写数字识别
- 左右手智能识别和应用(抖音案例)
-
端智能工程师学习长路线
- 回顾端智能完整流程
- 入门:对端智能技术有一定认识,可以协作完成端智能技术的需求落地。
- 进阶:对移动端技术、端智能技术和负责业务有更深入的理解,可以站在更全面的视野上设计端上智能解决方案,建设端上智能架构。
课前
实践准备 (必须)
-
下载 手写数字识别案例Demo,按readme指引搭建环境,跑通demo
-
下载 左右手识别案例Demo,按readme指引搭建环境,跑通demo
课中
01 什么是端智能技术
从端智能是什么、为什么要做端智能、端智能的发展历程带大家认识端智能技术。
1.1 端智能是什么
- 端智能(On-Device Machine Learning)是指把机器/深度学习算法模型应用和部署到端设备上,这里“端设备”是相对于云服务而言的,可以是手机,也可以是物联网IoT设备。
- Google对端智能的介绍 developers.google.com/learn/topic…
-
机器学习已进入生活中各个方面,在移动端上也有着非常多的应用场景:
-
视觉:扫码、人脸手势识别、肢体姿态估计、目标检测跟踪、图像分割、OCR文字识别提取、图片视频实时超分等
-
语音:语音分类识别等
-
文本:翻译、对话系统、文本分类、智能回复等
-
其他:推荐精排、无人驾驶、运动检测、智能预加载、AR、VR等
-
Google提供的端上机器学习能力介绍 developers.google.com/learn/topic…
-
Google提供的MLKit(developers.google.com/ml-kit)和华为提…
-
华为ML Kit demo下载参考 developer.huawei.com/consumer/en…
-
-
-
其他一些关于端智能业界案例
- [字节]你竟然是这样的端智能?
- [字节]字节跳动APP工厂的端云一体AI落地方案:端云深度融合,模型极速上线之路
- [阿里]构建人人可实践的端侧 AI
- [阿里]淘宝端智能探索和实践
- [阿里]淘系技术 | 端智能技术体系概述
- [阿里]端智能与MNN初探
- [阿里]AI运动:阿里体育端智能最佳实践
- [阿里]端智能是什么?端智能的发展有哪些变化?
- [大众点评]端智能在大众点评搜索重排序的应用实践
- [快手]端上智能在快手上下滑推荐取得APP时长+1%的应用实践
- [快手]重排序在快手短视频推荐系统中的演进
- [快手]快手端侧 AI SDK 框架,快手爆款特效落地背后的秘密
- [小米]MIUI 10加持AI预加载,更快更流畅,这个功能你该试一试!
- 听说,你还不了解“端智能”?
- 边缘计算与端智能,2022年会火吗?
1.2 为什么要做端智能
- 低延迟、实时性高:特征收集、模型推理、业务逻辑均在端侧完成,无需网络交互,端侧也更能够实时感应用户状态。
- 保护隐私:数据只在端侧使用,无需上传云端,可更好地保护用户隐私数据。
- 算力:移动端设备越来越强大,算力越来越高。
- 可离线:可以离线使用。
- 低成本:在端侧计算,利用端侧算力和存储空间,可节省大量的云端计算和存储资源。
\
-
端云协同: 端智能并不是云智能的替代,是云端机器学习的延伸,是要结合云和端各自的优势,在云端之间合理分配任务以获取问题最优解。
- 端设备上算力、内存、存储和功耗受限,运行的模型大小有限制;云端有海量数据和充足算力资源,两者配合协同可以发挥各自优势;典型的场景如移动端上的智能精排场景。
1.3 端智能的发展历程
- 2006年,深度学习被提出,又得益于大数据的发展和硬件算力提升,深度学习算法和框架也不断演进,人工智能领域迎来了一次大发展。
-
与此同时,端侧设备在算力、算法和框架上同样有突飞猛进的发展,各类端侧框架和案例层出不穷。
- 2015年Tensorflow 推出Mobile版SDK,随后2017年开源,腾讯NCNN开源,各大互联网公司相继推出自己的移动端机器学习框架,开启了移动端计算框架开源潮。
-
端侧机器学习框架
- Google:Tensorflow Lite、ML Kit
- Meta:PyTorch Mobile、PyTorch Live
- Apple: CoreML
- 腾讯:NCNN、TNN,(NCNN和TNN不得不说的故事)
- 阿里:MNN
- 百度:Paddle-Lite
- 华为: HiAI 、ML Kit
- 小米:Mace
- 其他:字节(ByteNN、Pitaya),快手(KwaiNN、Ykit)……
02 端智能技术案例
通过两个案例(“手写数字识别”、“左右手智能识别和应用”)理解如何做端智能。
2.1 手写数字识别
数字识别是很多机器学习的入门第一个案例,相关资料很多,如:
- www.tensorflow.org/tutorials/q… (全连接模型)
此例完整代码和数据集已存于 github.com/ahcyd008/Di…
2.1.1 需求背景
- App要做一个手写输入法模块,支持输入数字0~9,如何实现?
- 效果预览
暂时无法在飞书文档外展示此内容
2.1.2 端智能案例落地套路
-
分三个步骤
- 首先,把问题描述和定义清楚,并调研出通过机器学习解决问题的方法。
- 然后,设计和训练出机器学习模型,并针对端侧设备优化和转换模型。
- 最后,把优化后模型部署和集成到端侧设备应用中,执行推理预测,并以进预测结果解决定义的问题。
2.1.3 问题和方案
- 问题:App要做一个手写输入法模块,支持输入数字0~9。
-
解决方案:
- 训练一个可以识别手写数字的机器学习模型。
- 将此模型部署应用到App中,实现手写输入到识别的过程,并将识别结果供给用户选择输入。
-
机器学习部分:
- 输入:手写输入数字(图片)
- 输出:其可能代表的数字数值,多种可能时给出各自置信度(0.0~1.0)。
2.1.4 训练AI模型
数据收集
-
数据收集来源方式
-
开源数据 - ~~~~MNIST -
合成数据- 如将各种字体下的数字写在黑板上生成图片,作为样本。
-
人工收集和标注的数据 (本次模型训练仅使用此种方式)
- 设计和实际需求贴合的一个数据采集程序
- 兼顾不同年龄段、性别、左手/右手等
-
-
收集的数据被保存在收集sdcard中;同时为方便数据回收,会同步上传到云端。
-
数据从手机sdcard中导出的命令
-
adb pull /storage/emulated/0/Android/data/com.clientai.recog.digital/files/Track/ ./ -
数据上传的云端为 http://129.204.41.76:8000/
- 临时供课程学习收集数据使用,web数据存储服务介绍参考github.com/ahcyd008/Da…
-
-
数据增广
-
对原始图片进行旋转、平移、缩放变换,扩充更多数据集,增强模型适应性。
- 第一张是原图,其他15张为增广图片。
-
模型设计和训练
-
模型设计
-
先看一个可以在线模拟演示神经网络模型设计神奇网站,playground.tensorflow.org/。
-
模型的设计就像一个搭积木的过程,通过调整神经元个数和层数让预测更精确;神经元和层数越多相应耗时也会增加,需要综合权衡考虑。
- 暂时无法在飞书文档外展示此内容
-
-
参考Tensorflow样例 (www.tensorflow.org/tutorials/q…
-
-
-
模型训练
-
完整参考代码 notebook/digital_recognition.ipynb (notebook代码包括了运行结果缓存)
-
搭建训练环境
- 在vscode中安装jupyter notebook运行环境 code.visualstudio.com/docs/datasc…
- 安装Tensorflow和相关依赖
-
pip install matplotlib numpy Pillow tensorflow torch torchvision
-
训练样本处理
-
图片通过旋转、平移、缩放、剪切、翻转增广16倍,并缩放到28x28大小,转成numpy array 作为输入样本
-
org = Image.open(file) # 需要数据增广 w, h, c = (*org.size, len(org.mode)) # 数据增广配置 trans = torchvision.transforms.Compose([ torchvision.transforms.RandomAffine(translate=(0.25, 0.25), scale=(0.8, 1.2), degrees=40, interpolation=torchvision.transforms.functional.InterpolationMode.NEAREST), torchvision.transforms.RandomHorizontalFlip(p=0.2), torchvision.transforms.RandomCrop(size=(h, w))]) # 增广15倍,带原图16张 for i in range(16): if i == 0: img = org else: img = trans(org) img = img.resize((28, 28), Image.ANTIALIAS) gray = img.convert("L") x = np.asarray(gray)/255. y = label X.append(x) Y.append(y) -
模型构建-Keras
-
模型训练
-
# 全连接网络 def creat_nn(print_summary=False): model = Sequential() model.add(layers.Flatten(input_shape=(28, 28))) model.add(layers.Dense(128, activation='relu')) model.add(layers.Dropout(0.2)) model.add(layers.Dense(10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) if print_summary: model.summary() return model train_x = X # 将label转成向量,如 2 => [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] train_y = tf.keras.utils.to_categorical(Y, num_classes=10) model.fit(train_x, train_y, batch_size=24, epochs=16, verbose=1) -
验证模型准确度
-
y_pred = model.predict(valid_x) // 用y_pred预测值与真值对比,计算准确度
-
模型压缩和转换
-
为什么要做模型转换?
- 移动端使用的是被优化的推理引擎,可以在不同CPU和GPU架构下高效的执行模型推理计算。
-
这里我们使用Tensorflow Lite作为移动端上推理引擎。
- TensorFlow Lite 的设计旨在在各种设备上高效执行模型。这种高效部分源于在存储模型时,采用了一种特殊的格式。
- TensorFlow模型在被TensorFlow Lite 使用前,必须转换成这种格式。
- Tensorflow Lite 提供转换器 TFLite Convert。
-
#save tflite model converter = tf.lite.TFLiteConverter.from_keras_model(model) tflite_model = converter.convert() open(tflite_name, "wb").write(tflite_model)
2.1.5 AI模型在移动端部署和应用
- Tensorflow Lite 提供了很多样例供大家学习参考 (其中也包括了手写数字识别)
模型部署到移动端
-
核心:让移动端可以加载到前面转化的模型
-
简单做法:放在App包体内assets目录下。
-
进阶做法:通过url动态下载,支持动态更新,同时减少包体积。
-
-
在移动端加载模型,Tensorflow Lite官方给出了指引
- www.tensorflow.org/lite/guide/…
- 代码参考 DigitalClassifier
- Tensorflow Lite 将模型的加载和执行封装在Interpreter类中
-
// 引入tensorflow lite 依赖 implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly-SNAPSHOT' // 从文件加载模型 public Interpreter(File modelFile, Options options) // 将模型读取到bytebuffer,从bytebuffer加载模型 public Interpreter(ByteBuffer byteBuffer, Options options)
移动端推理预测
-
推理预测是指设备上执行 TensorFlow Lite 模型
- 代码位于 DigitalClassifier
- www.tensorflow.org/lite/guide/… (官方指导)
-
加载模型
-
var model: ByteBuffer = load(mymodel.tflite) val interpreter = Interpreter(model, options) // 可通过 interpreter 查看输入和输出格式 // inputShape:[1, 28, 28], outputShape:[1, 10]
-
-
构造模型输入
- 模型的输入28x28单通道灰度图,而用手写输入是数据是存在bitmap中
- 需要将bitmap图片 resize 缩放到28x28像素,转灰度,再转成bytebuffer,这是 interpreter 接受的输入格式。
-
执行推理预测
- 构造输出buffer,调用run执行计算
-
val result = Array(1) { FloatArray(10) } // outputShape:[1, 10] interpreter.run(inputByteBuffer,result)
-
获取推理预测结果
- result中的10个数值代表手写输入是 0~9 这10个数字置信度分值,对分值进行排序,把排序前三的数字作为预测输出,供用户选择。
拿到结果执行业务策略
- 视频中可以看出在输入8时不够准确,这受限于数据不足、模型简单。在实际场景中我们落地一个场景后,也需要不断迭代优化,让我们的智能方案更佳完善。
- 在demo中也提供了cnn版本的优化模型,准确率更高一些。
暂时无法在飞书文档外展示此内容
2.1.6 小结
- 一个端智能案例的落地套路流程
\
休息10分钟,稍后继续
\
2.2 左右手智能识别和应用(抖音案例)
左右手识别案例在GitHub上也有一份样例可参考 github.com/ahcyd008/Op…
案例Demo中放置的数据不多,模型准确率不高,作为同学们课后作业,继续:
- 搭建好左右手识别案例Demo运行环境
- 收集更多左右手训练数据,优化模型让其准确率更高,适应性更好。
- 将模型替换到案例Demo中,实测效果。
2.2.1 需求背景
- 很多App会以右手习惯来设计交互,并以此去引导用户点击它们期望的选项。
-
存在问题:用户左手使用手机时,引导效果会变差,有没有更智能更个性化的交互体验方式?
- 左手和右手单手拇指操作时的舒适区是相反的,针对右手的UI交互可能对左手并不友好。特别随着屏幕尺寸再不断增大,这种差异更明显。
- 另外一些数据统计看左手用的比例并不低。
- 依照前卖了提到的端智能场景落地套路,我们继续来拆解这个场景。
2.2.2 问题和方案
-
问题:设计一种方法检测用户是左手在操作手机、还是右手在操作手机,然后依据检测结果动态调整交互,提升用户体验。
- 如识别到右手时,交互设计保持右手适配模式;识别到左手时,交互设计更改为左手适配模式。
-
调研:手指在屏幕滑动轨迹识别左右手
- www.ncbi.nlm.nih.gov/pmc/article…
- 文章采用非机器学习方式,是通过
-
机器学习方案(我们的方案):
- 训练一个二分类的CNN神经网络模型来识别用户是左手 or 右手操作。
- 输入:用户在屏幕上的滑动轨迹
- 输出:左手 or 右手
2.2.3 训练AI模型
数据收集
-
数据格式:
- X样本:[p0, p1, p2, p3, p4, … ] ,p=(x, y, w, h, density, dt)。
- Y样本:left / right
-
收集方式:(特殊客户端,内测用户收集)
-
滑动轨迹:拦截Activity的dispatchTouchEvent
-
事件,缓存手指滑动产生的MotionEvent序列。
-
-
左右手标签:
- 开启左手样本收集时,只用左手持机操作,只录制左手滑动数据。
- 开启右手样本收集时,只用右手持机操作,只录制右手滑动数据。
-
模型设计
- 二分类模型( Left or Right ),卷积神经网络。
# 卷积神经网络
def creat_cnn(input_shape=(9, 6), print_summary=False):
model = Sequential()
model.add(layers.Conv1D(6, kernel_size=3, input_shape=input_shape, padding='same', activation='relu'))
model.add(layers.Conv1D(12, kernel_size=3, padding='same', activation='relu'))
model.add(layers.Conv1D(24, kernel_size=3, strides=2, padding='same', activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
if print_summary:
model.summary()
return model
模型训练和转换
-
训练样本处理
- 滑动轨迹X样本需要采样对齐成9个固定点输入方式
-
def getX(points): size = len(points) if size < 6 : # 过滤误触 return None sample_count = 9 tensor_size = 6 step = size * 1.0 / sample_count x = [] for i in range(sample_count): p = points[int(i*step)] if len(p) == 6: x.append(p) # x, y, w, h, density, dtime return x
-
模型训练
- 完整案例代码参考 notebook/ohr_recognition.ipynb
-
验证模型准确度
- 在抖音中,Android iOS的测试数据集中准确度95%+
-
模型转换
- 在Demo案例我们继续用Tensorflow Lite 提供的转换器 TFLite Convert,将模型转为移动端模型。
- 在抖音使用的端上推理计算框架是ByteNN,其也提供了相应的Tensorflow Lite模型进一步转成ByteNN模型的工具。
2.2.4 移动端推理预测 & 应用
模型部署到移动端
- 在抖音中模型是通过url下发的,动态更新,需要更新模型时也可以通过Libra实验进行对比。
- 在案例Demo中,是和案例1一样,放在安装包assets目录下。
移动端推理预测
-
接入模型后,在用户滑动后触发预测,将预测结果缓存,业务使用时获取最近识别结果。
-
案例中我们继续使用Tensorflow Lite作为推理引擎,加载和执行模型预测。
- 抖音中使用ByteNN推理引擎执行推理预测。
-
-
拿到结果执行业务策略
- 当App拥有了识别左右手能力后,也就有了更精细的优化用户体验的手段,譬如:
2.2.5 课后作业
-
案例Demo的数据很少,模型准确率不高,需要同学们继续去完善。
- 搭建好左右手识别案例Demo运行环境。
- 收集更多左右手训练数据,优化模型让其准确率更高,适应性更好。
- 将模型替换到案例Demo中,实测效果。
03 端智能工程师学习长路线
回顾端智能技术完整落地流程,并给出一些端智能技术的学习指引。
回顾端智能完整流程
入门
-
达成:对端智能技术有一定认识,可以协作完成端智能技术的需求落地
- 了解端智能技术是什么、可以做什么
- 掌握移动端开发、Machine Learning 基础知识,了解业内端智能框架
- 理解怎么做端智能,可以协作完成端智能技术解决实际业务问题的需求落地
-
学习资料:
- 了解端智能可以做哪些事情:Google MLKit、华为 MLKit
- 机器学习入门: Tensorflow入门、机器学习速成课、微软AI-EDU
- 机器学习在移动端应用:TensorflowLite入门
- 机器学习原理:吴恩达课程
进阶
- 达成:对移动端技术、端智能技术和负责业务有更深入的理解,可以站在更全面的视野上设计端上智能解决方案,建设端上智能架构。
\
感兴趣的同学可以多了解一下机器学习相关能力,以及机器学习可以在移动端做些什么,没准将来在实际业务需求中就可以用上了,遇到困难问题时也会多一条可以利用端智能技术来解决问题的思路。
\
参考资料
- Google对端智能的介绍 developers.google.com/learn/topic…
- 了解端智能可以做哪些事情:Google MLKit、华为 MLKit、Pytorch Live
- 机器学习入门: Tensorflow入门、机器学习速成课、微软AI-EDU
- 机器学习在移动端应用:TensorflowLite入门
- 机器学习原理:吴恩达课程
-
端智能框架
- Google:Tensorflow Lite、ML Kit
- Meta:PyTorch Mobile、PyTorch Live
- Apple: CoreML
- 腾讯:NCNN、TNN,(NCNN和TNN不得不说的故事)
- 阿里:MNN
- 百度:Paddle-Lite
- 华为: HiAI 、ML Kit
- 小米:Mace
- 其他:字节(ByteNN、Pitaya),快手(KwaiNN、Ykit)……
-
下载和二维码仅Android可用,链接中有更多平台demo可参考使用。
-
本文手写识别案例Demo 暂时无法在飞书文档外展示此内容github.com/ahcyd008/Di… 本文左右手识别案例Demo 暂时无法在飞书文档外展示此内容github.com/ahcyd008/Op… 华为 MLKit Demo developer.huawei.com/consumer/en…
Google ML Kit Demo github.com/googlesampl… Tensorflow Lite Demo github.com/tensorflow/…
-