iOS碎片化知识点汇总(持续更新)

Posted by P36348 on March 26, 2019

语言

weak, _ _weak 和 _ _block 修饰符

Objective-C中, weak用于修饰属性, __weak__block用于修饰局部变量.

  weak __weak __block
修饰对象 属性 局部变量 局部变量
意义 弱引用 弱引用 block以外弱引用, block以内保留引用, 需要在block内手动释放.

Swift中只有weak修饰符, 同时用于声明一个变量或者属性为弱引用, 并没有__block意义上的声明.

便利构造函数 (designated, convenience)

便利构造函数(designated initializer)是为了保证类型在初始化后, 其所有属性的值都是存在的.

在Swift中, 又多了一个关键字convenience去加强这个特性.convenience initializer的作用在于, 可以规定一个类型的属性的初始值.

  designated convenience
存在性 Objective-C, Swift Swift
  • 在Objective-C中
    • 声明一个便利构造函数的做法, 就是在声明函数的最后面加上宏NS_DESIGNATED_INITIALIZER
    • 当一个类型声明了designated initializer之后, 我们希望创建这个类型的实例, 就必须调用它的designated initializer.
    • 如果不调用designated initializer`, 编译器会发出警告.
  • 在Swift中
    • 每个类型都会有designated initializer, 一个类型的init函数在没有修饰的情况下, 它就是这个类型的designated initializer.
    • convenience initializer的调用优先级在designated initializer之上: 创建这个类型的实例, 如果这个类型有定义convenience initializer, 则必须调用.
    • 声明convenience initializer, 其内部必须调用这个类型自己的designated initializer.
    • 如果不按照上一条去做, 编译器会直接报错.

struct 和 class

很大路的知识点struct是值类型, 而class是引用类型.

关键在于,一个 struct实例的属性里面存在一个引用类型class的时候, 使用这个struct去赋值到一个新的变量的时候, struct属性里面的class会被怎么处理?

1
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
class Teacher: NSObject {
    var name: String
    init(name: String) {
        self.name = name
    }
}
struct Room {
    var memberNum: Int
    var teacher: Teacher
    
}
let room = Room(memberNum: 1, teacher: Teacher(name: "oreo"))
var room2 = room
room2.memberNum = 2
room2.teacher.name = "loli v"
print(room.memberNum,room.teacher.name)
print(room2.memberNum, room2.teacher.name)
print(room2.teacher == room.teacher)
room2.teacher = Teacher(name: "oreo")
print("modify room2's teacher")
print(room.memberNum,room.teacher.name)
print(room2.memberNum, room2.teacher.name)
print(room2.teacher == room.teacher)
/*
结果:
1 loli v
2 loli v
true
modify room2's teacher
1 loli v
2 oreo
false
*/ 

可以看到, room赋值之后room2, room2的值类型属性被拷贝了一份, 修改room2memberNum不会影响room.

但是room2的引用类型属性, 其实是拷贝了一份room对应属性的地址, 所以修改room2teacher的属性, 会影响room,而重新给room2赋值一个新的Teacher实例之后, 两个Room实例里面的引用型属性就不是同一个了.

运算符

在Objective-C中运算符是针对值类型使用的.

在Swift中, 用户可以自定义运算符, 运算符可以用在应用型对象身上. 并且用户可以自己定义运算符的功能.

协议 protocol

  • Objective-C:
    • 协议主要是MVC设计模式中的数据回调手段.
    • 因为用于回调, 协议本身可以被声明为弱引用, 因此协议都继承NSObject.
  • Swift:

    拥有Objective-C中已经有的全部特性, 并且被功能更强大:

    • 可以使用extension特性默认实现的协议函数或属性.
    • 协议中默认实现的函数可以在实现类型的代码中”重写”.
    • 在使用extension特性声明的时候可以添加约束条件, 这样默认实现的函数只会在满足条件的时候才会生效, 如果不满足则等于没有实现.
    • 因为使用范围不止是用于回调, Swift中的协议不必继承NSObjectProtocol.
    • structclass都可以实现的协议, 因此在使用协议的时候不能直接当做引用型来使用, 除非协议本身就是继承自引用型协议.

extension


Foundation

double 和 NSDecimalNumber

项目中我们使用double表示和存储小数, 而在需要对小数进行计算的时候, 比如电商项目的购物车功能要进行金额结算, 我们使用double就可能出现金额精度不正确的问题.

针对小数运算, Apple给我们提供了一个专门的类: NSDecimalNumber, NSDecimalNumber进行算数运算的方式是通过调用函数, 在Swift中可以用链式的方法计算, NSDecimalNumber运算结果仍然是一个NSDecimalNumber, 我们可以使用它的对应属性获得我们想要的值类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Item {
    let unitPrice: Double
}
struct Coupon {
    let value: Double
}
// 20件商品, 75折
let item = Item(unitPrice: 9.99)
let coupon = Coupon(value: 0.75)
let count: Int = 20
let dPrice = NSDecimalNumber(value: item.unitPrice)
let dCount = NSDecimalNumber(value: count)
let dCouponValue = NSDecimalNumber(value: coupon.value)
let result = dPrice.multiplying(by: dCouponValue).multiplying(by: dCount)
let doubleResult = result.doubleValue
let stringResult = result.stringValue

GCD

GCD会自动管理线程的生命周期(创建线程, 调度任务, 销毁线程). 我们调用GCD的时侯不会面对线程, 而是面对队列. 就是说:

即使N次调用了异步执行并发队列, 并不代表我们创建了N个的子线程.

  • 死锁

    GCD的死锁, 关键是阻塞. 有很多面试题里面提到在主线程里面同步调用主队列会引起死锁. 而之前在公司面试, 问起死锁很多人也是这样回答, 回答者没有真的明白GCD的死锁. 事实上, 即使不是在主队列做这种操作, 用一个任意的串行队列如此操作, 都会进入死锁.

    1
    2
    3
    4
    5
    6
    7
    8
    
    let serialQueue = DispatchQueue(label: "serial")
      
    serialQueue.async {
        serialQueue.sync {
            print("sync in async???")
        }
        print("may I run?")
    }
    

    以上这段代码同样是编译不通过的, 因为sync函数做的第一件事情就是阻塞当前队列的任务(也就是print("may i run?")这一行代码), 而串行队列同时间只能执行1个任务, sync调用之后, 而闭包里面的代码也要在serialQueue中执行, 而这个任务会被排在了serialQueue队列的最后方, 而串行队列后方的任务必须等待前方的任务执行完才能执行, 因此陷入一个无限等待的循环.

    而主队列其实就是一个串行的队列!!!

    再看下面的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    let concurrentQueue = DispatchQueue(label: "concurrent", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent)
      
    concurrentQueue.async {
        print(Thread.current)
        concurrentQueue.sync {
            print("sync in async???", "Thread:", Thread.current)
        }
        print("yes you can!", Thread.current)
    }
    /*
    结果:
    <NSThread: 0x600001a5c100>{number = 3, name = (null)}
    sync in async??? Thread: <NSThread: 0x600001a5c100>{number = 3, name = (null)}
    yes you can! <NSThread: 0x600002b32d00>{number = 3, name = (null)}
    */
    

    没错, 在并发队列里面, 这几串代码是正常运行的. 因为sync阻塞了print("yes you can!"), 但是concurrentQueue它可以同时执行多个任务, 闭包里面的代码不需要排到print("yes you can!")的后面, 而是可以使用concurrentQueue分配的其他任务资源去执行.

  • 线程和队列

    回头继续看上面的代码, 可以发现, 由始至终concurrentQueue都一直在用同一个线程!!!

    这就是GCD做了它应该做的事情: 管理线程的生命周期.

    因为上面的代码, 事实上concurrentQueue同一时间永远只有1个任务需要执行, 所以即使它本身可以同时执行多个任务, 它也没必要创建多个线程来给任务开始.

    (请脑补一下收银排队的情景, 一模一样.)

    也就如开头所说的, 并发队列异步执行命令, 并不等于就是开辟了新的线程.


UIKit

UIView 和 CALayer 的关系

当我们需要在屏幕上显示一些元素的时候, 一般会使用到UIView的各种子类. 而实际上如果我们只需要实现单纯的显示功能, 也可以直接使用CALayer去实现.

**相比起UIVIew, CALayer并不具备交互的功能, 就是不能响应任何点击拖拽行为. **查看UIView的类不难发现, 它自身也有一个layer的属性, 其对应的其实就是用于显示在屏幕上的CALayer对象.

待完成

Responder

待完成


BluetoothKit


RxSwift