swiftUI面试
# 类(class) 和 结构体(struct) 有什么区别?
在 Swift 中,class 是引用类型(指针类型), struct 是值类型 String,Array,Dictionary,Set
- 值类型: 比如结构体,枚举,是在栈空间上存储和操作的
引用类型
引用类型只会使用引用对象的一个"指向"; 赋值给var、let或者给函数传参,是将内存地址拷贝一份,类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件。属于浅拷贝(shallow copy)
引用类型: 比如 Class,是在堆空间上存储和操作的
类:继承,引用计数的。
结构体,不用考虑引用计数的问题。
# ?,??的区别
1)?用来声明可选值,如果变量未初始化则自动初始化nil;在操作可选值时,如果可选值是nil则不响应后续的操作;使用as?进行向下转型操作;
2)?? 用来判断左侧可选值非空(not nil)时返回左侧值可选值,左侧可选值为空(nil)则返回右侧的值。
# Any和AnyObject的区别?
AnyObject只能表示引用类型的任何实例,相当于Objective-C中的id类型。
Any可以表示类,结构体,枚举的任何实例。
AnyObject是Any的子集。
# Swift的Copy On Write机制了解过吗?
1)Swift中参数传递是值类型传递,它会对值类型进行copy操作,当传递一个值类型变量时(变量赋值,函数传参),它传递的是一份新的copy值,两个变量指向不同的内存区域。如果频繁操作的变量占内存较大,会产生性能问题。
2)Copy On Write是一种优化值类型copy的机制,对String、Int、Float等非集合数据类型,赋值直接拷贝,对于Array等集合类型数据,只有传递的内容值改变后才进行拷贝操作。
3)Copy On Write的实现:set函数中判断是否存在多个引用,只有存在多个引用的情况下才会进行拷贝操作。另外,自定义结构体是不支持Copy On Write的。
苹果建议当复制大的值类型数据的时候,使用写时复制技术,那什么是写时复制呢?我们现在看一段代码:
值类型(比如:struct),在复制时,复制对象与原对象实际上在内存中指向同一个对象,当且仅当修改复制的对象时,才会在内存中创建一个新的对象 为了提升性能,Struct, String、Array、Dictionary、Set采取了Copy On Write的技术
比如仅当有“写”操作时,才会真正执行拷贝操作
对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值 var array1: [Int] = [0, 1, 2, 3] var array2 = array1
print(address: array1) //0x600000078de0 print(address: array2) //0x600000078de0
array2.append(4)
print(address: array2) //0x6000000aa100
我们看到当array2的值没有发生变化的时候,array1和array2指向同一个地址,但是当array2的发生变化时,array2指向地址也变了,很奇怪是吧。
# 什么是自动闭包、逃逸闭包?
@autoclosure:自动闭包(默认非逃逸闭包),它是一种自动创建的闭包,用来包装作为参数传递给函数的表达式,不接受任何参数,被调用时返回被包装的表达式的值。自动闭包可以延迟计算,因为只有调用到这个闭包代码才会执行,这样我们便可以控制代码什么时候执行。
# Swift 中,什么可选型(Optional)
Optional是 OC 中没有的数据类型,是苹果在 Swift 中引入的全新类型,它的特点就是可有值,也可以没有值,当它没有值的时候就是 nil. 并且 Swift 中的nil 和 OC 中 nil 也不一样,在 OC 中只有对象才能为 nil, 而在 Swift 中,当基础类型(整型,浮点,布尔等)没有值的时候,也是 nil, 而不是一个初始值,没有初始值的值是不能使用的,所以就产生了 Optional 类型.定义一个 Optional 的值很容易,只需要在类型后面加上问号(?)就行了
在 Swift 中,可选型是为了表达一个变量为空的情况,当一个变量为空,他的值就是 nil
在类型名称后面加个问号? 来定义一个可选型
值类型或者引用类型都可以是可选型变量
# 访问控制关键字 open, public, internal, fileprivate, private 的区别?
Swift 中有个5个级别的访问控制权限,从高到低依次是 open, public, internal, fileprivate, private
它们遵循的基本规则: 高级别的变量不允许被定义为低级别变量的成员变量,比如一个 private 的 class 内部允许包含 public的 String值,反之低级变量可以定义在高级别变量中;
open: 具备最高访问权限,其修饰的类可以和方法,可以在任意 模块中被访问和重写.
public: 权限仅次于 open,和 open 唯一的区别是: 不允许其他模块进行继承、重写
internal: 默认权限, 只允许在当前的模块中访问,可以继承和重写,不允许在其他模块中访问
fileprivate: 修饰的对象只允许在当前的文件中访问;
private: 最低级别访问权限,只允许在定义的作用域内访问
https://blog.csdn.net/watertekhqx/article/details/90701418 (opens new window)
swift 中关于open ,public ,internal,fileprivate,private 修饰的说明
open:
用open修饰的类可以在本某块(sdk),或者其他引入本模块的(sdk,module)继承,如果是修饰属性的话可以被此模块或引入了此某块(sdk)的模块(sdk)所重写
public:
类用public(或级别更加等级更低的约束(如private等))修饰后只能在本模块(sdk)中被继承,如果public是修饰属性的话也是只能够被这个module(sdk)中的子类重写
internal
是在模块内部可以访问,在模块外部不可以访问,a belong A , B import A, A 可以访问 a, B 不可以访问a.比如你写了一个sdk。那么这个sdk中有些东西你是不希望外界去访问他,这时候你就需要internal这个关键字(我在导入第三方框架时发现其实没有定义的话sdk里面是默认internal的)
fileprivate
这个修饰跟名字的含义很像,file private 就是文件之间是private的关系,也就是在同一个source文件中还是可以访问的,但是在其他文件中就不可以访问了 a belong to file A, a not belong to file B , 在 file A 中 可以访问 a,在 file B不可以访问a
private
这个修饰约束性比fileprivate的约束性更大,private 作用于某个类,也就是说,对于 class A ,如果属性a是private的,那么除了A外其他地方都不能访问了(fileprivate 和private都是一种对某个类的限制性约束。fileprivate的适用场景可以是某个文件下的extension,如果你的类中的变量定义成了private那么这个变量在你这个类在这个类的文件的拓展中就无法访问了,这时就需要定义为fileprivate)
最后是 Guiding Principle of Access Levels (访问级别的推导原则):不能在低级别的修饰中定义比自身更高的级别修饰,如public不能修饰在private类中的属性
# 关键字:Strong,Weak,Unowned 区别?
Swift 的内存管理机制同OC一致,都是ARC管理机制; Strong,和 Weak用法同OC一样
Unowned(无主引用), 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained), 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)
# 什么是自动闭包、逃逸闭包、非逃逸闭包?
非逃逸闭包: 永远不会离开一个函数的局部作用域的闭包就是非逃逸闭包。
func player(complete:(Bool)->()){
complete(true) //执行闭包 传入真
}
self.player { bool in
print( bool ? "yes":"no")
} // yes
2
3
4
5
6
逃逸闭包:当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。在形式参数前写@escaping来明确闭包是允许逃逸的。
var completionHanglers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHangler: @escaping () -> Void) {
completionHanglers.append(completionHangler)
}
completionHanglers.first?()
2
3
4
5
6
7
8
9
自动闭包:是一种自动创建的闭包,用来把作为实际参数传递给函数的表达式打包的闭包.他不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值.
Autoclosure 是用于延迟执行闭包的一种技术。使用 Autoclosure,我们可以将闭包作为参数传递给函数或方法,但是闭包不会立即执行。相反,它会在需要时才会被执行。
public func assert(_ condition:@autoclosure () -> Bool,_ message: @autoclosure () -> String = String(), file:StaticString = #file, line: Unit = #line)
let num = 3
assert(num>3,"num不能大于3")
var customersInLine = ["李一", "张2", "刘3", "赵四", "王五"]
override func viewDidLoad() {
super.viewDidLoad()
print(customersInLine.count)
// 打印出“5”
let customerProvider = { self.customersInLine.remove(at: 0) }//自动闭包
print(customersInLine.count)//没有执行呢还 还是打印出“5”
print("移除了 \(customerProvider())!") //移除了 李一!
print(customersInLine)
serve(customer: customersInLine.remove(at: 0))
// 不用 @autoclosure 修饰
serve2(customer: { customersInLine.remove(at: 0) } )
}
func serve(customer customerProvider: @autoclosure () -> String) {
print("移除了 \(customerProvider())!")
print(customersInLine)
}
func serve2(customer customerProvider: () -> String) {
print("移除了 \(customerProvider())!")
print(customersInLine)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# swift的派发机制 参考文章[https://segmentfault.com/a/1190000008063625]
编译型语言有三种基础的函数派发方式: 直接派发(Direct Dispatch), 函数表派发(Table Dispatch) 和 消息机制派发
- 直接派发 (Direct Dispatch)
直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间, 例如函数内联等,然而, 对于编程来说直接调用也是最大的局限, 而且因为缺乏动态性所以没办法支持继承.直接派发也有人称为静态调用
- 函数表派发 (Table Dispatch )
函数表派发是编译型语言实现动态行为最常见的实现方式. 函数表使用了一个数组来存储类声明的每一个函数的指针. 大部分语言把这个称为 “virtual table”(虚函数表), Swift 里称为 “witness table”. 每一个类都会维护一个函数表, 里面记录着类所有的函数, 如果父类函数被 override 的话, 表里面只会保存被 override 之后的函数. 一个子类新添加的函数, 都会被插入到这个数组的最后. 运行时会根据这一个表去决定实际要被调用的函数. 看看下面两个类:
class ParentClass {
func method1() {}
func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
func method3() {}
}
2
3
4
5
6
7
8
在这个情况下, 编译器会创建两个函数表, 一个是 ParentClass 的, 另一个是 ChildClass的:
这张表展示了 ParentClass 和 ChildClass 虚数表里 method1, method2, method3 在内存里的布局.
let obj = ChildClass()
obj.method2()
2
3
4
当一个函数被调用时, 会经历下面的几个过程:
读取对象 0xB00 的函数表. 读取函数指针的索引. 在这里, method2 的索引是1(偏移量), 也就是 0xB00 + 1. 跳到 0x222 (函数指针指向 0x222)
查表是一种简单, 易实现, 而且性能可预知的方式. 然而, 这种派发方式比起直接派发还是慢一点. 从字节码角度来看, 多了两次读和一次跳转, 由此带来了性能的损耗. 另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化.
这种基于数组的实现, 缺陷在于函数表无法拓展. 子类会在虚数函数表的最后插入新的函数, 没有位置可以让 extension 安全地插入函数.
- 消息机制派发 (Message Dispatch )
消息机制是调用函数最动态的方式. 也是 Cocoa 的基石, 这样的机制催生了 KVO, UIAppearence 和 CoreData 等功能. 这种运作方式的关键在于开发者可以在运行时改变函数的行为. 不止可以通过 swizzling 来改变, 甚至可以用 isa-swizzling 修改对象的继承关系, 可以在面向对象的基础上实现自定义派发.
class ParentClass {
dynamic func method1() {}
dynamic func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
dynamic func method3() {}
}
2
3
4
5
6
7
8
Swift 会用树来构建这种继承关系:
这张图很好地展示了 Swift 如何使用树来构建类和子类.
当一个消息被派发, 运行时会顺着类的继承关系向上查找应该被调用的函数. 如果你觉得这样做效率很低, 它确实很低! 然而, 只要缓存建立了起来, 这个查找过程就会通过缓存来把性能提高到和函数表派发一样快
# 小结
Swift 的派发机制
使用 dynamic 修饰的时候会通过 Objective-C 的运行时进行消息机制派发. 总结起来有这么几点:
值类型总是会使用直接派发, 简单易懂
而协议和类的 extension 都会使用直接派发
NSObject 的 extension 会使用消息机制进行派发
NSObject 声明作用域里的函数都会使用函数表进行派发.
协议里声明的, 并且带有默认实现的函数会使用函数表进行派发.
# 什么是函数式编程?
面向对象编程:将要解决的问题抽象成一个类,通过给类定义属性和方法,让类帮助我们解决需要处理的问题(即命令式编程,给对象下一个个命令)。 函数式编程:数学意义上的函数,即映射关系(如:y = f(x),就是 y 和 x 的对应关系,可以理解为"像函数一样的编程")。它的主要思想是把运算过程尽量写成一系列嵌套的函数调用。 例: 数学表达式 (1 + 2) * 3 - 4 传统编程 var a = 1 + 2 var b = a * 3 var c = b - 4 函数式编程 var result = subtract(multiply(add(1,2), 3), 4)
函数式编程的好处: 代码简洁,开发迅速; 接近自然语言,易于理解; 更方便的代码管理; 易于"并发编程"; 代码的热升级。