手游SDK — 第三篇(SDK架构设计代码实现篇(上)- 基础库)

各位看官,上文 手游SDK— 第二篇(架构设计篇)已经介绍过了架构实现的基本思路,那废话就不多说了,进入正题,看下代码的具体实现。
PS:不具备实际项目用途,仅做框架Demo展示

项目结构搭建

首先咱们先整体搭建下项目工程的框架模块,根据之前的项目架构设计搭建的,这里也比较简单,我简单附上一张图。

项目结构.png

PS:相关的模块管理可参考

项目代码实现

第一部分:基础库

基础库涉及的功能库比较多,大概抽离部分模块大体讲解下,主要分两部分:涉及SDK架构的代码实现和部分三方框架的封装思路讲解。

项目/插件/渠道管理设计

项目的框架主体骨架

配置文件格式:Project_config.txt / Plugin_config.txt / Channel_config.txt
{
    "project": [
        {
            "project_name": "project",
            "class_name": "com.bzai.gamesdk.project.juhe.JuHeProject",
            "description": "聚合SDK项目",
            "version": "1.0.0"
        }
    ]
}
{
    "plugin": [
        {
            "plugin_name": "plugin_wechat",
            "class_name": "com.bzai.gamesdk.plugin.wechat.WechatPlugin",
            "description": "微信功能插件",
            "version": "5.1.4"
        },
        {
            "plugin_name": "plugin_alipay",
            "class_name": "com.bzai.gamesdk.plugin.alipay.AlipayPlugin",
            "description": "支付宝功能插件",
            "version": "15.5.5"
        }
    ]
}
{
    "channel": [
        {
            "channel_name": "channel",
            "class_name": "com.bzai.gamesdk.channel.test.TestChannelSDK",
            "description": "测试渠道SDK",
            "version": "1.0.0"
        }
    ]
}

比较简单就不细说,是Json格式的数据,大家可以根据需求进行数据拓展。
PS:备注说明下,其实是可以通过一个配置文件就能配置完的,但是将项目、插件、渠道分别配置加载的目的是方便快速的分别替换项目、插件、渠道配置。一个项目Project可以对应多个渠道、多个插件。后续可以在多渠道、多插件上进行快速的插拔和后台开关切换渠道,不过正常的需求都是一个项目对应零个或一个渠道、零个或多个功能插件。

代码实现
抽象类解析

1、抽象的项目Project类,主要是面向SDKAPI接口设计的,预定义对外的接口及Activity生命周期接口。但是通常对外的接口一般设计好后非必要就很少修改,为了对外接口设计进行扩展,预定义通用的拓展接口extendFunction(Activity activity, int functionType, Object object, CallBackListener callBackListener),通过不同的funtionType进行扩展。

public abstract class Project implements ProguardInterface{

    /*****************************   Project 加载接口    **********************************/

    public ProjectBeanList.ProjectBean projectBean;

    private boolean hasInited;
    protected synchronized void initProject() {
        if (hasInited) {
            return;
        }
        hasInited = true;
    }

    @Override
    public String toString() {
        return "Project{" + "projectBean=" + projectBean + ", hasInited=" + hasInited + '}';
    }

    /*****************************  顶层 Project 功能接口:初始化、登陆、支付、退出    **********************************/

    /**
     * 初始化
     */
    public void init(Activity activity, String gameid, String gamekey, CallBackListener callBackListener) {}

    /**
     * 登录
     */
    public void login(Activity activity, HashMap<String,Object> loginParams) {}


    /**
     * 支付
     */
    public void pay(Activity activity, HashMap<String,Object> payParams, CallBackListener callBackListener) {}

    /**
     * 切换账号
     */
    public void switchAccount(Activity activity){}

    /**
     * 登出
     */
    public void logout(Activity activity) {}

    /**
     * 退出
     */
    public void exit(Activity activity, CallBackListener callBackListener) {}

    /**
     * 上报数据
     */
    public void reportData(Context context, HashMap<String,Object> dataMap){}

    /**
     * 设置SDK账号监听
     */
    public void setAccountCallBackLister(CallBackListener callBackLister){}

    /**
     * 显示SDK悬浮窗,将登录、支付等信息回调
     */
    public void showFloatView(Activity activity){}

    /**
     * 关闭SDK悬浮窗
     */
    public void dismissFloatView(Activity activity){}
    
    /**
     * 拓展接口,处理渠道的定制接口
     */
    public void extendFunction(Activity activity, int functionType, Object object, CallBackListener callBackListener){}

    /**
     * 获取渠道ID
     * @return
     */
    public String getChannelID(){
        return null;
    }


    /*******************************  顶层 Project 生命周期接口 (目前实现各插件的生命周期)******************************/

    public void onCreate(Activity activity, Bundle savedInstanceState) {
        PluginManager.getInstance().onCreate(activity, savedInstanceState);
    }

    public void onStart(Activity activity) {
        PluginManager.getInstance().onStart(activity);
    }

    public void onResume(Activity activity) {
        PluginManager.getInstance().onResume(activity);
    }

    public void onPause(Activity activity) {
        PluginManager.getInstance().onPause(activity);
    }

    public void onStop(Activity activity) {
        PluginManager.getInstance().onStop(activity);
    }

    public void onRestart(Activity activity) {
        PluginManager.getInstance().onRestart(activity);
    }

    public void onDestroy(Activity activity) {
        PluginManager.getInstance().onDestroy(activity);
    }

    public void onNewIntent(Activity activity, Intent intent) {
        PluginManager.getInstance().onNewIntent(activity,intent);
    }

    public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
        PluginManager.getInstance().onActivityResult(activity, requestCode, requestCode, data);
    }

    public void onRequestPermissionsResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
        PluginManager.getInstance().onRequestPermissionsResult(activity,requestCode,permissions,grantResults);
    }
}

2、抽象的功能插件Plugin类,主要是面向第三方SDK设计的,预定义生命周期方法。具体功能实现接口根据功能设计。

public class Plugin implements LifeCycleInterface, ProguardInterface {

    public PluginBeanList.PluginBean pluginBean;

    private boolean hasInited;

    protected synchronized void initPlugin() {
        if (hasInited) {
            return;
        }
        hasInited = true;
    }

    @Override
    public String toString() {
        return "Plugin{" + "pluginMessage=" + pluginBean + ", hasInited=" + hasInited + '}';
    }

    /****************************************生命周期方法*********************************************/

    public void onCreate(Context context, Bundle savedInstanceState) {}

    public void onStart(Context context) {}

    public void onResume(Context context) {}

    public void onPause(Context context) {}

    public void onStop(Context context) {}

    public void onRestart(Context context) {}

    public void onDestroy(Context context) {}

    public void onNewIntent(Context context, Intent intent){}

    public void onActivityResult(Context context, int requestCode, int resultCode, Intent data) {}

    public void onRequestPermissionsResult(Context context, int requestCode, String[] permissions, int[] grantResults) {}
}

3、抽象的渠道Channel类,主要是面向渠道SDK设计的,主要分为业务必须接口和非业务必须接口。必须业务接口设计为抽象,子类必须实现的。

/**
 *  用于描述渠道SDK的顶层接口
 */
public abstract class Channel extends ChannelListenerImpl implements LifeCycleInterface, ProguardInterface {

    /*****************************   Channel 加载必须接口    **********************************/

    /**
     * 实例渠道插件对象,必须实现
     */
    protected abstract void initChannel();

    public ChannelBeanList.ChannelBean channelBean;

    @Override
    public String toString() {
        return "Channel{" + "channelBean=" + channelBean +'}';
    }

    /****************************** 必须业务逻辑接口 ****************************/

    public static final String PARAMS_OAUTH_TYPE = "PARAMS_OAUTH_TYPE";
    public static final String PARAMS_OAUTH_URL = "PARAMS_OAUTH_URL";

    /**
     * 返回渠道的ID(用于识别渠道)
     */
    public abstract String getChannelID();


    /**
     * 由于个别渠道只简单实现登录、支付接口,
     * 对外提供该接口给CP判断该接口是否已实现
     * @param FuncType
     * @return
     */
    public abstract boolean isSupport(int FuncType);

    /**
     * 渠道SDK初始化
     */
    public abstract void init(Context context, HashMap<String,Object> initMap, CallBackListener initCallBackListener);

    /**
     * 渠道SDK登录
     */
    public abstract void login(Context context, HashMap<String,Object> loginMap, CallBackListener loginCallBackListener);

    /**
     * 渠道切换账号
     */
    public abstract void switchAccount(Context context, CallBackListener changeAccountCallBackLister);

    /**
     * 渠道SDK注销账号
     */
    public abstract void logout(Context context, CallBackListener logoutCallBackLister);

    /**
     * 渠道SDK支付
     */
    public abstract void pay(Context context, HashMap<String,Object> payMap, CallBackListener payCallBackListener);

    /**
     * 渠道SDK退出
     */
    public abstract void exit(Context context, CallBackListener exitCallBackLister);

    /****************************** 非必须业务逻辑接口 ****************************/

    /**
     * 返回渠道版本号
     */
    public String getChannelVersion(){
        return null;
    }

    /**
     * 渠道SDK个人中心
     */
    public void enterPlatform(Context context, CallBackListener enterPlatformCallBackLister){}

    /**
     * 显示渠道SDK悬浮窗
     */
    public void showFloatView(Context context){}

    /**
     * 关闭渠道SDK悬浮窗
     */
    public void dismissFloatView(Context context){}

    /**
     * 渠道SDK上报数据
     */
    public void reportData(Context context, HashMap<String,Object> dataMap){}

    /**
     * 横竖屏
     * @return true为横屏,false为竖屏
     */
    public boolean getOrientation(Context context){
        boolean isLandscape = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
        return isLandscape;
    }

    /****************************** 非业务逻辑 生命周期接口 ****************************/

    @Override
    public void onCreate(Context context, Bundle savedInstanceState) {}

    @Override
    public void onStart(Context context) {}

    @Override
    public void onResume(Context context) {}

    @Override
    public void onPause(Context context) {}

    @Override
    public void onStop(Context context) {}

    @Override
    public void onRestart(Context context) {}

    @Override
    public void onDestroy(Context context) {}

    @Override
    public void onNewIntent(Context context, Intent intent) {}

    @Override
    public void onActivityResult(Context context, int requestCode, int resultCode, Intent data) {}

    @Override
    public void onRequestPermissionsResult(Context context, int requestCode, String[] permissions, int[] grantResults) {}

}
配置文件的bean类

配置文件的bean类,通过反射加载Project / Plugin / Channel具体实现对象。示例展示Project,其他两个是类似的,详看项目Demo。

public class ProjectBeanList extends ProguardObject{

    private List<ProjectBean> project;//注意解析的名字要跟文件一致,不然会导致解析错误

    public List<ProjectBean> getProject() {
        return project;
    }

    public void setProject(List<ProjectBean> project) {
        this.project = project;
    }

    public static class ProjectBean extends ProguardObject{

        private static final String TAG = "ProjectBean";
        /**
         * 反射插件的单例模式方法
         * 返回的插件可能为空
         * @return
         */
        public Project invokeGetInstance() {
            Project p = null;
            Class<?> glass = null;
            if (TextUtils.isEmpty(class_name)) {
                LogUtils.debug_w(TAG, "invokeGetInstance: the class_name is blank");
                return p;
            }
            try {
                glass = Class.forName(class_name);
            } catch (ClassNotFoundException e) {
                LogUtils.debug_w(TAG, "invokeGetInstance: " + "do not find " + class_name);
            }
            try {
                //尝试调用getInstance
                Method m = glass.getDeclaredMethod("getInstance", new Class<?>[]{});
                m.setAccessible(true);
                p = (Project) m.invoke(null, new Object[]{});
            } catch (NoSuchMethodException e1) {
                //调用getInstance失败后,尝试new其对象
                try {
                    p = (Project) glass.newInstance();
                } catch (Exception exception) {
                    LogUtils.debug_w(TAG, "glass.newInstance(): " + "do not find " + class_name);
                }
            } catch (Exception exception) {
                LogUtils.debug_w(TAG, "glass.getInstance(): " + "do not find " + class_name);
            }

            if (p == null) {
                LogUtils.debug_w(TAG, class_name + " is empty.");
            } else {
                p.projectBean = this;
            }
            return p;
        }

        /**
         * project_name : 项目名称
         * class_name : 项目入口类
         * description : 项目描述
         * version : 版本信息
         */
        private String project_name;
        private String class_name;
        private String description;
        private String version;

        public String getProject_name() {
            return project_name;
        }

        public void setProject_name(String plugin_name) {
            this.project_name = plugin_name;
        }

        public String getClass_name() {
            return class_name;
        }

        public void setClass_name(String class_name) {
            this.class_name = class_name;
        }

        public String getDescription() {
            return description;
        }

        public void setDescription(String description) {
            this.description = description;
        }

        public String getVersion() {
            return version;
        }

        public void setVersion(String version) {
            this.version = version;
        }

        @Override
        public String toString() {
            return "ProjectBean{" +
                    " project_name='" + project_name + '\'' +
                    ", class_name='" + class_name + '\'' +
                    ", description='" + description + '\'' +
                    ", version='" + version + '\'' +
                    '}';
        }
    }
}
配置管理类

主要加载配置文件及对外提供加载接口和实现部分逻辑控制。(示例展示Project,其他两个是类似的,详看项目Demo。注意:Plugin需遍历生命周期加载N个功能插件的生命周期。)

public class ProjectManager {

    private static final String TAG = "ProjectManager";
    private static String PROJECT_CONFIG = "Project_config.txt";

    private static Project project;
    private HashMap<String, ProjectBeanList.ProjectBean> projectBeans = new HashMap<>();

    /********************* 同步锁双重检测机制实现单例模式(懒加载)********************/
    private volatile static ProjectManager projectManager;
    public static ProjectManager init(Context context) {
        if (projectManager == null) {
            synchronized (ProjectManager.class) {
                if (projectManager == null) {
                    projectManager = new ProjectManager(context);
                }
            }
        }
        return projectManager;
    }

    public static ProjectManager getInstance() {
        return projectManager;
    }
    /********************* 同步锁双重检测机制实现单例模式 ********************/

    private ProjectManager(Context context) {
        parse(context, PROJECT_CONFIG);
    }

    private void parse(Context context, String pluginFilePath) {
        //从配置文件中,读取插件配置
        StringBuilder projectContent = FileUtils.readAssetsFile(context, pluginFilePath);
        String strProjectContent = String.valueOf(projectContent);

        //进行解析
        Gson gson = new Gson();
        if (!TextUtils.isEmpty(strProjectContent)) {
            try {

                ProjectBeanList projectBeanList = gson.fromJson(strProjectContent, ProjectBeanList.class);
                if (projectBeanList.getProject() != null && projectBeanList.getProject().size() != 0) {
                    //如果解析结果无误,载入到listPluginBean中去
                    for (ProjectBeanList.ProjectBean projectBean : projectBeanList.getProject()) {
                        projectBeans.put(projectBean.getProject_name(), projectBean);
                    }
                    //打印解析结果
                    LogUtils.debug_i(TAG, PROJECT_CONFIG +" parse: \n" + projectBeans.toString());
                } else {
                    //解析结果出错
                    LogUtils.e(TAG, PROJECT_CONFIG + " parse error.");
                }

            } catch (Exception e) {
                //解析结果出错
                LogUtils.e(TAG, PROJECT_CONFIG + " parse exception.");
                e.printStackTrace();
            }
        }else {
            LogUtils.e(TAG, PROJECT_CONFIG + " parse is blank.");
        }

    }


    private boolean hasLoaded;
    private static HashMap<String, Project> ProjectLists = new HashMap<String, Project>();


    /**
     * 加载所有的Project,可能存在多个项目
     */
    public synchronized void loadAllProjects() {
        if (hasLoaded) {
            return;
        }
        HashMap<String, ProjectBeanList.ProjectBean> entries = projectBeans;
        Set<String> set = entries.keySet();
        for (String key : set) {
            loadProject(key);
        }
        LogUtils.debug_i(TAG, "loadAllProjects:" + ProjectLists.toString());
        hasLoaded = true;
    }


    /**
     * 加载一个项目,返回的Project可能为空
     *
     * @param projectName
     * @return
     * @throws RuntimeException
     */
    private Project loadProject(String projectName) throws RuntimeException {

        // 1.查看从配置文件中读取的插件列表,是否存在此插件
        HashMap<String, ProjectBeanList.ProjectBean> entries = projectBeans;
        ProjectBeanList.ProjectBean projectBean = entries.get(projectName);
        if (projectBean == null) {
            LogUtils.debug_i(TAG, "The project [" +  projectName + "] does not exists in " + PROJECT_CONFIG);
            return null;
        }
        Project project = null;
        // 2.调用其单例模式方法
        project = projectBean.invokeGetInstance();
        if (project != null) {
            // 3.反射初始化插件
            project.initProject();
            // 4.将已加载好的插件,添加到插件列表中去
            ProjectLists.put(projectName, project);
        }
        return project;
    }


    /**
     * 获取特定项目
     * 可能为空
     *
     * @param projectName
     * @return
     */
    public Project getProject(String projectName) {
        if (!hasLoaded) {
            LogUtils.debug_i(TAG, "getProject: " + projectName + "Project not loaded yet");
            return null;
        }
        Project project = null;
        HashMap<String, Project> entries = ProjectLists;
        project = entries.get(projectName);
        return project;
    }
}

项目回调设计

任何一个项目都会存在模块之间的调用,任何事件的事件流都会有事件结果回调。都会涉及到回调机制。
相关原理可参考下这篇文章: Java回调机制解读

简单介绍下项目的回调设计思路:
定义基类的回调接口,定义成功和失败回调,失败回调可以通过错误码和描述信息来区分错误事件的详细信息。这里有个小技巧就是定义返回错误码,CP就可以根据错误码做相关的界面UI提示,这里就可以偷懒尽量避免做多语言和多UI处理啦。

public interface CallBackListener<T>  {

    /**
     * 成功回调
     * @param t 详细信息
     */
    void onSuccess(T t);

    /**
     * 失败回调
     *
     * @param code 错误码
     * @param msg 错误详细描述信息
     */
    void onFailure(int code, String msg);
}

项目数据设计

一般项目都会涉及到数据的交互与数据存储,数据存储一般常用的数据存储方式SharedPreferences存储、文件存储、SQLite数据库存储、ContentProvider存储、网络存储等混合使用。相关的存储方式及使用这里就不介绍了。由于游戏SDK涉及到的数据量不大,主要以内存存储和SharedPreferences存储和文件存储为主。
PS:如果数据设计到多进程数据交互,建议使用ContentProvider存储方式。

简单介绍下内存缓存的思路:
定义全局的集合,通过单例来管理相关的数据存储和读取接口。

public class BaseCache {

    private Application mAppContext;
    public Context getApplication() {
        return mAppContext;
    }

    /********************* 同步锁双重检测机制实现单例模式(懒加载)********************/

    private volatile static BaseCache sCache;
    private BaseCache(Application appContext) {
        mAppContext = appContext;
    }

    public static BaseCache getInstance() {
        if (sCache == null) {
            throw new RuntimeException("get(Context) never called");
        }
        return sCache;
    }

    public static BaseCache init(Application cxt) {
        if (sCache == null) {
            synchronized (BaseCache.class) {
                if (sCache == null) {
                    sCache = new BaseCache(cxt);
                }
            }
        }
        return sCache;
    }

    /********************* 同步锁双重检测机制实现单例模式(懒加载)********************/

    /**
     * hashMap是线程不安全的,做全局缓存时,用锁来保证存储值
     */
    private HashMap<String, Object> mConfigs = new HashMap<>();
    private ReentrantLock mLock = new ReentrantLock();

    public void put(String key, Object value){
        mLock.lock();
        mConfigs.put(key,value);
        mLock.unlock();
    }

    public Object get(String key){
        mLock.lock();
        Object object = mConfigs.get(key);
        mLock.unlock();
        return object;
    }

项目域名设计

因为SDK涉及到多个项目,每个项目肯定是会有不同的域名的,并且同一个项目也存在测试环境、沙盒环境、正式环境的域名区分,域名设计的主要的作用是统一管理域名及方便后续不同的项目来回切换不同的域名地址,这是比较重要的。

简单说下域名的设计思路:
一般修改域名不涉及到代码的修改,对外的包如何动态的修改域名设置呢?
1、通过后台配置预设域名,但是后台的设置会影响到线上环境。慎重。
2、通过包体的配置文件来读取域名,开发人员可以通过反编译包体修改域名调试代码。
PS:开发人员根据需求设计,但是切记要统一管理,方便维护。

public class UrlConfig {

    private static final String TAG = "UrlConfig";

    private static String Project_SDKUrl; //url

    //项目的基础ip域名地址
    private static String Project_BaseApi = "https://www.baidu.com/";

    public static String getSdkUrl(){
        return Project_SDKUrl;
    }

    public static void initUrl(){
        //通过配置读取域名
        String SDK_Base_Url = BaseCache.getInstance().getSdkUrl();

        if (!TextUtils.isEmpty(SDK_Base_Url)){//通过配置文件来修改域名
            Project_BaseApi = SDK_Base_Url;
        }
        LogUtils.debug_d(TAG,Project_SDKUrl);
    }

    /**
     *  预设设置修改当前网络的请求域名接口
     *  思考特殊的场景:
     *   多个项目交叉调用时,域名地址不一样,可通过修改临时域名做请求。
     */
    public static String getReSetUrl(String sdk_type, String channelId){
        String tempUrl = "";
        return tempUrl; //返回临时的域名
    }
}

项目混淆设计

SDK是对外提供功能的,而且从安全性考虑的话,对外提供的包体都应该是经常各种混淆和加密处理的。不然被攻击的可能性就很高了。
项目的设计是定义顶层的类或接口,具体的实现类继承该类或者实现该接口就不混淆了。关于相关的混淆规则及配置可参考: Android混淆打包那些事儿

/**
 *  定义基础混淆接口
 */
public interface ProguardInterface {
}


/**
 *  定义基础混淆对象
 */
public class ProguardObject {
}

项目三方库的封装设计

在实际开发中,会使用到大量的三方库,避免重复造轮子,特别是网络库、图片加载图等。但是,开源库过几年就可能会有更好的开源库,或者原有的开源库有局限性满足不了现有的项目需求,需要拓展或者替换的新的框架。所以需要做封装处理,避免后续替换改动太大。可参考:
对于有多种可替代解决方案的业务逻辑,提供一种快速替换方法

PS:项目中封装的是Volley,根据实际项目做了简单网络库的封装,不做任何商业模块使用,慎用!

public class RequestExecutor {

    public static final String GET = "GET";
    public static final String POST = "POST";

    private IRequestManager iRequestManager;

    private String method;
    private String url;
    private String header;
    private String userAgent;
    private Map<String,Object> params;
    private RequestCallback requestCallback;

    private RequestExecutor(RequestExecutor.Builder builder){

        this.method = builder.method;
        this.url = builder.url;
        this.header = builder.header;
        this.userAgent = builder.userAgent;
        this.params = builder.params;
        this.requestCallback = builder.requestCallback;
    }

    public void startRequest(){

        /**
         * 网络框架要替换成别的时候,实现具体封装就OK了,并修改具体实现
         * 比如换成okhttp写法 :return new OkHttpRequestManager();
         */
        iRequestManager = new VolleyRequestManager();
        iRequestManager.setHeader(header);
        iRequestManager.setUserAgent(userAgent);

        if (RequestExecutor.GET.equals(method)){

            iRequestManager.get(url,params,requestCallback);

        }else if (RequestExecutor.POST.equals(method)){

            iRequestManager.post(url,params,requestCallback);
        }

    }

    /**
     * 取消当前的网络请求,
     */
    public void cancel(){
        iRequestManager.cancel();
    }

    public static class Builder{

        private String method;
        private String url;
        private String header;
        private String userAgent;
        private Map<String,Object> params;
        private RequestCallback requestCallback;

        public RequestExecutor build(){
            return new RequestExecutor(this);
        }

        public RequestExecutor.Builder setMethod(String method){
            this.method = method;
            return this;
        }

        public RequestExecutor.Builder setUrl(String url){
            this.url = url;
            return this;
        }

        public RequestExecutor.Builder setHeader(String header){
            this.header = header;
            return this;
        }

        public RequestExecutor.Builder setUserAgent(String userAgent){
            this.userAgent = userAgent;
            return this;
        }

        public RequestExecutor.Builder setParams(HashMap<String,Object> params){
            this.params = params;
            return this;
        }

        public RequestExecutor.Builder setRequestCallback(RequestCallback requestCallback){
            this.requestCallback = requestCallback;
            return this;
        }
    }

}
结语:

由于篇幅原因,第二部分请看下一篇:
手游SDK —第四篇(SDK架构代码实现篇(下)- 项目需求开发)

如果觉得我的文章对你有帮助,请随意赞赏。您的支持将鼓励我继续创作!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336

推荐阅读更多精彩内容

  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,597评论 0 15
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,952评论 4 60
  • 各位看官大家好,经过第一篇 手游SDK — 第一篇(序言)的大体介绍,想必大家应该是对手游SDK有了大体的概念。废...
    Bzaigege阅读 24,192评论 8 19
  • hi,各位看官们。前面已经大概介绍了如何搭建一个比较符合业务场景的客户端架构实现。下面我们走进这个系列的游戏打包篇...
    Bzaigege阅读 12,211评论 6 17
  • 一直以来都羡慕那些能走走停停的人。到自己喜欢的地方,品味不一样的风景和味道。直到我终于有了自己的单反,终于可以去外...
    房园园阅读 220评论 0 0