美文网首页Android进阶之路Android开发Android开发经验谈
Android SDK开发艺术探索(一)开篇与设计

Android SDK开发艺术探索(一)开篇与设计

作者: 木木玩Android | 来源:发表于2020-08-27 16:52 被阅读0次

    原文链接:https://juejin.im/post/6864723217831952392

    一、前言

    Android SDK开发艺术探索系列基于实际生产中的业务型SDK开发实践经验,具有一定的实战性与技术性,不仅包含一定业务背景下的经验之谈,还系统性地介绍了一款第三方SDK的开发过程以及相关技术的选型。在这个系列中,你不仅能了解到如何开发一款第三方SDK,还能学习到通用的Android开发知识、软件工程思想,甚至一些奇奇怪怪的知识。

    在本篇文章中你可以了解到如何设计一个SDK,定义与第三方交互的数据结构、数据预处理。了解到SDK与APP交互的通讯机制,包括整个流程的交互逻辑设计,规避信任问题。更能从这些设计中了解到对应的通用技术要点与设计思路。

    系列文章:

    Android SDK开发艺术探索(一)开篇与设计

    Android SDK开发艺术探索(二)Exception or ErrorCode

    Android SDK开发艺术探索(三)初始化

    Android SDK开发艺术探索(四)个性化配置

    Android SDK开发艺术探索(五)安全与校验

    Android SDK开发艺术探索(六)压缩与优化

    Android SDK开发艺术探索(七)依赖原则与打包方法

    二、SDK的定义与场景

    2.1、SDK的定义

    SDK全称 Software Development Kit,广义上的SDK 是为特定的软件包、软件框架、硬件平台、操作系统等建立应用程序时所使用的开发工具的集合;狭义上的 SDK 则是基于系统基础组件进行二次开发封装的、独立的、能够完成特定功能并返回相关数据的一组工具的集合。

    说人话,SDK一般就是我们常见的jar包、so库、aar包,大到一套JDK,小到只有一个方法的jar包,都可以称之为SDK。SDK的本质是一系列方法、逻辑的集合,对资源与API封装后的产物。

    2.2、SDK的场景

    SDK的定义我们了解了,那么SDK在行业内的场景有哪些呢?SDK广泛存在于2B产品中(此处禁止联想...)。如推送SDK,支付SDK,地图SDK、OCR-SDK、人脸识别SDK、游戏SDK等等。标准化的、可复用的SDK产品,广泛提高了中小型公司开发APP的效率。

    三、SDK接口设计

    设计一个SDK,有两个明确的原则贯穿始终:

    一是:最小可用性原则,即用最少的代码,如无必要勿增实体; 二是:最少依赖性原则,即用最低限度的外部依赖,如无必要勿增依赖。

    首先我们需要明确一下这个SDK的职责与边界,定义与宿主App的交互参数。即SDK接收什么?输出什么?

    举个例子:

    关键要点:

    • 出入参都有一个token,用于本次调用的关联与凭证。
    • 前端SDK设计入参时,应当尽量减少前端参数交互,相关参数尽量在获取token时传入后端服务,以此保持SDK接口调用的简洁性与调用的灵活性。

    3.1、出入参设计

    大多数情况下,出入参需要交互多个字段。那么就产生了数据组装的问题。因此,必须设计一个便捷、清晰的数据结构。

    SDK出入参本质上是交互一组键值对(Key-Value),因此可以考虑的方式有:

    3.1.1、传递一个Map

    生成一个Map,并设置对应的Key-Value。可行,但缺点是暴露了过多生成参数的细节与内部逻辑,Map的生成与KEY的设置相对不可控,也埋下了数据不一致的隐患。

    Tips:采用HashMap实例在activity间传输时,需要使用bundle.putSerializable(KEY_REQ, value);

    3.1.2、传递一个String

    生成一个JSON格式的字符串,跟Map方案的缺点类似,组装过程中还容易产生JSON格式问题。

    3.1.3、传递一个自定义实体类

    通过SDK模块内置专用实体类的实例,动态设置相关参数,交互类的实例。该放方案简单易用,屏蔽了生成与设置的细节,直接通过简单的set方法接口为其赋值,并且可以在赋值时进行数据校验、限定。

    入参构建示例:

    //构建请求实体
    final ReqContent req = new ReqContent();
    req.setToken("a_valid_token");
    req.setSignature("a_valid_signature");
    复制代码
    

    返参获取示例:

    //获取返回实体
    ResultContent result = getResult(intent);
    int code = result.getCode();
    String message = result.getMessage();
    String token = result.getToken();
    复制代码
    

    Tips1:采用自定义实体类作为出入参在activity间传输时,无法直接传递实体,需要经过序列化。有两种方法可以实现

    1、实体类需要实现 JDK下的Serializable接口,通过bundle传输:

    bundle.putSerializable(KEY_REQ, req);
    复制代码
    

    2、实体类需要实现 Android SDK下的Parcelable接口,通过bundle传输:bundle.putParcelable(KEY_REQ, req);

    Tips2:需要注意的是:Intent 中的 Bundle 是使用 Binder 机制进行数据传送的,有大小限制,所以务必不要进行大数据传输,建议最大不超过500K,越小越好,避免传输图片。(实测不同版本有不同限制,500K不是一个确定的数目,感兴趣的可以自己测试一下)

    3.2、调用封装设计

    SDK的调用,需遵循简单、封装的原则。主要有两种类型,一是单纯的、无界面的、不与用户交互的代码逻辑,如常见的加密、摘要生成SDK等等,直接通过一个最简单的方法调用后直接返回即可。二是复杂的、有界面、直接接触用户的SDK,此类SDK也是本篇重点讨论的类型。

    背景:SDK内部有一系列承载不同业务逻辑的Activity,第三方通过启动SDK的入口Activity进行SDK调用与业务分发,那么如何做一个优雅的调用封装?

    封装前的调用:

    //繁琐的调用代码
    Bundle bundle = new Bundle();
    //这里要暴露或者约定一个静态KEY
    bundle.putParcelable(KEY_REQ, request);
    Intent intent = new Intent(context, EntryActivity.class);
    //屏蔽转场动画,让画面跳转更和谐
    intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
    intent.putExtras(bundle);
    context.startActivity(intent);
    复制代码
    

    封装后的调用:

    //简洁的调用代码
    MySDK.launchSdk(context,req);
    
    //将复杂逻辑或细节内置于SDK中,对外提供封装好的静态方法即可
    public static void launchSdk(Context context, ReqContent request) {
        Bundle bundle = new Bundle();
        bundle.putParcelable(KEY_REQ, request);
        Intent intent = new Intent(context, EntryActivity.class);
        //屏蔽转场动画,让画面跳转更和谐
        intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
        intent.putExtras(bundle);
        context.startActivity(intent);
    }
    复制代码
    

    如此一来,不仅简化了调用,还屏蔽了一系列中间过程的模板代码,何乐而不为呢?SDK多写几行代码,可以方便开发者千万家。这里只是简单提供了一个思路,具体流程具体分析,从这里发散开去即可,但是也要避免过度封装,导致扩展、兼容性的问题。

    值得一提的是:采用Activity来做SDK入口与业务分发是一个比较“重”的逻辑,这在SDK设计中也是一个值得考量的性能维度。

    3.3、数据返回设计

    数据返回是SDK设计中不可缺失的一环。遵循的是“调用即响应”的原则,响应具体可以分为返回码、抛异常。切勿设计出无响应、无回调的SDK逻辑。SDK内部流程处理完毕后必须给调用方一个返回,业务才能继续。具体来讲,处理了if,就必须关注else,处理了case,就必须关注default,这也是良好的java编程习惯的一部分。

    业务型SDK的数据返回机制需要怎么设计呢?首先我们看下需求背景:

    • 需要在App与SDK的Activity间进行数据传输
    • 调取SDK后,需要执行一段异步逻辑,结束后需及时关闭;
    • 需要在逻辑代码处理过程中随时随地的构造返回参数并返回;
    • SDK代码需集成于各类第三方App,需要通用的交互机制,避免引起更多兼容问题。

    采用startActivityForResult()? 理论可行,但不够灵活,弃用。

    采用EventBus ? 避免引入非必须第三方依赖,暂时放弃。

    采用BroadcastReceiver?Android 内置的四大组件之一,为组件间交互而生。而且灵活方便,与入参方法呼应,可以从Intent中传输数据。

    广播又可以分为全局广播应用内广播,全局广播可以跨App传递,基于Binder机制,在这个场景下显得过于大材小用。而应用内广播,基于Handler机制,仅限于同一应用内的数据交互,相比于全局广播无需进程间通信,效率更高、也无需担心其他应用接收广播带来的安全性问题。下面来看下应用实例:

    发送数据

    //SDK内部封装的静态方法,提供给SDK内部模块统一调用
    public static void sendResult(String token, int code, String message) {
    
        ResultContent result = new ResultContent();
        result.setToken(token);
        result.setCode(code);
        result.setMessage(message);
    
        Bundle bundle = new Bundle();
        bundle.putParcelable(KEY_RESULT, result);
    
        Intent intent = new Intent();
        intent.setAction(MY_SDK_RECEIVER_ACTION);
        intent.putExtras(bundle);
        LocalBroadcastManager.getInstance(MySDK.getContext()).sendBroadcast(intent);
    }
    复制代码
    

    接收数据

    //SDK内部封装的注册方法,提供给APP模块统一调用
    public static void registerReceiver(BroadcastReceiver receiver) {
        IntentFilter filter = new IntentFilter();
        filter.addAction(MY_SDK_RECEIVER_ACTION);
        LocalBroadcastManager.getInstance(getContext()).registerReceiver(receiver, filter);
    }
    
    //SDK内部封装的反注册方法,提供给APP模块统一调用,如果在onCreate()方法中注册,则在onDestroy()方法中反注册。
    public static void unregisterReceiver(BroadcastReceiver receiver) {
        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(receiver);
    }
    
    //APP 中定义的广播接收器,用于接收返回参数
    public class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            ResultContent result = MySDKHelper.getResult(intent);
            if (result == null) {
                return;
            }
            int code = result.getCode();
            String message = result.getMessage();
            String token = result.getToken();
            String log = String.format(Locale.CHINA,"返回码: %d 返回信息: %s Token: %s", code, message, token);
            Log.d(TAG, log);
        }
    }
    复制代码
    

    补充:出入参需要根据项目具体需要灵活设计,如无必要,勿增实体,简单就是美。如果sdk业务足够简单,就直接采用常见的方法调用结合参数返回即可;再复杂一些则采用接口回调的形式,如Interface回调、BroadcastReceiver回调来处理。

    四、参数过滤设计

    参数过滤是指在入参过程中对入参数据进行过滤,避免无意义后续业务流程,及时提供调用反馈。

    常见的过滤方式有:非空检测、数据类型检测,数据格式检测。还可以通过自定义注解的方式对参数进行标记,通过编译器的检测就可以及时纠正数据类型。

    一个简单的自定义注解示例

    public class SourceAnnotationDemo {
    
        public static final String TYPE_A = "A";
        public static final String TYPE_B = "B";
    
        private String type;
    
        @StringDef({TYPE_A, TYPE_B})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MyType {
        }
    
        public String getType() {
            return type;
        }
    
        //限定了该方法的参数只能是设置限定的TYPE_A或TYPE_B,否则编译器提示错误
        public void setType(@MyType String type) {
            this.type = type;
        }
    }
    复制代码
    

    五、交互流程设计

    “前端没有绝对的安全!”——鲁迅

    正如鲁迅所说,前端没有绝对的安全,因此我们在设计SDK的整体交互逻辑时,必须考虑返回结果的合法性问题。

    5.1、错误的返回

    在错误的返回中,由于错误的结果理论上并不会对业务造成损失,比如支付失败,业务处理流程上并不会产生异常结果,当做失败处理即可。

    5.2、成功的返回

    在成功的返回中,由于成功的结果理论上是继续业务流程的依据,比如支付成功后发货行为。因此必须严格判断返回的可靠性。

    在前端代码中,容易被破解者通过Hook等非正常手段篡改或者伪造返回,因此不能直接信任前端返回码。一般支付的流程,都是类似的设计。

    5.3、参考流程

    流程要点:

    • 交互凭证(Token)从后端接口交互中返回,用于对应当次调用,此时若有更多参数需要获取,也应当从该接口中传入,保持前端接口的简洁性与数据交互的灵活性;
    • SDK处理完内部逻辑后,返回前端返回码通知App流程结束,App根据返回码类型来确定是否需要进一步确认。此处也可以精简前端返回,将更多业务字段从后端查询确认接口中返回,保持前端接口的简洁性与数据交互的灵活性。

    五、结语

    本文从开篇的SDK定义与场景出发,介绍了SDK的接口设计,参数过滤,以及合时信任链下的流程设计。接下来将继续介绍SDK开发中对于异常与错误返回的讨论, Exception or ErrorCode?It's a question.

    如果本篇文档对您的开发有所帮助或启发,点赞或分享就是对作者持续创作最好的激励,感谢支持!

    相关文章

      网友评论

        本文标题:Android SDK开发艺术探索(一)开篇与设计

        本文链接:https://www.haomeiwen.com/subject/ovthsktx.html