SDK开发的一点点心得 | 经验之谈

6,168 阅读10分钟

前情提要

各位大佬大家好,我刚从枸杞岛团建完回来。这篇文章的大纲和一部分内容还是我在枸杞岛上编完的。接下来先给大家看看我们团建的枸杞岛风景吧。

image.png

image.png

顺便打个小广告,我们部门最近招人的 iOS Android 都有需要,有看机会的朋友联系我呀。我的github上有我的联系方式。

因为之前把一些老底都交待完了,手头上感觉没啥新的文章素材可以和大家聊的啦。而且最近一直都在写一些sdk相关的东西,所以打算给大家分享一下经验,从sdk设计的角度给大家水一篇文章。

正文

首先我手头上负责了三个sdk:支付,推送,debug组件,其中尤其是推送和支付非常的难搞。因为他们本身是业务的一部分,如果你把他们当做一个大礼包提供给别人使用的情况下,估计会被接入方骂死。

我觉得我的sdk设计的也不算是特别优秀吧,但是我觉得还是有些东西可以稍微分享点给大家的。有时候多和同行之间沟通交流才能获得更好的成长吗。

下面我会根据几个不同的小点给大家聊一聊我认为合理的一个sdk应该是怎么样的。

少依赖

我经常碰到的一个问题就是,使用方会说你们的sdk是一个大礼包啊,为啥会有这么多的仓库依赖啊。我明明只用了你们一小部分功能,但是因为用了SDK,所以被引入了一大推的依赖。

举个例子网络库,数据埋点,图片库等等一大堆东西都会被依赖进来。所以在没有使用的情况下,sdk所需要的就是用最小的依赖关系,来完成你所需要的功能。之前有个大佬对这部分有过分析。

对 Android SDK 开发的一些个人心得

我这边更多想给大家的小建议是这样的,有时间就多跑跑gradle dependcies命令,然后自己合理的分析下那些东西是需要的那些东西是不需要要的。

同时合理的使用设计模式,就类似我上篇文章介绍的异步责任链一样。这些东西可以帮助你在后续的迭代中,更好的维护你的代码。

另外就是可以通过接口的方式将图片,网络库,埋点这些库加载到sdk内。另外就是这部分我觉得也没必要写的特别极致,如果你像我一样是个对内的sdk,你直接引入这些库也是非常河里的。

当然并不一定要一步到位,可以拆开打散成多个阶段,每个阶段删除掉你觉得没有用的依赖,否则一次性不仅风险高,而且也太难了。

多模块

我在开发这些sdk的时候,考虑的是通过最简单的动态化能力,把这些三方sdk打散,之后通过适配器的形式把他们的共性聚合到一起。通过多个Module的形式提供给使用方自行排列组合,比方说A接了其中的12两个,B接了其中的13,接入方可自行选择自己需要的东西。这样就可以避免大礼包代码的出现,同时也可以丰富业务的接入能力。

同时就是很多三方库在设计的时候,都会考虑把功能进行更细力度的拆分。举个例子,retorfit就有很多adapterconvert的子module,之后使用方也可以通过自行组合的方式去对其进行使用。

如何拆分粒度呢?我最近的思路是这样的,一个能组合的库起码要有三层。

  1. 接口层 这部分我只负责定义所有三方的共性相关,从面相对象上来说,我们可以通过依赖倒置的形式使用他们的父类。
  2. 业务层 这部分我要调用下原始定义的接口层,之后通过一些动态化的方式(最简单的方式就是通过反射或者服务查找(SPI)相关的方式)获取到具体的实现类,完成抽象类到实现类之间的转化调用。
  3. 三方sdk层 这部分就和retrofit的adapter一样了,我们只要在这里对抽象接口进行实现就好了。

如果你的SDK内还有一些ui相关的,你可能还需要提供一份ui定制的能力给到使用方,这样如果业务方有对页面修改的需求,只需要他们自行调整即可了。

加开关

SDK也不是一成不变,在一个持续迭代的过程中,当一个相对来说比较重要的功能上线的时候,最好是能通过abtest的方式,配置一些线上的灰度策略和回滚机制,让你可以更稳妥的把你的新增功能推到线上去。

举个例子,我们之前在替换新旧支付sdk的时候,采取的策略就是两个版本同时存在,之后通过线上灰度测试,在新版sdk稳定之后,再把整体sdk替换到新版本上。

有什么好处呢? 首先可以观察下sdk是否稳定,之后有没有crash问题还有会不会影响到原来的流程上。如果有问题则直接线上开关关闭。

单一出口

支付的上一个负责的同学,在调用支付的时候定义了非常多方法。比如说不同参数调用不同的方法,activity或者context或者fragment都使用不同的方法,跨进程使用另外一批方法,然后还要声明页面回调onActivityResult等等。

这有什么缺点呢?

sdk的使用方要自己根据不同的参数来决定不同的函数调用是什么,那么当你的sdk要开始拓展参数,或者别的什么的时候,你就会发现WTF,这个也太难改了吧,业务方可能就会向你的上级直接投诉你的代码质量了。

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

这部分设计上,我的看法就是单一变量参数,然后在sdk内将主动判断其他的额外条件,比如进程回调,参数的增减,以及回调结果等。

同时越少的代码调用,也就意味着sdk内可以为所欲为,即时我内部做了再大的改动,只要我没有变更过调用方法的逻辑,那么业务调用方就不会感知到这部分相关的。

数据说话

当我们跑完一个测试流程发现都没有问题,之后我们发布线上之后,如果线上问题反馈,我们应该怎么办呢?

sdk内还是要有一套专门排查线上问题相关的埋点,以及关于sdk回调结果的埋点的,这样我们就可以通过用户所提供的id相关的数据去反向查找下用户相关的埋点,之后根据最后的埋点结果定位用户所发生的实际问题。

而另外一些数据也可以反向证明我们对于这部分sdk的优化如何,也可以帮助我们完成我的kpi指定计划。

与时俱进

现在项目内已经加入了协程的依赖了,所以对于sdk来说,你只提供了一个异步回调给到使用方也是不满足诉求的。

所以在原来出口方法的基础上,我额外添加了一个协程的依赖库,将异步函数改装成了一个suspend,这样有需要的使用方以后就能直接用这个函数获取到结果,之后更简单的写代码了。

同时因为是一个新增的仓库,所以也不会造成其他没有引入协程项目的困扰。

可调式

正常情况下一个sdk提供给使用方的都是一个aar,如果在接入的时候发现一些难以调试的问题,如果我们一个个aar的版本发布,会严重影响你的开发进度,所以一个合理的混合开发模式,可以帮助你减少这方面的困扰。

小贴士 我之前的文章有介绍过compose build 同时我之前写的一个插件也可以帮助大家更简单的做这项工作。传送门

更简单的接入方式

上面说的只是相比较于sdk方面的设计了。我其实在接入的时候打算让别人能更简单的接入,如果能根据不同的App自动生成不同的代码则是最佳了。

作为一个内部sdk,由于要降低sdk对外部的依赖问题,所以自然而然就会不方便初始化,但是写得多错的多的道理大家都懂,所以如果这部分接入也可以由代码自动化生成,那么就可以解决很多痛点问题。

这里我还是按照我之前写推送sdk的经验,采用Android厂商推送Plugin化的方式。动态生成了埋点类和微信支付回调的activity,接过的知道要根据应用包名写一个自己的回调接受的activity

为了紧随时代的潮流,这次还是用kotlinpoet生成的kt代码,也碰到了一些奇奇怪怪的问题。比如kotlin的Map类并不是java的Map吗,kotlin的String也和java的完全不同。

同时,由于plugin内是知道当前接入方的applicationid的,所以我们可以自己把匹配上的applicationid的依赖有plugin来自动导入添加,同时完成项目内依赖的版本清洗工作,让对方开发人员尽量不感知到我们的aar

TODO

我这边在添加埋点代码的时候增加了一些简单的逻辑判断,如果项目内有微信埋点库依赖的情况下,才会自动生成这部分逻辑,如果没有找到依赖的话就会不生成,避免类引用不到所导致的异常。

但是我这边使用的是gradle configuration里面的allDependencies方法,而这个方法是不会把传递进来的依赖进行判断的,所以还是有个小漏洞在这里的。

有了解的大佬可以在地下留言给我,让我拜读下,如果是kotlin最好了,感谢,感谢。

结尾

这篇文章主要内容现在都只是我的一些感慨和经验之谈,如果你认为有什么问题,欢迎各位大佬在评论区展开讨论啊。

还有一点就是因为我之前也是一个sdk的使用方,其实我对于使用方的诉求其实是有些了解的。所以我在封装的时候就会尽量把一些我不想写的东西,或者我之前用起来不方便的东西规避掉。

开发过程中我也会加入一些我自己的思考,比如我觉得一些东西我可以自动生成,我就会去自动生成这个类,这样其实对大家来说都会更方便。思考多了,其实很多东西你都能玩出各种不同的花样的。