在多年的迭代和升级工作中,组件化项目越来越庞大(几十个模块,近10个第三方播放SDK),直接导致发版困难、方法数超标、工作效率大大降低,质量问题频发等等。项目迫切需要一套方案来解决这些问题。由于我们是自行研发的系统和主板,如果直接使用第三方框架,可能会引起相关的适配问题而不好解决,所以需要实现一套自己的插件化框架,也便于后期进行更多的定制。于是进行了下面粗浅的研究。
项目是影视类项目,引进了很多第三方播放SDK,实际上用户在单次启动时并不会用到这么多的SDK,所以直接加载全部的SDK是很浪费性能的。同时,各个公司的SDK是相互不干扰的,按照以往统一升级的做法,在遇到一家有任何问题的时候只能整体应用升级,非常不方便。且在日常版本迭代中任何一个模块出问题都会直接导致项目延期。。。综合以上情况我们需要做到以下几点:
1、插件的动态加载,使用到再加载,且插件不能安装,不能修改第三方SDK的内容;
2、无差别加载插件,实现在不修改主工程的情况下直接接入新的SDK;
3、插件版本控制和独立升级
4、资源的动态加载
二:无差别加载插件,实现在不修改主工程的情况下直接上线新的SDK
前面说到项目中接入了很多第三方的播放SDK,我们经常需要通过整个应用的升级来实现新资源的上线。这样的升级代价比较大,涉及到的模块也多,工作较为繁琐,上线时间比较长,这些SDK的本质也就是提供了一套基于各自公司业务的播放器而已。而这些播放SDK的使用最终都可以抽成一套标准的接口,所以我们想,是不是可以生成一套标准的框架来使用这些SDK,同样在经常有新的第三方同类型SDK接入的情况下也可以使用同样的模式,针对这种情况我们进行了如下粗浅的优化,比较简单,但使用起来还是很方便的。
要做到主工程对各个公司SDK的无差别使用,首先就需要形成一套标准的调用接口,各个SDK均实现这个接口,主工程通过这个接口无差别调用各个SDK。这里以播放类项目为例,接口如下:
public interface ISkyMoviePlayer {
void initSDK(Context context, PlayerSDKInitResultListener listener);
void init(ViewGroup layout, SkyPlayerParameter.SkyPlayerDecoder decoder, String userId);
void setBusinessPlayerListener(BusinessPlayerListener listener);
void setPlayerStateChangeListener(PlayerStateChangeListener listener);
void load(SkyPlayerItem item, SkyPlayerResolution resolution);
void reLoad(SkyPlayerItem item, SkyPlayerResolution spr);
void pause();
void start();
void seek(int ms);
boolean release();
void stop();
boolean onKeyDown(KeyEvent event);
SkyPlayerParameter.SkyPlayerState getPlayerState();
View getPlayerView();
int getVideoHeight();
int getVideoWidth();
boolean isPlaying();
int getCurrentPosition();
int getDuration();
List getPlayerResolutionList();
SkyPlayerResolution getCurrentResulution();
void switchPlayerResolution(SkyPlayerResolution resolution);
SkyPlayerParameter.SKY_CFG_TV_DISPLAY_MODE_ENUM_TYPE getCurrentDisplayMode();
void setPlayerDisplayMode(SkyPlayerParameter.SKY_CFG_TV_DISPLAY_MODE_ENUM_TYPE mode);
}
以上接口放在一个独立的jar工程内,所有的插件apk和宿主apk都依赖该工程,将所有宿主和插件apk都需要的接口放在该工程内:
创建好公共接口之后,我们需要在宿主中创建一个通用的播放器,这个通用的播放器负责完成所有插件apk内播放器的调用。具体如下:
package com.tianci.playerplugin;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import com.coocaa.home.mycenter.plugin.Plugin;
import com.plugin.coocaa.plugin_common_lib.ISkyMoviePlayer;
import com.plugin.coocaa.plugin_common_lib.PlayerStateChangeListener;
import com.tianci.framework.player.data.SkyPlayerItem;
import com.tianci.framework.player.data.SkyPlayerResolution;
import com.tianci.framework.player.kernel.parameter.SkyPlayerParameter;
import com.tianci.movieplatform.application.MyApplication;
import com.tianci.player.api.manager.SkyApiManager;
import com.tianci.player.manager.BusinessAbsPlayer;
import com.tianci.player.manager.BusinessPlayerListener;
/**
* Created by Sea on 2019/4/25.
*/
public class SkyPluginPlayer extends BusinessAbsPlayer {
private ISkyMoviePlayer pluginPlayer;//插件内播放器,因所有的插件都会实现该接口,所以也可以将所有的插件播放器转换为
//该类型,方便调用
private String urlType;//播放资源类型,通过该字段来区分具体使用的插件apk
private String coocaaId;//账户相关字段
public SkyPluginPlayer(Context context, BusinessPlayerListener listener) {
super(context, listener);
}
@Override
public void setVideoLayout(int width, int height, int top, int left) {
}
@Override
public View getPlayerView() {
if(pluginPlayer != null)
return pluginPlayer.getPlayerView();
return null;
}
@Override
public int getVideoHeight() {
if(pluginPlayer != null)
return pluginPlayer.getVideoHeight();
return 0;
}
@Override
public int getVideoWidth() {
if(pluginPlayer != null)
return pluginPlayer.getVideoWidth();
return 0;
}
@Override
public boolean isPlaying() {
if(pluginPlayer != null)
return pluginPlayer.isPlaying();
return false;
}
@Override
public int getCurrentPosition() {
if(pluginPlayer != null)
return pluginPlayer.getCurrentPosition();
return 0;
}
@Override
public int getDuration() {
if(pluginPlayer != null)
return pluginPlayer.getDuration();
return 0;
}
@Override
public void pause() {
if(pluginPlayer != null)
pluginPlayer.pause();
}
@Override
public void start() {
if(pluginPlayer != null)
pluginPlayer.start();
}
@Override
public void seek(int ms) {
if(pluginPlayer != null)
pluginPlayer.seek(ms);
}
@Override
public boolean release() {
if(pluginPlayer != null)
return pluginPlayer.release();
return false;
}
@Override
protected boolean stop(boolean completed) {
if(pluginPlayer != null) {
pluginPlayer.stop();
return true;
}
return false;
}
@Override
public void load(SkyPlayerItem item, SkyPlayerResolution resolution) {
if(pluginPlayer != null)
pluginPlayer.load(item, resolution);
}
@Override
public void reLoad(SkyPlayerItem item, SkyPlayerResolution spr) {
if(pluginPlayer != null)
pluginPlayer.reLoad(item, spr);
}
@Override
public boolean resetBitStreamSetting() {
return false;
}
@Override
public boolean switchPlayerMute(boolean on) {
return false;
}
@Override
public int getBufferPercentage() {
return 0;
}
@Override
public SkyPlayerParameter.SkyPlayerDecoder getDecoderMode() {
return null;
}
@Override
protected void switchPlayerResolution(SkyPlayerResolution resolution) {
if(pluginPlayer != null)
pluginPlayer.switchPlayerResolution(resolution);
}
@Override
protected void setPlayerDisplayMode(SkyPlayerParameter.SKY_CFG_TV_DISPLAY_MODE_ENUM_TYPE mode) {
if(pluginPlayer != null)
pluginPlayer.setPlayerDisplayMode(mode);
}
@Override
public void init(final String urlType, final ViewGroup layout, final SkyPlayerParameter.SkyPlayerDecoder decoder) {
this.coocaaId = SkyApiManager.getInstance().getUserId();//初始化账户数据
this.urlType = urlType;
new Thread(new Runnable() {
@Override
public void run() {
if(!PluginManager.getInstance().hasInjected(urlType)) {//判断当前urlType对应的插件apk是否已经注入,
//如果还没有注入则调用统一的插件管理类PluginManager进行插件注入
int wait = 0;
PluginManager.getInstance().injectPlugin(urlType);
while (!PluginManager.getInstance().hasInjected(urlType)) {//插件在线程内注入,等待插件注入成功
wait ++;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(wait > 400) {//插件注入超时处理
if(mListener!=null) {
mListener.onError(SkyPlayerParameter.SkyPlayerError.ERROR_UNKNOWN, "加载插件失败,重新点播试试吧");
}
return;
}
}
}
if(!PluginManager.getInstance().hasInitSDK(urlType)) {//插件注入完成之后,判断该播放插件是否已经初始化
//一般第三方的播放SDK只需要进行一次初始化,
int wait = 0;
PluginManager.getInstance().initSDK(urlType);//如果还没有初始化则调用统一的管理类进行初始化
while (!PluginManager.getInstance().hasInitSDK(urlType)) {//等待初始化完成
wait ++;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(wait > 400) {//超时处理
if(mListener!=null) {
mListener.onError(SkyPlayerParameter.SkyPlayerError.ERROR_UNKNOWN, "播放器初始化失败,重新点播试试吧");
}
return;
}
}
}
pluginPlayer = PluginManager.getInstance().getPluginPlayer(urlType);//注入且初始化之后,获取该URLType
//对应的播放器实例,(Class> aClass = Class.forName(PLAYER_CLASS_PACKAGE_HEADER+urlType+PLAYER_CLASS_NAME))
//获取到播放实例之后即可通过之前的公共接口正常调用了
pluginPlayer.setPlayerStateChangeListener(new PlayerStateChangeListener() {
@Override
public void onPlayerStateChange(SkyPlayerParameter.SkyPlayerState state) {
setPlayerState(state);
}
});
pluginPlayer.setBusinessPlayerListener(new BusinessPlayerListener() {
@Override
public void onPlayerStateChangeDone(SkyPlayerParameter.SkyPlayerState state) {
if (mListener != null)
mListener.onPlayerStateChangeDone(state);
}
@Override
public void onInitDone(boolean done) {
if (mListener != null)
mListener.onInitDone(done);
}
@Override
public void onPrepared() {
if (mListener != null)
mListener.onPrepared();
}
@Override
public void onInfo(SkyPlayerParameter.SkyPlayerInfo info, String extra) {
if (mListener != null)
mListener.onInfo(info, extra);
}
@Override
public void onError(SkyPlayerParameter.SkyPlayerError error, String msg) {
if(mListener!=null) {
mListener.onError(error, msg);
}
}
@Override
public void onCompletion() {
if (mListener != null)
mListener.onCompletion();
}
@Override
public void onSeekComplete() {
if (mListener != null)
mListener.onSeekComplete();
}
@Override
public void onBufferingUpdate(int percent) {
if (mListener != null)
mListener.onBufferingUpdate(percent);
}
@Override
public void onAdLog(String adType, int adPos, int adDuration) {
if (mListener != null)
mListener.onAdLog(adType, adPos, adDuration);
}
@Override
public void onHeaderTailerInfoReady(int header, int tailer) {
if (mListener != null)
mListener.onHeaderTailerInfoReady(header, tailer);
}
@Override
public void onBackObject(String cmd, Object o) {
if (mListener != null)
mListener.onBackObject(cmd, o);
}
});
pluginPlayer.init(layout, decoder, coocaaId);
}
}).start();
}
@Override
public void initUserInfo() {
this.coocaaId = SkyApiManager.getInstance().getUserId();
}
@Override
public void setPlayerConfig(String key, String value) {
}
@Override
public String getSupportUrlType() {
return urlType;
}
}
以上是宿主对所有播放插件apk的无差别调用,通过urlType来区分播放插件,默认宿主是不需要考虑urlType的具体取值的,直接拿着urlType去匹配插件即可,有新插件时,只需要增加一个urlType取值并且在插件接口内上传对应插件信息即可。由此做到了在不修改宿主工程的情况下,直接后台上线新的SDK。
插件结构如下:
每个插件内都有一个SkyMoviePlayerImpl实现自ISkyMoviePlayer并且放在同样命名规则的路径下面,方便宿主按统一路径规则查找调用,每个SDK的jar和so都放在自己的插件apk内,宿主按固定规则加载注入之后即可正常使用,具体见https://blog.csdn.net/qq_22117359/article/details/89679477
对于插件内的实现就各不相同了,每个SDK的调用方法都不一样,但都可以回归到上述ISkyMoviePlayer接口的几个方法,这里不再详述。