面试总结六部曲
- 1、iOS基础系列(一):底层分析
- 2、iOS基础系列(二):零碎知识点
- 3、iOS基础系列(三):各种优化
- 4、iOS基础系列(四):扩展问题
- 5、iOS基础系列(五):Swift总结
- 6、iOS基础系列(六):Flutter总结
1、Defer 关键字的作用
defer语句:用来定义以任何方式(抛错误,return等)离开代码前必须执行的代码,defer语句将延迟至当前作用域之前执行
func test(){
defer {
print("打印defer")
}
do {
let _ = try divide(10, 0)
} catch let error {
print(error)
}
}
test()
复制代码
2、Swift 的写入时复制 集合类型有写入时复制功能
Swift 中 值类型 有“写时复制(Copy-On-Write)”的特性
我们都知道 Swift 有值类型和引用类型,而值类型在被赋值或被传递给函数时是会被拷贝的。在 Swift 中,所有的基本类型,包括整数、浮点数、字符串、数组和字典等都是值类型,并且都以结构体的形式实现。那么,我们在写代码时,这些值类型每次赋值传递都是会重新在内存里拷贝一份吗?
答案是否定的,想象一下,假如有个包含上千个元素的数组,然后你把它 copy 一份给另一个变量,那么 Swift 就要拷贝所有的元素,即使这两个变量的数组内容完全一样,这对它性能来说是多么糟糕。
而这个优化方式就是 Copy-On-Write(写时复制),即只有当这个值需要改变时才进行复制行为。
3、在oc中怎么调用swift文件和代码
- 1、创建桥接文件
- 2、在使用的地方直接引用”工程名-Swift.h”,不需要引用Swift文件
4、swift的构造器 便利构造器和init构造器 的东西 可以子类继承吗
-
1、指定初始化器必须从他的直系父类调用指定初始化器
-
2、便捷初始化器必须从相同的类里调用另一个初始化器
-
3、便捷初始化器最终必须调用一个指定初始化器
-
1、当重写父类的指定初始化器,必须加上override(即使子类的实现是便捷初始化器)
-
2、如果子类写了一个匹配父类便捷初始化器的初始化器,不用加上override
- 因为父类的便捷初始化器永远不会通过子类调用,因此,严格来说,子类无法重写父类的便捷初始化器
自动继承
- 1、如果子类没有自定义任何指定初始化器,它会自动继承父类所有的指定初始化器
- 2、如果子类提供了父类所有指定初始化器的实现(要么通过方式1继承,要么重写),子类自动继承了父类的所有便捷初始化器
- 3、就算子类添加了更多的便捷初始化器,这些规则仍然适用
- 4、子类以便捷初始化器的形式重写父类的指定初始化器,也可以作为满足规则2的一部分
required
- 如果用required修饰指定初始化器,表明其所有子类必须实现该初始化器
- 如果子类重写了required初始化器,子类也必须要加上required,不用加override
5、swift 的codelb协议 是干嘛用的
Swift 由于类型安全的特性,对于像 JSON 这类弱类型的数据处理一直是一个比较头疼的问题,虽然市面上许多优秀的第三方库在这方面做了不少努力,但是依然存在着很多难以克服的缺陷,所以 Codable 协议的推出,一来打破了这样的僵局,二来也给我们解决类似问题提供了新的思路。
通过查看定义可以看到,Codable其实是一个组合协议,由Decodable和Encodable两个协议组成:
Encodable和Decodable分别定义了encode(to:)和init(from:)两个协议函数,分别用来实现数据模型的归档和外部数据的解析和实例化。最常用的场景就是接口 JSON 数据解析和模型创建
6、Swift高阶函数的使用 去除数组中的可选值用哪个高阶函数
高阶函数的定义:
在Wikipedia中,是这么定义高阶函数(higher-order function)的,如果一个函数:
- 接收一个或多个函数当作参数
- 把一个函数当作返回值
至少满足以上条件中的一个的函数,那么这个函数就被称作高阶函数。
使用高阶函数进行函数式编程的优势:
- 简化代码
- 使逻辑更加清晰
- 当数据比较大的时候,高阶函数会比传统实现更快,因为它可以并行执行(如运行在多核上)
高阶函数在Swift语言中有大量的使用场景,map、flatMap、compactMap、filter、reduce。
7、mutating
Swift 的protocol不仅可以被class类型实现,也适用于struct 和 enum。
Swift 的mutating关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量,所以如果你没在协议方法里写 mutating 的话,别人如果用 struct 或者 enum 来实现这个协议的话,就不能在方法里改变自己的变量了
在使用 class 来实现带有mutating的方法的协议时,具体实现的前面是不需要加mutating修饰的,因为 class 可以随意更改自己的成员变量。所以说在协议里用mutating修饰方法,对于 class 的实现是完全透明,可以当作不存在的
protocol Vehicle {
var numberOfWheels:Int{get}
mutating func changeNumberOfWheels()
}
struct MyCar:Vehicle {
var numberOfWheels: Int = 4
mutating func changeNumberOfWheels() {
numberOfWheels = 4
}
}
class Cars: Vehicle {
var numberOfWheels: Int = 0
func changeNumberOfWheels() {
numberOfWheels = 2
}
}
复制代码
8、流程控制
fallthrough
使用fallthrough可以实现贯穿效果
let num = 1
switch num {
case 1:
print("num is 1")
fallthrough
case 2:
print("num is 2")
default:
break
}
num is 1
num is 2
复制代码
where
where其实就是多加的控制条件
let list = [1,-10,5,-7]
for num in list where num > 0 {
print(num)
}
1
5
复制代码
9、输入输出参数
可变形式参数只能在函数的内部做改变。如果你想函数能够修改一个形式参数的值,而且你想这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。
在形式参数定义开始的时候在前边添加一个 inout关键字可以定义一个输入输出形式参数。输入输出形式参数有一个能输入给函数的值,函数能对其进行修改,还能输出到函数外边替换原来的值。
你只能把变量作为输入输出形式参数的实际参数。你不能用常量或者字面量作为实际参数,因为常量和字面量不能修改。在将变量作为实际参数传递给输入输出形式参数的时候,直接在它前边添加一个和符号 ( &) 来明确可以被函数修改。
var b = 10
func test(a:inout Int) {
a = 20
}
test(a: &b)
print(b) //20
复制代码
可以用inout
定义一个输入输出参数:可以在函数内部修改外部实参的值
- 可变参数不能标记为
inout
inout
参数不能有默认值inout
参数只能传入可以被多次赋值的inout
参数的本质就是地址传递
10、typealias别名
按照swift标准库的定义Void
就是一个空元组
public typealias Void = ()
复制代码
我们知道swift中没有byte、short、Long
类型,如果我们想要这样的类型,就可以用typealias
实现
typealias Byte = Int8
typealias Short = Int16
typealias Long = Int64
复制代码
11、属性
- 1、存储属性
- 类似于成员变量这个概念
- 存储在实例的内存中
- 结构体,类可以定义存储属性
- 枚举不可以定义存储属性
- 2、计算属性
- 本质就是一个函数
- 不占用实例的内存
- 枚举,结构体,类都可以定义计算属性
12、闭包
闭包表达式语法有如下的一般形式:
{ (parameters) -> (return type) in
statements
}
复制代码
尾随闭包
- 如果你需要将一个很长的闭包表达式作为函数最后一个实际参数传递给函数,使用尾随闭包将增强函数的可读性。
- 尾随闭包是一个被书写在函数形式参数的括号外面(后面)的闭包表达式
func exec(v1:Int,v2:Int,fn:(Int,Int) -> Int) {
print(fn(v1,v2))
}
//调用
exec(v1: 10, v2: 20){
$0 + $1
}
复制代码
如果闭包表达式作为函数的唯一实际参数传入,而你又使用了尾随闭包的语法,那你就不需要在函数名后边写圆括号了
func exec(fn:(Int,Int) -> Int) {
print(fn(1,2))
}
exec(fn: {$0 + $1})
exec(){$0 + $1}
exec{$0 + $1}
复制代码
逃逸闭包(@escaping )与非逃逸闭包(@noescaping) )
逃逸闭包(**@escaping **** ) **
当闭包作为一个实际参数传递给一个函数的时候,我们就说这个闭包逃逸了,因为它是在函数返回之后调用的。当你声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写 @escaping
来明确闭包是允许逃逸的。
闭包可以逃逸的一种方法是被储存在定义于函数外的变量里
。比如说,很多函数接收闭包实际参数来作为启动异步任务的回调。函数在启动任务后返回,但是闭包要直到任务完成——闭包需要逃逸,以便于稍后调用
例如:当网络请求结束后调用的闭包。发起请求后过了一段时间后这个闭包才执行,并不一定是在函数作用域内执行的
override func viewDidLoad() {
super.viewDidLoad()
getData { (data) in
print("闭包返回结果:\(data)")
}
}
func getData(closure:@escaping (Any) -> Void) {
print("函数开始执行--\(Thread.current)")
DispatchQueue.global().async {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2, execute: {
print("执行了闭包---\(Thread.current)")
closure("345")
})
}
print("函数执行结束---\(Thread.current)")
}
复制代码
从结果可以看出,逃逸闭包的生命周期是长于函数的。
逃逸闭包的生命周期:
- 1、闭包作为参数传递给函数;
- 2、退出函数;
- 3、闭包被调用,闭包生命周期结束
即逃逸闭包的生命周期长于函数,函数退出的时候,逃逸闭包的引用仍被其他对象持有,不会在函数结束时释放。
非逃逸闭包(**@noescaping) ** )** **
一个接受闭包作为参数的函数, 闭包是在这个函数结束前内被调用。
override func viewDidLoad() {
super.viewDidLoad()
handleData { (data) in
print("闭包返回结果:\(data)")
}
}
func handleData(closure:(Any) -> Void) {
print("函数开始执行--\(Thread.current)")
print("执行了闭包---\(Thread.current)")
closure("123")
print("函数执行结束---\(Thread.current)")
}
复制代码
为什么要分逃逸闭包和非逃逸闭包
为了管理内存,闭包会强引用它捕获的所有对象,比如你在闭包中访问了当前控制器的属性、函数,编译器会要求你在闭包中显示 self 的引用,这样闭包会持有当前对象,容易导致循环引用。
非逃逸闭包不会产生循环引用,它会在函数作用域内释放,编译器可以保证在函数结束时闭包会释放它捕获的所有对象;使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用;此外非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上。
自动闭包
自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数
,并且当它被调用时,它会返回内部打包的表达式的值
这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。
func getFirstPositive1(_ v1:Int, _ v2:Int) -> Int {
return v1 > 0 ? v1 : v2
}
getFirstPositive1(1, 2)
func getFirstPositive2(_ v1:Int, _ v2:() -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
getFirstPositive2(1, 2) //这个报错
getFirstPositive2(1, {2})
func getFirstPositive3(_ v1:Int, _ v2:@autoclosure () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
getFirstPositive3(1, 2)
复制代码
- @autoclosure会自动的将
2
封装为{2} - @autoclosure只支持
() -> T
的格式参数 - 其中
??
就是一个@autoclosure
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
复制代码
13、discardableResult
class Calculate {
func sum(a:Int,b:Int) -> Int {
a + b
}
}
Calculate().sum(a: 1, b: 2)
复制代码
我们在调用Calculate().sum(a: 1, b: 2)
会有一个函数调用后,返回值未使用的警告,如果我们想要去掉警告,可以使用discardableResult
关键字
class Calculate {
@discardableResult
func sum(a:Int,b:Int) -> Int {
a + b
}
}
Calculate().sum(a: 1, b: 2)
复制代码
14、下标subscript
使用subscript
可以给任意类型(枚举、结构体、类)增加下标功能
subscript
的语法类似于实例方法,计算属性,本质就是方法(函数)
class Point {
var x = 0.0, y = 0.0
subscript(index:Int) -> Double{
set{
if index == 0{
x = newValue
}else if index == 1{
y = newValue
}
}
get{
if index == 0{
return x
}else if index == 1{
return y
}
return 0
}
}
}
var p = Point()
p[0] = 11.1
p[1] = 22.2
print(p.x) //11.1
print(p.y) //22.2
print(p[0]) //11.1
print(p[1]) //22.2
复制代码
subscript
中定义的返回值类型决定了
- 1、get方法中返回值的类型
- 2、set方法中newValue的类型
15、运算符
运算符重载
在类、结构体、枚举可以为现有的运算符提供自定义的实现,这个操作叫做:运算法重载
struct Point {
var x:Int, y:Int
static func + (_ p1:Point, _ p2:Point) -> Point{
Point(x: p1.x + p2.x, y: p1.y + p2.y)
}
static func - (_ p1:Point, _ p2:Point) -> Point{
Point(x: p1.x - p2.x, y: p1.y - p2.y)
}
static prefix func - (_ p:Point) -> Point{
Point(x: -p.x, y: -p.y)
}
}
复制代码
Equatable
想要知道两个实例是否等价,一般走法就是遵守Equatable协议,重载===运算符
struct Person :Equatable{
var age: Int
static func == (_ p1:Person, _ p2:Person) ->Bool{
p1.age == p2.age
}
}
复制代码
比较运算法Comparable
比较两个实例的大小,一般遵守Comparable协议,重载相应的运算符
struct Student:Comparable {
var age:Int
init(age:Int) {
self.age = age
}
static func < (_ s1:Student, _ s2:Student) -> Bool{
s1.age < s2.age
}
}
复制代码
自定义运算符
可以自定义新的运算符:在全局作用域使用operator
进行声明
prefix operator
前缀运算postfix operator
后缀运算infix operator
中缀运算
struct Point {
var x:Int, y:Int
static prefix func ++ (_ p: inout Point) -> Point{
p = Point(x: p.x + p.x, y: p.y + p.y)
return p
}
}
复制代码
16、Sequence
Sequence
是一系列相同类型的值的集合,并且提供对这些值的迭代能力。
迭代一个Sequence
最常见的方式就是 for-in 循环
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
for animal in animals {
print(animal)
}
复制代码
Sequence 协议的定义
protocol Sequence {
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
}
复制代码
Sequence
协议只有一个必须实现的方法 makeIterator()
makeIterator()
需要返回一个 Iterator
,它是一个 IteratorProtocol
类型。
也就是说只要提供一个Iterator
就能实现一个 Sequence
,那么 Iterator
又是什么呢?
Iterator
Iterator 在 Swift 3.1 标准库中即为 IteratorProtocol,它用来为 Sequence 提供迭代能力。对于 Sequence,我们可以用 for-in 来迭代其中的元素,其实 for-in 的背后是 IteratorProtocol 在起作用
IteratorProtocol 的定义如下:
public protocol IteratorProtocol {
associatedtype Element
public mutating func next() -> Self.Element?
}
复制代码
对于这个for...in
循环
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
for animal in animals {
print(animal)
}
复制代码
实际上编译器会把以上代码转换成下面的代码
var animalIterator = animals.makeIterator()
while let animal = animalIterator.next() {
print(animal)
}
复制代码
- 1、获取到 animals 数组的 Iterator
- 2、在一个 while 循环中,通过 Iterator 不断获取下一个元素,并对元素进行操作
- 3、当 next() 返回 nil 时,退出循环
17、元组(Tuple)
元组是swift编程语言中唯一的一种复合类型,他可以将指定有限个数的任何类型一次整理为一个对象,元组中的每一种类型都可以是任何的结构体、枚举或类类型,甚至也可以是一个元组以及空元组。
比如交换输入,普通程序员亘古以来可能都是这么写的
func swapMel1<T>(a:inout T, b:inout T) {
let temp = a
a = b
b = temp
}
复制代码
但是要是使用多元组的话,我们可以不使用额外空间就完成交换,一下子就达到了文艺程序员的写法
func swapMel2<T>(a:inout T, b:inout T) {
(a,b) = (b,a)
}
复制代码
18、属性访问控制
Swift代码中五种访问级别与定义实体的源文件和源文件所属的模块相关。open
的访问级别最高,private
的访问级别最低,internal
是默认的访问级别。
open和public
:使实体在其所定义模块的源文件中,或通过导入其所定义模块在另一个模块的源文件中使用。而public
修饰的类或类成员只能在当前模块被继承或被子类重写;而open
则可以在另一个模块中被继承或被子类重写。在定义框架的公共接口时,通常使用open
和public
;internal
:使实体可以在其所定义模块的任何源文件中使用,但不能在其他模块的的源文件中使用。在定义应用程序或框架的内部结构时,通常使用internal
。fileprivate
:使实体只能在其定义的源文件中使用。当这些详细信息在整个文件中使用时,使用fileprivate
隐藏特定功能块的实现细节。private
:使实体在其所声明的类,以及同一文件中该声明的扩展中使用。当这些详细信息仅在单个声明中使用时,使用private
来隐藏特定功能的实现细节。
open
可以在不同模块中访问,public
只能在当前模块中访问,internal
系统默认的访问控制权限,fileprivate
可以被继承但不能被子类修改或重写,private
不能被继承
19、UIApplicationMain
新建一个 Swift 的 iOS app 项目后,我们会发现所有文件中都没有一个像 Objective-C 时那样的 main
文件,也不存在 main 函数
。唯一和main
有关系的是在默认的 AppDelegate 类的声明上方有一个 @UIApplicationMain
的标签。
其实 Swift 的 app 也是需要 main 函数的,只不过默认情况下是 @UIApplicationMain
帮助我们自动生成了而已。
20、可选协议和协议扩展
Objective-C 中的 protocol
里存在 @optional
关键字,被这个关键字修饰的方法并非必须要被实现。我们可以通过协议定义一系列方法,然后由实现协议的类选择性地实现其中几个方法。最好的例子我想应该是 UITableViewDataSource 和 UITableViewDelegate。前者中有两个必要方法
-tableView:numberOfRowsInSection:
-tableView:cellForRowAtIndexPath:
复制代码
原生的 Swift protocol 里没有可选项,所有定义的方法都是必须实现的
protocol MyProtocol {
func mustProtocolMethod() //必须实现方法
func mustProtocolMethod1() //必须实现方法
}
class MyClass: MyProtocol {
func mustProtocolMethod() {
print("MyClass-->必须实现方法:mustProtocolMethod")
}
func mustProtocolMethod1() {
print("MyClass-->必须实现方法:mustProtocolMethod1")
}
}
复制代码
如果我们想要像 Objective-C 里那样定义可选的协议方法,就需要将协议本身和可选方法都定义为Objective-C 的,也即在 protocol
定义之前以及协议方法之前加上 @objc
。另外和 Objective-C 中的 @optional
不同,我们使用没有 @
符号的关键字 optional
来定义可选方法
@objc protocol MyProtocol1 {
@objc optional func optionalProtocolMethod() //可选方法
func mustProtocolMethod1() //必须实现方法
}
class MyClass1: MyProtocol1 {
func mustProtocolMethod1() {
print("MyClass1-->必须实现方法:mustProtocolMethod1")
}
}
let cls1 = MyClass1()
cls1.mustProtocolMethod1()
复制代码
一个不可避免的限制是,使用 @objc 修饰的 protocol 就只能被 class 实现了
,也就是说,对于 struct 和 enum 类型,我们是无法令它们所实现的协议中含有可选方法或者属性的
在 Swift 2.0 中,我们有了另一种选择,那就是使用 protocol extension。我们可以在声明一个 protocol 之后再用 extension 的方式给出部分方法默认的实现。这样这些方法在实际的类中就是可选实现的了
protocol MyProtocol2 {
func optionalProtocolMethod1() //可选方法
func optionalProtocolMethod2() //可选方法
func mustProtocolMethod1() //必须实现方法
}
extension MyProtocol2{
func optionalProtocolMethod1(){}
func optionalProtocolMethod2(){}
}
复制代码
21、String 还是 NSString
简单来说:没有特别需要,尽可能的还是使用String
,有以下三个原因
- 1、虽然
String
和NSString
有着良好的互相转换的特性,但是现在 Cocoa 所有的 API 都接受和返回String
类型。我们没有必要也不必给自己凭空添加麻烦去把框架中返回的字符串做一遍转换 - 2、因为在 Swift 中
String是struct
,相比起 NSObject 的 NSString 类来说,更切合字符串的 “不变” 这一特性。通过配合常量赋值 (let) ,这种不变性在多线程编程时就非常重要了,它从原理上将程序员从内存访问和操作顺序的担忧中解放出来。另外,在不触及 NSString 特有操作和动态特性的时候,使用 String 的方法,在性能上也会有所提升 - 3、因为 String 实现了 Collection 这样的协议,因此有些 Swift 的语法特性只有 String 才能使用,而 NSString 是没有的。一个典型就是
for...in
的枚举
22、class 和 struct 的区别
在 Swift 中类和结构体有很多共同之处
- 定义属性用来存储值;
- 定义方法用于提供功能;
- 定义下标脚本用来允许使用下标语法访问值;
- 定义初始化器用于初始化状态;
- 可以被扩展来默认所没有的功能;
- 遵循协议来针对特定类型提供标准功能。
类有而结构体没有的额外功能
- 继承允许一个类继承另一个类的特征;
- 类型转换允许你在运行检查和解释一个类实例的类型;
- 反初始化器允许一个类实例释放任何其所被分配的资源;
- 引用计数允许不止一个对类实例的引用。
22、如何声明一个只能被类 conform 的 protocol
声明协议的时候, 加一个 class 即可如
protocol SomeClassProtocl: class {
func someFunction()
}
复制代码
23、throws 和 rethrows 的用法与作用
throws 用在函数上, 表示这个函数会抛出错误.有两种情况会抛出错误, 一种是直接使用 throw 抛出, 另一种是调用其他抛出异常的函数时, 直接使用 try xx 没有处理异常.如
enum DivideError: Error {
case EqualZeroError;
}
func divide(_ a: Double, _ b: Double) throws -> Double {
guard b != Double(0) else {
throw DivideError.EqualZeroError
}
return a / b
}
func split(pieces: Int) throws -> Double {
return try divide(1, Double(pieces))
}
复制代码
rethrows 与 throws 类似, 不过只适用于参数中有函数, 且函数会抛出异常的情况, rethrows 可以用 throws 替换, 反过来不行如
func processNumber(a: Double, b: Double, function: (Double, Double) throws -> Double) rethrows -> Double {
return try function(a, b)
}
复制代码
24、try? 和 try!是什么意思
这两个都用于处理可抛出异常的函数, 使用这两个关键字可以不用写do catch.区别在于, try? 在用于处理可抛出异常函数时, 如果函数抛出异常, 则返回 nil, 否则返回函数返回值的可选值, 如:
print(try? divide(2, 1))
// Optional(2.0)
print(try? divide(2, 0))
// nil
复制代码
而 try! 则在函数抛出异常的时候崩溃, 否则则返会函数返回值, 相当于(try? xxx)!, 如:
print(try! divide(2, 1))
// 2.0
print(try! divide(2, 0))
// 崩溃
复制代码
25、关联类型(Associated Type)
我们在 Swift 协议中可以定义属性和方法,并要求满足这个协议的类型实现它们:
protocol Food { }
protocol Animal {
func eat(_ food: Food)
}
struct Meat: Food { }
struct Grass: Food { }
复制代码
struct Tiger: Animal {
func eat(_ food: Food) {
}
}
复制代码
因为老虎并不吃素,所以在 Tiger 的 eat 中,我们很可能需要进行一些转换工作才能使用 meat
associatedtype
声明中可以使用冒号来指定类型满足某个协议
protocol Animal {
associatedtype F: Food
func eat(_ food: F)
}
struct Tiger: Animal {
func eat(_ food: Meat) {
print("eat \(meat)")
}
}
struct Sheep: Animal {
func eat(_ food: Grass) {
print("eat \(food)")
}
}
复制代码
不过在添加associatedtype
后,Animal
协议就不能被当作独立的类型使用了。试想我们有一个函数来判断某个动物是否危险:
func isDangerous(animal: Animal) -> Bool {
if animal is Tiger {
return true
} else {
return false
}
}
复制代码
会报错
Protocol ‘Animal’ can only be used as a generic constraint because it has Self or associated type requirements
这是因为 Swift 需要在编译时确定所有类型,这里因为 Animal
包含了一个不确定的类型,所以随着 Animal
本身类型的变化,其中的F
将无法确定 (试想一下如果在这个函数内部调用 eat
的情形,你将无法指定 eat 参数的类型)。在一个协议加入了像是 associatedtype
或者 Self
的约束后,它将只能被用为泛型约束,而不能作为独立类型的占位使用,也失去了动态派发的特性。也就是说,这种情况下,我们需要将函数改写为泛型
func isDangerous<T: Animal>(animal: T) -> Bool {
if animal is Tiger {
return true
} else {
return false
}
}
isDangerous(animal: Tiger()) // true
复制代码
26、dynamic 的作用
由于 swift 是一个静态语言, 所以没有 Objective-C 中的消息发送这些动态机制, dynamic 的作用就是让 swift 代码也能有 Objective-C 中的动态机制, 常用的地方就是 KVO 了, 如果要监控一个属性, 则必须要标记为 dynamic
参考文章www.jianshu.com/p/ae26100b9…
27、什么时候使用 @objc
@objc 用途是为了在 Objective-C 和 Swift 混编的时候, 能够正常调用 Swift 代码. 可以用于修饰类, 协议, 方法, 属性.常用的地方是在定义 delegate 协议中, 会将协议中的部分方法声明为可选方法, 需要用到@objc
@objc protocol OptionalProtocol {
@objc optional func optionalFunc()
func normalFunc()
}
class OptionProtocolClass: OptionalProtocol {
func normalFunc() {
}
}
let someOptionalDelegate: OptionalProtocol = OptionProtocolClass()
someOptionalDelegate.optionalFunc?()
复制代码
28、静态方法时关键字 static 和 class 有什么区别
static 定义的方法不可以被子类继承, class 则可以
class AnotherClass {
static func staticMethod(){}
class func classMethod(){}
}
class ChildOfAnotherClass: AnotherClass {
override class func classMethod(){}
//override static func staticMethod(){}// error
}
复制代码
29、Swift 中的 as,as?,as! 的区别
在我的认知中,如果 as
能成功的话,说明 as?
和 as!
也一定会成功。在大多数情况下这是对的,但凡是总有例外。看到下面的代码,不知道你能不能看出问题。
print(4 as Double) // print 4.0
print(4 as! Double) // crash
复制代码
第一行代码成功执行,但第二行代码会有编译器⚠️,而且会造成崩溃,这是为什么呢?造成这种情况的原因就是 as 和 as? 与 as! 的执行机制不同。as 是在编译期执行的,而 as? 和 as! 是在运行时执行的。
在编译时执行的 as
因为 as 是在编译期执行的,而在编译期, 4 还只是个字面量,并没有给它赋值为 Int 类型,所以它会转型成功。4 as Double
与下面的代码是等价的。
let x = 4 as Double
let x: Double = 4
复制代码
在运行时实行的 as? 与 as!
当程序已经在运行时,上述代码中的 4 已经赋值为 Int 类型,所以当你执行第二句代码时,肯定会 crash,因为 Int 强转为 Double是不允许的。
as? 和 as! 操作结果是等价的,唯一的不同就是如果转型失败,as! 会 crash,而as? 不会。
30、Swift 全局 struct 实例什么时候初始化的
我们写下这么一段代码,在print出打断点
struct MyStruct {
init() {
print("------")
}
}
let data = MyStruct()
复制代码
我们不使用data,运行项目,发现断点并没有走到初始化里面
我们在UIViewController里面打印data
从上图可知,
- 懒初始化:全局struct变量是在首次使用时初始化。
- 线程安全:有swift_once保证。
31、Swift 派发机制
编译型语言有三种基础的函数派发方式:
- **直接派发(Direct Dispatch):**直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间,然而, 对于编程来说直接调用也是最大的局限, 而且因为缺乏动态性所以没办法支持继承
- **函数表派发(Table Dispatch):**函数表派发是编译型语言实现动态行为最常见的实现方式. 函数表使用了一个数组来存储类声明的每一个函数的指针. 大部分语言把这个称为 “virtual table”(虚函数表), Swift 里称为 “witness table”. 每一个类都会维护一个函数表, 里面记录着类所有的函数
- **消息机制派发(Message Dispatch):**消息机制是调用函数最动态的方式
在Swift中可以定义两种类:一种是从NSObject或者派生类派生的类,一类是从系统Swift基类SwiftObject派生的类。对于后者来说如果在定义类时没有指定基类则默认会从基类SwiftObject派生。SwiftObject是一个隐藏的基类,不会在源代码中体现
Swift类对象的内存布局和OC类对象的内存布局相似。二者对象的最开始部分都有一个isa成员变量指向类的描述信息。Swift类的描述信息结构继承自OC类的描述信息,但是并没有完全使用里面定义的属性,对于方法的调用则主要是使用其中扩展了一个所谓的虚函数表的区域
Swift语言中类定义的方法可以分为三种:OC类的派生类并且重写了基类的方法、extension中定义的方法、类中定义的常规方法
OC类的派生类并且重写了基类的方法
- 1、如果在Swift中的使用了OC类,并且还重写了基类的方法,对于这些类的重写的方法定义信息还是会保存在类的Class结构体中,而在调用上还是采用OC语言的Runtime机制来实现,即通过objc_msgSend来调用
- 2、如果在OC派生类中定义了一个新的方法,采用直接派发
extension中定义的方法
如果是在Swift类的extension中定义的方法(重写OC基类的方法除外)。那么针对这个方法的调用总是会在编译时就决定,也就是说在调用这类对象方法时,方法调用指令中的函数地址将会以硬编码的形式存在。在extension中定义的方法无法在运行时做任何的替换和改变!而且方法函数的符号信息都不会保存到类的描述信息中去。这也就解释了在Swift中派生类无法重写一个基类中extension定义的方法的原因了。因为extension中的方法调用是硬编码完成,无法支持多态
类中定义的常规方法
如果是在Swift中定义的常规方法,方法的调用机制和C++中的虚函数的调用机制是非常相似的。Swift为每个类都建立了一个被称之为虚表的数组结构,这个数组会保存着类中所有定义的常规成员方法函数的地址。每个Swift类对象实例的内存布局中的第一个数据成员和OC对象相似,保存有一个类似isa的数据成员。isa中保存着Swift类的描述信息。对于Swift类的类描述结构苹果并未公开(也许有我并不知道),类的虚函数表保存在类描述结构的第0x50个字节的偏移处,每个虚表条目中保存着一个常规方法的函数地址指针。每一个对象方法调用的源代码在编译时就会转化为从虚表中取对应偏移位置的函数地址来实现间接的函数调用
Swift类中成员变量的访问
系统会对每个成员变量生成get/set两个函数来实现成员变量的访问。系统不会再为类的成员变量生成变量偏移信息表,因此对于成员变量的访问就是直接在编译链接时确定成员变量在对象的偏移位置,这个偏移位置是硬编码来确定的。
Swift类会为每个定义的成员变量都生成一对get/set方法并保存到虚函数表中。所有对对象成员变量的方法的代码都会转化为通过虚函数表来执行get/set相对应的方法。 下
结构体中的方法
在Swift结构体中也可以定义方法,因为结构体的内存结构中并没有地方保存结构体的信息(不存在isa数据成员),因此结构体中的方法是不支持多态的,同时结构体中的所有方法调用都是在编译时硬编码来实现的。这也解释了为什么结构体不支持派生,以及结构体中的方法不支持override关键字的原因
类的方法以及全局函数
Swift类中定义的类方法和全局函数一样,因为不存在对象作为参数,因此在调用此类函数时也不会存在将对象保存到x20寄存器中这么一说。
类方法和全局函数就像C语言的普通函数一样被实现和定义,所有对类方法和全局函数的调用都是在编译链接时刻硬编码为函数地址调用来处理的。
OC调用Swift类中的方法
Swift语言调用OC的代码
对于Swift语言调用OC的代码的处理方法是系统会为工程建立一个桥声明头文件:项目工程名-Bridging-Header.h,所有Swift需要调用的OC语言方法都需要在这个头文件中声明
OC语言调用Swift语言
于OC语言调用Swift语言来说,则有一定的限制。因为Swift和OC的函数调用ABI规则不相同,OC语言只能创建Swift中从NSObject类中派生类对象,而方法调用则只能调用原NSObject类以及派生类中的所有方法以及被声明为@objc关键字的Swift对象方法。
如果需要在OC语言中调用Swift语言定义的类和方法,则需要在OC语言文件中添加:#import “项目名-Swift.h”。当某个Swift方法被声明为@objc关键字时,在编译时刻会生成两个函数,一个是本体函数供Swift内部调用,另外一个是跳板函数(trampoline)是供OC语言进行调用的。这个跳板函数信息会记录在OC类的运行时类结构中,跳板函数的实现会对参数的传递规则进行转换
关键字派发
Swift 有一些修饰符可以指定派发方式.
final
final允许类里面的函数使用直接派发. 这个修饰符会让函数失去动态性. 任何函数都可以使用这个修饰符, 就算是 extension 里本来就是直接派发的函数. 这也会让 Objective-C 的运行时获取不到这个函数, 不会生成相应的 selector.
dynamic
dynamic可以让类里面的函数使用消息机制派发. 使用dynamic, 必须导入Foundation框架, 里面包括了NSObject和 Objective-C 的运行时.dynamic可以让声明在 extension 里面的函数能够被 override.dynamic可以用在所有NSObject的子类和 Swift 的原声类.
@objc & @nonobjc
@objc和@nonobjc显式地声明了一个函数是否能被 Objective-C 的运行时捕获到. 使用@objc的典型例子就是给 selector 一个命名空间@objc(abc_methodName), 让这个函数可以被 Objective-C 的运行时调用.@nonobjc会改变派发的方式, 可以用来禁止消息机制派发这个函数, 不让这个函数注册到 Objective-C 的运行时里. 我不确定这跟final有什么区别, 因为从使用场景来说也几乎一样. 我个人来说更喜欢final, 因为意图更加明显.
final @objc
可以在标记为final的同时, 也使用@objc来让函数可以使用消息机制派发. 这么做的结果就是, 调用函数的时候会使用直接派发, 但也会在 Objective-C 的运行时里注册响应的 selector. 函数可以响应perform(selector:)以及别的 Objective-C 特性, 但在直接调用时又可以有直接派发的性能.
@inline
Swift 也支持@inline, 告诉编译器可以使用直接派发. 有趣的是,dynamic @inline(__always) func dynamicOrDirect() {}也可以通过编译! 但这也只是告诉了编译器而已, 实际上这个函数还是会使用消息机制派发. 这样的写法看起来像是一个未定义的行为, 应该避免这么做.
32、Swift环境及编译优化调研
swift成为了一门静态语言。Swift语言中对象的方法调用机制和OC语言完全不同,Swift语言的对象方法调用基本上是在编译链接时刻就被确定的,可以看做是一种硬编码形式的调用实现。
Swift优势
- 1、性能高、速度快:Swfit 中的类分为值类型和引用类型,值类型是没有引用计数的,纯粹的值类型是存储于栈区的。Swift 中的 struct 能替代绝大数 ObjC 中的类,通过入栈出栈方式进行分配和销毁,大幅减少了在堆区内存的分配和回收,提高了效率。Swift 还在一些细节上做了内存优化,比如集合类型的 Copy-on-Write 特性。
- 2、安全性:Swift 从设计之初就比基于 C 的语言更安全,另外还清除了不安全的代码。Swift 具有静态调度安全的特性,所以许多问题在编译时就能提前发现。
- 3、先进性
- 泛型、协议扩展在很大程度上提高了代码的复用率,使代码更加灵活。
- 元组、关联枚举、下标语法、自定义运算符、字面量语法等,这些 ObjC 不具备的特性,可以十分简单地实现场景定制化,让代码更加灵活自如。
- 结构体作为值类型的对象结构,可以满足绝大多数业务场景,做到更好的内存管理,也更符合线程安全的设计。
- Swift 引入模块的概念,解决了 ObjC 中长久以来为人诟病的命名空间问题。访问权限由 private 到 open 分为五级,使模块间的调用把控力更容易掌握。
- Swift 中闭包占有很重的戏份,但却拥有十分轻量级语法,许多便捷的高阶函数都是以闭包的形式展开进行的。这里面也体现了一些函数式编程的思想,而且其实所有的函数都可以作为一个闭包进行调用。
- 4、代码量小:由于 Swift 语言简洁明确,实现同样的功能,代码量明显比 ObjC 有所减少,估计减少 15~30%
- 5、跨平台:Swift 在应用上很广泛,并不局限于苹果平台的开发。它有专门的团队 Swift Server Work Group (SSWG) 在从事服务端的建设,还有相应而生的后端框架 Perfect,Vapor 等。在人工智能领域 Swift for TensorFlow 的发展也如火如荼,还有 Web 前端、Linux等等。
Swift劣势
- 对于 Swift 混编工程来说,Swift 是可以无障碍调用 ObjC 的,但是反之不一定。虽然苹果的优化让 Swift 完美地兼容了 ObjC,但 Swift 中一些独有的特性,ObjC 是不能够支持的,在语言的转义过程中很容易出现问题,造成程序错误
- Swift 工程在编译时长上一般大于 ObjC。主要原因是静态调度的 Swift 在编译期就需要做更多的工作,会做许多检查和优化。编译器在优化阶段,估计就要占用三分之一左右的编译时间。但目前 Xcode 中有一些 Swift 的编译选项,通过调整这些策略,编译时长上还是有一定的优化空间的。
- 单纯就某些简单的基础操作来说,如某些循环、拼接、元素增减等方面,Swift 速度上甚至不如 ObjC,但并不影响其复杂场景下以及整体的性能优势。
- 在 Swift5.0 之前,或者在低于 iOS 12.2 以下的操作系统中 Swift runtime 和标准库包仍然会打包到工程的包中,仍然占用和应用的包体积。