“踩坑”经验分享: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
- 罗列头文件的形式
framework module NewModule_Private { header "xxxxx.h" }
- 使用根头文件的形式,添加头文件NewModule_Private.h
framework module NewModule_Private { umbrella header "NewModule_Private.h" export * module * { export * } }
2、在组件build settings中配置MODULEMAP_PRIVATE_FILE路径,MODULEMAP_PRIVATE_FILE='NewModule.private.modulemap';百度App中在NewModule.boxspec中如下代码设置路径;
s.xcconfig = { 'MODULEMAP_PRIVATE_FILE' => '${BOX_ROOT}/NewModule.private.modulemap' }
3、将NewModule.private.modulemap添加到工程目录;百度App中在NewModule.boxspec中如下代码设置路径;
s.refer_files = [ "NewModule.private.modulemap", ]
4、将xxxxx.h设置为Private header,百度App中在NewModule.boxspec中如下代码设置xxxxx.h到Private header
s.private_headers = [ "Sources/xxxxx.h" ]
5、调用方式
import NewModule_Private let objectX = xxxxx() print(objectX)
注意:
-
添加的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中,能直接使用定义为常量的宏,不能使用带有方法调用的宏,也不能使用静态常量。
下面这种定义为常量的宏可以使用 #define APP_LANGUAGE_EN @"en" #define kNavigationBarHeight 44.0 下面带有方法调用的宏不可以使用 #define kScreenHeight [[UIScreen mainScreen] bounds].size.height #define kScreenWidth [[UIScreen mainScreen] bounds].size.width 下面带有静态常量swift不能使用,可以改成宏 static NSString *const StopTabRefreshNotifyNameHtml = @"TabRefreshNotifyNameHtml";
3.6 Swift与OC泛型的混编
- 在我分们基础框架中,有一个使用了OC泛型的类,如:
@interface BBAXYZ<T> : NSObject <BBAXYZEventProtocol> @property (nonatomic, weak) T page; @end
这个泛型的使用导致无法使用Swift来继承和开发BBAXYZ的子类。然而,这个基础框架是业务的核心部分,因此,我们需要在未来支持Swift的开发。
- 经过仔细观察和分析,我们发现泛型主要被用于指定page属性的类型。因此,我们可以考虑去掉泛型,改为提供一个返回适当类型的方法。这样,我们就可以在Swift中顺利地继承和使用这个基础框架。修改后的代码如下:
@interface BBAXYZ : NSObject <BBAXYZEventProtocol> - (id<BBAXYZEventProtocol>)page; @end
然后,我们可以创建一个OC类来实现这个基础框架,并让所有的子类继承这个OC类并实现 page 方法,以返回适当类型的对象。这样,我们就可以在Swift中顺利地继承和使用这个基础框架。
例如:
@interface BBAABC : BBAXYZ - (UIViewController<BBAXYZEventProtocol> *)page; @end
需要注意的是,虽然这样的修改增加了轻量级的中间OC类,但它仍然实现了Swift与OC的混编,并允许我们在Swift中开发新的子类。这种方式既保证了代码的兼容性,又使得我们可以继续利用OC的优点。
- 使用方式
class BBAEFG: BBAABC { }
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前端 研发工程师。
——END——
推荐阅读

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
MySQL 核心模块揭秘 |《发刊词》
1. 为什么要写专栏? 我还在做业务系统研发的时候,有一段时间,系统不稳定,慢 SQL 很多。我们团队花了很长时间持续优化 SQL。 我们有一个表格,从慢查询日志里整理出了很多慢 SQL。其中一些 SQL,按照我们的理解,根本不应该出现在表格里,但是它们却经常出现。 我对这些 SQL 印象深刻,它们是: update xxx set xxx where id = xxx commit truncate table xxx 以我们当时对 MySQL 有限的了解,这些 SQL 执行起来都很快,不应该出现在慢查询日志里。 我们不了解这些 SQL 执行过程中都干了些什么,不理解它们是怎么执行的,想要优化也就无处下手了。 随着逐渐深入研究 MySQL 源码,我已经能解释这些 SQL 为什么会出现在慢查询日志里了。 对 SQL 执行过程不了解,这是我曾经的痛点,相信也是很多业务系统研发和 DBA 的痛点。 我投入了很多时间研究 MySQL 源码,正在逐步解决这些痛点。 现在,我把这些内容写出来,分享给需要的各位读者,希望也能帮助大家解决工作过程中遇到的痛点。 2. 专栏包含哪些内容? 我正在研究 ...
- 下一篇
Reformer 模型 - 突破语言建模的极限
提示: 这篇文章链接和公式特别多,🤗 宝子们可以戳 阅读原文 获得最佳阅读体验哟! Reformer 如何在不到 8GB 的内存上训练 50 万个词元 Kitaev、Kaiser 等人于 20202 年引入的 Reformer 模型 是迄今为止长序列建模领域内存效率最高的 transformer 模型之一。 最近,人们对长序列建模的兴趣激增,仅今年一年,就涌现出了大量的工作,如 Beltagy 等人的工作 (2020) 、Roy 等人的工作 (2020) 、Tay 等人的工作 以及 Wang 等人的工作 等等。长序列建模背后的动机是,NLP 中的许多任务 (例如 摘要、问答 ) 要求模型处理更长的序列,这些序列长度超出了 BERT 等模型的处理能力。在需要模型处理长输入序列的任务中,长序列模型无需对输入序列进行裁剪以避免内存溢出,因此已被证明优于标准的 BERT 类模型 ( 见 Beltagy 等人 2020 年的工作)。 Reformer 能够一次处理多达 50 万个词元,从而突破了长序列建模的极限 (具体可参见本 笔记本)。相形之下,传统的 bert-base-uncased 模...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境