工作之中写过两个播放器,大同小异,由于需要的功能不是太多,github上的功能都比较多,正好想着顺手练练技术,于是手撸过两个播放器。播放器这个东西吧,说难不难,因为播放内核基本都是用的ijk,自己写其实就是在把UI和功能做个封装,说简单吧,也没那么简单。主要是功能太多了,经常写着写着就写乱了。每次写都是写之前胸有成竹,然后越写脑子越乱。通过这次写播放器的过程,简单记录一下为什么会产生这种问题,以及怎么解决方式,还有播放器设计的一些经验。
API设计
其实就一句话,功能繁多,难以界定。
咱们先说基础播放View,基础播放View一般是由三个部分组成:播放器内核(IPlayer),渲染层(IRender),跟布局(ViewRoot,也就是封装起来BasePlayView)。由于播放器内核、渲染层的实现都有很多种,所以我们一般来说是把这两个模块各自抽象一个接口或者抽象类出来,下边的文章就用他们的接口名称代指。现在持有一个IPlayer和IRender的ViewGroup(一般是FrameLayout)将会封装成一个包含基础播放能力,但是没有控制层Ui的BasePlayView。
在这一步中,最容易引起设计混乱的原因就是功能的模块划分。
其中BasePlayView是最终对应用层暴露出api的对象,BasePlayView中80%的功能是由Player接口提供的,10%的功能是由Render接口提供的,10%的功能是自己实现的如全屏, 小屏播放。
IPlayer的功能基本是由播放器内核提供的,这就造成了一套繁琐的功能转发逻辑,最终暴露的功能——>IPlayer——>播放器内核 。之前都是先设计播放器的接口 在设计Render 然后最后写BasePlayView。其实这样的顺序是错误的,我们应该先设计最终对外暴露的接口,在设计内部接口,有外到里,这样就会大大减少后期出现设计不合理,功能短缺的问题,这个顺序相当于先设计需求,在设计实现。我之前用的顺序是先设计实现,在反推需求。很容易造成发现需求短缺 在重新设计接口。这样很容易打乱原有的设计思路,导致初期设计合理的框架 为了实现某个需求不得不重新设计/生插进去一个功能。前者浪费时间,需要从新缕思路,会忘记之前的思路, 后者可能短期内不会暴露问题,但是会埋雷,必经是不符合前期设计的东西。初期功能尽量设计全 否则后期改动真的太麻烦了。还有就是功能的界定,需不需要我在最基础的base层去实现,base功能定义是最基本的播放相关功能吗,有哪些复杂功能需要分层设计,以及分层设计的思路,用继承分层,还是利用多层View分层。哪些功能定义在控制器 哪些功能定义在播放器实现。这些都是功能划分上的难点。这些没有统一的定论,但是必须要有统一规范。
还有释放的管理,由于设计到多个模块释放,需要规定哪些部分是由应用调用释放的,哪些是内部根据自己处理的。
状态管理
还是上边的三个模块,播放器的状态是由这三个模块共同产生的,不规划好管理职责也会导致后期的代码逻辑混乱
- 状态必须由一个set api统一管理,这是最重要的
- 状态集的定义基础播放状态,播放器功能状态 等等提前定义 我需要管理多少状态,其他状态我是交由下层管理,还是通过回调事件分发出去。
- 是否允许叠加状态,单状态能满足大部分情况,但是极少部分情况需要一个叠加状态,比如播放器 全屏 小屏 普通厅 | 播放 暂停 等待 等等...
- 最好BasePlayView做状态管理, Player 和Render 自身状态通过回调通知BasePlayView
- 设计方便下层view 扩展状态管理的逻辑
状态切换的switch语句
状态切换也是比较麻烦的一个事情,需要做大量的判断,比如仅仅是一个start操作,就得根据是一个空闲播放器、正在播放中、暂停、播放完成、播放错误这五个状态做不通的操作。这还没考虑网络状态,是否在后台等。
所以如果根据状态的操作,以及操作产生的状态切换这一快非常容易产生大量的switch或者if代码。如果在这里代码逻辑过于混乱,后期会引起很多难以控制的问题。我的在处理这个问题的时候用了一种类似状态机的设计理念,但是并不是标准的状态机。大致思路就是生成一个BaseState类,api是BasePlayView所有可能触发的操作。比如 播放、暂停、发生错误、断网、播放完成等。播放器持有一个BaseState对量,每当播放器切换一个状态,都会切换BaseState具体的子类,然后将每当播放器接受到一个事件,则将事件转发到当前BaseState去实现具体操作。因为此时的BaseState 对象中是知道自己是处于什么状态的,也就避免了大量的逻辑判断操作。
功能分层的设计
现在一个完善的播放器的功能种类繁多,在一个或者几个类中完成所有功能代码是不显示,必须分层设计,我之前写的播放器的功能分成是用继承实现的,最基础的播放器——>带进度条——>手势控制——>支持无缝衔接
不同功能的播放器 只能通过继承合适层级的播放器 然后再去添加自己的代码实现。扩展性很差,其他不熟悉内部实现的同事用起来很难受,只能用我封装好的。
通过看DK播放器的实现,他使用组合控制器实现的功能分层。用一个没有UI的根控制器做容器,跟控制器负责所有控制器的统一性功能,以及转发播放器状态变化。上述的功能实现就可以改成基础功能播放器——>支持无缝衔接(这是播放器强相关功能,只能继承实现)+进度条控制器+手势控制器。耦合性更低 。通过组合不同的控制器就可实现不同的播放器。
如果把和播放器强相关的功能也用上诉组合的形式 分层实现 我现在的思路是 把所有的api调用改为命令模式,可以自定义命令处理器,链式调用 完成功能拦截,或者功能增强,自定义拦截器级别,类似Okhttp 拦截器设计。
管理模块
这个角色基本上就是管理全局的播放器实例对象,或者提供几个产生播放器实例的工厂方法。一般全局性的播放设置也放在这里。这里最容易发生的问题就是全局设置功能和播放器的设置API的设置冲突,原则是以播放器的设置为主。然后就是播放器的设置api必须包含全局的设置项,也就是假如全局可以设置ABC三个属性,那么播放器的设置API最少要包含ABC三个设置API。当然如果设置不是针对个体播放器的可以不用满足上述条件。
这篇文章基本就是个随笔,想到哪写到哪。记录了下写一个播放器想到的东西,给以后做功能设计留一些参考资料,不写出来估计过几天就忘了,年纪大了记性实在不行了。