导语
在Bmob
离职前写给同事的一个小总结,大致阐述了SDK相关,后续会继续修改,此为初稿。
正文
什么是SDK
-
Software Development Kit
,一般都是帮使用者(开发者)做好了较为底层或琐碎的API的封装
,使得开发者能简单调用就能实现原本较多代码实现的功能。根据个人理解,看到的基本是java和Android的库,很多是对业务的封装并结合开放平台API提供SDK,或者是基础性的SDK比如网络,图片处理类的,或者是音视频SDK这种封装了底层API的,或者是特定领域业务SDK的,比如笔者现在从事的公交地铁之类的SDK。 -
区分
framework
和library
,本身咬文嚼字意义不大,简单说只是library着重于封装,使用者能方便替换,而framework着重于控制和设计(比如经典的SSH
,IOC
框架),侵入性较强,也不好单独替换升级,一言以蔽之,作为程序员,使用库的时候,基本是coder call library
,使用框架时,能感觉到是框架在背后主导程序的运行流程,你能做的就是在一些声明周期函数中写写业务逻辑,不管是Spring还是Android Framework,比如控制onCreate()
,onResume()
的调用顺序等都是framework做的,甚至组件的创建,比如Activity
对象的new过程,也是程序员无法正常干预的,所以使用framework时经常需要多点学习成本,特别是在理清运行流程时。 -
SDK是
framework
还是library
两者的区别只是在于侵入性
或可替换性
(类似内聚和耦合,此消彼长)但是关于侵入性具体多寡确实是难以界定和见仁见智了,经验上看,大部分SDK都是扮演library的角色,或者说比较流行的都是这样,好用,替换成本低才是SDK的本意。也有类似比如阿里系的热修复产出,不过此时有的直接称framework了。
SDK开发和App开发
笔者13年底开始入坑Android学习,第一次用的是极光推送SDK,后陆续用了其他的SDK,在16年初从事SDK的开发和维护。 关于SDK开发和App开发,基本区别如下:
- 技能上,其实写SDK门槛并不高,只是市面上一般知名的SDK都是用户量有些并不错的,所以一般SDK开发是比App开发要多点经验,一般都是建议有App开发经验;
- 技术栈上,根据不同类型SDK而异,有的需要你懂一些音视频或地图的领域知识,不过通识来看,基本都是需要有较好(扎实)的语言功底,SDK一般要做得更细腻(很多App用),必要的时候需要懂点代码重构,用点模式去改善代码,设计或者持续改进好用的API;
- 版本迭代频率上,SDK一般要求稳定,离终端用户较远,不会有非常频繁的需求,相对的,bug也较少,所以SDK发版频率较稳定有规律,相对而言,对于热更新的需求,App就比较强烈;
- 工作内容上,大多数小公司的App开发基本偏重业务逻辑和UI交互,一些基础功能可以选用比如图片加载库,推送SDK等,一般是做好开发即可,而SDK面向的是写App的程序员,一般需要App的通用知识,也更该深谙开发技术,除了日常开发维护SDK,还要完善使用文档,技术教程,提供技术咨询和技术支持,指导开发者从接入到使用甚至到App开发的指导的流程,更需要负责SDK的可扩展,易维护和健壮性,毕竟多人使用的App可能内部质量一般,但是SDK要做到受欢迎,就必须通用性好才能hold住;
- 面向用户(使用者)上,App开发者面向的是终端用户,SDK开发者面向的是App开发者;
- 生命周期,一般知名的SDK生命周期较长,笔者所在公司的BmobSDK,第一个版本是2012年的;
- 变更影响上,SDK需要更为严谨,因为单个App下可能有大量的终端用户,作为SDK开发者,你的错漏将会被随着终端用户规模放大,具体来说:一个是升级新版本的替换成本(开发者编码阶段),一个是运行时候bug的数量(终端用户运行App时),前者决定了SDK的对开发者的使用体验,后者关乎用户是否流失;
- SDK更偏向于成为App业务无关的透明的类似middle ware(虽然源自后端的概念,不过一些大型App也慢慢有了客户端中间件),才能普适各种类型的App;
- 需求理解和把握,App的需求一般较为细化,业务相关,而SDK在采纳和提炼开发者的需求时,经常需要在更抽象或技术角度去思考是否要实现,对SDK原本定位和既有用户是否友影响,简言之,SDK对于需求需要更为审慎,帮开发者理清和从表象需求提炼出实际需求,考虑需求是否普遍,看是否有变通的做法,而非以为一味由SDK实现;
- 简单理解:
SDK –> 开发者(App) –> 终端用户
好的SDK应该是怎样的
谈谈理想中的良好设计的SDK应该是怎样的,实际中鉴于历史版本只能持续小步改进,也很难有一步到位的设计良好的SDK(受限于经验和持续增加的需求);
为何要谈谈设计,因为一般App都先着眼于功能实现,而SDK最好初期就要注意基本设计和代码结构,因为有后续版本向下兼容的考量,对外暴露的Api应该力求稳定,SDK内部核心层可以适当重构,所以SDK开发一般都需要较好的代码素质和适当的抽象封装,较好掌握重构技能,不像App开发,下个版本可以大改特改。
思路: 尽可能把错误在编译时能发现出来。
易学
虽然开发者普遍学习能力强,不过作为SDK还是要尽量降低使用者的学习负担,实际上,使用Bmob的开发者相当一部分是学生或者初级开发,此时更需要提供较好的使用体验,尽量降低使用门槛,减少开发者在使用中遇到的疑惑和受挫;
比如Bmob数据SDK的保存数据操作,v3.5.0
之前的代码类似这样:
Coder coder = new Coder();
coder.setName("宸笙");
coder.save(this, new SaveListener() {
@Override
public void onSuccess() {
// ...
}
@Override
public void onFailure(int code, String msg) {
// ...
}
});
至于Coder,仅是继承了BmobObject的bean类,和平时开发者编码习惯很类似,也类似一些ORM框架(一些设计思路经常是类似的)如ActiveAndroid类似,区别仅仅在于ORM库是保存到本地数据库,一般较为及时和快速,一般是同步的,而Bmob你可以理解为ORM是把本地的对象直接映射到了云端的数据库表中的记录,耗时操作故而需要异步回调。
值得注意的是,SDK还帮你做好了线程切换,在回调方法中直接能操作UI,这点在前文SDK回调方法在哪个线程中有具体提及到;
对于使用过基础的gson解析和网络库如Volley的开发者来说,写一个Bean类和类似Volley的回调方法都是很容易的;
易学更偏重于开发者了解尽量少的概念就能完成任务,SDK开发者需要尽量保留较少的概念(类),如有必要,请尽量暴露开发者熟悉的类(从JDK和AndroidSDK中找),举例说明,
- Android的中Handler发送消息时用到的Runnbale,其实和Thread没直接关系,作用只是在于对代码块的封装和让虚拟机复用或少加载自定义的类了,从用户(App开发者)看,也较为熟悉;
- Android中如果需要自己实现和线程相关的消息队列,自己实现的话,需要熟悉Looper,Handler,Message等的用法,慢慢可以看到HandlerThread和AsyncTask的实现,开发者已经不需要懂Message和Handler了,慢慢也能体验下代码封装和设计的心路;
- 做网络相关的封装时,用Map封装请求头就是一个很直接和基础的例子。
从java看,尽量做到易学,可以从三个角度考量:
- 类 尽量不要随意创建新的类,如有必要,请尽量做到见名知义,精简易懂的类簇结构才能较少开发者的疑惑;
- 方法
尽量用静态方法,比如BmobUser.getCurrentUser()方法 - 对象 尽量少需要创建对象
- 参数
- 尽量少出现参数较多,如较多可以使用使用Builder模式封装成为对象,避免开发者出现理解困惑和同类型参数但传的顺序不对导致的编译能通过的错误,这点在Okhttp,Retrofit等库中屡试不爽,虽然Builder模式的本意是分离对象的创建和表示(参考Android中AlertDialog的使用),也可以用来解决一些定制化和非必要参数的问题,看下BmobSDK的例子:
//设置BmobConfig,允许设置请求超时时间、文件分片上传时每片的大小、文件的过期时间(单位为秒) BmobConfig config =new BmobConfig.Builder(this) //设置appkey .setApplicationId(APPID) //请求超时时间(单位为秒):默认15s .setConnectTimeout(30) //文件分片上传时每片的大小,默认512*1024字节 .setUploadBlockSize(1024*1024) //文件的过期时间(单位为秒):默认1800s .setFileExpiration(5500) .build(); Bmob.initialize(config); // v3.5.2开始可以对查询条件等提供链式调用的写法,如下: BmobQuery<Book> query = new BmobQuery<>(); query.setLimit(8).setSkip(1).order("-createdAt") .findObjects(new FindListener<Book>() { @Override public void done(List<Book> object,BmobException e) { if (e == null) { // ... } else { // ... } });
- 可以使用考虑传
Context
参数,Context在Android中的角色类似于上帝组件,有了Context就能做很多操作,不过建议适度使用,滥用会有内存泄露的风险;
易用
API尽量符合直觉,不要出现一些很反人类的思路,这方面Bmob数据SDK有正面例子也有做的不太好的;
比如开发者需要查询,那就需要new一个BmobQuery
对象,至于查询条件和过滤,可以一步一步加,链式调用.setLimit(100).order(“-createdAt”).query()等操作,其实这里可以把setXxx修改的更自然点的,后续版本会改进;
比较不太合理的例子就是数据关联部分的建模设计,在一些场景下不是那么好理解,导致开发者经常会有这方面的问题。
开发者的用户体验
这里指的用户体验具体指开发者从进入官网下载SDK,搭建项目,集成SDK到开发上线的链路,这里讲下SDK能改进和优化的;
- 文档
- 快速入门
先让开发者能快速集成Build后Run起来;
- 详细开发文档
详细的Api用法介绍,注意事项等;
- 快速入门
- 教程
- 案例
- 一般推荐有完善的SDKDemo,随着每个版本发布都会更新,比如你从Bmob官网下载到的SDK压缩包都附带有Demo;
- 也有社区开发者贡献的源码和官方提供的其他Demo;
- 建议可以出一些最佳实践,SDK开发者深知SDK的实现细节,开发者可能会滥用,误用SDK,此时可以提供一些官方建议的最佳实践或推荐用法,比如经典的Bean类的字段类型是包装类型(如Integer)而不建议为基础类型(如int),当然,还有另一种思路,可以写一个基于IDEA的扩展,在开发者编写代码时候检查一些代码,基本的思路就是把运行时的报错提前到编译时(编码时),也能有效减少技术支持的压力; - 技术教程
- 对于新手而言,都文档和Demo或许比较枯燥,视频教程也是辅助手段,Bmob下的不同SDK均有对应的视频教程;
-
SDK集成
这里说下开发者集成SDK的成本,在早期的版本中,sdk仅仅是一个jar包,开发者在Eclipse中集成,复制到libs文件夹下即可,若出现报错,可以右键add to build path,后续版本增加了自动更新功能,开始有了一些UI上的资源,还需要放到res文件夹下,随着开发环境从Eclipse到AndroidStudio迁移的趋势,BmobSDK放到了github远程maven仓库,对SDK的集成也支持了远程依赖,配置下maven地址和compile具体的版本即可,到了3.4.7版本,开始采用so文件,很多初级开发者不会在Eclipse或AndroidStudio中配置so文件,一时间工单很多压力,随即就用打成aar包的方式,aar区别于jar,能放资源文件如图片,so文件等,所以仓库上在V3.4.7还有一个附带版本3.4.7-aar,就是为了解决这个问题,之后,开发者只需要两行配置,也无需知道so文件和其他资源文件的存在,提升了易用性,其实也有同事提供的另一种思路,把so文件序列化为其他形式的文件,运行时去读取该文件并生成so文件,读者可以自行试下;
-
版本更新
版本更新尽量做到向下兼容;
-
ApiDocs
虽然很少有开发者会去看,不过还是有必要提供,用JavaDoc生成的代码文档;
可扩展和可定制
对于一些功能,应该提供基础默认配置和定制化参数,如数据SDK的初始化,SDK内部的实现细节,Rx,OkHttp相关包装类的Builder等;
API的设计
SDK开发者和App开发者沟通的语言就是API!
-
统一性
Api应该统一,比如保存操作用法的回调接口为
SaveListener
,查询单条数据的接口就不太推荐为GetCallback
; -
流式API
类似
BmobQuery
提供的API,使用较为自然合理,还有如addWhereEqual
等API; -
确定性
Api应该是确定唯一的,比如完成update操作就用一个
UpdateListener
,不可多,多的话容易让人纠结和混淆,也不可为了节省和其他Listener混用,也会让人疑惑,应该尽量一个接口只完成一个功能,完成某个功能就用该接口;让开发者在依赖于IDE的代码提示的时候能根据提示写出合理自然的代码逻辑,和上面一条原则类似都要符合直觉; -
见名知义
尽量让代码(类名)有明确的含义,如
BmobBatch
,QueryListener
,而不用单独写注释,参数名也要尽量做到这样,开发者在编译器自动提示的时候能看清楚参数的含义;
错误码的定义
在实际开发中,很有必要让SDK定义的错误码和后端返回的错误码分开,采用错误码 + 错误描述 的定义;
- SDK客户端定义的错误码
SDK定义的错误码很多是在发起网络请求之前做的校验,比如参数为空,网络不可用
9016
等操作,try catch相关的代码,产生的其他异常统称为9015
错误; - 后端(restful Api)返回的错误码
比如传入
bql
有误,后端直接返回的错误,发短信次数达到限制返回的错误等; - 清晰和完善的
错误描述文档
;
持续迭代和改版
- 渐进式优化
不太推荐大步改造和重构对外的Api,在
3.4.7
到3.5.0
的迭代中,由于内部重构,加上接口合并,很多初级开发者替换成本略高,给技术支持和客服工作带来了很多压力; - 版本兼容性
除少数相邻版本如
3.4.7
和3.5.0
,笔者自己发的版本都基本做到了向下兼容,也会在更新日志changelog声明; - changelog
较为合理的
changelog
应该是对更新内容的恰当描述,比如解决了BmobInstallation
调用Push功能内存吃紧的问题,新增了数据迁移功能,内部优化等,尽量简洁,无需过分冗长; - 版本号的细节
一般持续优化的版本都是修改第三位数,比如
3.5.8
->3.5.9
,内部重构和改变较大的就修改第二位数字,如3.4.7
—>3.5.0
; - 代码混淆 SDK对暴露给开发者的类和Api,不管是在SDK发布和开发者的App打包的时候都是不希望被混淆的,SDK开发者有必要告知开发者合适的混淆规则,包括SDK中使用的三方库相关的混淆;
- @Deprecated
对于后续不再用的类,SDK应该稳妥使用
@Deprecated
注解,保证开发者在替换新版SDK的时候代码尽量不报错,某些版本就吃过这方面的亏,虽然简单,但是容易缺漏;
封装粒度的把握
SDK应该同时考虑到不同层次的开发者;
- 对
初级
开发者,尽量省事省力才是他们的需求,此时可以采用很多默认实现和封装粒度较大的,比如一两行代码即可实现很多功能等; - 对
中高阶
开发者,应该尽可能提供较细粒度的Api或可定制化参数的方法,对于熟手或者高手来说,较为复杂的配置和逻辑都不是很大的压力;
适当建模的取舍
BmobObject
用put的API风格还是用类似ORM库的方式,语法静态检查的约束和子类化的实现难度;