转载自:http://www.infoq.com/cn/articles/wangyi-cartoon-swift-mixed-practice?utm_campaign=rightbar_v2&utm_source=infoq&utm_medium=articles_link&utm_content=link_text
网易漫画App在Swift上的实践。
主要内容:
-
使用Swift历程?
-
Swift混编实践
-
基于Swift的架构演变及建议
1. 使用Swift历程?
在公司量级产品中尝试新的不稳定技术其实是风险比较高的而矛盾的。一方面有来自产品的需求,需要保证产品的稳定性及快速迭代,另一方面有来自技术人员迫切想使用新技术的需求。因此,我们转到Swift也是逐步尝试地过程。
-
Swift 1.0 Beta版本发布到1.2版本,中间经历了各种Beta版本的迭代及改进,Swift其实并不稳定。 ----> 考虑到产品稳定性及迭代周期,我们对是否采用Swift还持保留态度。
-
Swift 1.2版本发布后,Swift在编译速度、类型安全及稳定性上进行了进一步的改进。同时Objective-C增加了Nullability等特性以增强对Swift交互的支持。 ----> 我们也决定先小范围尝试Swift混编,主要是比较独立的、不涉及网络请求的独立模块,比如一些自定义View,但此时我们对Swift的写法也是停留在Objective-C面向对象的Swift重写。
-
Swift 2.0版本发布,Swift增加了一些新特性,如面向协议编程范式、Guard、Defer、异常处理等等。Objective-C也增加轻量级泛型支持和__kindof关键字。 ----> 基于之前版本的小范围尝试,我们认为Swift和Objecitve-C混编也没有太大的技术门槛,至少从1.2版本到2.0的过渡,我们并没有花费过多的时间。Swift面向协议等新特性也足够吸引到了我们,因此,我们决定从2.0版本开始,所有新的业务代码,包括公用组件采用Swift开发。
-
Swift 2.0 ~ Swift 2.2 ,Swift进一步进行了各种改进并开源 ----> 此时,我们Swift基础组库也逐步丰富,比如NESwiftKits(HTTP),NEExtensionHelps,NEShortCutManager,NEUserDefaults,PublicUI(CustomLoading,NEAnimatedTabbar)...
-
Swift 3.0 ~ 不久的将来 ----> Swift层架构逐渐探索演变中... TODO:业务层架构逐步改进,底层库改用Swift封装、SPM替代Cocoapods...
2. 项目中Swift混编及建议
Apple其实为我们做了大部分混编所需要做的工作,苹果的官方文档对Swift & Objective-C混编也总结地很好,并可以很好地解决你混编的大部分问题。
Swift和Objective-C混编无非涉及到两个方向的调用:
-
Swift调用Objective-C;
-
Objective-C调用Swift,两个方向的调用其实是一致的。
如果你的项目based on Objective-C,那在混编初期,大部分的情况都是Swift调用Objective-C的代码。通过Bridging Header文件即可import需要提供给Swift的Objective-C头文件,Swift即可调用对应的Objective-C代码。 这里只是概括下网易漫画App混编实践中,语言层面上,值得关注的8条实践总结。
2.1 Optional
上图为Optional在标准库中的定义,Optional其实为可解包的遵循NilLiteralConvertible协议的枚举类型。在 Swift 中,nil用来表示值缺失,任何可选类型都可以被设置为nil。而Objective-C中nil表示空指针,完全不同的意义。
建议:
-
如果你无法更改你原有Objective-C代码添加Nullability属性,比如用到第三方Framework。此时,如果你不确定返回的object是否为空,则需要加判断条件if object != nil {},然后进行强制解包。
-
避免对可选类型强解包,除非你确定该可选值不为nil,或者希望该值为nil值触发运行时错误(Debug时)。
-
Objective-C属性或者方法如果不加Nullability属性的话,则默认为隐式可选类型。要慎用隐式可选类型,如果确定你的数据一直有值,则可用隐式解析可选。
-
建议所有Objective-C代码所有可空的属性前加上nullable标识,并用if let可选绑定进行Optional类型的判断。 在涉及网络请求中,使用Optional需特别注意。如果Model的定义都是用Objective-C定义,最好用Nullability属性表明data是nullable or __nonnull。事实上,很多涉及网络请求的业务不太确定某一值是否为空。比如用户昵称String,如果你在业务开发时认为服务端不可能返回一个空的昵称(可能你旧版本的bug导致很多昵称为空的用户昵称),然后强制解包,此时就会触发运行时错误。
2.2 Closure
在混编时Objective-C block可以映射到Swift closure类型。如:大部分网络请求都是block回调,如下图为网易漫画项目中Swift调用Objective-C的网络请求接口。
(点击放大图像)
(点击放大图像)
关于Closure有以下几点需要注意:
-
Closure会自动持有被截获变量的引用,这样可以在内部直接修改变量。Swift同时做了一些性能优化,由于持有变量的引用的开销比直接持有变量开销大,Swift会判断你是否在Closure中或者外面是否修改了该变量,如果没有修改则Closure会直接持有该变量。
-
Swift循环引用weak & unowned:当一个引用在其生命周期中可能为nil,就把这个引用定义为weak。相反,则定义成unowned引用。
-
非class类型的协议不能被标识为weak, 当一个协议需求所定义的行为能够确保:遵循这个协议的类型是引用类型而非值类型的时候,使用class类型协议。
-
尽量使用尾随闭包,代码更简洁。
2.3 AnyObject
Swift中AnyObject定义为Protocol,所有的Swift类类型都遵循AnyObject协议,AnyObject的类型需要在运行时才能确定。Objective-C的id定义为指向对象的指针,Objective-C id可以无缝地转换到Swift AnyObject类型。
-
慎用as!:建议使用as?进行AnyObject类型的转换,if let xxx = aAnyObject as? aObjectType {xxx},除非你能够确定AnyObject的类型才使用as!进行强制类型转换,或者先使用is进行类型判断。
-
Objective-C llvm 7.0 编译器开始支持轻量型泛型,集合类型NSArray、NSDictionary等转换为Swift时对应的Object都默认转换为AnyObject类型。建议你的Objective-C代码对应的集合类型都指定泛型类型,如NSArray<BookCityUpdateItem *>。
2.4 抛弃OOP? 拥抱POP?或者FP?
Swift是多编程范式的语言,支持面向协议、面向对象、函数式编程、泛型编程,同时Swift更推荐值类型而非引用类型,值类型相对引用类型是线程安全的,并且Swift对值类型的拷贝进行了足够的优化。Swift对枚举、结构体、函数给予了更大的能力。 关于语言编程范式的问题其实已经超过了语言层面关于混编的范畴。但我们在开发一些组件或者业务时,使用Swift & Objective-C混编必然会导致我们在老和新的编程范式上进行抉择。
-
如果你的模块不涉及混编,那你可以很大胆地去使用 Swift的面向协议&函数式范式,只不过在Objective-C调用你的Swift模块时,你需要在接口层考虑对Objective-C的兼容性。
-
如果当你现在的模块基于Objective-C,当用Swift去扩展现有Objective-C模块时,你需要在一定程度上做出取舍。继续沿用之前Objective-C(面向对象)架构或者用Swift进行重构。
我们目前项目中大部分还是基于OOP的代码。无法抛弃也完全没必要抛弃OOP的原因:
-
Cocoa的核心是基于OOP,比如我们要去自定义UIKit相关组件时必须使用继承。
-
我们必须继承一个现有Objective-C代码的基类来去获取基类定义的方法或者属性。事实上,我们工程中还存在不少Super Class,如各种Objective-C工厂类。然而当我们用Swift去扩展时,不破坏原有框架地同时很简单地方法就是采用继承(多态)。
-
我们项目Model是基于Objective-C Class定义。
当然,Swift POP的引入也给我们在架构上带来了更多的空间。
-
协议能够被类、结构体和枚举遵守,而基类和继承只能限制在类上使用。
-
协议扩展为值类型和类提供了一种定义默认行为的能力。比如通过协议扩展很容易将UITableViewDelegate、UITableViewDataSource分离。
-
一个类型能够实现多于一个协议,从而实现多继承所拥有的能力。
-
值类型是线程安全的。
我们目前混编中:
-
在业务层,我们目前也尽量不去更改原有Objective-C的代码来过渡到Swift,因为原有代码已经足够稳定了。
-
在框架层,我们会逐渐过渡到Swift,除了运用Objective-C一些黑魔法而无法实现的功能,当然这种过渡也是需要时间周期去逐渐演化的。
-
我们目前项目中还没有运用太多函数式编程范式。
2.5 Enum
Enum在Swift赋予了更大的能力,支持原始值、关联值、定义函数、扩展、遵循协议等特性。
-
使用typedef NS_ENUM(NSUInteger, xxxx) {},不要使用C-Style枚举定义。
-
如果你的Objective-C代码用到Swift定义的枚举,相对于Objective-C的新特性将无法使用。
2.6 Objective-C调用Swift
Swift 的类或协议必须用@objc属性来标记,以便在 Objective-C 中可访问。这个属性告诉编译器Swift代码可以从Objective-C 代码中访问。如果你的Swift 类是 Objective-C 类的子类,编译器会自动为你添加@objc。
Runtime支持 Swift语言本身对Runtime并不支持,需要在属性或者方法前添加dynamic修饰符才能获取动态型,继承自NSObject的类其继承的父类的方法也具有动态型,子类的属性和方法也需要加dynamic才能获取动态性。
2.7 With C
Swift对C的交互性也提供了很好地支持,如原始类型CBool、CUnsignedLongLong,指针类型CConstVoidPointer、COpaquePointer,类型化指针CMutableVoidPointer<Type>,我们项目中需要Swift与C直接交互的代码很少,在这里就不展开讲了。
但是目前,Swift对C++的交互不是很好的支持(原因苹果认为C++是个很复杂的语言,与C++的交互性需要考虑很多东西,是件很长远的事情,至少在3.0及3.0版本之前Swift不支持),所以如果有些库需要与C++混编可以考虑用Objective-C作为桥接。
2.8 其它更多
如宏定义、基本类型和Foundation类型转换、Swift方法重命名和重定义(NS_SWIFT_NAME、NS_REFINED_FOR_SWIFT),这里就不一一展开讲了。
但需要注意的是,Swift所特有的特性而Objective-C没有是无法在Objective-C调用的, 解决办法是通过Objective-C所支持的特性去重新封装外部接口。
3. 基于Swift的架构演变及建议
3.1 现有架构
我们基于Swift的架构也不断在演变和探索中。下图这是我们目前大概的一个混编架构图。
(点击放大图像)
其中红色部分和红色箭头是我们目前需要考虑Objective-C和Swift兼容的地方。
-
Service层统一对业务层的Swift & Objective-C接口兼容。其中包括:网络请求NEKits,数据缓存(图片、文件、数据库等),Hybird,统计,Crash组件,Hotpatch,Autolayout组件、动画、公用UI组件等等,又分为外部组件和公司内部组件(公司内部组件考虑到稳定性全部采用Objective-C进行编写)。
-
Model的定义,我们一直沿用Objective-C定义,用Mantle进行Runtime解析。这部分主要是业务层调用,兼容Swift并没有花费我们太多时间。由于Swift对Runtime并不是很好地支持,我们目前没有打算用Swift对Model进行重写。
-
业务层部分Swift业务模块和老的Objective-C业务模块相互调用,此时也需要考虑接口的兼容性。
3.2 现有问题
在Swift混编前期,很多情况下是业务逻辑开发过程中,才发现原有的Objective-C代码(特别是C类型的接口)无法很好地用在Swift中,在一定程度上影响了我们业务开发的效率。这种不兼容和接口不友好等问题基本对外封装兼容性接口就能解决。
我们现有的混编架构还在不断尝试和演变中,我们的Swift模块也逐渐在业务&基础组件化。
-
Swift编码规范。
-
OC的旧接口,如果涉及Swift代码调用,需要考虑旧OC代码的Swift接口兼容性。
-
Swift的新接口,如果涉及旧OC代码的调用,需要考虑OC的接口兼容性。
-
新架构考虑,我们也在探索中...
-
POP + MVVM? 或者 POP + MVP? 或者 …… NO OOP? NO Inherit?Only Protocol? RxSwift? Swift Hotpatch? Runtime?
-
未来:Swift 3 compatible ?
下图,当然不是我们期待的架构?
(点击放大图像)
3.3 混编建议
-
先小范围、不重要的业务模块尝试Swift。
-
Swift组件化。
-
Objective-C考虑与Swift的交互性,如范型、Nullability。如果可能,对你现有的Objective-C代码也提高与Swift的交互。
-
没必要去更改现有的Objective-C代码,成本很大。除非现有的Objective-C代码需要重构,而Swift在设计层面很好地解决了你的重构问题。
-
没必要去追求你工程中Swift的代码占有量,用Objective-C能够解决但是Swift解决不了的问题,那就使用Objective-C吧,虽然这种情况比较少,如Objective-C Runtime。
-
考虑适合你们产品的Swift架构和最佳实践。
未来,Swift的开放性、跨平台、多编程范式、核心库的逐渐丰富,也给予了我们更大的发挥和想象空间。 谢谢大家,今天的内容分享结束,希望对大家有帮助,也期待后续与大家共同交流探讨~ ~
QA环节
Q:Swift对你们开发效率的提升有多大作用,有这方面的统计么?
A:Swift并没有提升我们的效率,现在的开发效率和OC差不多,同时开发效率也和个人开发者对Swift的熟练程度有关。
Q:Swift组件化问题:如果某个pod内部带有Swift代码,则会导致整个app最低版本支持iOS 8,无法支持iOS 7,请问这个是如何解决的?
A:我们目前也在考虑组件话的问题,我们app最低支持iOS 7,不过由于产品属性(偏年轻),我们不久会只支持iOS 8,到时会考虑用Carthage。目前所有Swift代码都放在工程内的。
Q:Swift3之后还需要在应用中带Swift运行时吗?ABI是否固定了?
A:Swift 3.0会保持 ABI的稳定性,意味着,即便源代码语言发生了变化,用以后版本的 Swift 开发的应用程序和编译库能在二进制层次上和 Swift 3.0 版本的应用程序和编译库相互调用。
Q:Swift的高效率表现在什么地方?
A:非运行时对消息事件的处理,值类型的使用,对高阶函数地支持等等。
Q:请问你们混合业务组件的接口是怎么设计的?
A:混合业务组件的接口如果涉及到OC的调用则需要考虑Swift的兼容,基本方法就是语言重新封装为OC的接口。
Q:Swift的优势在哪里,与OC相比有什么不同?
A:1.性能。 2.多编程范式。 3.跨平台并开源。
Q:Swift为了与OC兼容,在某些特性上做了一些妥协,比如不完善的runtime支持,您是否认为这是Swift的一个缺点?
A:Swift其实做了很多妥协,比如Cocoa的妥协,我认为这并不是Swift缺点,兼容需要过渡时期。
Q:在混编的情况下,单元测试怎么做呢?可以用Swift写单元测试,来测OC代码吗?
A:如果Swift的代码需要OC调用的话,我们会用OC去写单元测试。但是也并没有统一规定。
|
请发表评论