背景

我们都了解播放器的作用就是把音视频压缩数据转换成原始的音视频数据渲染出来,这样我们就可以看到画面、听到声音了。这里的播放器就存在两个问题,第一个问题是视频源存在云端,我们每次看完视频之后重新观看,需要重新请求远端视频数据,这就会造成带宽的浪费。第二个问题是视频的秒开体验当我们从Feed流点击视频播放的时候,由于需要从云端获取视频,造成秒开体验较差。我们该如何去解决这两个问题呢?这就是我们今天要说的视频缓存库的核心功能了。下面让我们走进Android视频缓存库,看它是如何设计、如何优雅的解决这两个问题的。

AndroidVideoCache

下面会详细分析AndroidVideoCahce的实现原理、AndroidVideoCahce存在的问题、AndroidVideoCahce的优化重构。最终打造一款适合业务需求、稳定的视频缓存库。

AndroidVideoCahce的基本原理

AndroidVideoCache通过代理的策略将我们的网络请求代理到本地服务,本地服务首先判断播放器获取的数据本地缓存是否存在,如果存在就从本地缓存获取数据返还给播放器;如果不存在需要发起网络请求就先向本地写入数据,再从本地服务获取数据给播放器,从而做到数据的复用。

上面的时序图,大致描述了AndroidVideoCahce内部的基本流程,但内部是如何去实现的还是一头雾水,下面具体分析下AndroidVideoCahce代码结构、AndroidVideoCahce的线程模型、有多少模块、每个模块的作用、模块之间的交互;了解完了他们,AndroidVideoCahce中的知识点基本全部掌握了。

AndroidVideoCahce代码结构

file

DiskUsage: 本地磁盘文件缓存策略接口
LruDiskUsage: 根据LRU算法实现的本地磁盘文件缓存策略抽象类
TotalCountLruDiskUsage: 文件数量+LRU算法实现的本地磁盘文件缓存策略实现类
TotalSizeLruDiskUsage: 文件缓存大小+LRU算法实现的本地磁盘文件缓存策略实现类
UnlimitedDiskUsage:本地磁盘文件缓存无任何限制策略实现类

FileNameGenerator:网络资源缓存到本地文件的命名接口
Md5FileNameGenerator: 网络资源缓存到本地,本地文件根据文件的md5命名实现类

FileCache: RandomAccessFile 的封装
1. 把网络获取的数据缓存到本地
2. 把本地缓存的数据提供给播放器
3. 由于网络模块写功能、本地缓存模块读功能都需要操作同一个File,需要加锁。

headers

HeaderInjector: 添加自定义服务器请求头接口
EmptyHeadersInjector:自定义服务器请求头空实现

Sourcestorage

SourceInfoStorage: SourceInfo本地存储化接口
NoSourceInfoStorage: SourceInfo本地存储化接口的空实现
DatabaseSourceInfoStorage: SourceInfo本地存储化SQLite实现
SourceInfoStorageFactory:获取SourceInfoStorage实例的工厂

Cache

数据缓存接口;通过实现该接口我们可以把音视频数据缓存到本地文件(FileCache)或内存(ByteArrayCache)中;

Source

获取数据接口;根据数据源存储的方式(网络、本地、内存)不同,我们可以有不同的实现;

CacheListener

当前视频缓存进度回调接口

HttpUrlSource

Source接口的实现,内部通过HttpURLConnection与云端建立链接获取云端音视频数据

ByteArrayCache

数据缓存在内存中的实现

ByteArraySource

从内存中获取数据的实现

Config

业务端自定义数据的封装,AndroidVideoCahce内部会根据Config的配置使用

ProxyCache

通过封装Source、Cache接口,实现从Source中获取数据保存到Cache中;

HttpProxyCache

通过封装Source、Cache的具体实现类:HttpUrlSource、FileCache,实现从HttpUrlSource获取数据保存到FileCache中;

HttpProxyCacheServerClients

站在本地代理缓存服务的角度去看,HttpProxyCacheServerClients起到一个客户端的作用,它通过创建、封装HttpUrlSource、FileCache、HttpProxyCache、CacheListener起到一个统筹全局的作用;

HttpProxyCacheServer

内部通过ServerSocket维护一个本地服务,通过把云端的视频播放地址(url)转换为http://127.0.0.1:port/url 本地代理请求链接,这样播放器就会与我们的本地服务建立链接。然后通过HttpProxyCacheServerClients实现从云端获取数据,保存到本地,再从本地获取数据返还给播放器的逻辑。

AndroidVideoCahce模块总结

这里从功能的角度去分析下AndroidVideoCahce;主要分析AndroidVideoCahce包含哪些功能,这些功能之间是如何联系、如何协作实现视频边播边缓冲的。

数据下载模块

数据下载模块主要功能是从网络获取音视频数据到手机内存中;AndroidVideoCahce的主要实现类是HttpUrlSource;

数据本地缓存模块

数据本地缓存模块主要有两部分功能:1. 写功能: 把数据下载模块从云端获取的数据保存到本地;2. 读功能:从本地缓存中读取数据;AndroidVideoCahce的主要实现类是FileCache;

服务处理模块

服务处理模块是一个控制模块;它通过创建、封装HttpUrlSource、FileCache、HttpProxyCache负责调控从网络获取数据并保存到本地,再从本地获取数据返回给请求端;

本地数据清理模块

对某个视频缓存的数量或大小满足LRU条件时进行清除;

本地服务代理模块

内部通过ServerSocket维护一个本地服务,通过把云端的视频播放地址(url)转换为http://127.0.0.1:port/url 本地代理请求链接,这样播放器就会与我们的本地服务建立链接。然后通过服务处理模块调用数据下载模块从云端获取数据,调用数据本地缓存模块保存到本地,再调用数据本地缓存模块获取数据返还给播放器的逻辑。

AndroidVideoCahce的线程模型

AndroidVideoCahce是允许多个视频并发缓存的,当前只讨论单个视频缓存的情况下,AndroidVideoCahce内部会创建几个线程及每个线程的功能;后面再讨论多个视频并发缓存的情况就比较容易了。
AndroidVideoCahce只考虑单视频缓存的情况下会涉及到5个线程:

  1. 主线程:负责AndroidVideoCahce的创建、执行、销毁;
  2. 本地服务线程:负责监听客户端的网络请求;
  3. 服务处理线程:负责从本地缓存读取数据,并返回给请求端;
  4. 数据下载线程:负责从网络获取数据并保存到本地;
  5. 本地数据清理线程:负责对某个视频缓存的数量或大小满足LRU条件时进行清除

AndroidVideoCache的不足

AndroidVideoCache在实现上还有好多需要改进的地方,这里列出了几个不足之处,后面会逐个解决,达到在功能、性能、稳定都达到行业标准。

  1. AndroidVideoCache没有实现预下载的功能,预下载的同时不能抢占当前播放视频的带宽。
  2. 数据本地缓存不支持分片缓存,当前的缓存策略存在2个问题:
  3. 拖动视频大于20%不能缓存到本地
  4. 如果文件很大,当用户seek操作刚好等于20%,如果是1G的文件也需要缓存200M,用户等待体验太差。
  5. AndroidVideoCache不支持HLS格式的缓存,当前播放大部分的格式都是mp4,这个优先级还比较低。

AndroidVideoCache 数据本地缓存分析

AndroidVideoCache的本地缓存在FileCache中实现;由于网络获取的数据需要保本到本地,同时服务处理需要从本地缓存中获取数据给到请求端,所以这里使用了RandomAccessFile,由于RandomAccessFile支持“随机访问”的方式访问文件,可以直接跳转到文件的任意地方来读写数据。
当网络获取的数据需要保本到本地时,通过下面的方式就可以了

@Override
public synchronized void append(byte[] data, int length) throws ProxyCacheException {try {if (isCompleted()) {throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!");}dataFile.seek(available());dataFile.write(data, 0, length);} catch (IOException e) {String format = "Error writing %d bytes to %s from buffer with size %d";throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e);}
}

当服务处理需要从本地缓存中获取数据给到请求端时:

@Override
public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException {try {dataFile.seek(offset);return dataFile.read(buffer, 0, length);} catch (IOException e) {String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);}
}

这样的本地缓存其实存在的问题还是比较大的;最大的问题就是当视频很大,我们拖动视频时,就会出现视频从上个缓存点到当前拖动点缓存时间会很长,体验很差。为什么会出现这种情况就是因为这里的缓存实现只能从文件尾一点点写入缓存,只能从上个缓存点缓存到了当前拖动点才能继续播放;

AndroidVideoCache内部解决方案

public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {OutputStream out = new BufferedOutputStream(socket.getOutputStream());String responseHeaders = newResponseHeaders(request);out.write(responseHeaders.getBytes("UTF-8"));long offset = request.rangeOffset;if (isUseCache(request)) {responseWithCache(out, offset);} else {responseWithoutCache(out, offset);}
}private boolean isUseCache(GetRequest request) throws ProxyCacheException {long sourceLength = source.length();boolean sourceLengthKnown = sourceLength > 0;long cacheAvailable = cache.available();// do not use cache for partial requests which too far from available cache. It seems user seek video.return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
}

当拖动视频之后,AndroidVideoCache内部会自己判断下,当前拖动点需要缓存的视频数据是否是在整个视频长度的20%以内,如果是就继续执行本地缓存,否则不进行本地缓存;
但这样也会有问题,当需要播放的视频量太大时,这20%的视频量也是挺多的,都必须缓存到本地,才能后面继续播放,体验也是比较差的;
例如现在有一个1G的视频,当视频缓存到100M的时候,把视频seek到300M的地方,这时候就需要把100M~300M之间的视频也需要缓存到本地才能继续播放;这200M的数据的缓存也是挺长的,会一直处在loading状态。

总结

AndroidVideoCache内部实现的以文件内容追加的形式进行缓存的方案,适应场景比较有限,但视频比较小或视频比较大但不允许拖动是可以使用的。一旦出现视频比较大又频繁拖动的情况下,就会出现视频缓存造成体验差与不缓存造成高带宽费用的问题。这里如果想两者兼得就需要新的分片缓存实现方案了。

AndroidVideoCache分片缓存的实现

分片缓存就是为了解决视频拖动导致视频大量缓存造成体验差或者不缓存造成高带宽费用的问题;这里的分片与HLS协议里面的ts分片文件是不一样的;
HLS协议里面的ts分片文件是把整个文件分成一段一段的ts文件。
分片缓存核心点有三个步骤:

  1. 分片缓存文件的创建和查找
  2. 分片缓存文件的合并
  3. 分片缓存文件的删除

分片缓存文件的创建和查找

分片缓存文件的创建需要同时满足下面几个条件,才会创建新的分片文件;

  1. 拖动操作;如果不存在拖动操作,只存在一个分片文件0.download; 0代表当前分片文件的起始点从0字节开始。后续从云端下载的所有的数据都会缓存到0.download文件中,直到下载完成,会把0.download重命名为xxx.mp4;
  2. 本地缓存分片文件中不存在拖动点的缓存;当通过播放器拖动到某一时刻的时候,这时候播放器内部会向本地代理服务发出一个带有range请求头的字段,表示从什么位置开始获取数据;代理服务解析出来range之后,就会判断本地分片文件中是否包含该range,如果包含就以该分片文件为缓存操作对象;否则就以该range命名新的缓存文件;
  3. 拖动点距离当前缓存的点大于分片文件间隔阀值;这里通过举例说明下为什么要设置一个分片文件间隔阀值;假设当前存在1个分片文件;0.download,大小50M;当播放器拖动到52x1024x1024的位置的时候,可以发现现在的位置距离0.download的尾部只有6M的空间大小。这时候我们完全可以使用0.download作为缓存文件的操作对象,没必要重新创建新文件。每一个新文件的创建最后都需要合并和销毁,占用CPU,影响性能;
    分片缓存文件的创建查找流程图:

分片缓存文件的合并

这里举个例子来理解分片缓存文件的合并;当前播放的视频存在3个缓存文件0.download,大小10M;12x1024x1024.download,大小5M;20x1024x1024.download,大小10M;下面画了一张图加深对分片缓存文件的合并过程的理解;

分片缓存文件的删除

本地数据的清理模块会对某个视频缓存的数量或大小满足LRU条件时进行清除;这里就涉及到对分片缓存的删除;

AndroidVideoCahce的后续优化与重构

在AndroidVideoCache的不足中说到 AndroidVideoCache没有实现预下载的功能,预下载的同时不能抢占当前播放视频的带宽。这个是后续优化的重点,等到后面实现并上线了,这里会继续补上优化与重构过程。

参考文献

  1. 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频: https://www.likecs.com/show-204344094.html
  2. Qzone视频下载如何做到多快好省? https://cloud.tencent.com/developer/article/1031666
  3. AndroidVideoCache源码分析: https://mp.weixin.qq.com/s/LWTAAXgjojoQF6vFio7MCg
  4. Android视频技术探索之旅:美团外卖商家端的实践: https://mp.weixin.qq.com/s/2Dn0TxlnKqcDO9-twqpVJg
  5. 鸿蒙开源第三方件:VideoCache组件: https://mp.weixin.qq.com/s/JMXkHqEaFxZ0DQdZcNiuhg
  6. 好看视频Android重构——围绕于播放器的重构实践: https://mp.weixin.qq.com/s/R2EHg7kVN8VWkGgZ1z-7pQ
  7. 拒绝卡顿,揭秘盒马鲜生 APP Android 短视频秒播优化方案: https://mp.weixin.qq.com/s/2qVCBt4ozz6jSdfLiZI-fg

浅谈Android视频缓存库相关推荐

  1. 浅谈Android中的MVP与动态代理的结合

    浅谈Android中的MVP与动态代理的结合 本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 在Android开发平台上接触MVP足足算起来大概已经有一个年头左右.从最开始到现在经 ...

  2. 浅谈Android保护技术__代码混淆

    浅谈Android保护技术__代码混淆 浅谈Android保护技术__代码混淆 代码混淆 代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读 ...

  3. 浅谈Android系统进程间通信(IPC)机制Binder中的Server和Client获得Service Manager接口之路

    原文地址: http://blog.csdn.net/luoshengyang/article/details/6627260 在前面一篇文章浅谈Service Manager成为Android进程间 ...

  4. android 存储空间监控,浅谈 Android 内存监控(中)

    前言 在上篇 浅谈 Android 内存监控(上) 中,我们聊了 LeakCanary,微信的 Matirx 和美团的 Probe,它们各自有不同的应用场景,例如,在开发测试环境,我们会偏向用 Lea ...

  5. 《浅谈-Android系统越用反应越慢的问题》

    <浅谈-Android系统越用反应越慢的问题> android应用程序和iphone应用程序不一样,用过iphone的都知道,点击图标进入程序后,如果还想用其他程序,必须先按返回退出然后进 ...

  6. 浅谈Android Architecture Components

    浅谈Android Architecture Components 浅谈Android Architecture Components 简介 Android Architecture Componen ...

  7. 浅谈Android文件管理器的几种实现方式(原理篇)--对我有帮助

    转自 https://blog.csdn.net/weixin_33698823/article/details/87269955 浅谈Android文件管理器的几种实现方式 为了完成毕业设计,我花费 ...

  8. 2022浅谈前端八大UI库

    之前在3月份的时候做过一个<浅谈前端八大UI库>的公开课,反馈还不错,当时就想着等有时间了,就把公开课讲的东西,组织成一个博客.这一等,就等了两个多月. 先说一下什么样的同学适合看这个博客 ...

  9. 浅谈Android引用计数(2)

    在浅谈Android引用计数(1)中讲了LightRefBase实现对象计数管理的原理,这篇文章将要分析重量级的引用基类:RefBase的实现和它的作用. 下面是RefBase和相关类的类图: 图中可 ...

最新文章

  1. 为什么我的python没有run_为什么我的returncode=0而没有stdoutsubprocess.run?
  2. 分布式链路追踪zipkin
  3. Go 语言 2019 调查报告发布(内含 Go 语言图谱下载)
  4. JS导出 excel
  5. 在长文本中当中使用正则表达式匹配限定长度范围的数字串的方法
  6. 【项目管理】你理解的项目管理是什么样的?
  7. 第1课:接口测试和jmeter总结
  8. 链表创建、逆置、删除详解
  9. 「2019 嵌入式智能国际大会」 399 元超值学生票来啦,帮你豪省 2600 元!
  10. 小技巧:Go怎么样获取常驻内存子进程的输出
  11. 解决NSTextContainer分页时文本截断问题
  12. python学习手册-python学习手册第5版pdf
  13. Python 开发音乐下载器实践
  14. 计算机专业不同行业薪资,各专业薪资对比:这些专业薪资高
  15. 游戏源代码是什么意思_什么是游戏
  16. 微信读书产品体验报告
  17. SLAM④----李群与李代数
  18. 世界计算机销量排名2015,全球电脑销量排名出炉,苹果位居第四,“榜首”为国产品牌!...
  19. 问题 A: Jugs BFS
  20. vmware虚拟机更改MAC地址方法

热门文章

  1. 计算机网络实践网线制作,一种用于计算机网络对接网线接头的制作方法
  2. 视觉欺骗:你绝不会相信A和B颜色相同!
  3. 测试学习--云测试平台
  4. 2023年考研数学测试卷(预测)
  5. cs架构交互_架构,功能和交互
  6. Android使用WebView打包网页成app
  7. 移动硬盘插到台式机,外接网卡无法连接wifi处理
  8. 游戏服务器被攻击了怎么办?
  9. Python程序员必备的四款开发工具
  10. java源码——计算立体图形的表面积和体积