Android插件化的一种简单实现-SDK快速上线

在多年的迭代和升级工作中,组件化项目越来越庞大(几十个模块,近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都需要的接口放在该工程内:

Android插件化的一种简单实现-SDK快速上线_第1张图片

创建好公共接口之后,我们需要在宿主中创建一个通用的播放器,这个通用的播放器负责完成所有插件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。

插件结构如下:

Android插件化的一种简单实现-SDK快速上线_第2张图片

Android插件化的一种简单实现-SDK快速上线_第3张图片

每个插件内都有一个SkyMoviePlayerImpl实现自ISkyMoviePlayer并且放在同样命名规则的路径下面,方便宿主按统一路径规则查找调用,每个SDK的jar和so都放在自己的插件apk内,宿主按固定规则加载注入之后即可正常使用,具体见https://blog.csdn.net/qq_22117359/article/details/89679477

对于插件内的实现就各不相同了,每个SDK的调用方法都不一样,但都可以回归到上述ISkyMoviePlayer接口的几个方法,这里不再详述。

你可能感兴趣的:(Android)