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

weak, _ _weak 和 _ _block 修饰符

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

weak __weak __block
修饰对象 属性 局部变量 局部变量
意义 弱引用 弱引用 声明该变量可在block里被使用,block对其不会进行值拷贝而是地址引用

Swift中:

  • 只有weak修饰符, 同时用于声明一个变量或者属性为弱引用
  • 没有__block意义上的声明
  • Swift中的Closure捕获的局部变量就等于Objective-C中被__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.

    • 如果不按照上一条去做, 编译器会直接报错.

struct 和 class

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

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


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) // 1 loli v

print(room2.memberNum, room2.teacher.name) // 2 loli v

print(room2.teacher == room.teacher) // true

room2.teacher = Teacher(name: "oreo")

print("modify room2's teacher")

print(room.memberNum,room.teacher.name) // 1 loli v

print(room2.memberNum, room2.teacher.name) // 2 oreo

print(room2.teacher == room.teacher) // false

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

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

运算符

在OC中运算符是针对值类型使用的.

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

协议 protocol

  • Objective-C:

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

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

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

OC中的 category 和 extension

OC中的categoryextension都可以在一个已知的类型上添加额外的东西.

category extension
声明位置 h文件 m文件
实现位置 m文件 m文件
可添加内容 函数 函数, 属性
内容访问权限 公开 私有
添加的时机 运行时 编译时

category

  • category添加的内容是在运行时添加到对应class的表上的
  • categoryclass直接添加属性会引起编译器警告: 编译器不会为category里的属性自动生成gettersetter, 但是开发者可以自己通过runtimeobjc_setAssociatedObjectobjc_setAssociatedObject实现这两个函数.
  • category所添加的属性x在编译阶段不会给对应的class生成变量_x.
  • category可以重写类中的函数, 而且它的调用优先级更高.
  • 某类型ClassA的子类在调用super的函数时, 如果ClassA函数被category重写, 系统会调用被重写的这个函数.

extension

  • extension添加的内容在编译的时候就已经确认了在对应的class的表上
  • extension可以给class添加属性, 并且会生成对应的gettersetter函数
  • extension必须在类的.m文件里实现, 如果一个类只知道.h文件, 无法通过extension对其增加属性或者函数

重写(override) 和 重载(overload)

  • override, 子类重新实现父类的函数.
  • overload, 在同一个类型中, 两个函数名字相同, 但是参数和返回类型不一样.
  • Objective-C只支持override.
  • Swift同时支持overrideoverload. 但是, 一旦在swift函数的声明加上@objc修饰, 那么这个函数就不支持overload特性了.

反射机制 reflection

当我们知道任意一个类型的时候, 就可以知道类型的函数和变量.
当我们知道一个函数的名称, 可以通过函数的名称去查找这个函数的指针.
当我们知道一个函数的指针时, 也可以知道这个函数的名字.

  • Objective-C:
    NSObject
  • Swift
    Mirror

double 和 NSDecimalNumber

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

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

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的死锁. 事实上, 即使不是在主队列做这种操作, 用一个任意的串行队列如此操作, 都会进入死锁.

    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队列的最后方, 而串行队列后方的任务必须等待前方的任务执行完才能执行, 因此陷入一个无限等待的循环.

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

    再看下面的代码

    let concurrentQueue = DispatchQueue(label: "concurrent", qos:.default, attributes: .concurrent)
    
    concurrentQueue.async {
        print(Thread.current)
        concurrentQueue.sync {
            print("sync in async???", "Thread:", Thread.current)
        }
        print("yes you can!", Thread.current)
    }
    /*
    结果:
    {number = 3, name = (null)}
    sync in async??? Thread: {number = 3, name = (null)}
    yes you can! {number = 3, name = (null)}
    */
    

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

  • 线程和队列

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

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

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

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

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

  • 线程池
    GCD提供了全局队列, 通过DispatchQueue.global()函数获取, 调用者不用直接面对线程, 因为GCD内部维护了一个数量有限的线程池.

    iOS碎片化知识点(持续更新)_第1张图片
    Apple官方提供的GCD线程池示意图

这个线程池也有被使用完的时候.
而当线程池暂时用满了, 那后续要添加进队列的任务, 就只能等待了. 这个时候, 即使是调用DispatchQueueasync()函数, 也会出现任务阻塞, 因为async()函数也要等待线程池腾出空间来.
所以爆炸性调用DispatchQueue.global().async()函数, 有机会造成应用冻结甚至Crash. 开发过程中应该考虑把部分异步操作加入到串行队列中, 避免过度占用GCD的线程池.(因为串行队列不会再开辟新的线程)


OC中的Block

每一个Block本身是一个对象

  • Block是在C++中实现的类型, 一共有3种: Global, Stack, Malloc
struct objc_class _NSConcreteGlobalBlock;
struct objc_class _NSConcreteStackBlock;
struct objc_class _NSConcreteMallocBlock;
  • 不能直接选择Block的类型, 一个Block的类型由以下两点决定:
    • 其声明的作用域
    • 其引用方式

捕获&修改局部变量

Block声明的时候, 会把在其内部使用的局部变量捕获:

  • 对于局部值类型会对其数据进行拷贝
  • 对于局部引用内类型会捕获其引用地址

局部变量的修改:

int value = 10;
void ^blk = {
    NSLog("%d", value);
};
value = 20;
blk(); // 输出10
  • 由于局部变量在Block被声明时被拷贝了一份, 所以在Block以外修改被Block捕获过的普通局部变量, 不会影响Block里对应变量的值
  • 接上一条, 如果想在Block内修改变量value, 需要在声明value的时候在前面加上__block修饰

循环引用

一个Block对象blk捕获的一个强引用instance, 而instance自身也持有了blk的引用.


UIKit

UIView 和 CALayer 的关系

当我们需要在屏幕上显示一些元素的时候, 一般会使用到UIView的各种子类. 而实际上如果我们只需要实现单纯的显示功能, 也可以直接使用CALayer去实现. CALayer没有了和用户交互相关的属性和函数, 在理论上比UIView拥有更低的内存占用.

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

UIViewsubviewsCALayersublayers是对应关系.

你可能感兴趣的:(iOS碎片化知识点(持续更新))