前言
不知不觉加上实习和正式入职36氪已经半年左右,从第一次去实习以及接触较为正式的软件开发,对很多事情的懵懵懂懂,到琦哥让我自己去全新设计我们基于Swift重写的下一个大版本的底部交互,虽然物理时间比较短,却感觉已经走过了很长的一段路。在这之间,总觉得有很多东西想总结一下,特别是这次设计底部交互的逻辑以及在切向Swift过程中对这个语言的进一步的体会。
这是第一次除了公司内部分享外写技术文章,除了因为觉得这些感悟有分享的价值之外,可能更希望和别人碰撞出一些思维的火花,让我们在开发的过程中继续改进这种架构。虽然毫无疑问我认为自己设计的这套架构是很优雅的,但是确实也没有看到别的程序采用这么多的基础数据类型.对很多问题的解决思路都是根据优雅做判断, 对性能的判断也是模模糊糊的基于在国外 Mooc 平台上的几门介绍计算机底层的公开课上收获的一些东西做出的.
实话说我本来想先写总体的架构(也是第一个完成的事情), 但是因为我只是设计了对我们客户端业务区的对外接口封装, 内部实现大部分都是选用公开的第三方库. 所以, 对里面的实现并不能做到百分之百的把握(实话说, 我觉得一个人不自己写一个那种库,只是看看代码,真的很难说自己了解那个东西,计算机太容易给一个人造成我懂了的假象, 但是实际上懂的人也许没有那么几个).综合以上, 打算自己在后面的时间利用晚上和周末, 自己实现那些东西的底层(这里所说的底层,是说自己利用苹果自己的框架提供的接口)之后, 再逐步的分模块的介绍每一个部分的架构,总体的架构要放到最后面去写了.
说明
这篇文章在讲一些设计决策的时候, 会涉及到一些底层的考虑, 我不能保证这些讲解都是百分百正确的, 因为它们都是建立在我以前上的课程和实验的基础上, 有很多都已经模糊了, 但是又不想查资料写一些无关痛痒的简介(请原谅我的懒惰)!虽然我现在在学一些反编译和黑客技术, 力求能够从底层的层面彻底搞清楚这些东西,但精力有限, 也只是刚刚开始.总之,大家带着批判的思维来阅读这篇文章,欢迎指出里面的错误.
程序示例和 Cupid 库的代码在下面, 程序示例包含我们这一部分底部构建的全部代码!
Cupid
正文
业务区的搭建
我做东西一般不会一开始就去选择第三方库, 因为觉得看别人的东西特别是做东西的时候看别人的东西容易给自己造成误导, 做出一些并不是很适合自己的客户端的设计决策.所以我一般都会根据自己的业务区先设计具体的上层接口, 然后再去选用第三方库, 看哪一个能够比较好的实现自己的接口.
下面是我们客户端的上层接口的示例图, 因为我们主要采用的是通过 OAuth 获取认证信息, 然后让服务器拉用户数据的方式; 所以可能和你们的客户端的方式不是很一样.
业务区具体代码可以从 Github 的代码实例中查看. 但是可以从这个图示中可以看到设计的大体思维方式, 就是更多的使用 Swift 中枚举和结构体这两种基础数据类型, 然后使用闭包做回调, 减少使用类这种引用类型; 结构体扮演的角色主要是信息的流通通道和较为复杂单一的数据封装, 枚举则主要作为轻量型的数据传递; 虽然类仍然是业务区的中流支柱,毕竟无论是 MVC, MMVC, VIPER等各种架构 程序的方式, 都无法绕开苹果提供的最上层的 Framework, 而那些仍然大部分是基于 NSObject 的.
为什么我更喜欢枚举和结构体,下面你会看到,越往下其实用到的类会越少.一个很重要的原因可能就是个人偏好, 有时候我也在想是不是用 Tuple 会是更好的解决方案, 但是因为对计算机底层技术的了解有限, 无法做出这种决策. 真的期待哪位大牛能写一本叫做 Swift, The Good Parts 的书, 高级语言的灵活性对编程的泛滥确实并不是那么好, 现在越来越少的能够看到让自己能够眼睛一亮的代码了.还有一个原因,就是使用基础数据类型不用担心内存泄露的问题, 而且一个 run loop 完成后, 整个过程中调用栈会自动的被清空, 实际上,在写 Cupid 的过程中, 碰到几次因为一行代码的原因, service Provider 被提早释放, 造成 completion handler 没有被调用. 记得前一段时间因为学 Hand off 和 state restoration 技术, 用 Swift 翻译了苹果的 PhotoOff demo, 然后将它的下面的数据交互更改为 Struct + NSCache 的实现, 最后稳定状态下内存占用量大概只有原来的一半, 内存泄露数量从60多处降到了10处不到(当然, 我觉得和 Swift 的实现更加安全也有一定的关系), CPU 的使用效率也更加的高效.最后, 越往下面的东西, 可能涉及到的数据操作会更少, 因为真正的数据在服务器或者数据库的底层才会操作, 而数据库的底层自己写可能真的没有成熟的第三方实现更加高效.苹果自己的 News 都是使用第三方的 FMDB. 这些地方更多的是担当一个信息流通的通道, 并没有必要使用类这种重量型的数据类型(虽然, 现在编译器的优化已经让类和结构体的性能影响可以忽略), Swift 的语言特性, 让结构体+枚举+闭包的实现更加优雅!
业务区和 Cupid 库的交互
业务区和 Cupid 的交互可能就比较简单了, 因为 Cupid 存在的目的就是为业务区服务的, 至少之所以选在这个库最主要的原因是它可以更好地为业务服务, 当然, 开发一个库的目的是为了更适应普遍大众(开发者)的需要.实际上, 我一开始的实现是使用 MonkeyKing, 但是因为公司业务需要我们没有办法去掉支付宝的分享功能, 在仔细审查 MonkeyKing 的代码后,觉得并不适合这种业务需求才决定重写一个自己的库, 十分感谢 MonkeyKing 的开发者 @nixzhu 和@Limon-catch, 实际上, 很多 ServiceProvider 的实现方法都是 Copy 的 MonkeyKing 的代码, 当然, 为了适应自己的逻辑做了很多的修改. 在下面的一节会详细的介绍 Cupid 库和其它分享库的核心区别.
下面是业务区和 Cupid 的交互图:
这一块全部实现可以在 Github 的代码实例中查看, 就不多做介绍.
Cupid 内部库的构造
其实 MonkeyKing 写的很好, 但是可能有人问, 为什么有一个很好的东西, 还要自己再花时间去写一个一样的东西, 答案当然是, 他们不一样! Cupid 从内到外都采用了完全不同的实现方式, 它的一些优点可以在主页上查看, 我觉得 Demo 也很优美, 从界面到代码.
内存结构
下面一张图是 Cupid 的交互和其它第三方分享库和核心的区别:
需要注意的是这个上面关于调用栈的思维是推测的, Swift 中如何处理结构体和枚举对我来说还是一个黑箱子; 我觉得假如我是编译器的作者就会采用这种思维方式.通过这种设计思维, 可以很有效的降低程序中单例类数量, 更好的利用内存空间, 特别是对于我这种一年分享不了几次的用户.
数据类型
之所以选择重写 MonkeyKing 还有一个很重要的原因就是我们的应用要支持剪切板和支付宝的分享, 我觉得基于 enum 封装大批量数据的思维虽然调用的简洁度可以更高, 但是在可扩展性上面并没有做到更强. 所以 Cupid 选择了基于 Protocol 的方式来实现特定的 Service Provider, 如果你们看源代码的话, 可能会发现实现一个特定的 Service provider是十分的 easy 的, 而且你不需要更改任何其它的地方, 可以做到绝对的兼容.
本来我在 Service Provider 基于基类实现和基于接口实现做了一些取舍, 最后之所以选择基于接口实现除了因为我不喜欢各种 override, 还有一个原因是因为每一个 Service Provider都有各种特定的需求, 是很独特的, 基本上没有任何共同的特点.
下面是 Cupid 处理一个分享或者认证的调用路径, 这样就不用像很多的第三方分享一下, 要 register 账户, 而且维持许多的单例:
结束语
就像一开始的说明,这里面的很多底层交互都是自己的猜测或者说自己觉得会那么实现底层,和实验的结果, 请用批判的角度理解这里面的内容, 在自己的工程中灵活的使用这些东西.如果各位读者有了解里面细节的, 欢迎补充!