源码阅读#PlayerBase如何实现组件间通讯

2,126 阅读9分钟

做过播放器的朋友一定知道,播放器是一个非常复杂的自定义View,它的复杂主要在于各种元素之间的相互作用,比如说:

  • 外部事件作用于播放器(播放器根据开关wifi控制是否自动播放)
  • 某个遮罩控件上的元素被点击(点击全屏按钮播放器进入全屏)
  • 播放器解码错误(解码错误需要显示错误遮罩,提供重试按钮)

等等。

如果把这些元素、功能统统写在一个类中,会显得非常复杂,难以维护。而PlayerBase 是一个拥有良好架构的播放器组件,它对播放器以及遮罩(Cover)的行为有着良好的抽象,也提供一系列的回调接口。前端时间重构项目中的视频组件我就参考了这个项目。接下来我们分析一下PlayerBase的的结构与实现原理。

一、播放器中View的组成结构

我们平时项目中封装的播放器,由两个主要元素组成——遮罩(Cover)播放器(Player),他们的View层级一般是这样的:

PlayerBase也是类似的,它的稍显复杂一点(嵌套层级变多了):

根布局是BaseVideoView,这就是封装的播放器。他包含了SuperContainer,这是一个自定义View,这里面包含着播放器中所有的功能View,包括mRenderContainer,它是SurfaceView或者TextureView的容器(为什么定义这个呢,主要是为了实现SurfaceView与TextureView的切换,个人感觉不是一个很重要的功能),还有“遮罩根布局”,这个根布局可以自定义,默认的是一个FrameLayout,里面包含着“Height、Low、Medium”三种等级的遮罩容器,所有的遮罩会按等级放入其中,“Heigh”在最上,“Low”在最下,提供分组管理遮罩的功能。

二、整体结构

在View结构图的基础上,我们再加上一些关键角色:

1、SuperContainer

SuperContainer中首先包含一个FrameLayout——mRenderContainer,上面已有简单介绍,它会通过addView加入SuperContainer中。

其次,有一个ICoverStrategy对象,默认实现是DefaultLevelCoverContainer,这个是可以自定义的,只要实现ICoverStrategy即可,默认实现中,它包含了所有的遮罩——BaseCover,BaseCover拥有Cover的行为,继承了ICover,它还是一个信息接收者,继承了IReceiver。ICoverStrategy有getContainView接口,用于获取第一张图里所说的的“遮罩根布局”,最后会通过addView加入SuperContainer中。

最后,还包含一个IEventDispatcher,它不是View,是一个信息中转站,它持有了一个列表,包含了所有的IReceiver,其中就有所有的BaseCover(BaseCover实现了IReceiver)。所有的信息都会回调到IEventDispatcher中,再传送给所有的IReceiver。

2、AVPlayer

AVPlayer是播放器的一层装饰,它持有一个播放器的实例对象BaseInternalPlayer,PlayerBase框架中适配了三种播放器:SysMediaPlayer(适配了Android自带的MediaPlayer)、ExoMediaPlayer(适配了谷歌封装的SimpleExoPlayer)、IjkPlayer(适配了bilibili的IjkMediaPlayer),它们适配的接口是IPlayer。

AVPlayer在BaseInternalPlayer之上增加了一点功能,使用了装饰器模式;三种实现的播放器,使用了对象适配器模式,从而我们可以做到轻松切换这三种播放器。

3、IRender

IRender接口统一了SurfaceView与TextureView的行为,同样是用了适配器模式,作者这里是为了实现SurfaceView与TextureView的切换。IRender会在BaseVideView的setRenderType方法中创建,此时会创建一个匿名内部类的实例mRenderCallback,它会对SurfaceHolder.Callback或者SurfaceTextureListener的事件进行回调,在surfaceCreatedonSurfaceTextureAvailable时绑定播放器。最终,会通过IRender的getRenderView方法获取SurfaceView或TextureView,使用addView加入mRenderContainer。

三、BaseVideoView

PlayerBase中有着复杂但有规律的回调关系,要弄懂这些关系,最好的方式就是画图,接下来会展示几张关于方法、对象的调用关系图,他们的作图规则如下:

浅蓝色大方框代表当前类;深蓝色方框代表类中供外界调用的重要公共方法;肉色方框代表对象,紫色文字是对象名,加粗斜体的白色文字是接口名或抽象类名,正常的白色文字是实现类的名字;浅绿色方框与黑色方框都是说明文字。

好,我们来看BaseVideoVIew中的调用关系是怎样的:

BaseVideoView大图

(图中箭头上的“包含”意思是被指向的对象会被赋值到发起指向的对象中)

首先我们看三个黑色虚线方框,第一个方框中包括了BaseVideoView中的四个主要角色:mSuperContainer、mStyleSetter(可以忽略)、mPlayer、mRender,前面都有所介绍。往右看是第二个方框——通过匿名内部类实现的接口,这些接口的实现类似下面的代码:

private OnReceiverEventListener mInternalReceiverEventListener = (int eventCode, Bundle bundle) -> {
    ......
};

他们统一的命名方式是“mInternalXXXListener”,当BaseVideoView内部有事件触发时,会统一先调用对应的“Internal Listener”,然后回调到通过set方法传入的接口或者其他回调接口或者角色,所以上图信息传递方向是这样的:

而我们可以在BaseVideoView之外实现“Set Listener”定义我们自己的事件回调。举个例子,我们看图中最顶端的一个关系链(看白色斜体文字,图中后两个对象属于同一个接口类):

mSuperContainer中设置了mInternalReceiverEventListener,而mInternalReceiverEventListener是BaseVideoView中的匿名内部类,可以调用其中的mOnReceiverEventListener,而mOnReceiverEventListener正是我们在外部定义好后设置进来的回调接口。从而这里实现了将mSuperContainer中的事件回调给外部的功能。

整个PlayerBase框架中经常使用这种多个回调接口“接力”的方式来实现回调。

四、SuperContainer

SuperContainer大图

看图中右侧的两个黑色方框,SuperContainer也拥有着与BaseVideoView同样的结构——“Internal Listener”与“Set Listener”,“接力”回调。

1、View容器

图中顶部的黑色方框——View容器,相信大家都能理解,mRenderContainer直接addView加入SuperContainer中;mCoverStrategy获取ContainerView后,再通过addView加入SuperContainer中,之后会被加入各种遮罩(BaseCover)。

2、EventDispatcher

EventDispatcher是整个播放器的事件分发中心,这是PlayerBase最核心的设计,它实现了播放器中所有角色都可以发起事件,并且所有实现IReceiver的角色都可以接收到事件。

EventDispatcher中包含着一个IReceiverGroup,它存放着一系列的IReceiver,这些IReceiver可以通过setReceiverGroup方法设置,这些IReceiver可以是BaseCover,也可以是纯粹的IReceiver。

在设置ReceiverGroup的时候,mReceiverGroup会通过OnLoopListener遍历每一个IReceiver: 源码如下:

// 传入接口实例,其中调用了attachReceiver
mReceiverGroup.forEach(new IReceiverGroup.OnLoopListener() {
	@Override
	public void onEach(IReceiver receiver) {
		attachReceiver(receiver);
	}
});
private List<IReceiver> mReceiverArray;
    
@Override
public void forEach(OnLoopListener onLoopListener) {
	forEach(null, onLoopListener);
}

@Override
public void forEach(OnReceiverFilter filter, OnLoopListener onLoopListener) {
	for(IReceiver receiver:mReceiverArray){
		if(filter==null || filter.filter(receiver)){
			onLoopListener.onEach(receiver);
        }
	}
}

在attachReceiver方法中,如果IReceiver实例是BaseCover的子类,就会调用mCoverStrategy.addCover,将BaseCover加入ICoverStrategy对象中

attachReceiver还会调用receiver.bindStateGetter,用于receiver内部获取当前播放器的状态、进度等数据。回顾BaseVideoView中的关系我们发现,mStateGetter实际上是BaseVideoView中的匿名内部类的实例——mInternalStateGetter,然后通过mInternalPlayerStateGetter直接调用mPlayer返回对应数据。

BaseVideoView创建StateGetter的源码:

//Internal StateGetter for SuperContainer
private StateGetter mInternalStateGetter = new StateGetter() {
    @Override
    public PlayerStateGetter getPlayerStateGetter() {
        return mInternalPlayerStateGetter;
    }
};

//Internal PlayerStateGetter for StateGetter
private PlayerStateGetter mInternalPlayerStateGetter = new PlayerStateGetter() {
    @Override
    public int getState() {
    	// 直接调用player方法
        return mPlayer.getState();
    }
    @Override
    public int getCurrentPosition() {
    	// 直接调用player方法
        return mPlayer.getCurrentPosition();
    }
	......
};

所以,每个receiver都可以实时获取播放器的状态、进度等数据

最后,attachReceiver还会调用receiver.bindReceiverEventListener,给每个receiver设置一个回调监听——mInternalReceiverEventListener,它会调用mEventDispatcher的方法,将事件传递给所有IReceiver。同时,它也会调用一个(在BaseVideoView中)通过set方法传入的相同类型的接口——mOnReceiverEventListener,这个接口又会回调我们通过set方法设置的两个自定义回调监听:

仔细看完整的大图,你会发现所有的事件都会回调到EventDispatcher中,并传递给每一个IReceiver;每一个IReceiver又可以通过自身内设的接口回调事件给EventDispatcher,从而实现每个Cover都可以主动发起事件。

看,播放器的事件也是会回调给EventDispatcher(并且还可以回调给外部的回调接口):

3、EventProducer

我们看到中间“addEventProducer”的一条线:

EventProducer有什么用?它可以让事件从外部发起,播放器响应。mProducerGroup可以存放多个EventProducer,而BaseEventProducer中包含一个ReceiverEventSender接口,我们在实现它的时候可以通过ReceiverEventSender发送事件,然后会调用mDelegateReceiverEventSender的相关方法,最后调用mEventDispatcher的分发事件方法,发送给播放器中的每一个IReceiver。

五、AVPlayer

AVPlayer大图

与BaseVideoView类似,AVPlayer的回调接口也分为两大部分——“Internal Listener”与“Set Listener”。大家可以根据上面给出的三个大图了解整个PlayerBase的运行原理,核心就是一个词——信息分发

1、switchDecoder

前面我们知道AVPlayer内包含一个BaseInternalPlayer实例,而我们常用的三种播放器——MediaPlayer、SimpleExoPlayer、IjkMediaPlayer都适配了BaseInternalPlayer(实现IPlayer接口),所以这三种播放器可以在AVPlayer中自由切换,这正是AVPlayer的强大之处。

在使用PalyerBase之前,我们需要配置对应的播放器策略:

PlayerConfig.addDecoderPlan(new DecoderPlan(PLAN_ID_IJK, IjkPlayer.class.getName(), "IjkPlayer"));
PlayerConfig.addDecoderPlan(new DecoderPlan(PLAN_ID_EXO, ExoMediaPlayer.class.getName(), "ExoPlayer"));
PlayerConfig.setDefaultPlanId(PLAN_ID_IJK);

switchDecoder方法会通过PlanId获取对应的DecoderPlan,通过反射创建对应的播放器,然后赋值给mInternalPlayer,完成播放器的切换。

2、DataSource

我们回顾BaseVideoView的大图,AVPlayer的setDataSource方法实际是由BaseVideoView的setDataSource调用的,其中会先调用setRenderType,这个方法会生成对应的IRender(Surface或者Texture),然后绑定播放器与SurfaceView或者TextureView。其次就调用AVPlayer中的setDataSource方法,然后将DataSource传递给播放器,同时生成一系列的回调接口实例。

3、DataProvider

DataProvider提供了异步获取DataSource的功能,最终也会回调到interPlayerSetDataSource方法,给播放器设置DataSource。在BaseVideoView中也有着setDataProvider方法,会调用到AVPlayer中的setDataProvider。

六、结语

其实播放器的难点就是如何传递信息:Cover的事件如何传递给其他Cover以及Player,Player的事件如何通知Cover,以及如何响应外部事件,如何将内部事件回调给外部:

PlayerBase的核心就是EventDispatcher,根据三张大图搞清楚都有哪些组件的事件会回调给EventDispatcher,又有哪些组件会接收到这些信息,那么你对PlayerBase就算是摸透啦~


彩蛋:BaseCover继承于BaseReceiver,每个BaseReceiver都维护着一个相同IReceiverGroup,所以,每个Cover之间其实还实现了单独通讯(Private Event),demo中GestureCover的324行使用了这种方法,有兴趣的朋友可以去研读一下~