整理了些iOS相关的基础问题,每个问题可能会再写些扩展,需要具体了解可以看题目下方的链接
如有错漏,欢迎指出,谢谢
一.Swift
1.给一个数组,要求写一个函数,交换数组中的两个元素(swift可用元组)
func swap<T>(_ 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
- 根本目的:暴露接口给OC的运行时
- 添加
@objc
并不意味着这个方法或属性会采用OC的方式变成动态派发,Swift仍有可能将其优化为静态调用 @objc
可修改Swift接口暴露到OC后的名字
- dynamic
用于表明使用runtime机制,可动态调用
加了@objc标识的方法、属性无法保证都会被运行时调用,因为Swift会做静态优化。要想完全被动态调用,必须使用dynamic修饰。使用dynamic修饰将会隐式的加上@objc标识。
swift中的函数是静态调用,静态调用的方式会更快,但是静态调用的时候就不能从字符串查找到对于的方法地址,这样 与OC交互的时候,OC动态查找方法就会找不到,这个时候就可以通过使用 dynamic 标记来告诉编译器,这个方法要被动态调用的
4.OC与Swift区别
- 大的方面:
Swift是静态类型语言,OC是动态类型语言。
OC堆类型要求不严格,而Swift格外严格,是类型安全的语言,他不仅对类型是否匹配严格,也对一个类型是否有值严格
编程思想上,用Swift编程的时候不能套用OC的编程思想,着重于函数式编程/面向协议编程
当然,Swift和OC可以混编,从OC过渡Swift比较简单,有很多地方是相通的
Swift优势:
1.语法易读,文件结构更简单
2.更安全,强类型语言(默认类型安全)
3.代码更少,省去大量冗余代码
4.速度更快,运算性能更高
- 小细节的方面:
- Swift 类型,值类型/引用类型
- Swfit get/set方法,引入存储属性、计算属性
- 严格的初始化
- 面向协议编程,Swift对Protocol有更好的支持,比如继承、
- 泛型
- 抛弃了C语法的 ++ --, switch的break
- struct和class类型很像
- 重载运算法
- 函数式编程
扩展:
- 强/弱类型:指语言类型系统的类型检查严格程度,强类型偏向不容忍隐式类型转换,而弱类型不行
- 静态/动态类型:指变量与类型的绑定方法,前者是编译器在编译阶段执行类型检查,而后者编译器在运行时执行类型检查,即声明一个变量后,不能改变它的类型的语言,是静态语言,能随时改变它类型的语言是动态语言
5.protocol 与 extension
protocol
定义某种约定,来表示共性,而不是类型,相比OC,不仅用做代理,也可用作对接口的抽象,代码的复用
-
协议内定义属性/方法/构造器
- 定义属性 {get set} / 类型, 不能设置默认值
- 定义方法(参数不能有默认值,没有实现)
- 构造器
-
协议是一种类型
- 作为函数/构造器中的参数或返回值类型
- 作为常量/变量/属性类型
- 作为数组/字典或其他容器中的元素类型
-
用于委托模式
- 委托模式允许某个类型的部分指责转移到另一个类型的实例来实现
- 可通过协议来实现委托模式
-
protocol-extension
- 对实例使用,令已有类型遵循某个协议
- 对协议使用,可遵循其他协议
- 提供默认实现,相当于变相设定成了optional
- 搭配where使用,可增加限制条件,限制类型
-
协议的继承
- 可继承一个或多个其他协议
- 使用
&
关键字同时遵循多个协议 - 协议通过继承AnyObject,使其被限制只适用于class类型
-
检查一致性
- 使用
is
检查某个实例是否符合协议 - 使用
as?
返回协议类型的可选值 - 使用
as!
强制转换为协议类型
- 使用
-
可选协议
- 关键字
optional
- 协议及可选项使用
@objc
标记 - 结构体/枚举不能使用,只能由继承OC类或@objc类使用
- 关键字
-
关联类型
面向协议编程
依赖倒置原则:
高层模块不依赖于低层模块,二者依赖于抽象
抽象不依赖于细节,细节依赖于抽象
- 面向对象编程呈现的是金字塔结构,面向协议编程提倡的是扁平化和去嵌套的代码
- Swift中,协议定义了方法、属性的蓝图,然后类、结构体或枚举都能够使用协议,使用继承的思想,模拟了多继承关系,不存在is-a关系
- 将与类型无关的共性从类型定义上移出去,用一个协议封装好,让它成为这个类型的一种修饰
- 如果某个类型有多个修饰,那么使用多个协议对其修饰,大大降低了代码的耦合度
- 依赖反转/接口分离
extension
- 为class/struct/enum或protocol添加新特性(计算属性、方法、初始化方法)
注意,添加新的,但不能覆盖已有的特性
- 添加计算属性(),但不能添加存储属性,也不能添加属性观察者
- 添加构造器,但需保证该类型的初始化方法结束时,每一个属性都被完全初始化了
- 添加实例方法/类方法
- 添加mutating方法(如果struct和enum定义的方法想改变自身或自身的属性,那么实例方法必须标记为突变的)
- 添加附属脚本-subscripts(一种访问的对象/集合的快捷方式,如array[index])
- 添加嵌套类型-nested types,如给结构体嵌套枚举类型
- 可让某个类型实现一个或多个协议
6. struct 和 class 什么区别
Swift 浅谈Struct与Class
理解Swift中struct和class在不同情况下性能的差异
深拷贝&浅拷贝本质
- 是否开启新的内存地址
- 是否影响内存地址的引用计数
- struct是值类型,class是引用类型
- struct不能继承,class可以继承
- struct比class更"轻量级",前者分配在栈上,后者分配在堆上
- struct有默认的带参数的构造函数,class无
- struct无析构,class有
struct作为数据模型注意事项
- 安全性:值类型,没有引用计数,不会导致内存泄漏
- 速度:以栈的形式分配(编译时分配空间),而不是堆(运行时分配),速度更快
- 拷贝:引用类型拷贝需注意深拷贝/浅拷贝,值类型拷贝更轻松
- 线程安全:值类型是自动线程安全的
缺点:
- 与OC混编时,OC无法调用Swift的struct(因为OC调用的对象需继承NSObject)
- 不能相互继承
如何抉择
选择值类型:
- 要用==运算符来比较实例的数据时
- 希望实例的拷贝能保持独立的状态
- 数据被多个线程使用
选择引用类型:
- 需要使用NSObject相关功能时,必须用引用类型class
- 要用==运算符比较实例身份时
- 希望创建一个共享、可变对象
模型较小,无需继承、无需OC使用,建议使用Struct
值类型与引用类型的嵌套
- 值类型嵌套值类型: 内部值是外部值的一部分,拷贝外部值到新的空间,也会拷贝内部值
- 值类型嵌套引用类型:外部值被拷贝到新的内存区域,但内部的引用类型只被拷贝了内部值的引用
- 引用类型嵌套引用类型:复制时只是拷贝了引用,新/原变量都指向同一个实例
- 引用类型嵌套值类型:与引用类型嵌套引用类型一样
7. copy on write 写时复制
Swift Copy-On-Write 写时复制
只有当这个值需要改变时才进行复制行为
在结构体内部存储了一个指向实际数据的引用reference,在不进行修改操作的普通传递过程中,都是将内部的reference的引用计数+1,在进行修改时,对内部的reference做一次copy操作,再在这个复制出来的数据进行真正的修改,防止和之前的reference产生意外的数据共享
值类型嵌套引用类型的写时复制
- 私有化,让外部无法对引用类型进行修改
- 另提供一个接口控制这个引用类型写入操作(使用
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.闭包/逃逸闭包/自动闭包
闭包
- 闭包会自动捕获外部变量的引用
- 可在闭包内修改变量的值(声明为var)
- 可通过捕获列表来获取变量中的内容,存储到本地常量中
- 默认闭包行为更像是在OC中使用
__block
逃逸闭包/非逃逸闭包/自动闭包
-
逃逸闭包:
一个接受闭包为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域(例如:网络请求在请求结束后才调用闭包,并不一定是在函数作用域内执行)
-
非逃逸闭包:
一个接受闭包为参数的函数,闭包在这个函数结束前内被调用
闭包会强引用它捕获的所有对象,比如在闭包中访问了当前控制器的属性、函数,这样闭包会持有当前对象,容易导致循环引用
非逃逸闭包不会产生循环引用,它会在函数作用域内释放,编译器可保证在函数结束时闭包会释放它捕获的所有对象,非逃逸闭包它的上下文的内存可保存在栈上而不是堆上
-
自动闭包:简化参数传递,延迟执行时间
- 一种自动创建的闭包,包装传递给函数作为参数的表达式,不接受任何参数,被调用时,返回被包装在其中的表达式的返回值
- 普通表达式与
@autoclosure
的区别:前者传入参数时,会马上被执行,然后将执行结果作为参数传递给函数,而后者不会立马执行,而是由调用的函数内来决定它具体执行时间
weak & unowned 处理循环引用
弱引用:不会被ARC计算,引用计数不会增加
-
unowned
:- 捕获的原始实例永远不会为nil,闭包可直接使用它,并直接定义为显示解包可选值,如原始实例被析构后,在闭包中使用这个捕获值将导致奔溃
- 闭包和捕获对象的生命周期相同,所以对象可以被访问,也意味着闭包也可以被访问
[unonwed self]
-
weak
:- 捕获的实例在使用过程中可能为nil,必须将引用声明为
weak
,并在使用之前验证这个引用的有效性; - 闭包的生命周期和捕获对象的生命周期相互独立,当对象不能再使用时,闭包依然能够被引用
- 捕获的实例在使用过程中可能为nil,必须将引用声明为
使用unowned
引用不会去验证引用对象的有效性,weak
引用添加了附加层,间接得把unowned
引用包裹到了一个可选容器里面,在指向的对象析构之后变成空的情况下,处理更清晰,而这附加的机制需要正确处理可选值
弱应用的实现原理
OC 和 Swift 的弱引用源码分析
Swift 4 弱引用实现
Swift4之前旧实现:
Swift对象有两个引用计数:强引用计数和弱引用计数,当强引用计数为0而弱引用计数不为0时,对象会销毁,但内存不会被立即释放,内存中会保留弱引用指向的僵尸对象,在加载弱引用时,运行时会对引用对象进行检查,如果是僵尸对象,则会弱引用计数进行递减操作,一旦弱引用计数为0,对象内存将会被释放。
问题:
如果对象的弱引用数一直不为零,那么对象占用的剩余内存就不会完全释放。这些死而不僵的对象还占用很多空间的话,累积起来也是对内存造成浪费
Swift4后:
SideTable机制:与OC不同的是,系统不再把它作为全局对象使用
- 针对有需要的对象创建,为目标对象分配一块新的内存来保存该对象额外的信息(SideTable可有可无),对象会有一个指向SideTable的指针,同时SideTable也有一个指回原对象的指针
- 为了不额外多占用内存,只有创建弱引用时,会把对象的引用计数放到新创建的SideTable,在把空出来的空间存放SideTable的地址,而runtime会通过一个标志位来区分对象是否有SideTable
- 弱引用从指向对象本身改为指向SideTable
10.map、flatMap/compactMap、filter、reduce
- map/flatMap都可以用在Optional和SequenceTypes上
- 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<Wrapped> {
case none
case some(Wrapped)
}
泛型枚举
15. ?/ !/ ?? 的作用
?
1.声明时添加?,告诉编译器是可选值(表示一个变量可能有值,也可能没有值为nil),自动初始化为nil
2.对变量操作前加?,判断如果变量为nil,则不响应后面的方法
!
1.声明时添加!,告诉编译器是可选值,并且之后对变量操作时,都隐式在操作前添加!
2.对变量操作前加!,表示默认为非nil,直接解包处理
??
设置默认值,判断变量是否为nil,如果不为nil,则对该变量解包,否则用??后面的默认值
16.lazy / inout 的作用
-
lazy: 延迟初始化,当变量在用到的时候才加载(全局变量不用lazy也是懒加载)
-
inout:
- 方法的参数默认不可改变类型,方法内部改变参数值会导致编译错误,需要改变参数值时,需要使用inout关键字(传递的参数不能为let,不能有默认值)
- 传递过程:方法调用->参数值被拷贝->方法体内部,被改变的是拷贝的值->方法结束,拷贝的值重新分配给原来的参数
17.什么是高阶函数
其参数或者返回值是闭包的函数,如sort、map、filter
18.mutating 的作用是什么
对结构体、枚举,mutating用于表示某个实例方法可以改变自身实例或者实例中的属性的函数
对协议,用于那些会改变遵循该协议的类型的实例的函数
19.一个 Sequence 的索引是不是一定从 0 开始?
不是
ArraySlice是Sequence的子类,ArraySlice就不是
20.defer使用场景
作用:提供一种延时调用的方式,defer内的代码块会在当前作用域结束之后执行,代码块会被压入栈中,待函数结束时弹出栈运行。
其目的就是进行资源清理和避免重复的返回前需要执行的代码
注意:前提是必须执行到defer才会触发,多个defer,按栈的后进先出顺序执行
-
try catch结构:相当于finally
-
清理、回收资源,例如:加解锁
lock.lock() defer { lock.unlock() }
-
调super方法:override一些方法时,需要在super方法前写,比如autolayout的约束写动画,重写updateContaints方法,可以用defer将super方法调用写在前面
-
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
- 元类型:类型的类型
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 ),用什么类型表示
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.给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明
extension Set where Iterator.Element == String {
func test() { }
}
["", ""].test()
25.不通过继承,代码复用(共享)的方式有哪些
- extension
- protocol
使用继承可能的问题:
- 代码在多个子类中重复
- 很难知道所有子类的全部行为
- 子类很多时候会有不同的行为
- 改变父类牵一发而动全身
(又要扯到依赖倒置原则了)
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
- 优点:
- 可用于多核的并行运算
- 自动利用更多的CPU内核
- 自动管理线程的生命周期(创建、调度任务、销毁线程)
- 任务: 执行操作,即线程中执行的那段代码
队列(Dispatch Queue)
指执行任务的等待队列,即用来存放任务的队列(FIFO)
- 串行队列:每次只有一个任务执行,一个任务执行完毕后再执行下一个任务
- 并发队列:让多个任务同时执行(并发队列的并发只有在异步有效)
- 主队列(串行):所有放在主队列的任务都会在主线程中执行
- 全局队列:并发队列
区别 | 串行队列 | 并发队列 | 主队列 |
---|---|---|---|
同步 | 当前线程执行,不开启新线程,串行执行任务 | 当前线程执行,不开启新线程,串行执行任务 | 主线程调用:死锁卡住不执行;其他线程调用:不开启新线程,串行执行任务 |
异步 | 开启新线程,串行执行任务 | 可开启多个线程,并发执行任务(无序执行,多条线程) | 不开启新线程,串行执行任务(任务在同一线程) |
GCD 栅栏
异步执行一组任务 -> barrier任务 -> 异步执行另一组任务
// OC
dispatch_barrier_async
// Swift
let item = DispatchWorkItem(qos: .default, flags: .barrier) {
}
队列组 group
- 多个任务并发无序执行
- 完成上述任务后在回到主线程执行任务
// 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:
dispatch_semaphore_create
: 创建一个Semaphore并初始化信号的总量dispatch_semaphore_signal
: 发送一个信号,让信号总量+1dispatch_semaphore_wait
: 当信号总量为0时,就会一直等待(阻塞所在线程),否则就可以正常执行, 并使总信号量-1
Swift:
DispatchSemaphore(value: 1)
初始化信号量的总量wait()
使信号量减1,如果信号量大于0则返回.success
,否则返回timeout
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)
优点:
- 基于GCD的更高一层封装,易用,代码可读性高
- 可添加完成的代码块,在操作完成后执行
- 添加操作之前的依赖关系,方便控制执行顺序
- 设定操作执行的优先级
- 很方便地取消一个操作的执行
- 使用KVO观察
-
Operation(操作):
- 执行操作,即在线程中执行的那段代码
- GCD放在block中,在Operation中我们使用其子类NSInvocationOperation,NSBlockOperation或自定义子类来封装
-
OperationQueue(操作队列):
- 存放操作的队列,不同于GCD的调度队列是FIFO,操作队列对于添加到队列的操作,首先进入准备就绪的状态(取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定
- 通过设置
maxConcurrentOperationCount
最大并发操作数控制并发、串行 - 有两种队列:主队列和自定义队列,主队列运行在主线程上,自定义队列在后台执行
步骤:
- 创建操作
- 创建队列
- 将操作加入到队列中
之后,系统自动将操作队列中的操作取出,在新线程中执行操作
操作 Operation
- 使用子类 NSInvocationOperation(只有OC有,swift没有)
- 使用子类 NSBlockOperation
- 自定义继承 NSOperation子类,通过实现内部相应方法来封装操作
不使用OperationQueue的情况
-
在没使用OperationQueue,在主线程中单独使用子类NSInvocationOperation、NSBlockOperation或自定义继承Operation子类执行一个操作的情况下,操作是在当前线程执行,没有开启新线程
-
BlockOperation与InvocationOperation类似,但还多提供了一个方法
addExecutionBlock
来添加额外的操作,额外操作是在不同线程中异步执行的 -
一般情况下,如果BlockOperation封装多个操作,是否开启新线程,取决于操作个数,开启的线程数由系统决定
使用OperationQueue的情况
OperationQueue 有两种队列:主队列、自定义队列(包含串行、并发功能)
-
主队列:凡是添加到主队列的操作,都会放到主线程中执行(不包括
addExecutionBlock
添加的额外操作,可能在其他线程执行) -
自定义队列:在自定队列中会自动放到子线程中执行,包含串行、并发功能
-
maxConcurrentOperationCount
:控制的不是并发线程的数量,是一个队列中同时能并发执行的最大操作数,开启线程数量由系统决定- 默认为-1,表示不限制,可进行并发执行
- =1,队列为串行队列,只能串行执行
-
1, 队列为并发队列,操作并发执行,其值为min(自己设定的值,系统设定默认最大值)
依赖/优先级
29.线程安全
-
线程安全:当一段代码被多个线程执行,执行后的结果和多个线程依次执行后的结果一致,那么这段代码就是线程安全
-
互斥锁: 当新线程访问,发现有线程正在执行锁定代码,新线程进入休眠,避免占用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:
dispatch_semaphore_create
: 创建一个Semaphore并初始化信号的总量dispatch_semaphore_signal
: 发送一个信号,让信号总量+1dispatch_semaphore_wait
: 当信号总量为0时,就会一直等待(阻塞所在线程),否则就可以正常执行, 并使总信号量-1
Swift:
DispatchSemaphore(value: 1)
初始化信号量的总量wait()
使信号量减1,如果信号量大于0则返回.success
,否则返回timeout
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 给某个对象属性赋值/取值
方法:
- 点语法
- 私有属性:setValue:forKey / setValue:forKeyPath
- 字典转模型:setValueForKeysWithDictionary
KVO: 利用一个Key找到其属性并监听(观察者模式)
-
使用步骤:
- 添加观察者 addObserver:forKeypath:options:context:
- 观察者实现监听方法
- 移除监听者
-
底层实现:
runtime机制动态创建被监听类的派生类,重写setter方法,在调用原setter方法之前和之后通知观察者值的改变,并将原被监听类的isa指针指向这个派生类
2.消息调用与转发的过程
objc_msgSend(id self, SEL cmd, …)
首先要区分方法和消息的概念:
- 方法:一段实际代码 + 特定名字 + 方法类型
- Method = SEL + IMP + method_types
- SEL: 选择器,相当于char*, 看作方法的名字,所有类,方法名相同,产生的selector相同
- IMP: 函数指针,指向方法实现的首地址
- 消息:发送给对象的名称和一组参数
消息发送的过程:
- 检查selector是否忽略,比如Mac OS X 开发有了垃圾回收旧就不会理会retain/release函数
- 检查selector的target是否为nil,OC允许对一个nil对象执行任何方法不会crash
- 查找这个类的实现IMP,先从cache查找,如有运行
- cache没有就找该类的方法列表是否有对应方法
- 类的方法列表没有就找其父类的方法列表中查找,一直找到NSObject为止
- 还没有就进入动态方法解析和消息转发
动态方法解析和消息转发:
当上述没有找到方法实现,程序在异常抛出前,runtime会有3次拯救的机会
- Method resolution
- Fast forwarding
- Normal forwarding
- 动态方法解析:
resolveInstanceMethod (实例方法) / resolveClassMethod(类方法)
, 在该方法内利用class_addMethod
绑定,返回YES
- Fast forwarding:
forwardingTargetForSelector
替换消息的接受者为其他对象,即将A类的某个方法,转发到B类的实现中去,如果return nil/self则进入第三完整转发, - 完整转发:
forwardInvocation / methodSignatureForSelector
第一个方法转发具体实现,第二个方法返回一个方法签名,二者互相依赖,只有返回了正确的方法签名,才会执行另一个方法,与上者类似,都是将A类的某个方法转发B类的实现中,不同的是,它更灵活,前者只能固定转发到一个对象,后者能转发多个对象中去
3.RunLoop
iOS 多线程:『RunLoop』详尽总结
深入理解RunLoop
解密-神秘的 RunLoop
我认为的 Runloop 最佳实践
RunLoop在循环中用来处理程序运行过程中出现的各种事件,从而保持程序的持续运行
在没有事件处理时,会使线程进入睡眠模式,从而节省CPU资源,提高性能
RunLoop 和 线程
- 一条线程对应一个RunLoop对象
- RunLoop不保证线程安全,我们只能在当前线程内部操作当前线程的RunLoop对象
- RunLoop对象在第一次获取时创建,销毁则是线程结束的时候
- 主线程的RunLoop对象系统自动创建好了,而子线程的RunLoop则需要自行创建和维护
- RunLoop 与 主线程
UIApplicationMain
自动开启了主线程的RunLoop,内部无限循环
RunLoop是线程中的一个循环,RunLoop会在循环中不断检测,通过Input sources(输入源)和Timer sources(定时源)两种来源等待接收事件,然后对接收到的事件通知的线程进行处理,并在没有事件的时候让线程休息
RunLoop 相关类
Core Foundation框架(括号为Swift写法):
- CFRunLoopRef(CFRunLoop): RunLoop对象
- CFRunLoopModeRef(CFRunLoopMode): RunLoop的运行模式
- CFRunLoopSourceRef(CFRunLoopSource): 输入源/事件源
- CFRunLoopTimerRef(CFRunLoopTimer): 定时源,基于时间的触发器
- 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前,会判断模式是否为空,为空直接退出
- 通知Observer:即将进入Loop
- 通知Observer:将处理Timer
- 通知Observer:将要处理Source0
- 处理Source0
- 如果有Source1,跳9
- 通知Observer: 线程即将休眠
- 休眠,等待唤醒,直到:
* Source0
* Timer
* 外部手动唤醒 - 通知Observer: 线程被唤醒
- 处理唤醒时收到的消息,之后跳2
- 通知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
场景:用户再拖拽时不显示图片,拖拽完成时显示图片
- 监听ScrollView滚动
- 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
提供了一种可以向一个对象延迟发送 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(哨兵对象)作为入参,于是:
- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
- 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
AutoreleasePool 流程
自动释放池就是若干个AutoreleasePoolPage以双向链表的形式实现
-
objc_autoreleasePoolPush
,往AutoreleasePoolPage中的next位置插入一个哨兵对象(POOL_SENTINEL),并返回它的内存地址- page不存在,创建第一个page,并将对象添加到这个新创建的page中
- page存在且没有满,直接将对象添加到当前page中,即next指向的位置
- page存在且已满,创建一个新的page,并将对象添加到新的page中,然后关联child page
-
添加autorelease对象
-
objc_autoreleasePoolPop
,将之前返回的哨兵对象传入pop函数,根据这个对象地址找到哨兵对象所在的page,然后对晚于哨兵对象插入的所有autorelease对象都发送依次release消息,并向回移动next指针(可以跨越若干page,直到哨兵所在的page)
Autorelease对象什么时候释放
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop休眠时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
5.讲一讲响应链
UIResponder
史上最详细的iOS之事件的传递和响应机制-原理篇
- 什么是响应者链: 一系列响应者对象构成 -》 响应者
- 为什么需要: 用户交互,触摸点击了怎么判断由谁来作出反应
- 怎么实现的: 构成响应者链条(视图树状结构构建的同时也构建了一条条事件响应链) -> 确定第一响应者 -> 响应顺序
响应者对象 UIResponder
继承了UIResponder的对象,能接受处理、传递事件
响应链
由一系列响应者对象构成的链条,能清楚地呈现每个响应者之间的联系,可让一个事件多个对象处理
确定第一个响应者
即事件传递机制
- 发生触摸/其他事件
- 系统将事件加入UIApplication管理的事件队列中,并将队列中最前面的事件分发出去
- 事件传递给主窗口
keyWindow
,再在视图层次结构中找一个合适的视图来处理事件 - 传递顺序:UIApplication -> UIWindow -> Superview -> Subview
- 不能接受事件情况:
userInteractionEnable=NO
,isHidden=NO
,alpha<=0.01
- 通过
hitTest:withEvent:
方法来查找第一响应者
如何找到这个合适的视图
- 判断是否能接受事件
- 判断触摸点是否在自己身上,不在返回nil,再转3
- 从后往前遍历子控件(即从上面往下找)的
hitTest:withEvent:
方法,以此类推 - 直到找到点击区域最上面的子视图,并逐步返回给
UIApplication
响应顺序
找到第一响应者后,应用程序会先调用第一响应者的处理事件,如果不能处理则调用nextResponder
将事件传递给下一个响应者,其顺序:Subview -> Superview -> UIViewController -> UIWindow -> UIApplication
注:
UIViewController没有hitTest:withEvent:方法,所以控制器不参与查找响应视图的过程。但是控制器在响应者链中,如果控制器的View不处理事件,会交给控制器来处理。控制器不处理的话,再交给View的下一级响应者处理。
响应链与手势/UIControl
关于手势
- 手势的执行优先级高于响应者链
- 手势也通过
hitTest
方式查找响应视图 - 查找到第一响应者后,
UIApplication
向其派发消息,如果响应者链中存在能处理事件的手势,则手势响应事件,并执行touchesCancelled
将响应者链打断
关于UIControl
UIControl
也通过hitTest
查找第一响应者- 如果第一响应者是
UIControl
,则Application
直接派发事件,并不再向响应者链派发消息 - 如
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的响应范围
pointInside:withEvent
用来判断一个点是否在视图中,而这个方法是通过bounds来判断的,如果要扩大响应范围,可重写该方法,将判断bounds的范围扩大- bounds扩大的范围如果大于superview,那么扩大的地方也是响应不到的
不规则点击区域判断也可重写该方法
6.3.一个事件多个对象处理
多个对象实现touches
并调用super
方法
7.属性
property = ivar + getter + setter 成员变量+存取方法
- assign:
- 基本数据类型
- 直接赋值,不会更改值的引用计数
- 当引用计数为0时,对象销毁,编译器不会置为nil,指针仍指向被销毁内存, 产生野指针
- weak:
- OC 对象
- 非拥有关系,不会更改值的引用计数
- 对象被销毁时,weak修饰属性自动赋值为nil
- strong:
- 拥有关系,对旧值减少引用计数,新值增加引用计数
- 当一个对象不在有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的类型
- 全局块(_NSConcreteGlobalBlock),存于全局内存,相当于单例
- 栈块 (_NSConcreteStackBlock),存于栈内存,超出其作用域马上被销毁
- 堆块 (_NSConcreteMallocBlock),存于堆内存,是一个带引用计数对象,当引用计数为0时会被销毁
如何判断Block类型
- Block不访问外界变量(包括栈中和堆中的变量)
Block既不在栈中也不在堆中,在代码段中,ARC和MRC皆是,此时为全局块 - Block访问外界变量
MRC:访问外界变量的Block默认存储栈中
ARC:访问外界变量的Block默认存储在堆中(实际在栈区,然后ARC情况下自动拷贝到堆区),自动释放
Block复制
配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题
ARC有效时,以下情况栈上的Block会自动复制到堆上:
- 调用block的copy方法
- 将block作为函数返回值时(MRC无效,需手动)
- 将block赋值给__strong修改的变量(MRC时无效)
- 向Cocoa框架含有usingBlock的方法或GCD的API传递Block参数时
其他情况向方法的参数中传递block时,需手动调用copy
捕获变量
-
默认情况
对block外的变量引用,默认将其复制到数据结构中,存储在block的结构体内部,此时,block只能访问不能修改变量 -
__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: 释放内存
- objc_destruchInstance: 调用C++的清理方法和移除关联引用
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触发条件:
- init初始化不触发,但initWithFrame且rect不为zero时会触发;
- addSubview
- 设置frame且frame有变化
- 滚动UIScrollView
- 旋转Screen触发父UIView上的layoutSubviews
- 改变UIView大小也会触发父UIView上的layoutSubviews
- 直接调用setLayoutSubviews
- 直接调用setNeedsLayout
drawRect触发条件:
- UIView初始化时未设置rect大小,则不自动调用,调用顺序在Controller->loadView,viewDidLoad方法之后
- sizeToFit后被触发(可先调用sizeToFit计算size再系统自动调用drawRect方法)
- 设置contentMode为UIViewContentModeRedraw,则每次设置或更改frame时自动调用drawRect
- 直接调用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的工作原理
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. 进程间通信
- URL Scheme
- Keychain
- UIPasteboard
- UIDocumentInteractonController
- local socket
- AirDrop
- UIActivityViewController
- 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可把它可它划分短一点再传,若太小,也可累积足够多的字节后再构成报文发送出去