sdk的开发与设计

5,793 阅读18分钟
原文链接: www.jianshu.com

了解SDK

sdk,又称api;Software Development Kit的缩写,译为”软件开发工具包”,通常是为辅助开发某类软件而编写的特定软件包,框架集合等,SDK一般包含相关文档,范例和工具。
SDK可以分为系统SDK应用SDK.所谓的系统SDK是为特定的软件包,软件框架,硬件平台,操作系统等简历应用时所使用的开发工具集合.而应用SDK则是基于系统SDK开发的独立于具体业务而具有特定功能的集合.

什么是Library

Library即我们所说的库,通常是一组或者几组类的集合,通常是应用中某些功能的具体实现或者对系统已有功能的增强或补充.对Android开发者而言,最常见的莫过于是Support Library,另外就是我们经常使用各种网络请求库(OkHttp,Volley),数据库操作,图片加载库(Glide,ImageLoader)等.

什么是Framework

Framework即我们所说的框架,通常是系统或者应用的骨架,很多时候,它表现为一组抽象的构建及构件实例间交互的方法.因此,可以认为,Framework规定了应用的体系结构,阐明了整体设计,写作构件之间的依赖关系以及控制流程.注意自处的Framework并不完全等同于你所熟知的Android Framework框架,可以认为Android Framework中体现了Framework的思想,并进行了实现.

什么是API

API是Application Programming Interface,又称为应用编程接口,是软件系统不同组成部分衔接的约定。更加通俗的说就API就是我们常见和编写的方法或函数.

SDK构成

SDK主要包含Framework,API及Library的三部分.
Framework定义了SDK整体的可重用设计,规定了SDK各功能模块的职责以及依赖关系.其中个功能模块体现为Library.模块之间的内部通信及SDK外部通信(SDK对外提供服务的接口)则通过API进行.

另外完整的SDK还应该包含大量的示例和其他工具.比如在Android SDK的tools目录下提供了大量的辅助开发工具.

浅谈SDK实现目标

SDK的实现目标.任何应用都应具备:简洁易用,稳定,高效,轻量,SDK作为一种特定应用当然也不例外.

1.简洁易用

1-1 不应该让使用者花费太长时间学习的
一个好的产品对第三方使用者使用而言应该是简洁易用,不应该让使用者花费太长时间学习的.

1-2 SDK不应该对宿主应用有过多的代码侵入
要保证较少的代码侵入主要在对外提供服务时充分考虑到使用者的使用场景来设计出优良的API.
一个优良的API在定义的时候应该满足绝大数开发者所预期的方式—语义上要求通俗易懂,使用上要求简单可靠.
一个优良的API首先是简单可靠的.在正常使用的情况下体现为稳定可靠的执行,在异常情况下体现为及时的告知使用者使用错误.初次之外,遵循一致的明明规则,并是所有的API呈现出一致的风格对开发而言无疑是个好消息.

1-3 不应该有复杂频繁的接入工作
不应该有复杂频繁的接入工作.比如当开发者需要使用SDK的服务时,只需要在缘由的代码中新增一行即可
当我们需要使用该SDK的服务时,通过一行代码便可启用

Ad.init(this,params)

2.稳定

第三方的SDK服务应该是稳定高效的,体现在提供稳定可靠的服务,在不影响宿主稳定性的前提下足够的高效,这就要求我们SDK设计者在设计并实现SDK时要尽可能的做到以下几点:
2-1 稳定的API
对外提供稳定的API.SDK的API一旦确定,如无非常严重情况不可更改.作为提供服务方,发生API变更所带来的变更成本非常大.
2-2 稳定的业务
对外提供稳定的业务.在稳定的API后,必须要有稳定的业务来支撑.
2-3 自身运行的稳定
SDK运行时的稳定,作为服务提供方,我们必须确保SDK自身运行的稳定,并且保证接入方不会因为我们的SDK产生不稳定的情况.
2-4 版本稳定更新
版本稳定更新.和面向普通用户的应用相比,SDK版本的迭代是非常缓慢的.并且需要尽可能的对开发者屏蔽迭代过程,以免给开发者带来不必要的适配开销.

3.高效

无论是普通的应用开发还是SDK开发,都应该考虑到性能问题,SDK设计者应该着重考虑以下问题:
3-1 更少的内存占用
在不使用多进程的情况下,SDK服务和宿主程序运行在同一进程中,这种情况下必须要求限制SDK内存的占用,不能因为说因为我们SDK占用太多的内存资源,导致应用的存活时间变短.
3-2 更少的内存抖动
在占用更少内存的前提下,SDK设计者必须刻意的减少反复GC造成的内存抖动问题.
3-3 更少的电量消耗
尽管很多时候无法对电量消耗做一个很好的权衡,但是仍然有一些可以参考的做法,比如减少使用耗电模块的时间.比如在使用定位服务时,不要求非常高的精度下优先使用网络定位而不是GPS定位.
3-4 更少的流量消耗

.SDK整体架构设计

1.模块化开发

1-1 单一职责
根据单一职责将系统拆分为不同的小模块,每个模块保持相对独立。
1-2 协议或接口通信
模块之间通过协议或接口通信,以减少相互之间的依赖耦合.模块内部按照设计的几大原则进行实现,以保证模块本身可以灵活实现
对于现代开发而言,模块化是常用的手段,从宏观角度来看,模块是系统最小的组成单元.

2.组件化开发

组件是对逻辑的封装,并具备单个可移植性.比如可以把日志记录做成一个组件,之后它可以被轻松在应用在不同的项目中.
对于android 开发者而言,Android 提供的每个UI 控件同样也是组件,比如Button,TextView等
将整个项目划分成多个模块,几个模块或者单个模块作为一个组件,开发过程中我们可以对每个组件进行并行开发,最后发布时通过依赖将组件合并成完整的应用.
随着android的逐渐成熟,现在的app业务越来越复杂,与此同时,android工程也变得日益庞大,代码行数十几万已经是常态,此时有几个问题便会凸显出来:
工程任何一点改动都会造成整个工程的重新编译
业务耦合度非常高,牵一发而动全身.这就造成了”老人不敢改,新人无法改”,
协作开发基本上是不可能的

根据实际情况对其进行组件化,比如我们将分享功能组件化,可以轻松的支持多种渠道的分享,在需要更新分享功能时,可以对其进行单独的编译和测试.

4.插件化开发

SDK不同于普通应用,不能频繁的进行更新,以免让开发者觉得SDK不稳定或者让开发者频繁的集成.SDK看起来变化较慢,实则变化频繁.就以以前做的广告SDK而言,有时候经常需要对某类机型进行数据采集或者及时更新反作弊模块,在没有使用插件化之前,解决该问题是非常麻烦的.
我们将SDK整体划分为两部分:宿主和插件.宿主只向开发者提供必要的服务接口,并提供了自定义插件加载器.而核心的逻辑则是存在于插件中.当需要采集数据的时候,只需要由开发人员开发好数据采集插件并下发到指定设备即可;当需要修复SDK缺陷时,同样也只需要下发新的插件包即可.
通过将整个工程分为宿主和插件可以实现宿主的并行开发和分开编译,并且能有效的解决方法数65535的限制.

SDK初始化

Context
很多情况下SDK没有自身的上下文Context,而必须要借助应用提供.SDK初始化的常见做法:

Ad.init(Context context,AdParams params)

Application组件中的onCreate()中去掉用该方法,这就意味着该初始化过程是同步的,
假如SDK本身初始化时间较长,就会影响应用的启动速度
通常将SDK服务进一步划分成核心服务和辅助服务,之后通过并行初始化和延迟初始化的手段来减少SDK初始化耗时.

云更新控制

云控服务作为一种服务端控制客户端的手段在SDK中开发中非常重要
现在的SDK开发可以不支持插件化,但是必须要提供云控服务,以便让服务端能控制SDK
在不需要进行数据采集的时候,可以通过云控服务关闭SDK采集功能,在需要的时候在将其打开.
云控服务分为服务端主动和客户端主动.服务端主动是指服务端会将最新的云控开关的信息推送到SDK,而客户端主动则是SDK在进行操作之前会首先请求云控信息.

安全

1.SDK自身安全

通常会为开发者分配api key和api secret,SDK会读取开发者配置的api key和api secret,并用于随后的网络通信中。 它也许需要你提供api key和api secret,如果没有则需要到官网进行申请.

2.核心逻辑采用C/C++

数据加密类,模块算法类都都应该采用NDK开发,将其封装在so文件当中 由于.so文件是通过c/c++编译出的文件,相对于java的反编译文件来说,可读性更差,另外大部分的Android开发者并不具备较深的C/C++能力,因此一定程度上增加了被破解的能力.

3.通讯加密

实际情况对通讯协议进行加密,具体是采用对称加密还是非对称加密,则需要根据实际情况做选择.另外,请尽可能使用https来代替http.

4.设备安全

如广告SDK中,有一些开发者会通过虚拟机来刷广告,因此有必要针对此情况做判断.一旦SDK检测出非法请求后可以采取两种方案,一种是SDK拒绝服务,另外一种则是正常服务,SDK会将作弊信息上传至服务器,以便后端服务定向排除数据.

减少传输数据大小

在设计SDK和服务端通讯之间的数据协议时,需要根据实际情况考虑,但有以下几条建议值得我们接受:

如果对传输的数据大小有要求,建议对数据进行压缩. 可以采用json/xml/Protobuf等协议,如果它们仍然不能满足则可以考虑自定义二进制协议.

选择支持最低系统版本

不得不考虑开发者应用所支持的系统最小版本,
为了让SDK支持更广泛的设备,我们需要降低最低支持的系统版本.比如现在失眠上主流的系统版本是Android 5.0,那么对SDK而言,起码要支持到Android 4.0,
降低最低支持版本看起来很容易,但是我们不得不做更多的工作来确保SDK能表现出一致的工作行为(通常,我们在SDK内部检测当前系统版本来确定哪些方法可以被调用).更残酷的真相是我们花费了很大的精力去支持2.3,但来自2.3系统版本的请求量却连1%都不到.

权限管理

Android中任何开发都避不开权限申请.作为SDK的设计者,对于权限遵循”如无必要,无需增加”
用不到的权限,就不要加上去,这也是我们所谓的最小权限原则,该原则同样适用于普通应用开发
从android 6.0以上,google改变了权限申请的策略,因此需要单独对此做适配.

日志服务

无论系统大小,日志服务是基本的服务.一个良好的日志服务能够帮助我们快速的发现问题,定位缺陷,从而获得问题的解决方案.
SDK的日志服务和其他常见的日志服务并无太大的不同,但是要保证以下几点:
日志服务能够记录有效的信息,在SDK要关键位置进行打点.
日志服务上传日志信息到服务器时,要保证最大的可靠性,不能发生上传失败后抛弃日志的情况.
日志服务不能影响对正常的操作流程有过多的性能影响.SDK产生的日志信息往往是非常多的,因此必须考虑日志IO操作所带来的开销.

深究API设计

API的设计在任何开发中都是非常重要的,很多时候软件的质量好不好在API的设计可以得到体现.在普通的应用开发中,API只会在应用开发人员间流通而不会暴露给非本应用开发的其他人员,但是SDK作为一种服务,需要向开发者暴露一部分API.通常我们将内部流通的API称之为内部API,而开放给开发者的称之为SDK API.

两者使用场景虽然不同,但是都遵循着一些通用的设计规则,这里无法细说,只列出我认为需要重点关注的十一条原则:

1.方法名能够表明其用途

方法名是理解方法含义的第一渠道.一个好的方法名首先是能够向他人展示自身功能,这样做的好处就是能够减少不必要的沟通成本,对于开发者而言,还有什么比直接读代码更直观呢.

2.参数的合法性检验

对参数进行合法性检验是非常重要的,请不要想当然的认为可以用运行时异常来代替.当合法性校验不通过时,针对方法权限不同分别对应不同不同的处理策略:

2-1 对于公开方法通过显示检查抛出异常的方式,并且使用javadoc的@throw来说明抛出异常的原因
2-2 对于私有方法通过断言的方式来检查参数的合法
检查构造方法的参数的合法性,以使对象处在统一状态
2-3 需要注意,如果检查的代价太大,需要综合考量,比如如果接受的是一个很大的List,此时检查的代价可能很大

3.方法要明确其单一的功能

一个方法应该具有单一的功能,尽可能做更少,但是更专的事情.这也是我们常说的单一职责原则.另外一定要记住宁可提供小而美的方法也不要提供大而全的方法,经验正面大而全的方法往往发生变动,产生风险的可能性更高,因此不如提供更小的方法以便组合使用

4.方法异常问题

对于需要暴露给开发者的方法要及时的抛出可查异常来帮助开发者在编译阶段发现问题,另外,对于运行时异常,SDK设计者必须保证该类异常不会导致宿主程序出问题并且需要告知开发者.

5.方法权限控制

方法的权限也是需要着重考虑的,SDK设计者必须同时从安全和业务的角度考虑哪些方法是可公开的,哪些是不可公开以及哪些是静态的.

6.避免过长参数

过长的参数会造成记忆上困难,需要慎重对待.在无法避免过长参数的情况下,需要考虑其他的方法进行解决:

a. 通过使用Builder模式来实现
b. 通过使用辅助类,通常采用静态内部类的方式,具体见静态内部类的使用
c. 通过将多个参数封装成类对象
d. 通过将参数拆解成多个方法的参数

7.谨慎使用方法重载

重载不应该让使用者感到疑惑,即不应该出现这种情况:同样的参数,但是开发者不能明确哪个方法会被执行.换言之就是不要产生歧义性.

另外需要注意,不要存在参数类型经过自动转换就可以运行在另外一个方法的情况,我曾经在code review中看到这样的代码:list中的remove(Object)和remove(int),请务必保证自己不会犯类似的错误.尽管在java当中能够使用重载,但是我不建议使用,尤其是不要重载变长参数,在需要重载的时候宁可使用不同方法名来代替也要好的多.关于这点java中提供的ObjectOutputStream类给我们做了很好的示范:它的write对于每个基本类型都有一个变形,比如写出字符,写出boolean等操作,我们发现设计者,并没有使用重载将其设计成write(Long l),write(Boolean b),而是将其设计为writeLong(l),writeBoolean().
对于构造函数,则可以通过是用静态工厂的方式来代替重载.

8.谨慎使用变长参数

多数情况下不需要使用变长参数,一般方法的参数在5个以上的时候,才 建议使用变长参数.在还有其他非变长参数的情况下,我觉得变长参数放在形参列表的最后.

9.避免方法直接返回NUll

对于需要返回数组或这集合的方法,不要返回null.比如我们去买糕点店买面包,面包没了是一种正常状态,就不应该返回null,而是返回长度为0的数组或集合.

10.必要时进行保护性拷贝

当类接受来自客户端的对象或者需要向客户端返回对象,如果该类不能容忍进来的对象再发生变化,那么有必要对对象进行保护性拷贝.另外要注意参数的合法性检验发生在保护性拷贝之后. 需要注意的是如果需要进行保护性拷贝的对象非常大,比如list集合中存在十多万个对象,需要权衡处理.

SDK开发流程

关于SDK开发流程,我会从以下三个方面写:
一时团队中如何协同开发,
二是SDK的持续集成,
三是SDK多仓库拆分和管理.

这三方面会再另外的篇章中展现(具体什么时候写完目前还未确定)

sdk开发者清楚程序员需要的是什么。

一份可用的文档
代码需要维护,文档同样需要维护,而这个持续的重复过程对有原则
1 文档从代码中自动化生成。
2 文档第一个读者。确保一个并不是很擅长编程的程序员也能使用sdk
3.减少重复,避免冗余,整洁的代码是这样的,优秀的程序员面对文档也是这个态度。
4.避免使用人员的打扰
一份可用的示例代码
文档是首要的,但是不是必须的,大部分是无用的
示例代码无需任何处理,可以运行,不需要任何改变,直接可以拷贝,那真是太理想了
高效的示例程序
规范的命名,模块化,小函数,异常检测
优秀的示例代码会在进行接口调用,通过清晰的代码结构和必要的说明来让其他程序员看清楚调用过程。
并且对一些代码注意事项进行必要说明。
一份可用的接口
1 简,少。尽可能少,用其极。接口数量越少越好,接口参数越少越好,调用流程越少越好。越少越好,一是后续维护更加方便。

参考

blog.csdn.net/swjbjxr/art…
juejin.cn/post/684490…