“踩坑”经历共享:Swift言语落地实践

作者 | 路涛、艳红

导读

Swift 是一种适用于iOS/macOS应用开发、服务器端的编程言语。自2014年苹果发布 Swift 言语以来,Swift5 完结了 ABI 稳定性、Module 稳定性和Library Evolution,与Objective-C(下文简称“OC”)相比,Swift 在开发功率、安全、编译优化、运转性能和内存办理方面具有明显优势。(官方博客: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 include, use “quotes” instead。

  • 原因:这是因为其他Target对主模块引证时默认是从当前项目下引证头文件,而尖括号办法从体系库或用户库中引证;

  • 解决方案:其他Target将Private Header 装备到其 HEADER_SEARCH_PATHS,运用双引号和尖括号均可。

05 总结

以上是咱们在Swift开发进程中所遇到的一些常见问题及其相应的解决方案。但是,跟着咱们不断深入Swift开发这片浩渺的海洋,更多共同的问题将会逐渐显现。咱们会继续将这些新问题以及其对应的解决方案收拾并发布出来,为广阔的开发者们供给有价值的参考。欢迎大家留言讨论。

百度查找大前端团队,继续招聘 iOS/Android/Web前端 研制工程师。

简历欢迎投递至joinefe@baidu.com

——END——

引荐阅览

移动端防截屏录屏技能在百度账户体系实践

AI Native工程化:百度App AI互动技能实践

揭开事情循环的神秘面纱

百度查找展示服务重构:前进与优化

百度APP iOS端包体积50M优化实践(七)编译器优化