iOS 知识点整理 (持续更新...)

整理了些iOS相关的基础问题,每个问题可能会再写些扩展,需要具体了解可以看题目下方的链接
如有错漏,欢迎指出,谢谢

一.Swift

1.给一个数组,要求写一个函数,交换数组中的两个元素(swift可用元组)

func swap(_ arr: inout [T],  a: Int, b: Int) {
	(arr[a], arr[b]) = (arr[b], arr[a])
}

2.Any / AnyObject

// Any 的定义
typealias Any = protocol<>

// AnyObject 定义
@objc protocol AnyObject {
}

  • Any 是空协议集合的别名,用于所有类型
  • AnyObject 是一个空协议,用于所有class实例, 所有类隐式遵守的协议

3.@objc / dynamic

OC 对象基于运行时,方法或属性使用动态派发,在运行调用时再决定实际调用的具体实现,而Swift为了追求性能,如无特殊需要,是不会在运行时再来决定这些,即Swift类型的成员/方法在编译时已经决定。

OC中基本所有类继承自NSObject,Swift类如要供OC调用,必须也继承自NSObject

  • @objc
  1. 根本目的:暴露接口给OC的运行时
  2. 添加@objc并不意味着这个方法或属性会采用OC的方式变成动态派发,Swift仍有可能将其优化为静态调用
  3. @objc可修改Swift接口暴露到OC后的名字
  • dynamic
    用于表明使用runtime机制,可动态调用

加了@objc标识的方法、属性无法保证都会被运行时调用,因为Swift会做静态优化。要想完全被动态调用,必须使用dynamic修饰。使用dynamic修饰将会隐式的加上@objc标识。

swift中的函数是静态调用,静态调用的方式会更快,但是静态调用的时候就不能从字符串查找到对于的方法地址,这样 与OC交互的时候,OC动态查找方法就会找不到,这个时候就可以通过使用 dynamic 标记来告诉编译器,这个方法要被动态调用的

4.OC与Swift区别

OC与Swift区别

  • 大的方面:
    Swift是静态类型语言,OC是动态类型语言。
    OC堆类型要求不严格,而Swift格外严格,是类型安全的语言,他不仅对类型是否匹配严格,也对一个类型是否有值严格
    编程思想上,用Swift编程的时候不能套用OC的编程思想,着重于函数式编程/面向协议编程

当然,Swift和OC可以混编,从OC过渡Swift比较简单,有很多地方是相通的

Swift优势:
1.语法易读,文件结构更简单
2.更安全,强类型语言(默认类型安全)
3.代码更少,省去大量冗余代码
4.速度更快,运算性能更高

  • 小细节的方面:
  1. Swift 类型,值类型/引用类型
  2. Swfit get/set方法,引入存储属性、计算属性
  3. 严格的初始化
  4. 面向协议编程,Swift对Protocol有更好的支持,比如继承、
  5. 泛型
  6. 抛弃了C语法的 ++ --, switch的break
  7. struct和class类型很像
  8. 重载运算法
  9. 函数式编程
扩展:
  • 强/弱类型:指语言类型系统的类型检查严格程度,强类型偏向不容忍隐式类型转换,而弱类型不行
  • 静态/动态类型:指变量与类型的绑定方法,前者是编译器在编译阶段执行类型检查,而后者编译器在运行时执行类型检查,即声明一个变量后,不能改变它的类型的语言,是静态语言,能随时改变它类型的语言是动态语言

5.protocol 与 extension

protocol

Swift 协议(protocol)详解

Protocol小结

定义某种约定,来表示共性,而不是类型,相比OC,不仅用做代理,也可用作对接口的抽象,代码的复用

  1. 协议内定义属性/方法/构造器

    • 定义属性 {get set} / 类型, 不能设置默认值
    1. 定义方法(参数不能有默认值,没有实现)
    2. 构造器
  2. 协议是一种类型

    • 作为函数/构造器中的参数或返回值类型
    • 作为常量/变量/属性类型
    • 作为数组/字典或其他容器中的元素类型
  3. 用于委托模式

    • 委托模式允许某个类型的部分指责转移到另一个类型的实例来实现
    • 可通过协议来实现委托模式
  4. protocol-extension

    • 对实例使用,令已有类型遵循某个协议
    • 对协议使用,可遵循其他协议
    • 提供默认实现,相当于变相设定成了optional
    • 搭配where使用,可增加限制条件,限制类型
  5. 协议的继承

    • 可继承一个或多个其他协议
    • 使用&关键字同时遵循多个协议
    • 协议通过继承AnyObject,使其被限制只适用于class类型
  6. 检查一致性

    • 使用is检查某个实例是否符合协议
    • 使用as?返回协议类型的可选值
    • 使用as!强制转换为协议类型
  7. 可选协议

    • 关键字optional
    • 协议及可选项使用@objc标记
    • 结构体/枚举不能使用,只能由继承OC类或@objc类使用
  8. 关联类型

面向协议编程

依赖倒置原则:
高层模块不依赖于低层模块,二者依赖于抽象
抽象不依赖于细节,细节依赖于抽象

  1. 面向对象编程呈现的是金字塔结构,面向协议编程提倡的是扁平化和去嵌套的代码
  2. Swift中,协议定义了方法、属性的蓝图,然后类、结构体或枚举都能够使用协议,使用继承的思想,模拟了多继承关系,不存在is-a关系
  3. 将与类型无关的共性从类型定义上移出去,用一个协议封装好,让它成为这个类型的一种修饰
  4. 如果某个类型有多个修饰,那么使用多个协议对其修饰,大大降低了代码的耦合度
  5. 依赖反转/接口分离

extension

Swift - 基础之extension

  1. 为class/struct/enum或protocol添加新特性(计算属性、方法、初始化方法)
    注意,添加新的,但不能覆盖已有的特性
  • 添加计算属性(),但不能添加存储属性,也不能添加属性观察者
  • 添加构造器,但需保证该类型的初始化方法结束时,每一个属性都被完全初始化了
  • 添加实例方法/类方法
  • 添加mutating方法(如果struct和enum定义的方法想改变自身或自身的属性,那么实例方法必须标记为突变的)
  • 添加附属脚本-subscripts(一种访问的对象/集合的快捷方式,如array[index])
  • 添加嵌套类型-nested types,如给结构体嵌套枚举类型
  1. 可让某个类型实现一个或多个协议

6. struct 和 class 什么区别

Swift 浅谈Struct与Class
理解Swift中struct和class在不同情况下性能的差异

深拷贝&浅拷贝本质
  1. 是否开启新的内存地址
  2. 是否影响内存地址的引用计数
  • struct是值类型,class是引用类型
  • struct不能继承,class可以继承
  • struct比class更"轻量级",前者分配在栈上,后者分配在堆上
  • struct有默认的带参数的构造函数,class无
  • struct无析构,class有
struct作为数据模型注意事项
  1. 安全性:值类型,没有引用计数,不会导致内存泄漏
  2. 速度:以栈的形式分配(编译时分配空间),而不是堆(运行时分配),速度更快
  3. 拷贝:引用类型拷贝需注意深拷贝/浅拷贝,值类型拷贝更轻松
  4. 线程安全:值类型是自动线程安全的

缺点:

  1. 与OC混编时,OC无法调用Swift的struct(因为OC调用的对象需继承NSObject)
  2. 不能相互继承
如何抉择

选择值类型:

  1. 要用==运算符来比较实例的数据时
  2. 希望实例的拷贝能保持独立的状态
  3. 数据被多个线程使用

选择引用类型:

  1. 需要使用NSObject相关功能时,必须用引用类型class
  2. 要用==运算符比较实例身份时
  3. 希望创建一个共享、可变对象

模型较小,无需继承、无需OC使用,建议使用Struct

值类型与引用类型的嵌套

Swift 中的嵌套类型

  • 值类型嵌套值类型: 内部值是外部值的一部分,拷贝外部值到新的空间,也会拷贝内部值
  • 值类型嵌套引用类型:外部值被拷贝到新的内存区域,但内部的引用类型只被拷贝了内部值的引用
  • 引用类型嵌套引用类型:复制时只是拷贝了引用,新/原变量都指向同一个实例
  • 引用类型嵌套值类型:与引用类型嵌套引用类型一样

7. copy on write 写时复制

Swift Copy-On-Write 写时复制
只有当这个值需要改变时才进行复制行为

在结构体内部存储了一个指向实际数据的引用reference,在不进行修改操作的普通传递过程中,都是将内部的reference的引用计数+1,在进行修改时,对内部的reference做一次copy操作,再在这个复制出来的数据进行真正的修改,防止和之前的reference产生意外的数据共享

值类型嵌套引用类型的写时复制
  1. 私有化,让外部无法对引用类型进行修改
  2. 另提供一个接口控制这个引用类型写入操作(使用isKnownUniquelyReferenced检查类的实例是不是唯一的引用,来决定setter时是否需要复制)

8.在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么

1.Runloop
2.事件传递与响应

响应链大概有以下几个步骤:

设备将touch到的UITouch和UIEvent对象打包, 放到当前活动的Application的事件队列中

单例的UIApplication会从事件队列中取出触摸事件并传递给单例UIWindow

UIWindow使用hitTest:withEvent:方法查找touch操作的所在的视图view

RunLoop这边我大概讲一下:

主线程的RunLoop被唤醒

通知Observer,处理Timer和Source 0

Springboard接受touch event之后转给App进程

RunLoop处理Source 1,Source1 就会触发回调,并调用_UIApplicationHandleEventQueue() 进行应用内部的分发。

RunLoop处理完毕进入睡眠,此前会释放旧的autorelease pool并新建一个autorelease pool

9.闭包/逃逸闭包/自动闭包

非逃逸/逃逸闭包
闭包捕获语义第一弹:一网打尽!

闭包

  1. 闭包会自动捕获外部变量的引用
  2. 可在闭包内修改变量的值(声明为var)
  3. 可通过捕获列表来获取变量中的内容,存储到本地常量中
  4. 默认闭包行为更像是在OC中使用__block

逃逸闭包/非逃逸闭包/自动闭包

  • 逃逸闭包:

    一个接受闭包为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域(例如:网络请求在请求结束后才调用闭包,并不一定是在函数作用域内执行)

  • 非逃逸闭包:

    一个接受闭包为参数的函数,闭包在这个函数结束前内被调用

闭包会强引用它捕获的所有对象,比如在闭包中访问了当前控制器的属性、函数,这样闭包会持有当前对象,容易导致循环引用

非逃逸闭包不会产生循环引用,它会在函数作用域内释放,编译器可保证在函数结束时闭包会释放它捕获的所有对象,非逃逸闭包它的上下文的内存可保存在栈上而不是堆上


  • 自动闭包:简化参数传递,延迟执行时间

    1. 一种自动创建的闭包,包装传递给函数作为参数的表达式,不接受任何参数,被调用时,返回被包装在其中的表达式的返回值
    2. 普通表达式与@autoclosure的区别:前者传入参数时,会马上被执行,然后将执行结果作为参数传递给函数,而后者不会立马执行,而是由调用的函数内来决定它具体执行时间

weak & unowned 处理循环引用

弱引用:不会被ARC计算,引用计数不会增加

  • unowned

    1. 捕获的原始实例永远不会为nil,闭包可直接使用它,并直接定义为显示解包可选值,如原始实例被析构后,在闭包中使用这个捕获值将导致奔溃
    2. 闭包和捕获对象的生命周期相同,所以对象可以被访问,也意味着闭包也可以被访问[unonwed self]
  • weak

    1. 捕获的实例在使用过程中可能为nil,必须将引用声明为weak,并在使用之前验证这个引用的有效性;
    2. 闭包的生命周期和捕获对象的生命周期相互独立,当对象不能再使用时,闭包依然能够被引用

使用unowned引用不会去验证引用对象的有效性,weak引用添加了附加层,间接得把unowned引用包裹到了一个可选容器里面,在指向的对象析构之后变成空的情况下,处理更清晰,而这附加的机制需要正确处理可选值

弱应用的实现原理

OC 和 Swift 的弱引用源码分析
Swift 4 弱引用实现

Swift4之前旧实现:

Swift对象有两个引用计数:强引用计数和弱引用计数,当强引用计数为0而弱引用计数不为0时,对象会销毁,但内存不会被立即释放,内存中会保留弱引用指向的僵尸对象,在加载弱引用时,运行时会对引用对象进行检查,如果是僵尸对象,则会弱引用计数进行递减操作,一旦弱引用计数为0,对象内存将会被释放。

问题:

如果对象的弱引用数一直不为零,那么对象占用的剩余内存就不会完全释放。这些死而不僵的对象还占用很多空间的话,累积起来也是对内存造成浪费

Swift4后:

SideTable机制:与OC不同的是,系统不再把它作为全局对象使用

  1. 针对有需要的对象创建,为目标对象分配一块新的内存来保存该对象额外的信息(SideTable可有可无),对象会有一个指向SideTable的指针,同时SideTable也有一个指回原对象的指针
  2. 为了不额外多占用内存,只有创建弱引用时,会把对象的引用计数放到新创建的SideTable,在把空出来的空间存放SideTable的地址,而runtime会通过一个标志位来区分对象是否有SideTable
  3. 弱引用从指向对象本身改为指向SideTable

10.map、flatMap/compactMap、filter、reduce

  1. map/flatMap都可以用在Optional和SequenceTypes上
  2. compactMap是Swift4加入的新特性,其实是把之前的flatMap改了个名字
  • map: 每个元素根据闭包中的方法进行转换,然后按转换后的元素输出

  • Optional map/flatMap

    • map是闭包内return为非可选项,但最终返回值为可选项
    • flatMap是闭包内return为可选项,最终返回值也为可选项
  • Sequence.map/flatMap/compactMap

    • flatMap数组降纬度
    • compactMap过滤nil+可选解包
  • filter: 过滤,筛选出满足闭包条件的元素

  • reduce: 组合计算

11.try? 和 try!是什么意思

try 出现异常处理异常
try? 不处理异常,返回一个可选值类型,出现异常返回nil
try! 不让异常继续传播,一旦出现异常程序停止,类似NSAssert()

12.associatedtype 的作用

  • 在协议定义里声明关联类型,相当于给需要用到的类型一个占位符名称,直到采纳协议时,再指定用于该关联类型的实际类型
  • 可以给关联类型添加约束

13.什么时候使用 final/ class与static区别

类不想被继承,函数、属性不想被重写(只能修饰类)

class 和 static 都可表示类方法,前者子类可重写,后者不能重写,static自带final class性质

14.Optional(可选型) 是用什么实现的

public enum Optional {
    case none
    case some(Wrapped)
}

泛型枚举

15. ?/ !/ ?? 的作用

1.声明时添加?,告诉编译器是可选值(表示一个变量可能有值,也可能没有值为nil),自动初始化为nil

2.对变量操作前加?,判断如果变量为nil,则不响应后面的方法

1.声明时添加!,告诉编译器是可选值,并且之后对变量操作时,都隐式在操作前添加!
2.对变量操作前加!,表示默认为非nil,直接解包处理

??

设置默认值,判断变量是否为nil,如果不为nil,则对该变量解包,否则用??后面的默认值

16.lazy / inout 的作用

  • lazy: 延迟初始化,当变量在用到的时候才加载(全局变量不用lazy也是懒加载)

  • inout:

    1. 方法的参数默认不可改变类型,方法内部改变参数值会导致编译错误,需要改变参数值时,需要使用inout关键字(传递的参数不能为let,不能有默认值)
    2. 传递过程:方法调用->参数值被拷贝->方法体内部,被改变的是拷贝的值->方法结束,拷贝的值重新分配给原来的参数

17.什么是高阶函数

其参数或者返回值是闭包的函数,如sort、map、filter

18.mutating 的作用是什么

对结构体、枚举,mutating用于表示某个实例方法可以改变自身实例或者实例中的属性的函数
对协议,用于那些会改变遵循该协议的类型的实例的函数

19.一个 Sequence 的索引是不是一定从 0 开始?

不是
ArraySlice是Sequence的子类,ArraySlice就不是

20.defer使用场景

作用:提供一种延时调用的方式,defer内的代码块会在当前作用域结束之后执行,代码块会被压入栈中,待函数结束时弹出栈运行。
其目的就是进行资源清理和避免重复的返回前需要执行的代码

注意:前提是必须执行到defer才会触发,多个defer,按栈的后进先出顺序执行

  1. try catch结构:相当于finally

  2. 清理、回收资源,例如:加解锁

    lock.lock()
    defer {
    	lock.unlock()
    }
    
    
  3. 调super方法:override一些方法时,需要在super方法前写,比如autolayout的约束写动画,重写updateContaints方法,可以用defer将super方法调用写在前面

  4. completion闭包调用:有些函数分支较多,遗漏调用completion

21.Self / self

  • self: 在实例方法中表示当前实例,在类方法中表示当前类
  • Self: 可用于协议中限制相关的类型,类中来充当方法的返回值类型

例如:

protocol Copyable {
	func copy() -> Self
}

class A: Copyable {

	var num = 1
	required init() { } // 保证当前类和其子类都能响应这个init方法
	
	func copy() -> Self {
	   // type(of: self)获取当前对象的类型
		let copy = type(of: self).init()
		copy.num = num
		return copy
	}
}

22. .Type / .self

理解 Swift 中的元类型:.Type 与 .self

  • 元类型:类型的类型
  • let intMetatype: Int.Type = Int.self .Type是类型,.self是元类型的值
  • AnyClass: typealias AnyClass = AnyObject.Type 任意类型的元类型的别名

获得元类型后可以访问静态变量和静态方法,例子:

func register(AnyClass?, forCellReuseIdentifier: String)

tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
  • type(of:) : type(of:)获取的是运行时的元类型,也就是这个实例的类型,而.self获取的是静态的元类型,声明时是什么类型就是什么类型

23.一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示

Swift选项集合(OptionSet)

OptionSet 选项集合

Swift使用struct来遵循OptionSet协议,引入选项集合

创建与使用

一个类型为整型的原始值(rawValue)+ 一个初始化构造器(struct有默认的,不需要写)

struct Sports: OptionSet {
	let rawValue: Int
	
	static let running = Sports(rawValue: 1 << 0)
	static let cycling = Sports(rawValue: 1 << 1)
   static let swimming = Sports(rawValue: 1 << 2)
}

let sports = [.running, .swimming]

24.给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明

Swift 中的 Sequence(一)

extension Set where Iterator.Element == String {
	func test() { }
}

["", ""].test()

25.不通过继承,代码复用(共享)的方式有哪些

  1. extension
  2. protocol

使用继承可能的问题:

  1. 代码在多个子类中重复
  2. 很难知道所有子类的全部行为
  3. 子类很多时候会有不同的行为
  4. 改变父类牵一发而动全身
    (又要扯到依赖倒置原则了)

26.如何让自定义对象支持字面量初始化

swift定义了一些协议,可通过使用赋值运算符,来用文字值初始化类型,采用相应的协议并提供公共初始化允许特定类型的文本初始化

  • ExpressibleByArrayLiteral 数组形式初始化
  • ExpressibleByDictionaryLiteral 字典形式初始化
  • ExpressibleByNilLiteral 由nil值初始化
  • ExpressibleByIntegerLiteral 整数值初始化
  • ExpressibleByFloatLiteral 浮点数初始化
  • ExpressibleByBooleanLiteral 布尔值初始化
  • ExpressibleByUnicodeScalarLiteral ExpressibleByExtendedGraphemeClusterLiteral ExpressibleByStringLiteral 以上三种由字符串初始化,上面两种包含Unicode自负和特殊字符

例子:

struct TestFloat {
    var value: Float
}

//一般情况下,初始化
var test = TestFloat(value: 4.5)

// 遵循ExpressibleByFloatLiteral协议
extension TestFloat: ExpressibleByFloatLiteral {
    typealias FloatLiteralType = Float
    
    init(floatLiteral value: TestFloat.FloatLiteralType) {
        self.init(value: value)
    }
}

// 
var testt: TestFloat = 4.5

27.访问修饰符

  • private: 只能在当前类访问
  • fileprivate: 在当前Swift文件可访问
  • internal(默认):在源代码所在的整个模块可访问(在app内,整个app都可以访问,在框架/库中,则整个框架内部访问,框架外代码不能访问)
  • public:被任何人访问,其他模块中不可被重写和继承
  • open:被任何人访问,包括重写和继承

28.多线程

iOS多线程全套
iOS 多线程:『pthread、NSThread』详尽总结
iOS GCD
iOS Swift GCD 开发教程
iOS 多线程:『NSOperation、NSOperationQueue』详尽总结

概念

  • 并发&并行:
    前者指多个任务交替占用CPU,后者指多个CPU同时执行多个任务

  • 同步&异步:

    • 同步:同步添加任务到指定队列,必须执行完队列中的任务才能继续执行,只能在当前线程执行任务,不具备开启新线程的能力
    • 异步:异步添加任务到指定队列,无需等待,可继续执行,可在新的线程中执行任务,具备开启新线程的能力(但不一定开启新线程)

pthread

跨平台、C语言编写,需要自己管理线程的生命周期,难度大

NSThread(swift为 Thread)

比pthread简单,可直接操作线程对象,但也需要自己管理线程的生命周期

performSelector

GCD

  • 优点:
  1. 可用于多核的并行运算
  2. 自动利用更多的CPU内核
  3. 自动管理线程的生命周期(创建、调度任务、销毁线程)
  • 任务: 执行操作,即线程中执行的那段代码
队列(Dispatch Queue)

指执行任务的等待队列,即用来存放任务的队列(FIFO)

  • 串行队列:每次只有一个任务执行,一个任务执行完毕后再执行下一个任务
  • 并发队列:让多个任务同时执行(并发队列的并发只有在异步有效)
  • 主队列(串行):所有放在主队列的任务都会在主线程中执行
  • 全局队列:并发队列
区别 串行队列 并发队列 主队列
同步 当前线程执行,不开启新线程,串行执行任务 当前线程执行,不开启新线程,串行执行任务 主线程调用:死锁卡住不执行;其他线程调用:不开启新线程,串行执行任务
异步 开启新线程,串行执行任务 可开启多个线程,并发执行任务(无序执行,多条线程) 不开启新线程,串行执行任务(任务在同一线程)
GCD 栅栏

异步执行一组任务 -> barrier任务 -> 异步执行另一组任务

// OC
dispatch_barrier_async

// Swift
let item = DispatchWorkItem(qos: .default, flags: .barrier) {
    
}
队列组 group
  1. 多个任务并发无序执行
  2. 完成上述任务后在回到主线程执行任务
// OC
dispatch_group_t group =  dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    });
    
     dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    });
    
    dispatch_group_enter、dispatch_group_leave

// Swift
DispatchQueue.global().async(group: group, execute: <#T##DispatchWorkItem#>)

group.enter()
group.leave()

group.notify(queue: queue) {
}

group enter/leave

延迟执行

并不是在指定时间之后才开始执行处理,而是在指定时间之后才将任务添加到队列中

// OC
dispatch_after

// Swift
asyncAfter
GCD 快速迭代方法 apply

按照指定的次数将指定任务追加到指定队列中, 添加的任务并发异步执行,这些任务全部执行完毕后再继续往下执行

// OC
dispatch_apply

// Swift
DispatchQueue.concurrentPerform(iterations: 100) { (index) in
}
信号量 semaphore

持有计数的信号,计数为0时等待,不可通过,计数>=1时,计数减1且不等待,可通过

OC:

  1. dispatch_semaphore_create: 创建一个Semaphore并初始化信号的总量
  2. dispatch_semaphore_signal: 发送一个信号,让信号总量+1
  3. dispatch_semaphore_wait: 当信号总量为0时,就会一直等待(阻塞所在线程),否则就可以正常执行, 并使总信号量-1

Swift:

  1. DispatchSemaphore(value: 1) 初始化信号量的总量
  2. wait()使信号量减1,如果信号量大于0则返回.success,否则返回timeout
  3. signal()使信号量+1,返回当前信号量

应用:

1.保持线程同步,将异步执行任务转换成同步执行任务

func semaphoreSync() {
    print("current thread: \(Thread.current)")
    
    print("semaphore begin")
    
    let se = DispatchSemaphore(value: 0)
    
    var num = 0
    
    DispatchQueue.global().async {
        Thread.sleep(forTimeInterval: 2)
        print("----> \(Thread.current)")
        num = 100
        se.signal()
    }
    
    se.wait()
    
    print("---> num: \(num)")
}

2.保证线程安全,为线程加锁

例子:多个窗口卖票,票源总数固定,
wait相当于加锁,signal相当于解锁

NSOperation/NSOperationQueue(swift没有前缀NS)

优点:

  1. 基于GCD的更高一层封装,易用,代码可读性高
  2. 可添加完成的代码块,在操作完成后执行
  3. 添加操作之前的依赖关系,方便控制执行顺序
  4. 设定操作执行的优先级
  5. 很方便地取消一个操作的执行
  6. 使用KVO观察
  • Operation(操作):

    • 执行操作,即在线程中执行的那段代码
    • GCD放在block中,在Operation中我们使用其子类NSInvocationOperation,NSBlockOperation或自定义子类来封装
  • OperationQueue(操作队列):

    • 存放操作的队列,不同于GCD的调度队列是FIFO,操作队列对于添加到队列的操作,首先进入准备就绪的状态(取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定
    • 通过设置maxConcurrentOperationCount最大并发操作数控制并发、串行
    • 有两种队列:主队列和自定义队列,主队列运行在主线程上,自定义队列在后台执行

步骤:

  1. 创建操作
  2. 创建队列
  3. 将操作加入到队列中

之后,系统自动将操作队列中的操作取出,在新线程中执行操作

操作 Operation
  1. 使用子类 NSInvocationOperation(只有OC有,swift没有)
  2. 使用子类 NSBlockOperation
  3. 自定义继承 NSOperation子类,通过实现内部相应方法来封装操作
不使用OperationQueue的情况
  1. 在没使用OperationQueue,在主线程中单独使用子类NSInvocationOperation、NSBlockOperation或自定义继承Operation子类执行一个操作的情况下,操作是在当前线程执行,没有开启新线程

  2. BlockOperation与InvocationOperation类似,但还多提供了一个方法addExecutionBlock来添加额外的操作,额外操作是在不同线程中异步执行的

  3. 一般情况下,如果BlockOperation封装多个操作,是否开启新线程,取决于操作个数,开启的线程数由系统决定

使用OperationQueue的情况

OperationQueue 有两种队列:主队列、自定义队列(包含串行、并发功能)

  • 主队列:凡是添加到主队列的操作,都会放到主线程中执行(不包括addExecutionBlock添加的额外操作,可能在其他线程执行)

  • 自定义队列:在自定队列中会自动放到子线程中执行,包含串行、并发功能

  • maxConcurrentOperationCount:控制的不是并发线程的数量,是一个队列中同时能并发执行的最大操作数,开启线程数量由系统决定

    • 默认为-1,表示不限制,可进行并发执行
    • =1,队列为串行队列,只能串行执行
    • 1, 队列为并发队列,操作并发执行,其值为min(自己设定的值,系统设定默认最大值)

依赖/优先级

29.线程安全

【Swift】iOS 线程锁
iOS-线程安全

  • 线程安全:当一段代码被多个线程执行,执行后的结果和多个线程依次执行后的结果一致,那么这段代码就是线程安全

  • 互斥锁: 当新线程访问,发现有线程正在执行锁定代码,新线程进入休眠,避免占用CPU资源,锁的持有者的任务完成,会检测是否存在等待执行的线程,如有,唤醒执行任务

  • 自旋锁:新thread会用死循环的方式一直等待锁定的代码执行完成,消耗性能

NSLocking 协议

public protocol NSLocking {
	public func lock()
	public func unlock()
}

遵循NSLocking协议,包括NSLock, NSCondition, NSConditionLock, NSRecursiveLock

NSLock

最常用的锁,lock & unlock, 注意需要在同一线程上调用

NSConditionLock 条件锁

确保线程仅在condition符合情况时上锁,并执行相应代码,然后分配新的状态

NSCondition 基本的条件锁

手动控制线程wait和signal

NSRecurisiveLock 递归锁

可以多次给相同线程上锁并不会造成死锁

GCD信号量(DispatchSemaphore)

持有计数的信号,计数为0时等待,不可通过,计数>=1时,计数减1且不等待,可通过

OC:

  1. dispatch_semaphore_create: 创建一个Semaphore并初始化信号的总量
  2. dispatch_semaphore_signal: 发送一个信号,让信号总量+1
  3. dispatch_semaphore_wait: 当信号总量为0时,就会一直等待(阻塞所在线程),否则就可以正常执行, 并使总信号量-1

Swift:

  1. DispatchSemaphore(value: 1) 初始化信号量的总量
  2. wait()使信号量减1,如果信号量大于0则返回.success,否则返回timeout
  3. signal()使信号量+1,返回当前信号量

@synchronized (OC的方法)

会对访问的变量加互斥锁

objc_sync_enter/objc_sysn_exit(Swift方法)

与OC的synchroned关键字类似,对某一个对象加互斥锁

自旋锁

  • OSSpinLock: iOS10后废弃
  • os_unfair_lock: iOS10新方法

二.OC

1.KVO的实现原理

KVC: Key-Value-Coding 给某个对象属性赋值/取值

方法:

  1. 点语法
  2. 私有属性:setValue:forKey / setValue:forKeyPath
  3. 字典转模型:setValueForKeysWithDictionary
KVO: 利用一个Key找到其属性并监听(观察者模式)
  • 使用步骤:

    1. 添加观察者 addObserver:forKeypath:options:context:
    2. 观察者实现监听方法
    3. 移除监听者
  • 底层实现:
    runtime机制动态创建被监听类的派生类,重写setter方法,在调用原setter方法之前和之后通知观察者值的改变,并将原被监听类的isa指针指向这个派生类

2.消息调用与转发的过程

详细:iOS 消息发送与转发详解

objc_msgSend(id self, SEL cmd, …)

首先要区分方法和消息的概念:

  1. 方法:一段实际代码 + 特定名字 + 方法类型
  • Method = SEL + IMP + method_types
  • SEL: 选择器,相当于char*, 看作方法的名字,所有类,方法名相同,产生的selector相同
  • IMP: 函数指针,指向方法实现的首地址
  1. 消息:发送给对象的名称和一组参数

消息发送的过程:

  1. 检查selector是否忽略,比如Mac OS X 开发有了垃圾回收旧就不会理会retain/release函数
  2. 检查selector的target是否为nil,OC允许对一个nil对象执行任何方法不会crash
  3. 查找这个类的实现IMP,先从cache查找,如有运行
  4. cache没有就找该类的方法列表是否有对应方法
  5. 类的方法列表没有就找其父类的方法列表中查找,一直找到NSObject为止
  6. 还没有就进入动态方法解析和消息转发

动态方法解析和消息转发:

当上述没有找到方法实现,程序在异常抛出前,runtime会有3次拯救的机会

  • Method resolution
  • Fast forwarding
  • Normal forwarding
  1. 动态方法解析:resolveInstanceMethod (实例方法) / resolveClassMethod(类方法), 在该方法内利用class_addMethod绑定,返回YES
  2. Fast forwarding: forwardingTargetForSelector 替换消息的接受者为其他对象,即将A类的某个方法,转发到B类的实现中去,如果return nil/self则进入第三完整转发,
  3. 完整转发: forwardInvocation / methodSignatureForSelector
    第一个方法转发具体实现,第二个方法返回一个方法签名,二者互相依赖,只有返回了正确的方法签名,才会执行另一个方法,与上者类似,都是将A类的某个方法转发B类的实现中,不同的是,它更灵活,前者只能固定转发到一个对象,后者能转发多个对象中去

3.RunLoop

iOS 多线程:『RunLoop』详尽总结
深入理解RunLoop
解密-神秘的 RunLoop
我认为的 Runloop 最佳实践

RunLoop在循环中用来处理程序运行过程中出现的各种事件,从而保持程序的持续运行

在没有事件处理时,会使线程进入睡眠模式,从而节省CPU资源,提高性能

RunLoop 和 线程

  1. 一条线程对应一个RunLoop对象
  2. RunLoop不保证线程安全,我们只能在当前线程内部操作当前线程的RunLoop对象
  3. RunLoop对象在第一次获取时创建,销毁则是线程结束的时候
  4. 主线程的RunLoop对象系统自动创建好了,而子线程的RunLoop则需要自行创建和维护
  • RunLoop 与 主线程
    UIApplicationMain自动开启了主线程的RunLoop,内部无限循环

RunLoop是线程中的一个循环,RunLoop会在循环中不断检测,通过Input sources(输入源)和Timer sources(定时源)两种来源等待接收事件,然后对接收到的事件通知的线程进行处理,并在没有事件的时候让线程休息

RunLoop 相关类

Core Foundation框架(括号为Swift写法):

  1. CFRunLoopRef(CFRunLoop): RunLoop对象
  2. CFRunLoopModeRef(CFRunLoopMode): RunLoop的运行模式
  3. CFRunLoopSourceRef(CFRunLoopSource): 输入源/事件源
  4. CFRunLoopTimerRef(CFRunLoopTimer): 定时源,基于时间的触发器
  5. CFRunLoopObserverRef(CFRunLoopObserver): 观察者,能监听RunLoop的状态变化
  • 相互关系:
    • 一个RunLoop对象包含若干个运行模式(RunLoopMode)
    • 一个运行模式包含若干输入源、定时源、观察者
    • 每次runloop启动只能指定其中一个模式,这个模式被称作当前运行模式
    • 切换运行模式,只能退出当前loop,再重新指定一个运行模式进入,保证不同组的输入源,定时源,观察者互不影响
CFRunLoopModeRef(CFRunLoopMode)
  • kCFRunLoopDefaultMode: 默认运行模式,通常主线程是在这个模式下运行
  • UITrackingRunLoopMode: 跟踪用户交互事件,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
  • kCFRunLoopCommonModes: 伪模式,不是一种真正的运行模式,一种标记模式,打上Common modes标记的模式下运行,kCFRunLoopDefaultMode和UITrackingRunLoopMode 都为标记上Common modes
CFRunLoopTimerRef(CFRunLoopTimer)

基于时间的触发器
基本上说得就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响
CCD的定时器不受RunLoop的Mode影响

CFRunLoopSourceRef(CFRunLoopSource)
  • 按官方文档分类:

    • Port-Base Source (基于端口)
    • Custom Input Sources (自定义)
    • Cocoa Perform Selector Sources
  • 按函数调用栈分类:

    • Source0: 非基于Port
    • Source1: 基于Port,通过内核和其他线程通信,接收、分发系统事件,再分发到Source0中处理
  • Source0: event事件,只含回调,需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop。

  • Source1: 包含了一个mach_port和一个回调,被用于通过内核和其他线程互相发送消息,能主动唤醒RunLoop线程

CFRunLoopObserverRef(CFRunLoopObserver)

可监听的状态变化有:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即将处理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即将进入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒:64
    kCFRunLoopExit = (1UL << 7),                // 即将从Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
};

RunLoop 处理逻辑

注:进入RunLoop前,会判断模式是否为空,为空直接退出

  1. 通知Observer:即将进入Loop
  2. 通知Observer:将处理Timer
  3. 通知Observer:将要处理Source0
  4. 处理Source0
  5. 如果有Source1,跳9
  6. 通知Observer: 线程即将休眠
  7. 休眠,等待唤醒,直到:
    * Source0
    * Timer
    * 外部手动唤醒
  8. 通知Observer: 线程被唤醒
  9. 处理唤醒时收到的消息,之后跳2
  10. 通知Observer: 即将退出Loop

应用

1.NSTimer的应用
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

上述代码会自动将定时器加入到RunLoop的默认模式下,相当于一下两句代码:

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

比如,图片轮播器,拖拽时模式从默认到Tracking,此时定时器不响应,停止轮播

2.ImageView

场景:用户再拖拽时不显示图片,拖拽完成时显示图片

  1. 监听ScrollView滚动
  2. RunLoop 设置只在默认模式下显示图片
3.PerformSelector

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

4.线程常驻

开启一个常驻线程,让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件

1.创建常驻线程 thread
2.对常驻线程运行一下代码

// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

3.需要常驻线程执行任务时,将任务放到该线程

5.自动释放池

在休眠前(kCFRunLoopBeforeWaiting)进行释放,处理事件前创建释放池,中间创建的对象会放入释放池

特别注意:
在启动RunLoop之前建议用 @autoreleasepool {…}包裹

意义:创建一个大释放池,释放{}期间创建的临时对象

4. autorealeasePool

自动释放池的前世今生 ---- 深入解析 autoreleasepool

黑幕背后的Autorelease

提供了一种可以向一个对象延迟发送 release 消息的机制,由若干个AutoreleasePoolPage以双向链表的形式组合而成

AutoreleasePoolPage

class AutoreleasePoolPage {
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};
  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • 每一个AutoreleasePoolPage的大小都是4096字节,这4096字节中,有56bit用于存储page的成员变量,剩下的用来存储加入到自动释放池中的对象
  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
  • 对象调用autorelease方法,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置
POOL_SENTINEL(哨兵对象)

哨兵对象是nil的别名

#define POOL_SENTINEL nil

每个自动释放池初始化调用时,都会把一个哨兵对象push到自动释放池的栈顶,并返回这个哨兵对象的地址

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

  1. 根据传入的哨兵对象地址找到哨兵对象所处的page
  2. 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
  3. 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
AutoreleasePool 流程

自动释放池就是若干个AutoreleasePoolPage以双向链表的形式实现

  1. objc_autoreleasePoolPush,往AutoreleasePoolPage中的next位置插入一个哨兵对象(POOL_SENTINEL),并返回它的内存地址

    • page不存在,创建第一个page,并将对象添加到这个新创建的page中
    • page存在且没有满,直接将对象添加到当前page中,即next指向的位置
    • page存在且已满,创建一个新的page,并将对象添加到新的page中,然后关联child page
  2. 添加autorelease对象

  3. objc_autoreleasePoolPop,将之前返回的哨兵对象传入pop函数,根据这个对象地址找到哨兵对象所在的page,然后对晚于哨兵对象插入的所有autorelease对象都发送依次release消息,并向回移动next指针(可以跨越若干page,直到哨兵所在的page)

Autorelease对象什么时候释放

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop休眠时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

5.讲一讲响应链

UIResponder
史上最详细的iOS之事件的传递和响应机制-原理篇

  1. 什么是响应者链: 一系列响应者对象构成 -》 响应者
  2. 为什么需要: 用户交互,触摸点击了怎么判断由谁来作出反应
  3. 怎么实现的: 构成响应者链条(视图树状结构构建的同时也构建了一条条事件响应链) -> 确定第一响应者 -> 响应顺序

响应者对象 UIResponder

继承了UIResponder的对象,能接受处理、传递事件

响应链

由一系列响应者对象构成的链条,能清楚地呈现每个响应者之间的联系,可让一个事件多个对象处理

确定第一个响应者

即事件传递机制

  1. 发生触摸/其他事件
  2. 系统将事件加入UIApplication管理的事件队列中,并将队列中最前面的事件分发出去
  3. 事件传递给主窗口keyWindow,再在视图层次结构中找一个合适的视图来处理事件
  4. 传递顺序:UIApplication -> UIWindow -> Superview -> Subview
  5. 不能接受事件情况:userInteractionEnable=NO, isHidden=NO, alpha<=0.01
  6. 通过hitTest:withEvent:方法来查找第一响应者
如何找到这个合适的视图
  1. 判断是否能接受事件
  2. 判断触摸点是否在自己身上,不在返回nil,再转3
  3. 从后往前遍历子控件(即从上面往下找)的hitTest:withEvent:方法,以此类推
  4. 直到找到点击区域最上面的子视图,并逐步返回给UIApplication

响应顺序

找到第一响应者后,应用程序会先调用第一响应者的处理事件,如果不能处理则调用nextResponder将事件传递给下一个响应者,其顺序:Subview -> Superview -> UIViewController -> UIWindow -> UIApplication

注:
UIViewController没有hitTest:withEvent:方法,所以控制器不参与查找响应视图的过程。但是控制器在响应者链中,如果控制器的View不处理事件,会交给控制器来处理。控制器不处理的话,再交给View的下一级响应者处理。

响应链与手势/UIControl

关于手势
  1. 手势的执行优先级高于响应者链
  2. 手势也通过hitTest方式查找响应视图
  3. 查找到第一响应者后,UIApplication向其派发消息,如果响应者链中存在能处理事件的手势,则手势响应事件,并执行touchesCancelled将响应者链打断
关于UIControl
  1. UIControl也通过hitTest查找第一响应者
  2. 如果第一响应者是UIControl,则Application直接派发事件,并不再向响应者链派发消息
  3. UIControl不能处理事件,再交给手势处理或响应者链传递

6.响应链相关问题

6.1. 如何通过一个view查找它所在的viewController

通过响应者链,循环查找nextResponder是否为UIViewController
 - (UIViewController *)parentController { UIResponder *responder = [self nextResponder]; while (responder) { if ([responder isKindOfClass:[UIViewController class]]) { return (UIViewController *)responder; } responder = [responder nextResponder]; } return nil; }

6.2.如何扩大view的响应范围

  1. pointInside:withEvent用来判断一个点是否在视图中,而这个方法是通过bounds来判断的,如果要扩大响应范围,可重写该方法,将判断bounds的范围扩大
  2. bounds扩大的范围如果大于superview,那么扩大的地方也是响应不到的

不规则点击区域判断也可重写该方法

6.3.一个事件多个对象处理

多个对象实现touches并调用super方法

7.属性

property = ivar + getter + setter 成员变量+存取方法

  • assign:
    1. 基本数据类型
    2. 直接赋值,不会更改值的引用计数
    3. 当引用计数为0时,对象销毁,编译器不会置为nil,指针仍指向被销毁内存, 产生野指针
  • weak:
    1. OC 对象
    2. 非拥有关系,不会更改值的引用计数
    3. 对象被销毁时,weak修饰属性自动赋值为nil
  • strong:
    1. 拥有关系,对旧值减少引用计数,新值增加引用计数
    2. 当一个对象不在有strong类型指针指向它,它就会被释放,即使还有weak指针
copy和strong的区别

前者深拷贝,赋值时,会对新变量的重新生成一份新的内存空间,后者浅拷贝,只是复制对象的指针

assign可以用于OC对象吗

可以,但是当OC对象的引用计数为0时,对象销毁,编译器不会置为nil,产生野指针

8. weak如何实现自动赋nil

iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)

Runtime对注册的类会进行内存布局,有个SideTable结构体是负责管理类的引用计数表和weak表,weak修饰的对象地址作为key存放到全局的weak引用表中,value是所有指向这个weak指针的地址集合,调用release会导致引用计数器减一,当引用计数器为0时调用dealloc,在执行dealloc时将会在这个weak的hash表中搜索,找到这个key的记录,将记录中所有附有weak修饰符的变量地址,设置为nil,并从weak表中删除记录。

SideTable

主要用于管理对象的引用计数和weak表

struct SideTable {
     spinlock_t slock;   // 保证原子操作的自旋锁
     RefcountMap refcnts; // 引用计数的 hash 表
     weak_table_t weak_table; // weak 引用全局 hash 表
 }

weak 表,全局弱引用表,使用不定类型对象的地址作为key,用weak_entry_t类型结构体对象作为value,weak_entry_t负责维护和存储指向一个对象的所有弱引用hash表

// weak 表
struct weak_table_t {
	weak entry_t *weak_entries;  // 保存了所有指向指定对象的weak指针
	size_t num_entries;  // 存储空间
	uintptr_t mask; // 参与判断引用计数辅助量
	uintptr_t max_hash_displacement; // 最大偏移量
}

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

9.Block

Block

函数指针+捕获上下文变量

block的类型

  • 全局块(_NSConcreteGlobalBlock),存于全局内存,相当于单例
  • 栈块 (_NSConcreteStackBlock),存于栈内存,超出其作用域马上被销毁
  • 堆块 (_NSConcreteMallocBlock),存于堆内存,是一个带引用计数对象,当引用计数为0时会被销毁
如何判断Block类型
  1. Block不访问外界变量(包括栈中和堆中的变量)
    Block既不在栈中也不在堆中,在代码段中,ARC和MRC皆是,此时为全局块
  2. Block访问外界变量
    MRC:访问外界变量的Block默认存储栈中
    ARC:访问外界变量的Block默认存储在堆中(实际在栈区,然后ARC情况下自动拷贝到堆区),自动释放

Block复制

配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题

ARC有效时,以下情况栈上的Block会自动复制到堆上:

  1. 调用block的copy方法
  2. 将block作为函数返回值时(MRC无效,需手动)
  3. 将block赋值给__strong修改的变量(MRC时无效)
  4. 向Cocoa框架含有usingBlock的方法或GCD的API传递Block参数时

其他情况向方法的参数中传递block时,需手动调用copy

捕获变量

  1. 默认情况
    对block外的变量引用,默认将其复制到数据结构中,存储在block的结构体内部,此时,block只能访问不能修改变量

  2. __block修饰外部变量
    block复制其引用地址来实现访问,可修改__blcok修饰的外部变量的值

原理:将栈上用__block修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问或修改同一份数据

循环引用

因为对象obj在Block被copy到堆上的时候自动retain了一次。因为Block不知道obj什么时候被释放,为了不在Block使用obj前被释放,Block retain了obj一次,在Block被释放的时候,obj被release一次。

retain cycle问题的根源在于Block和obj可能会互相强引用,互相retain对方,这样就导致了retain cycle,最后这个Block和obj就变成了孤岛,谁也释放不了谁。

10.isa、对象、类对象、元类和父类之间的关系?

(那张表isa/superclass 表)

  • 对象是类的一个实例,类对象是元类的一个实例,元类保存了类的类方法,当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如没有,则该元类会向它父类查找该方法,一直找到继承链根部,找不到就转发
  • 元类的isa指针指向一个根元类,根元类指向自己形成一个闭环
  • 类对象和元类对象有同样的继承关系
  • isa:objc_class 结构指针,在OC中,一个对象所属于哪个类,是由它的isa指针指向的

11. OC对象释放的流程

  • release: 引用计数器-1,直到0开始释放
  • dealloc:对象销毁的入口
  • dispose: 销毁对象和释放内存
    • objc_destruchInstance: 调用C++的清理方法和移除关联引用
      • clearDeallocating: 把weak置为nil,销毁当前对象的表结构
    • free: 释放内存

12.数据持久化

  • plist::XML文件,读写都是整个覆盖,需读取整个文件,适用于较少的数据存储,一般用于存储app设置相关信息
  • NSUserDefault:通过UserDefault对plist文件进行读写操作,作用和应用场景同上
  • NSKeyedArchiver: 被序列化的对象必须支持NSCoding协议,可指定任意数据存储位置和文件名,整个文件覆盖重写,读写大数据性能低
  • Sqlite:轻量级数据库
  • CoreData:大规模数据持久化方案,基本逻辑类似于SQL数据库,每个表为entity,可增删查改对象实例,提供模糊搜索、过滤搜索、表关联等各种复杂操作,但学习曲线高,操作复杂

13.Array和Set的区别

  • array:分配的时一片连续的存储单元,有序的,查找时需要遍历整个数组查找,查找速度不如hash
  • set:不是连续的存储单元,数据无序,通过hash映射存储的位置,直接对数据hash即可判断对应的位置是否存在,查找速度快,不允许重复数据
  • 遍历速度:array的数据结构时一片连续的内存单元,读取速度快,set不是连续的非线性的,读取速度慢

14.nil、Nil、NULL、NSNull的区别

nil:空实例对象(给对象赋空值)
Nil:空类对象(Class class = Nil)
NULL:指向C类型的空指针
NSNull:类,用于空对象的占位符(用于替代集合中的空对象,还有判断对象是否为空对象)

15.loadView的作用

在UIViewController对象的view属性被访问到且为空的时候调用

用来自定义view,只要实现了这个方法,其他通过xib或storyboard创建的view都不会被加载,不能调用super方法

16. layoutIfNeeded和setNeedsLayout的区别

UIView的setNeedsLayout,layoutIfNeeded等方法介绍

layout机制相关方法:

  • layoutSubviews: 内部调整子视图时重写,若在外部设置subviews位置则不写(iOS6前方法缺省实现为空,iOS6后缺省实现是使用在此view上的constraints,即auto layout)
  • layoutIfNeeded: 如有需要刷新标记,立即调用layoutSubviews进行布局,此方法会遍历整个view层次请求layout(没有标记,则不掉用layoutSubviews)
  • setNeedsLayout: 标记为需要重新布局,不立即刷新,在系统runloop的下一个周期自动调用layoutSubviews(layoutSubviews一定会被调用)

1.如果要立即刷新,先调用setNeedsLayout,标记为需要布局,再调用layoutIfNeeded,实现布局
2.视图第一次显示之前默认标记需要刷新
3. layoutIfNeeded不一定会调用layoutSubviews,但setNeedsLayout一定会调用layoutSubviews


  • sizeThatFits:传入的参数是receiver当前的size,返回一个适合的size
  • sizeToFit: 自动调用sizeThatFits方法,不应在子类中重写,应重写sizeThatFits,可手动直接调用

以上两个方法没有递归,对subviews不负责,只负责自己


  • drawRect: 重写此方法,执行重绘任务
  • setNeedsDisplay: 标记为需要重绘,异步调用drawRect,在下一个draw周期自动重绘,iPhone设备的刷新频率是60hz,即1/60秒后重绘
  • setNeedsDisplayInRect: 标记为需要局部重绘

layoutSubviews触发条件:

  1. init初始化不触发,但initWithFrame且rect不为zero时会触发;
  2. addSubview
  3. 设置frame且frame有变化
  4. 滚动UIScrollView
  5. 旋转Screen触发父UIView上的layoutSubviews
  6. 改变UIView大小也会触发父UIView上的layoutSubviews
  7. 直接调用setLayoutSubviews
  8. 直接调用setNeedsLayout

drawRect触发条件:

  1. UIView初始化时未设置rect大小,则不自动调用,调用顺序在Controller->loadView,viewDidLoad方法之后
  2. sizeToFit后被触发(可先调用sizeToFit计算size再系统自动调用drawRect方法)
  3. 设置contentMode为UIViewContentModeRedraw,则每次设置或更改frame时自动调用drawRect
  4. 直接调用setNeedsDisplay/setNeedsDisplayInRect(rect不为0)触发

以上1,2推荐,3,4不提倡

基于约束的AutoLayer方法:

  • setNeedsUpdateConstraints: 当view的某个属性改变,并可能影响到constrain时,需效用此方法去标记constrains需要再未来的某个点刷新,系统然后调用updateConstraints
  • needsUpdateConstraints:
  • updateConstraintsIfNeeded: 立即触发约束布局,自动更新布局
  • updateConstraints: view重写此方法在其中建立constraints(在实现最后调用[super updateConstraints])

17.UIView和CALayer的区别

  • 都是UI操作的对象,都是NSObject的子类,发生在UIView上的操作本质上也发生在对应的CALayer上
  • CALayer时绘制内容的,UIView是CALayer用于交互的抽象,UIView是UIResponder的子类,提供了很多CALayer没有的交互接口,主要负责处理用户触发的种种操作
  • CALayer在图像和动画渲染上性能更好,因为UIView有冗余的交互接口,而且相比CALayer还有层级之分,CALayer在无需处理交互时进行渲染可节省时间

18.applicationWillEnterForeground和applicationDidBecomeActive都会在哪些场景下被调用?

App 生命周期

  • Not running (未运行) : 程序未启动
  • Inactive(未激活): 激活和后台状态切换时出现的短暂状态
  • Active (激活):在屏幕显示的正常运行状态,该状态下可接收用户输入并更新显示
  • Backgroud (后台): 程序在后台且能执行代码。
  • Suspended (刮起) :程序在后台不能执行代码

Inactive/Active的切换:
一般:前后台应用切换,Inactive会在Active和Background之间短暂出现

其他:Active和Inactive在前台运行时切换,比如来电拒接、拉下通知栏、系统弹出Alert

AppDelegate协议

  • application:didFinishLaunchingWithOptions: 程序首次已完成启动时执行
  • applicationWillResignActive(将进入后台): 程序将要失去Active状态,比如按下Home键、来电,这个方法用来:
    • 暂停正在执行的任务
    • 禁止计时器
    • 减少OpenGL ES帧率;
    • 若未游戏应暂停游戏
  • applicationDidEnterBackgroud(已进入后台) :进入后台时调用,这个方法用来:
    • 释放共享资源
    • 保存用户数据(写到硬盘)
    • 作废计时器
    • 保存足够的程序状态以便下次恢复
  • applicationWillEnterForeground(将进入前台) :用来撤销applicationWillResignActive
  • applicationDidBecomeActive (已经进入前台) :若程序之前在后台,最后在此方法内刷新用户界面
  • applicationWillTerminate: 程序将退出时调用,记得保存数据

19.CocoaPods的工作原理

将所有依赖库放到一个名为Pods的项目中,然后让主项目依赖Pods项目,使源码工作从主项目移到了Pods项目中,Pods项目最终会编译一个libPods.a 的文件,主项目只需要依赖这个.a文件即可

使用CocoaPods前引用第三方:
1.复制依赖库的源码
2.添加依赖框架/动态库
3.设置参数
4.管理更新

20.ARC的工作原理

深入理解Objective C的ARC机制

ARC

auto reference count 自动引用计数,编译器会自动插入对应的代码,再结合Objective C的runtime,实现自动引用计数

内部实现:

  • retain 增加引用计数
  • release 降低引用计数,引用计数为0的时候,释放对象
  • autorelease 在当前autorealsepool释放后,进行release
SideTable内部实现
- (id)retain {
    return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

其本质是调用sidetable_retain

id objc_object::sidetable_retain()
{
    //获取table
    SideTable& table = SideTables()[this];
    //加锁
    table.lock();
    //获取引用计数
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
         //增加引用计数
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    //解锁
    table.unlock();
    return (id)this;
}

SideTable结构

struct SideTable {
    spinlock_t slock; // 自旋锁
    RefcountMap refcnts; // 引用计数表,以地址作为key,引用计数的值作为value
    weak_table_t weak_table; //weak 表
     //省略其他实现...
};

release的实现

  SideTable& table = SideTables()[this];
    bool do_dealloc = false;
    table.lock();
    //找到对应地址的
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) { //找不到的话,执行dellloc
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {//引用计数小于阈值,dealloc
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
    //引用计数减去1
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        //执行dealloc
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
Autorelease
//autorelease方法
- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

//rootAutorelease 方法
inline id objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;

    //检查是否可以优化
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    //放到auto release pool中。
    return rootAutorelease2();
}

// rootAutorelease2
id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

autorelease方法会把对象存储到AutoreleasePoolPage的链表里。等到auto release pool被释放的时候,把链表内存储的对象删除。所以,AutoreleasePoolPage就是自动释放池的内部实现。

21. 进程间通信

  1. URL Scheme
  2. Keychain
  3. UIPasteboard
  4. UIDocumentInteractonController
  5. local socket
  6. AirDrop
  7. UIActivityViewController
  8. App Groups

CFMessagePort
mach port

三.计算机网络

1. get和post的区别

在使用上:
  • 1.get提交的数据有长度限制,而post没有,这是浏览器设置的不同引起的
  • 2.get通过URL传递参数,而post放在body中
  • 3.post比get安全,因为数据放在URL上
在本身上:

get请求是幂等性的,即同一个URL的多个请求返回同样的结果,而post不是(因为get是幂等的,在网络不好的隧道中会尝试请求,如果用get来请求增删改数据,会有重复操作的风险)

2. 为什么说TCP是面向字节流,而UDP是面向报文

  • UDP面向报文,发送方的UDP对应用层交付下来的报文,在添加首部后直接向下发送交付给IP层,不拆分也不合并,保留报文边界,所以需要选择合适的报文大小;而接收方的UDP收到报文后,拆除首部就原封不动地交付上层的应用层,一次交付一个报文
  • TCP将数据看成一连串无结构的字节流,TCP有一个缓冲区,当应用层传送的数据库太大,TCP可把它可它划分短一点再传,若太小,也可累积足够多的字节后再构成报文发送出去

3. HTTPS是如何保证安全的/SSL建立连接的过程/数字证书怎么验证

你可能感兴趣的:(iOS)