Ty是一个在Twitter的Android技术负责人,专职于Fabric开发工具团队。他曾经负责架构了Fabric平台和Twitter的AndroidSDK,推动了Digits和TwitterSDK的开源事业,可以说是他一手创建了更大的Twitter体系结构。他一直专注于Android超过7年了,也是GDE(谷歌开发专家)组织的一员。他经常在Android开发者的国际会议和各大Android开发者大会上进行演讲。在到Twitter之前,Ty曾在Evernote的Android团队工作,他带领构建SDK和合作伙伴的集成工作,带领开发短信平台,并建立ZagatforAndroid,Zagat之后被谷歌收购。
----------------------------------------------------------
Twitter的Fabric是知名的注重质量的SDK,并已部署在数十亿的设备。在这次redev演讲中,来自Twitter的TySmith,揭示了Fabric团队创建他们Fabric的各种原则,特别是在Android方面。通过深入参与技术决策团队,Ty了解到很多信息,他展示了团队在创建这个SDK过程中,学到的各种经验心得,关于稳定性、性能、SDK体积控制、以及对于一些特殊情况的处理这些方面。无论你现在或将来想要建设一个SDK,通过这次演讲你应该能收益很多关于设计SDK的伟大想法。
我们建立FabricSDK,我们保持了几个目标,帮助引导我们进行开发。这些原则决定了我们开发API和做决策的选择。这些想法可以融入你自己的SDK或甚至你的应用。我们很高兴能够看到大家离开这里后,在未来开发的你们的SDK中采纳和我们一致的想法。
在我们进入library或SDK编码之前,我们有必要考虑几个方面。
首先要考虑的是要找出你在library里的实际服务的对象是谁,是内部开发人员还是公共开发人员?谁会使用它?它带来的新价值是什么?市场上已经有了一个解决办法吗?如果是这样的话,你应该是去对其进行开发和贡献而不是重新创造一个“轮子”。
考虑开源与闭源是一个大问题。开源通常会让你更好地通过社区,获得更稳定的软件,以及更热心的内部工程师。然而,需要思考的事,你的SDK仅仅是集中于一个工程点呢?还是说它是一个完整的产品,但有一个后台服务呢?
因此,仔细考虑你将采用哪一种许可证(开源协议)。例如,如果你使用GPL许可,那么将会使得用了你的SDK或library的人也必须得使用GPL开源协议。更灵活的许可证可能是Apache2或MIT许可。
特别是对于Android,打包你的代码并不一定是简单的。你有三个问题需要虑。首先是对于一个标准的库项目,开发者将他们包含到他们的代码中,并且由IDE帮忙连接它们,它是非常灵活的,但是如果他们需要分叉(fork),他们得如何保持更新?
最后,在Android世界还有一种打包方式即aar,它是谷歌现在支持二进制打包方式。这是一个压缩的容器,包含了编译的源代码以及资源文件,它可以通过GradleMaven依赖源从而非常方便快捷地分发给开发者。
最后的考虑是在哪里托管你的打包结果。MavenCentral,是标准的仓库。然而,他们都需要开源许可,因为他们想保护他们的服务的用户,他们不希望有人会隐式地拉下来一个二进制包,并选择一个他们没有得到审查的服务条款。如果使用了另外的资源库(如果你有一个专有的二进制文件),则开发人员必须手动添库到编译脚本中。
在创造FabricSDK的工作是一个梦幻般的学习过程。我们的目标就是涵盖这五大方面:易用、稳定、轻巧、灵活,很好的支持。我们相信伟大的SDK要实现这些得走很长的路。
其中的一个关键就是可用性。我们认为产品应该是易于使用的。
那么所谓的易用到底是什么呢?我们想创造一种最简单的方式,让人们在他们的应用中开始使用Fabric.如果它是易用的,它应该是不需要侵入太多你的代码或者你需要做很多繁琐的集成工作。只要在你的代码中新增一行我们的代码,就可以使用它了,类似这样:
Fabric.with(this,newCrashlytics());ReceivenewsandupdatesfromRealmstraighttoyourinbox
但易用的同时,有时还得能够定制,许多开发者可能希望更多的定制。要做到这一点,我们使用的Builder生成器模式设置一些选项,比如设置一个监听器好让程序在应用程序崩溃之前通知你。
Crashlyticscrashlytics=newCrashlytics.Builder().delay(1).listener(createCrashlyticsListener()).pinningInfo(createPinningInfoProvider()).build();Fabric.with(this,crashlytics);对于FabricSDK,我们需要一个APIkey作为连接我们网络服务器的验证密钥。这是我们要开发人员处理的事情,但需要尽量减少所需的工作量或者说繁琐度。我们的标准方法是:通过我们的构建插件提供的方式,并将其注入到清单(manifest)文件中。这里是一个例子,使用metadata在清单文件中插入数据:
除了我刚才提到的实施细节,我们喜欢在设计API时考虑这些特点:第一个是直觉。如果一个接口调用的行为恰好是开发人员预期的方式,而无需参考文档。
我们发现,在你的SDKAPI中使用一致的命名,也是有助于使用者理解。使用平常的表达语言来命名你的方法,以及类似的设计模式。并且遵循各个平台约定俗成的命名规则,比如iOS和Android平台,它们各有不同的命名规则。
最后,如果API很难被误用,将可以防止一些错误的发生。验证输入的参数,和书写明确的文档,将使得开发者在使用的时候,能够有信心和避免错误。也会带来一个更愉快的体验。
让我们看一个反直觉的例子:
但是实际上反直觉的是,equals代表如果这两个URL解析到相同的IP地址,在Java中的实现,将返回true,这里的原因是这个API的实现十分有趣:它发射同步的DNS请求。谁会想到?阻塞调用线程是一个意外行为的例子,在API中应该是非常明确指出的。
举一个例子,Fabric和Crashlytics的初始化方式便都是一致的。在初始化Fabric或Crashlytics,两个不同的二进制依赖库文件,正如我们之前看到的我们允许它们使用同一模式建造。用户可以使用无参数构造函数,或定义辅助方法来设置默认值,另外,这两者都提供了一个可用于重定义对象的生成器(builder)。
最后该讲到如何防止误用了。例如,从FabricBuilder的构造函数我们可以得知,Context对象是必须的,而其它一些setter是可选的。一旦我们在构建的阶段中创建实例,这些可选参数也就一并被初始化。
我们如何才能设计出高品质的API呢?让我们来看看我们的设计流程。设计API是很难的,它通常不只是一个工程师独自坐在一个黑暗的房间,决定该是什么样子,它需要整个团队付出大量的工作。
我们在Fabric的API设计上第一个重点就着眼于我们将支持的几个平台。我们创建一个设计文档之前,任何实施工作都是这样做的,进行讨论在这些平台上,不同的方法的优点和缺点。
有一句话我很喜欢:一个API就像一个婴儿。他们很有趣,但他们需要18年的支持。任何API我们都必须要长期地支持,所以我们要让大家感觉到,我们正走在正确的路上,才能才久坚持支持下去。
现在我们已经设计了一些很容易使用的东西,让我们来讨论一下我们如何能获得开发者的信任,相信这是非常重要的。因此,确保软件开发工具包是可靠的,他们不影响应用程序本身的稳定性。大家都知道,相比开发应用程序,开发一个SDK需要更高的稳定性要求。让我们来看看如果产生了一个错误将会有什么影响。
如果一个应用程序有一个关键的错误,阻碍了它的用户使用,它可能仅仅需要发送一个新版应用程序给顾客进行更新即可。而如果是我们SDK发现了一个漏洞,我们很快修复它,它可能还需要一个月才能到达你的用户,在此期间,你的用户就会有很不好的体验了。
作为开发人员我们可以做什么,以确保尽可能高的稳定性?有一些事情是我们开发过程中的关键。首先,代码审查是非常重要的,必须得认真对待它们。然后,通过不断地问自己“这个代码有什么问题吗?”我们可以这样试着去问自己,以达到尽可能的防守。
如果能够自动获得一些基本的正确性保证,也可以在早期帮助捕捉错误,所以单元测试是非常有用的。
另一方面,人们经常忽略的是:在用户使用初次使用进行测试时候,使它能够运行你的一些SDK代码,这样做他们可以在你的SDK集成时进行捕捉bug。
有一些技巧可以让你的SDK具备更好的可测试性。其中,为了测试,有时我们需要进行模拟,模拟(mock)类作为真实类的仿制类,它没有真实操作,并且允许被重写调用和验证方式。
通过避免静态方法,您可以允许在模拟实例上进行操作任何方法的调用。如果您将使用静态方法,需要确保它可以被隔离,并且您将提供所有的依赖关系,并且没有基于任何状态。
许多mockinglibraries对于final的类也会产生许多问题,所以要考虑你的类扩展。在你的模拟类中不应该存在public属性,所以需要被访问的一切都应该通过一个访问的方法来运行。
在你的API中使用接口。如果您的输入点使用接口,设置类来测试将更容易。该接口允许开发人员进行重写的行为,比如契合模拟服务器或在内存中存储,来替代真实场景真实存储的开销。
最后,需要考虑到测试人员不需要构造多个层次深度的模拟。这个鼓励测试的原则应该被写入你的指引文档,并提供更稳定的测试框架。
有一些class很难被模拟,比如final类型的,它将创建它自己的依赖关系,并且是一个基于状态的单例。这在Java中是很常见的,虽然它通常是一个反模式。这使得它在隔离测试中非常具有挑战性。那么,我们能做什么来解决它?其实只要有一些小修改,我们可以使这些难解的点变得更容易测试一些。
开发者经常是容易不耐烦的,所以有一些错误越尽早抛出就越好。如果你一直在使用Gradle你应该会明白我的意思,一些错误如果在build期间不能通过总是好于build完成之后5分钟才出现错误。你应该把一些可以预期的异常抛出,以便于开发者能够尽快知道这些异常,比如在这种情况下,开发者试图设置一个null的logger到我们的builder里,我们得马上抛出一个异常,这样他们就可以很快知道并解决他们的错误。
然而,你得保证你的SDK在生产环境中绝不会出错,让你的代码持续运行在他们的应用中,是你保证开发者们信心的唯一方法。他们的应用程序往往是他们生计的依赖,所以他们不会喜欢去赌着使用一个经常崩溃的库。所以当他们在调试过程中出现问题的时候,你可以提供额外的信息,写清楚这个Exception,但要隐藏在生产过程中可能出现的问题,这样允许他们的应用程序的其余部分继续运行。你的SDK的出现问题可能对你来说是一个大问题,但并不是世界末日。
作为一个开发者,你使用SDK,你的应用程序应该增加价值,而最糟糕的事情就是你引进了某物反而使得原本多价值降低了甚至完全破坏用户体验。开发者们,包括我自己,不需要任何人来帮助我们写一个糟糕的App。
除了稳定,用户不太可能下载大的应用程序,这意味着安装包的大小是一个关键内容。下载软件产生的流量需要用户去付钱,所以即使你的应用是完全免费的,用户也得为下载它付出流量。在许多新兴市场,因为下载速度太慢,所以很多用户不爱下载大型应用程序;在某些市场,用户主动选择更新的应用仅仅基于更新日志和添加新的特性是否值得,因为他们需要支付每千字节流量费用。
让我们来讨论一些Fabric用于保持轻量的技术吧。有一些伟大的第三方库,可以真正给予贡献于你的应用程序,但当涉及到size规模和影响时,他们会他们显得不自由。例如,图像加载方面有各种不同体积大小的图片加载库。其中Fresco,是比其他任何一个第三方库都还大量级的一个库。然而,它对于旧设备有更好的支持,加载速度快而且内存友好,并支持渐进式JPEG。
作为一个SDK你应该努力平衡你的尺寸与功能。因此,要注意引入第三方库,以确保它们只满足所需的内容。
使用开源库有很大的优势,因为这些库往往经过很多的测试了,大家都使用得很好,并且他们有定期向他们提供更新的社区。这通常提供了一个更好的方案。在我们的TwitterSDK,我们利用Square的RetroFit这个库作为一个依赖来简化我们的API,而且也能使我们提供更好的可扩展性,这是值得的。
很多人应该都会遇到过Dalvik65K的限制吧?不过你们可能不熟悉这个错误的具体原因,对于方法的调用,可以通过在Android一个DEX文件,而它的引用的数量有限。问题的关键是,DEX的工具,在编译时,试图把所有的方法引用到一定的空间中,但引用数目大于空间所能容纳的数目,导致分配失败。
但如果开发者遇到这个问题,在他们去使用multidex或类似的东西之前,他们经常决定审核他们的第三方库,选择是否可以减少应用程序方法数量的库。所以我们的目标和建议是尽可能地使你的库模块化和精益。
我们想要模块化开发人员需要的特定功能。我这样做是通过指定一个树的传递依赖关系。所以,我们有两个例子展示如何初始化Fabric:
容易整合的:
Fabric.with(this,newTwitter());更多控制的:
Fabric.with(this,newTweetUi(),newTweetComposer());第一个例子让你马上开始,其次是一个拥有更多定制的版本,在这里你可以选择需要的特定组件,然后它们才生效。
让我们的SDK尽可能小,我们着眼于模块化来设计我们的架构。就像我们讨论过的,在Android上二进制文件的大小和方法计数是非常重要的,所以这使我们能够尽可能高效。因为如果我们利用AAR(标准的通过Maven提供标准的库),我们可以使用分解依赖来满足我们的需求。
有效地发射是避免消耗过多电源功率的关键。Android有三种典型的能量状态:全功率、低功耗、空闲状态或称待机状态。
不过对于这个方面,有一个例子,说的是天真统计分析SDK,它们有时会不停地ping你的服务器,大约20秒一次,仅仅是为了告知服务器你的应用当前处于前台。这么做的后果就是会造成网络接收模块一直处于激活状态,并且把电源耗尽又没有传输什么实质上的数据内容。
为了缓解这一点,我们做了非常有限的一些同步工作,然后立马返回到开发者的application,同时在后台继续做一些毕竟耗时的运行工作,以保持您的应用程序下次能够快速启动。
有些事情需要用同步做的一个例子:如果你使用在使用我们的Crashlytics,你得立即使用它的crashhandler,因为一旦崩溃异常发生在异步初始化crashhandler完成之前,就会捕捉不到这个异常。
一个SDK的开发者没有像应用开发者那么多的选择权。你不能选择你的设备,APIlevel,或客户。你需要支持更大范围的设备,应用程序开发者并不局限在选择你的SDK,所以提供最大程度的灵活性是很重要的。
灵活性的一个体现是,可以让开发者选择不同的依赖管理器或者构件工具来引入或集成你的库。
我们提供一些主要的开发工具插件支持,包括Gradle,Maven,和Ant.我们还为通常的IDE提供GUI插件,以及一个帮助开发Mac和iOSapp的应用。尽管我们这是主要在讲Android方面开发SDK的内容,但我还是忍不住想告诉大家一个好消息就是我们最近完成了令人兴奋的CocoaPods支持,这将非常方便于iOS开发者使用我们的SDK.
灵活性的关键是了解您的SDK用户的需求。然后做出需要支持的最低系统版本的决定。我们很希望我们的SDK能够尽可能支持更多的系统设备。对于这一点,降低支持最低操作系统版本是很有必要尽力去做的。
但另一方面,兼容低版本也是要付出代价的。并没有什么直接的法则能够告诉我们如何才能在繁琐度和更好的兼容性上确定平衡。支持旧的操作系统版本,通常意味着不利用更加好用的新接口,同时还要面对一些旧版本存在的问题。初次之后,你还要花费更多精力去测试你的代码之正确性。
另一个重要的部分是能够检测出你正在运行的Android版本,所以你才能知道,哪些是可以调用的方法。通常,SDK支持更老版本的AndroidSDK。这对我们来说是非常重要的,因为我们想提供最大限度的设备支持。
protectedbooleancanCheckNetworkState(Contextcontext){Stringpermission=Manifest.permission.ACCESS_NETWORK_STATE;intresult=context.checkCallingOrSelfPermission(permission);return(result==PackageManager.PERMISSION_GRANTED);}我们只需要检查上下文对象中的权限,以判断是否已授予该权限。如果没有获得该权限也没办法了,我们不能为了权限而可能影响到安全异常。一个SDK可以在运行时检查是否被允许使用某权限,如果可用再调用相应的API.
很多时候我们都需要回退机制,但是在这种情况下,如果我们不知道WiFi或互联网的状态,我们必须假定它总是连接尝试,让超时发生。
有很多Android设备的存在,同时有很多各式各样的特性或者功能可能其它机器设备并没有,比如有的有KindleFire而有的没有,有的设备甚至还没有摄像头。
如果你正在构建一个基于相机的SDK,你会在清单文件中明使用相机。这就要求商店不要将应用程序展现给一个没有摄像头的设备。我鼓励你,把这个功能列为可选的,并允许你的库在运行时检测和修改它的行为。
在运行时检测硬件功能非常简单。您只需要查询packagemanager该特定功能是否存在即可。这样以后你的应用程序可以确定哪些功能可以使用了。以我们的相机库示例,您可能还允许用户浏览照片和上传照片,只是他们不能够在没有相机的设备上拍照就是了。
除了运行时检测,我们不能满足每个开发者的需求。我们的TwitterSDK提供了易于使用和流行的一些特性让用户去发现。我们预先就会准备好,帮你简化签名操作,我们使用持续的token,签署了所有的输出请求。
但是,如果你想使用一个我们SDK目前没有提供的TwitterAPI功能怎么办呢?如果是这样,你可以继承TwitterAPIclient,并且提供你的retroift接口,我们可以接受它并帮你进行签名。
正如我们所知道的,开发人员有很多工具,有很多的选择,他们需要在他们的代码中保持灵活性。这是很现实的。我们提供可扩展的接口,使用Fabric,使开发人员可以利用扩展他们想要的功能接口。
这里还有一个例子是日志记录。有许多不同的库来使用,我们提供了日志接口,以便它可以在Fabric开始前提供实现,然后我们将尊重并使用开发人员的日志记录需求。
但如果开发者选择不配置他个性的日志内容,我们提供一个健全的默认日志,便是标准的AndroidLogger.
另外,对于Java之外的方面,我们也支持让用户自定义他们想要的界面风格,比如开发者可以修改color的值,这些值将被应用于TwitterUISDK.
这里有一个需要注意的点,因为dex合并点时候不支持在name中使用空格符号,所以不要使用类似”backgroundcolor”这样的name.
制定灵活代码的一部分,就是允许开发人员选择监听一些事件发生并获得通知。在我们的builder,我们可以设置同步或异步任务结束的时候进行回调,也可以设置如果出错了可以得到回调通知。它允许开发者根据自己的状态和所获取的信息进行定制决策。
当你完成了你的SDK开发的时候,并不代表着你的SDK真正完成了,你还需要有很多开发之外的内容要做,要建立开发者交流的社区,还要有Apple文档、Java文档,以及README文档,另外还有很重要的就是要有使用你的库的示例教程。
添加注释给你的所有public的内容,以及顺带说明一些使用案例。
记得思考一些你想废弃的旧版本和方法。你们有多少人没有回头看旧的代码?除了去重写重构这些代码和方法,你可以需要对于旧的不要的代码进行废弃注解提醒。
尊敬版本更新日志,这是一个你和开发者通过你的SDK沟通的方式,你可以去知道他们需要什么,而他们从你们的SDK更新日志中获得他们是否需要你这个新版本的信息,如果值得,开发者们就会决定更新到目标的这一个版本。
虽然我们认为所有的这些观点都非常重要,但它们确实是伟大的库/SDK建设中的冰山一角。我们希望你喜欢我们作为内部开发人员在FabricSDK开发中的思考,那你觉得有用的建议和特性,或许就将出现在你现在手头或者将来要开发的SDK身上。谢谢!