自从前段时间离职后,因为个人的事情一直没有选择再工作,也导致原有的文章并没有按时产出.最近个人的事情整理的也差不多了,恰好有不少朋友来问有关SDK开发方面的事情,在此就做个简单的梳理,希望能帮助各位.
目前更多开发者热衷于应用开发,极少数的开发者才有机会从事SDK开发工作,而市面上关于SDK开发介绍的文章少之又少,以至于让大家觉得SDK开发是相对比较难而且非常无聊的工作,今天我们就来简单的聊聊SDK开发的哪点事.
另外最近好像有博客之星评选,我也来凑个热闹,只求一票,不求其他:
点我,点我,投一票,一切为了妹纸
点我,点我,投一票,一切为了妹纸
关于SDK的解释
什么是SDK
在开始正文之前,首先来聊聊SDK是个啥玩意.
SDK是Software Development Kit的缩写,译为"软件开发工具包",通常是为辅助开发某类软件而编写的特定软件包,框架集合等,SDK一般包含相关文档,范例和工具.
SDK可以分为系统SDK和应用SDK.所谓的系统SDK是为特定的软件包,软件框架,硬件平台,操作系统等简历应用时所使用的开发工具集合.而应用SDK则是基于系统SDK开发的独立于具体业务而具有特定功能的集合.
比如在进行Android 应用开发时,我们使用Google提供的系统SDK(Android SDK),而我们经常使用的友盟SDK,极光SDK则是基于系统SDK开发的.
明确SDK的概念之后,再来聊一聊这三个概念:Library,API,Framework
什么是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主要包含Framework,API及Library的三部分.Framework定义了SDK整体的可重用设计,规定了SDK各功能模块的职责以及依赖关系.其中个功能模块体现为Library.模块之间的内部通信及SDK外部通信(SDK对外提供服务的接口)则通过API进行.
另外完整的SDK还应该包含大量的示例和其他工具.比如在Android SDK的tools目录下提供了大量的辅助开发工具.
对我们而言,大部分情况下是为某种具体的业务需求开发对应的SDK,以便作为第三正提供给其他需求方使用.比如百度推送的SDK主要实现消息推送功能,需求方只需要集成百度推送的SDK便可以使自己应用具备推送功能.
到现在已经介绍了SDK的主要构成,接下来我们重点来介绍SDK的实现目标以及在SDK架构中的一些核心点.
浅谈SDK实现目标
上面介绍了开发中常见的概念,现在来谈谈SDK的实现目标.任何应用都应具备:简洁易用,稳定,高效,轻量,SDK作为一种特定应用当然也不例外.
简洁易用
按照"奥卡姆剃须刀"理论,一个好的产品对第三方使用者使用而言应该是简洁易用,不用改让使用者花费太长时间学习的.这对SDK同样适用---SDK不应该对宿主应用有过多的代码侵入,也不应该有复杂频繁的接入工作.比如当开发者需要使用SDK的服务时,只需要在缘由的代码中新增一行即可.常见的SDK初始化如下:
public class Ad{
@TargetApi(9)
public synchronized static void init(Context context, SdkParams params) {
//省略多行代码
}
}
当我们需要使用该SDK的服务时,通过一行代码便可启用Ad.init(this,params)
要保证较少的代码侵入主要在对外提供服务时充分考虑到使用者的使用场景来设计出优良的API.一个优良的API在定义的时候应该满足绝大数开发者所预期的方式---语义上要求通俗易懂,使用上要求简单可靠.
一个优良的API首先是简单可靠的.在正常使用的情况下体现为稳定可靠的执行,在异常情况下体现为及时的告知使用者使用错误.初次之外,遵循一致的明明规则,并是所有的API呈现出一致的风格对开发而言无疑是个好消息.
稳定
站在SDK使用者角度来看,我们期望第三方的SDK服务应该是稳定高效的,体现在提供稳定可靠的服务,在不影响宿主稳定性的前提下足够的高效,这就要求我们SDK设计者在设计并实现SDK时要尽可能的做到以下几点:
- 对外提供稳定的API.SDK的API一旦确定,如无非常严重情况不可更改.作为提供服务方,发生API变更所带来的变工成本非常大.
- 对外提供稳定的业务.在稳定的API后,必须要有稳定的业务来支撑.
- SDK运行时的稳定,作为服务提供方,我们必须确保SDK自身运行的稳定,并且保证接入方不会因为我们的SDK产生不稳定的情况.
- 版本迭代稳定.和面向普通用户的应用相比,SDK版本的迭代是非常缓慢的.并且需要尽可能的对开发者屏蔽迭代过程,以免给开发者带来不必要的适配开销.
高效
无论是普通的应用开发还是SDK开发,都应该考虑到性能问题,SDK设计者应该着重考虑以下问题:
- 更少的内存占用.在不使用多进程的情况下,SDK服务和宿主程序运行在同一进程中,这种情况下必须要求限制SDK内存的占用,不能因为说因为我们SDK占用太多的内存资源,导致应用的存活时间变短.
- 更少的内存抖动.在占用更少内存的前提下,SDK设计者必须刻意的减少反复GC造成的内存抖动问题.
- 更少的电量消耗.尽管很多时候无法对电量消耗做一个很好的权衡,但是仍然有一些可以参考的做法,比如减少使用耗电模块的时间.比如在使用定位服务时,不要求非常高的精度下优先使用网络定位而不是GPS定位.
- 更少的流量消耗.
SDK整体架构设计
SDK的架构实现决定了SDK后续的维护难度,因此有必要在此对SDK整体架构中的一些点做些简单的说明.
模块化开发
根据单一职责将系统拆分为不同的小模块,每个模块保持相对独立。
模块之间通过协议或接口通信,以减少相互之间的依赖耦合.模块内部按照设计的几大原则进行实现,以保证模块本身可以灵活实现
对于现代开发而言,模块化是常用的手段,从宏观角度来看,模块是系统最小的组成单元.
组件化开发
组件开发同样是个老生常提的概念,但从我个人的感受来说,组件是对逻辑的封装,并具备单个可移植性.比如可以把日志记录做成一个组件,之后它可以被轻松在应用在不同的项目中.对于android 开发者而言,Android 提供的每个UI 控件同样也是组件,比如Button,TextView等.
在明确了组件这一概念之后,组件化开发也就不难理解:所谓的组件化就是将整个项目划分成多个模块,几个模块或者单个模块作为一个组件,开发过程中我们可以对每个组件进行并行开发,最后发布时通过依赖将组件合并成完整的应用.
那为什么要使用组件化呢?
随着android的逐渐成熟,现在的app业务越来越复杂,与此同时,android工程也变得日益庞大,代码行数十几万已经是常态,此时有几个问题便会凸显出来:
- 工程任何一点改动都会造成整个工程的重新编译.记忆最深的就是早期在没有进行组件化的时候,庞大的工程动辄需要十几分钟的编译时间,一杯茶的时间就出来了,很多时候,不得不眼巴巴的等着,尽管现在可以使用facebook出品的buck以及来自阿里的feeline来加速编译过程,单仍然不够.
- 整个工程中充斥的大量重复或者冗余的子模块,业务耦合度非常高,牵一发而动全身.这就造成了"老人不敢改,新人无法改",因为谁也不能预知在做修改之后,会产生什么影响.
- 协作开发基本上是不可能的,天知道彼此在做什么.代码合并的的时候更是令人痛苦.
- 不方便测试.高度耦合的业务和模块导致无法下手进行测试,只能草草了事.
通过引入组件化,上面遇到的问题便可迎刃而解.在SDK当中,根据实际情况对其进行组件化,比如我们将分享功能组件化,可以轻松的支持多种渠道的分享,在需要更新分享功能时,可以对其进行单独的编译和测试.
通过组件化,我们也可以轻松的实现SDK的定制功能,通过编写编译脚本,我们可以决定哪些组件被依赖,最终合并到完整的应用当中.比如友盟中的提供的可定制分享组件(如下图)的原理就是如此.
插件化开发
什么是插件化开发这里就不做介绍了,一方面插件化并不是个新概念,另外就是插件化到目前为止理论层次上已经非常成熟,不想15念开始研究的时候资料相对较少.
在SDK中为什么使用插件化呢?SDK不同于普通应用,不能频繁的进行更新,以免让开发者觉得SDK不稳定或者让开发者频繁的集成.SDK看起来变化较慢,实则变化频繁.就以以前做的广告SDK而言,有时候经常需要对某类机型进行数据采集或者及时更新反作弊模块,在没有使用插件化之前,解决该问题是非常麻烦的.但是在我们利用插件化之后,解决该问题就变得非常容易:我们将SDK整体划分为两部分:宿主和插件.宿主只向开发者提供必要的服务接口,并提供了自定义插件加载器.而核心的逻辑则是存在于插件中.当需要采集数据的时候,只需要由开发人员开发好数据采集插件并下发到指定设备即可;当需要修复SDK缺陷时,同样也只需要下发新的插件包即可.
通过在SDK使用插件化方案,可以有效的对开发者屏蔽手动更新的过程.宿主相对稳定,一旦确定,一般不会变动,而后续的业务变化则只需要通过更新插件来支撑.
除了上面谈到的利用插件化解决动态更新之外,通过将整个工程分为宿主和插件可以实现宿主的并行开发和分开编译,并且能有效的解决方法数65535的限制.在没有使用插件化之前,我们整个项目是由很多组件通过依赖形成的庞大工程,不得不通过
SDK初始化
和应用开发不同,很多情况下SDK没有自身的上下文Context,而必须要借助应用提供.SDK初始化的常见做法:Ad.init(Context context,AdParams params)
,我们往往推荐开发者在应用Application组件中的onCreate()中去掉用该方法,这就意味着该初始化过程是同步的,假如SDK本身初始化时间较长,就会影响应用的启动速度.
在这种情况下,作为SDK的设计者必须着手解决该问题.通常将SDK服务进一步划分成核心服务和辅助服务,之后通过并行初始化和延迟初始化的手段来减少SDK初始化耗时.曾经在我所负责的广告SDK中,有开发者反馈我们的SDK启动较慢,通过对整个SDK启动流程进行分析后,我们将插件加载服务和云控服务并行初始化,而对于像日志服务则采用颜值初始化,通过该手段有效的减少了初始化耗时
云更新控制
云控服务作为一种服务端控制客户端的手段在SDK中开发中非常重要,现在的SDK开发可以不支持插件化,但是必须要提供云控服务,以便让服务端能控制SDK,比如在不需要进行数据采集的时候,可以通过云控服务关闭SDK采集功能,在需要的时候在将其打开.
对本身是基于插件化开发的SDK而言,云控服务更是不可或缺.
从实现的角度而言,云控服务分为服务端主动和客户端主动.服务端主动是指服务端会将最新的云控开关的信息推送到SDK,而客户端主动则是SDK在进行操作之前会首先请求云控信息.对有推送开发经营的同学而言,这非常容易理解,就是像是为了实现消息推送功能,我们可以通过客户端轮训也可以通过服务端保持长连接进行消息推送一样.
安全
SDK自身安全
为了区分接入者并挑高SDK自身安全性,我们通常会为开发者分配api key和api secret,SDK会读取开发者配置的api key和api secret,并用于随后的网络通信中.这是非常常见的做法,比如当你集成极光推送SDK的时候,它也许需要你提供api key和api secret,如果没有则需要到官网进行申请.
核心逻辑采用C/C++
为了安全起见,数据加密类,模块算法类都都应该采用NDK开发,将其封装在so文件当中.有很多开发者不明白为什么这样会增强安全性.这里我们简单的做个说明.由于.so文件是通过c/c++编译出的文件,相对于java的反编译文件来说,可读性更差,另外大部分的Android开发者并不具备较深的C/C++能力,因此一定程度上增加了被破解的能力.
通讯加密
针对实际情况对通讯协议进行加密,具体是采用对称加密还是非对称加密,则需要根据实际情况做选择.另外,请尽可能使用https来代替http.
设备安全
在很多情况下,比如广告SDK中,有一些开发者会通过虚拟机来刷广告,因此有必要针对此情况做判断.一旦SDK检测出非法请求后可以采取两种方案,一种是SDK拒绝服务,另外一种则是正常服务,SDK会将作弊信息上传至服务器,以便后端服务定向排除数据.
减少传输数据大小
在设计SDK和服务端通讯之间的数据协议时,需要根据实际情况考虑,但有以下几条建议值得我们接受:
- 如果对传输的数据大小有要求,建议对数据进行压缩.
- 可以采用json/xml/Protobuf等协议,如果它们仍然不能满足则可以考虑自定义二进制协议.
选择支持最低系统版本
作为SDK的设计者,面临一个很大的问题是我们不得不考虑开发者应用所支持的系统最小版本,但是在SDK发布之前,我们并不知道会什么样的开发者使用我们提供的服务,因此为了让SDK支持更广泛的设备,我们需要降低最低支持的系统版本.比如现在失眠上主流的系统版本是Android 5.0,那么对SDK而言,起码要支持到Android 4.0,甚至是Android 2.3.
降低最低支持版本看起来很容易,但是我们不得不做更多的工作来确保SDK能表现出一致的工作行为(通常,我们在SDK内部检测当前系统版本来确定哪些方法可以被调用).更残酷的真相是我们花费了很大的精力去支持2.3,但来自2.3系统版本的请求量却连1%都不到.
权限管理
Android中任何开发都避不开权限申请.作为SDK的设计者,对于权限遵循"如无必要,无需增加",换句话说就是用不到的权限,就不要加上去,这也是我们所谓的最小权限原则,该原则同样适用于普通应用开发.
在刚接触SDK开发时,某些早期功能需要某些权限,但是后期该功能被砍掉了,但是权限却忘记去掉,这就导致不必要权限仍然存在的情况.
另外过多的权限申请,会让开发者怀疑你的目的.比如一个广告SDK的你申请照相机权限是想干嘛?恩,我怀疑你在偷拍我....好吧,这里我只是开个玩笑.
另外,从android 6.0以上,google改变了权限申请的策略,因此需要单独对此做适配.
日志服务
无论系统大小,日志服务是基本的服务.一个良好的日志服务能够帮助我们快速的发现问题,定位缺陷,从而获得问题的解决方案.
SDK的日志服务和其他常见的日志服务并无太大的不同,但是要保证以下几点:
- 日志服务能够记录有效的信息,在SDK要关键位置进行打点.
- 日志服务上传日志信息到服务器时,要保证最大的可靠性,不能发生上传失败后抛弃日志的情况.
- 日志服务不能影响对正常的操作流程有过多的性能影响.SDK产生的日志信息往往是非常多的,因此必须考虑日志IO操作所带来的开销.
深究API设计
API的设计在任何开发中都是非常重要的,很多时候软件的质量好不好在API的设计可以得到体现.在普通的应用开发中,API只会在应用开发人员间流通而不会暴露给非本应用开发的其他人员,但是SDK作为一种服务,需要向开发者暴露一部分API.通常我们将内部流通的API称之为内部API,而开放给开发者的称之为SDK API.
两者使用场景虽然不同,但是都遵循着一些通用的设计规则,这里无法细说,只列出我认为需要重点关注的十一条原则:
方法名能够表明其用途
方法名是理解方法含义的第一渠道.一个好的方法名首先是能够向他人展示自身功能,这样做的好处就是能够减少不必要的沟通成本,对于开发者而言,还有什么比直接读代码更直观呢.
参数的合法性检验
对参数进行合法性检验是非常重要的,请不要想当然的认为可以用运行时异常来代替.当合法性校验不通过时,针对方法权限不同分别对应不同不同的处理策略:
- 对于公开方法通过显示检查抛出异常的方式,并且使用javadoc的@throw来说明抛出异常的原因
- 对于私有方法通过断言的方式来检查参数的合法
- 检查构造方法的参数的合法性,以使对象处在统一状态
需要注意,如果检查的代价太大,需要综合考量,比如如果接受的是一个很大的List,此时检查的代价可能很大
方法要明确其单一的功能
一个方法应该具有单一的功能,尽可能做更少,但是更专的事情.这也是我们常说的单一职责原则.另外一定要记住宁可提供小而美的方法也不要提供大而全的方法,经验正面大而全的方法往往发生变动,产生风险的可能性更高,因此不如提供更小的方法以便组合使用
方法异常问题
对于需要暴露给开发者的方法要及时的抛出可查异常来帮助开发者在编译阶段发现问题,另外,对于运行时异常,SDK设计者必须保证该类异常不会导致宿主程序出问题并且需要告知开发者.
方法权限控制
方法的权限也是需要着重考虑的,SDK设计者必须同时从安全和业务的角度考虑哪些方法是可公开的,哪些是不可公开以及哪些是静态的.
避免过长参数
过长的参数会造成记忆上困难,需要慎重对待.在无法避免过长参数的情况下,需要考虑其他的方法进行解决:
a. 通过使用Builder模式来实现
b. 通过使用辅助类,通常采用静态内部类的方式,具体见静态内部类的使用
c. 通过将多个参数封装成类对象
d. 通过将参数拆解成多个方法的参数
谨慎使用方法重载
重载不应该让使用者感到疑惑,即不应该出现这种情况:同样的参数,但是开发者不能明确哪个方法会被执行.换言之就是不要产生歧义性.
另外需要注意,不要存在参数类型经过自动转换就可以运行在另外一个方法的情况,我曾经在code review中看到这样的代码:list中的remove(Object)
和remove(int)
,请务必保证自己不会犯类似的错误.尽管在java当中能够使用重载,但是我不建议使用,尤其是不要重载变长参数,在需要重载的时候宁可使用不同方法名来代替也要好的多.关于这点java中提供的ObjectOutputStream
类给我们做了很好的示范:它的write对于每个基本类型都有一个变形,比如写出字符,写出boolean等操作,我们发现设计者,并没有使用重载将其设计成write(Long l)
,write(Boolean b)
,而是将其设计为writeLong(l)
,writeBoolean()
.
对于构造函数,则可以通过是用静态工厂的方式来代替重载.
谨慎使用变长参数
多数情况下不需要使用变长参数,一般方法的参数在5个以上的时候,才 建议使用变长参数.在还有其他非变长参数的情况下,我觉得变长参数放在形参列表的最后.
避免方法直接返回NUll
对于需要返回数组或这集合的方法,不要返回null.比如我们去买糕点店买面包,面包没了是一种正常状态,就不应该返回null,而是返回长度为0的数组或集合.
必要时进行保护性拷贝
当类接受来自客户端的对象或者需要向客户端返回对象,如果该类不能容忍进来的对象再发生变化,那么有必要对对象进行保护性拷贝.另外要注意参数的合法性检验发生在保护性拷贝之后.
需要注意的是如果需要进行保护性拷贝的对象非常大,比如list集合中存在十多万个对象,需要权衡处理.
这十一条原则是我在团队中推广并要求严格遵守的,下面,将对这十条原则分别进行说明.
SDK开发流程
关于SDK开发流程,我会从以下三个方面写:一时团队中如何协同开发,二是SDK的持续集成,三是SDK多仓库拆分和管理.
这三方面会再另外的篇章中展现(具体什么时候写完目前还未确定)
SDK版本管理策略
SDK 版本号命名及修改原则
SDK版本号命名和我们以往的命名规则并无太大不同,通由4部分组成,格式为:
V主版本号子版本号阶段版本号_日期版本号加希腊字母版本号.比如V1_1_2_161209_beta
.
希腊字母版本号说明
- Alpha版:内部测试版,此版本表示该软件在该阶段主要是以实现功能为主,Bug相对较多,需要继续修改,通常只在内部流通流通而不对外开放.
- Beta版:外部测试版,该版本相对Alpha已经有了很大的改进,不存在严重的Bug,但还是存在一些缺陷,需要进一步的测试以检查和消除Bug.
- RC版:该版本已经相当成熟,不存在导致错误的Bug.与正式版相差无几.
- Release版:该版本意味着"最终版本",是最终交付用户或者公开发布的版本,也称为标准版.需要注意的是,该版本在发布的时候回以符合R来代替Release单词.
版本号修改规则
- 主版本号变化:当功能模块有较大的变化或者整体架构发生变化
- 子版本号变化:当功能有一定变化
- 阶段版本号变化:一般是Bug修复或者较小的变动,根据反馈,需要经常发布修订版本.
- 日期版本号(161209):用于记录修改项目的当前日期,每天对项目的修改都要更改日期版本号.
- 希腊字母版本号:此版本�号用于标注当前软件处于那个开发阶段,当软件进入到另一个阶段是需要修改.
API版本管理
和普通应用API版本管理不同,SDK设计者需要着重关注SDK API的管理.原则上SDK API一旦公开发布后其状态(签名和具体实现)应为不可变.
对于特殊情况下API的变更,需要遵守"开闭原则",即一个类,模块,方法应该对扩展开发,对修改关闭.这就要求我们做到以下几点:
- 在需要调整SDK API时,优先选择添加新方法,而不是在原方法上修改.对于实现相同功能的新方法,尽可能的要兼容原始方法.
- 在需要废除某些方法时,需要在正式版发版前使用@deprecated标识,并给出替代方案和废弃的时间(通常是SDK版本号)
接入文档和API文档版本管理
接入文档是用来告诉SDK使用者,如何使用SDK,使用的详细步骤和可能发生的问题,每个公司会有自己的一套规则,这个不需要做太多的解释.
另外,接入文档通常分为两份:内部版和公开版.内部版通常用于内部开发人员和测试人员,信息较为详细,而公开版则是面向开发者,相比内部版会省略的一些信息.
API文档其实就是对SDK API的更详细说明,类似java中的api doc,可以借助jdk的自带javadoc直接生成,当然在android studio也提供了便捷的生成方式.
无论是接入文档还是api说明文档,其变更一般发生在SDK版本发生变化时.当SDK发生变更时,文档必须随之更新,不能出现SDK更新后说明文档不与之匹配的情况.
集成Demo版本管理
集成Demo通常是一个简单的app,用来展示如何快速的接入SDK.其版本变更策略和SDK版本的变化保持一致.
总结
SDK开发中需要关注的点非常多,每个点都不能用三言两语完成的,后面会在此基础上慢慢的补充.