“踩坑”经验分享:Swift 语言落地实践
作者 | 路涛、艳红
导读
Swift 是一种适用于 iOS/macOS 应用开发、服务器端的编程语言。自 2014 年苹果发布 Swift 语言以来,Swift5 实现了 ABI 稳定性、Module 稳定性和 Library Evolution,与 Objective-C(下文简称“OC”)相比,Swift 在开发效率、安全、编译优化、运行性能和内存管理方面具有显著优势。(官方博客:https://www.swift.org/about/ )
百度 App 已在工程和环境上支持 Swift 开发,百度搜索大前端团队负责搜索服务的稳定落地,我们积极探索 Swift 的应用,希望能大幅提升开发效率和灵活性、提升端用户的搜索体验。然而,在实施过程中可能会遇到各种问题,例如代码陈旧且不支持 Swift,人员对 Swift 掌握不够熟练、意识不足,协作方对 Swift 的支持不足等。
对于其他语言来说,Swift 相对年轻,我们在实践过程中整理一些常见问题及其解决方法,希望能帮助读者更顺利地使用 Swift 进行编程,提高研发效率。
全文 6947 字,预计阅读时间 18 分钟。
01 Swift 适用场景
在决定是否引入 Swift 前,我们需要判断场景是否适合。通常情况下,可以用 OC 的场景均适合使用 Swift,但也有一些不太适合直接替换的场景,需要慎重,比如:
1、涉及 OC 动态性,频繁在 runtime 时操作属性和方法;
2、核心基础功能,出现问题影响面较大的逻辑;
3、调用 C++(目前 Swift 不能直接调用 C++);
4、继承不支持 Swift 组件的类。
此外,对使用 OC 比较久远的工程,使用 Swift 前也应注意:
1、能在工程环境和单独模块上支持 Swift;
2、模块较多的工程,可以内外 OC 和 Swift 混编;
3、为了避免 Swift Waring 带来的潜在问题,可以把 SWIFT_TREAT_WARNINGS_AS_ERRORS 设置为 YES,这样警告会作为错误,辅助程序员更好的规范代码;
4、模块 Module 化后,要注意维护 umbrella header 中的公开头文件。
注:本文中的“组件”均指代工程中的“Target”。
02 Swift 的基本用法
2.1 Swift 的字符串为什么这么难用?
如:字符串不能通过索引取字符
原因:Swift 认为字符串是由一个个字形群集(grapheme clusters) 组成的,字形群集的大小不固定所以不能用整数去索引 (字形群集其实就是 Swift 中的 Character(字符)类)。
解决方案:如要通过下标取字符可以为 String 添加扩展在下标 subscript 实现通过传入 Int 索引,在 subscript 转为 String.index 获取对应字符的方式。
2.2 try try? try! 的区别
当你进行文件操作时,可能会遇到需要使用 try、try?和 try!的情况。它们在异常处理方面有所不同。
1、使用 try 时,如果出现异常,程序会进入异常处理流程,你可以在 catch 语句块中处理这个异常。
2、使用 try?时,如果发生异常,它不会进入异常处理流程,而是返回一个可选值类型。也就是说,如果出现异常,它将返回 nil。
3、使用 try!时,它不允许异常继续传播。一旦出现异常,程序会立即停止执行。
因此,在文件操作中,你可以根据需要选择合适的异常处理方式。在百度 App 中一般推荐使用 try?。
2.3 public 和 open 的区别
在 Swift 语言中,public 和 open 都是用于在模块中声明需要对外界暴露的函数的关键字,但它们在继承和公开程度上有所不同。
1、public 关键字修饰的类在模块外部无法被继承。 这意味着,如果其他模块试图继承这个类,编译器会报错。这样的限制可以保护类的完整性,但也可能限制了其在其他模块中的可重用性。
2、open 关键字则允许任意继承。 如果一个类被 open 关键字修饰,那么其他模块中的类可以自由地继承这个类,不受任何限制。这样的公开程度使得 open 关键字修饰的类在模块间的重用性和扩展性更加灵活。
从公开程度上来说,public 的限制比 open 更严格,所以可以说 public < open,即 public 的公开程度比 open 要低。
2.4 解析 JSON 情况
在 Swift 中解析 JSON 的情况,如果自行将 JSON 转换为字典,需要涉及到类型判断、转换等操作,代码比较复杂。这时可以使用第三方库 SwiftyJSON、ObjectMapper 或者系统库 JSONEncoder 来简化操作,提高开发效率。
2.5 UIView 子类必须添加 init?(coder decoder: NSCoder)的原因
1、这是 NSCoding protocol 定义的,遵守了 NSCoding protocol 的所有类必须继承。只是有的情况会隐式继承,而有的情况下需要显示实现。
2、当我们在子类定义了指定初始化器(包括自定义和重写父类指定初始化器),那么必须显示实现 required init?(coder aDecoder: NSCoder),而其他情况下则会隐式继承,我们可以不用理会。
3、当我们使用 storyboard 实现界面的时候,程序会调用这个初始化器。
4、注意要去掉 fatalError,fatalError 的意思是无条件停止执行并打印。
2.6 Swift 类和子类的初始化
Swift 的类和子类初始化涉及到两个关键阶段。首先,确保所有的存储属性被赋予初始值,然后,在实例准备使用之前,可以自定义存储属性的值。为了确保这两个阶段成功,实施了四步安全检查,详细如下:
1、在完成本类所有存储属性赋值之后,指定构造器才能向上代理到父类的构造器。
2、在为继承的属性设置新值之前,指定构造器必须向上代理调用父类构造器。
3、便利构造器必须先调用其他构造器,再为任意属性(包括所有同类中定义的)赋新值。
4、在第一阶段构造完成之前,构造器不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。
总之,类初始化必须完成的一个任务就是让所有的存储属性都有初始值(optional 除外)。如果父类有指定初始化,子类必须也有指定初始化,并且必须调用父类的其中一个指定初始化(如果是必须初始化,就是重载),并遵循两段式初始化的规则。一个便利初始化必须调用同一类中的初始化方法(可以是另一个便利初始化,也可以是指定初始化),但最终一定会调用到一个指定初始化。便利初始化不遵循两段式初始化的规则,不能被子类调用或者重载。
03 OC 与 Swift 的互相调用及跳转
3.1 组件内 Swift 文件调用公开 OC 头文件
将公开 OC 头文件(如:xyz.h)添加到组件(如:ABC)umbrella header 中(如:#import);
Swift 文件中直接调用公开 OC 头文件内容。
3.2 组件内 Swift 文件调用非公开(私有)的 OC 文件
组件应该尽可能少的公开暴露头文件,但 Swift 和 OC 混编不可避免使用 OC 非公开头文件,因此我们可以采取以下措施:将 Framework 中将私有头文件声明为一个私有 module(modulemap 内声明),由组件内的 Swift 源码 import 该私有 module 即可。
1、创建 Private.modulemap 文件,以 NewModule 做为组件名为例,可以命名为 NewModule.private.modulemap,内容为下,module 后面加_Private
罗列头文件的形式
使用根头文件的形式,添加头文件 NewModule_Private.h
2、在组件 build settings 中配置 MODULEMAP_PRIVATE_FILE 路径,MODULEMAP_PRIVATE_FILE='NewModule.private.modulemap';百度 App 中在 NewModule.boxspec 中如下代码设置路径;
3、将 NewModule.private.modulemap 添加到工程目录;百度 App 中在 NewModule.boxspec 中如下代码设置路径;
4、将 xxxxx.h 设置为 Private header,百度 App 中在 NewModule.boxspec 中如下代码设置 xxxxx.h 到 Private header
5、调用方式
注意:
添加的 Private 头文件可能存在传递头文件的情况,即 import 其他头文件,也需要将传递的头文件添加到 NewModule_Private 中,同时 import 需要使用尖括号;
Private Header 也会暴露在 framework 中,所以可以约定外部组件使用 Public Header,而避免使用 Private Header,因为随着业务发展和 Swift&OC 混编,Private Header 是不稳定的。
3.3 组件内 OC 文件如何调用 Swift 文件?
Swift 类需要继承 NSObject,方法前面加上 @objc 标识,并且是 public 或者 open 的;
引入方式 #import"
3.4 OC 中的向前声明,被 Swift 文件引用该组件会报错
如 error: cannot find protocol definition for 'xxxProtocol'
原因:此报错在 OC 中是代码警告,百度 App 中默认情况 Swift 中 SWIFT_TREAT_WARNINGS_AS_ERRORS 设置为 YES,导致 OC 中的 Warning 视为 Error;
解决方案:三选一
1、暂时设置 SWIFT_TREAT_WARNINGS_AS_ERRORS 为 NO
2、import xxxProtocol 不要向前声明
3、使用 pragma 忽略警告
3.5 Swift 怎么用 OC 定义的宏?
在 Swift 中,能直接使用定义为常量的宏,不能使用带有方法调用的宏,也不能使用静态常量。
3.6 Swift 与 OC 泛型的混编
在我分们基础框架中,有一个使用了 OC 泛型的类,如:
这个泛型的使用导致无法使用 Swift 来继承和开发 BBAXYZ 的子类。然而,这个基础框架是业务的核心部分,因此,我们需要在未来支持 Swift 的开发。
经过仔细观察和分析,我们发现泛型主要被用于指定 page 属性的类型。因此,我们可以考虑去掉泛型,改为提供一个返回适当类型的方法。这样,我们就可以在 Swift 中顺利地继承和使用这个基础框架。修改后的代码如下:
然后,我们可以创建一个 OC 类来实现这个基础框架,并让所有的子类继承这个 OC 类并实现 page 方法,以返回适当类型的对象。这样,我们就可以在 Swift 中顺利地继承和使用这个基础框架。
例如:
需要注意的是,虽然这样的修改增加了轻量级的中间 OC 类,但它仍然实现了 Swift 与 OC 的混编,并允许我们在 Swift 中开发新的子类。这种方式既保证了代码的兼容性,又使得我们可以继续利用 OC 的优点。
使用方式
3.7 Swift 调用 OC 接口,OC 的 nullability 标注使用时的注意事项
问题场景:
1、OC 接口定义为 nonnull,swfit 调用时正常是当做不可选类型使用,这时如果 OC 接口不规范返回 nil,则出现运行时崩溃。
2、OC 接口未定义 nonnull 或 nullable,在这种情况下,编译器会将 OC 的指针类型当成是隐式解析可选类型(例如 String!)导入到 Swift 中。swift 调用时,OC 接口如果返回 nil,将会因为隐式解析一个为 nil 的可选值导致运行时崩溃。
解决方式:
1、Swift 调用 OC 接口时,如果 OC 的接口声明为 nonnull 或未指定 nullability 时,只有明确 OC 接口不为空的情况下才可调用。
2、在 OC 环境下,将 nil 赋值给 nonnull 指针也没有关系,编译器只会产生警告。这就需要程序员按规范编写 OC 代码,正确使用 nullability 标注,并增加运行时判空的断言,以支持向后兼容。
04 其他常见问题
4.1 Xcode 编译只提示编译错误,提示信息非常少
原因:使用 Swift 语言开发的组件,依赖了不支持 Module 化的组件,导致组件都能编译成功,但整个工程却编译失败了;
解决方案:二选一
1、检查并保障所有依赖的组件都已经 Module 化了,如配置 build settings;
2、在组件中新增 Swift 文件(空文件也行)。
4.2 由于组件开启了 Library Evolution 导致的编译报错
错误显示:@objc' instance method in extension of subclass of 'xxxxx' requires iOS 13.0.0
这是由于组件开启了 Library Evolution 导致,开关 BUILD_LIBRARY_FOR_DISTRIBUTION 控制的。
一个库开启了 Library Evolution,在依赖链下游的库中将:
1、对它的类实现 @objc 子类。
2、对它的类使用 extension 实现 @objc 的方法(这在 UIKit 的 protocol 中经常会遇到)。
3、对它的类实现子类,并添加 @objc 方法,且方法中使用父类的类型作为参数。
这些功能是实现的局限。估计是需要在 Swift 运行时有一些对应的更改,所以只在 swift 5.1 (iOS 13)运行时里才可以运行。
除此之外,还会有一些编译时没有报错,但运行时 crash 或结果不正确的情况。
百度 App 中默认开启 Library Evolution,一个组件关闭 Library Evolution 会导致二进制存在不兼容的情况,暂时无解决方案。
4.3 暴露的 Private 头文件如果使用双引号 import,会报警告,需要修改为尖括号
如果使用 import <xxxx.h>,Project 下其他 Target 就引用不到了,如百度 App 中 Debug 模块引用此 Private 头文件时,会报错 not found with <angled> include, use "quotes" instead。
原因:这是由于其他 Target 对主模块引用时默认是从当前项目下引用头文件,而尖括号方式从系统库或用户库中引用;
解决方案:其他 Target 将 Private Header 配置到其 HEADER_SEARCH_PATHS,使用双引号和尖括号均可。
05 总结
以上是我们在 Swift 开发过程中所遇到的一些常见问题及其相应的解决方案。然而,随着我们不断深入 Swift 开发这片浩渺的海洋,更多独特的问题将会逐渐浮现。我们会持续将这些新问题以及其对应的解决方案整理并发布出来,为广大的开发者们提供有价值的参考。欢迎大家留言探讨。
百度搜索大前端团队,持续招聘 iOS/Android/Web 前端 研发工程师。
简历欢迎投递至 joinefe@baidu.com
——END——
推荐阅读
版权声明: 本文为 InfoQ 作者【百度Geek说】的原创文章。
原文链接:【http://xie.infoq.cn/article/cc6c4aa8a3adecbd442844938】。文章转载请联系作者。
评论