iOS 面试总结
1、UI视图
1.1、常见面试问题
- UITableView 重用机制
- UITableView数据源同步
- 事件传递&视图响应
- 图像显示原理
- UI卡顿、掉帧
- UI绘制原理、异步绘制
- 离屏渲染
1.2、UITableView重用机制
2、事件传递
1 | // 返回最终哪个UIView相应点击事件 |
3、UIView 和 CALayer
- UIView提供内容以及负责处理触摸等事件,参与事件响应链
- CALayer负责显示内容contents
- 体现了系统设置上的单一性原则
4、图像绘制原理
4.1、UIView绘制原理
4.2、离屏渲染
4.2.1 什么是离屏渲染
- On- Screen Rendering 当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行
- Off-Screen Rendering 离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
4.2.2 什么时候会出发离屏渲染
- 圆角和maskToBounds一起使用
- 图层蒙版
- 阴影
- 光栅化
4.2.3 为什么要避免离屏渲染
创建新的渲染缓冲区
在触发离屏渲染时会增加GPU的工作量,很可能造成掉帧和卡顿
4.3 异步绘制
4.4、滑动优化方案
GPU
- 纹理渲染
- 视图混合
CPU
- 对象创建、调整、销毁
- 预排版、(布局计算、文本计算)
- 预渲染(文本异步绘制、图片编解码)
5、分类
5.0、常见面试问题
- 分类
- 关联对象
- 扩展、代理
- KVC、KVO
- NSNotification(通知)的原理是什么
- 属性关键字
5.1、分类做了那些事?
- 申明私有方法
- 分解体积庞大的类文件
- 把Framework的私有方法公开
- 系统方法做一个扩展
5.2、分类的特点
- 运行时决议,在运行时添加到宿主类上
- 可以为系统类添加分类
5.3、分类可以添加哪些内容
- 实例方法
- 类方法
- 协议
- 属性(只添加了get方法,和set方法)
- 分类可以通过runtime关联对象的方式添加实例变量
5.4、分类方法
- 分类添加的方法可以覆盖原类方法
- 同名分类方法谁能生效取决于编译顺序
- 名字相同的分类会引起编译报错
5.5、怎样为分类添加成员变量
- 不能在分类申明时添加成员变量,只能用关联对象的方法添加成员变量
1 | objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key) |
1 | objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, |
1 | objc_removeAssociatedObjects(id _Nonnull object) |
5.6、分类添加的对象被添加到哪了?
- 关联对象由
AssicucationsManager
管理并在AssociationsHashMap
存储。 - 所有对象的关联内容都在同一个全局容器中。
6、扩展
6.1、一般用扩展做什么
- 申明私有属性
- 申明私有方法
- 申明私有成员变量
.2、扩展的
- 编译时决议
- 只以申明的形式存在,多数情况下寄生于宿主类的.m中
- 不能为系统类添加扩展
7、代理
7.1、什么是代理
- 代理是一种软件设计模式
- iOS中以@protocol形式体现
- 代理是一对一的
7.2、通知
- 是使用观察者模式来实现的用于跨层传递消息机制
- 通知是一对多的
7.3、KVO
- KVO 是观察者模式的一种实现方式
- Apple使用了isa混写(isa-swizzling)来实现KVO
- 使用setter方法改变值KVO才会生效
- 使用setValue:forKey:改变值KVO才会生效
- 成员变量直接修改需手动添加KVO才会生效
8、关键字
- 读写权限 (readonly,)
8.1、weak
- 不改变被修饰对象的引用计数
- 所指对象在被释放之后会自动置为nil
- 多数用于解决循环引用计数
8.2 原子性
- atomic 对赋值和获取保证线程安全,
- nonatomic 不保证线程安全
8.3 引用计数
retain 在MRC中使用
strong 在ARC中使用
assign 修饰基本数据类型,如Int
- 修饰基本数据类型,
- 修饰对象类型是,不改变其引用计数
- 会产生垂直指针
unsafe_unretained 在MRC中使用
8.4 weak 和 assign的区别和相同
区别:
- weak只能修饰对象类型,assign既能修饰对象类型又能修饰基本数据类型
- weak修饰的对象被释放后自动置为nil, assign所修饰的对象在被释放后assign指针依旧指向这个对象的地址
相同:
- 在修饰对象时,weak 和 assign 都不改变对象的引用计数
8.5 copy
源对象类型 | 拷贝方式 | 目标对象类型 | 拷贝类型(深/浅) |
---|---|---|---|
mutable对象 | copy | 不可变 | 深copy |
mutable对象 | mutableCopy | 可变 | 深copy |
immutable对象 | copy | 不可变 | 浅copy |
immutable对象 | mutableCopy | 可变 | 深copy |
- 可变对象的copy和mutableCopy都是深copy
- 不可变对象的copy是浅copy,mutableCopy是深copy
- copy方法返回的都是不可变对象
9、RunTime
常见面试问题:
- 消息发送流程
- 消息转发流程
- 类方法和实例方法动态解析
- 类的本质
- 分类初始化
- 分类方法覆盖原类方法
- KVO和KVC的原理
- Method Swizzling原理,解决Crash问题
- 如何使用RunTime动态创建类
- 如何使用RunTime进行hook
- Method Swizzling使用
9.1、objc_object
iOS中所有的对象都是的最终父类都是id类型,在RunTime中最终都转换成了objc_object
结构体,该结构体主要包含四个部分
isa_t
共用体- 关于
isa
操作相关 - 弱引用相关,如该对象是否被弱引用过
- 关联对象相关,如是否关联属性
- 内存管理相关方法的实现
9.2、objc_class
iOS中所有的Class
对象在RunTime
中最终都转换成了objc-class
结构体,objc_class
继承自objc_object
,objc_class
包含以下内容:
Class superClass
,是一个Class类型的类对象,指向其父类对象cache_t cache
方法缓存class_data_bits_t bits
定义的属性
9.2、isa指针
1 | union isa_t |
isa_t
分为指针型isa和非指针型isa- 对象的isa指针指向其类对象
- 类对象的isa指针指向其元类对象
9.3、cache_t
- 用于快速查找方法执行函数
- 是可增量扩展的哈希表结构
- 是局部性原理的最佳应用
cache_t
数组中存储的数据是key
和IMP
,我们可以通过查找key
的方法实现来查找IMP
的方法实现
9.4、class_data_bits_t bits
-
class_data_bits_t
主要是对class_rw_t
的封装 class_rw_t
代表了类相关的读写信息,对class_ro_t
的封装class_ro_t
代表了类相关的只读信息
9.5、Tagged Pointer原理
10、多线程相关
10.0、常见面试问题
- GCD
- NSOperation / NSOperationQueue
- Thred
- 线程同步、资源共享
- 互斥锁、自旋锁、递归锁
10.1、iOS系统提供了几种多线程技术各自的特点是怎样的
- GCD 实现一些简单的线程同步
- Thread + RunLoop = 常驻线程
一般常用于线程保活,分为三步:
1、在线程的Block中取当前RunLoop
2、为RunLoop添加NSMachPort或者 Source
3、启动这个RunLoop
1 | let thread = Thread { [weak self] in |
- Operation和OperationQueue,可以方便对任务的状态的控制,所以一些常见的第三方框架,
SDWebImage,AFNetworking
等
10.2 、怎样用GCD实现多读单写
- 写: 开启多个任务去修改数据,保证资源部被抢占,比如卖票系统,此处使用异步栅栏任务
- 读: 允许多个任务同时加入队列,但要保证一个一个执行,此处使用同步并行
1 | // 并发队列 |
10.3、Operation对象在Finished之后是怎样从queue当中移除的
通过KVO的方式达到对Operation
移除的目的。
10.4、你都使用过哪些锁?结合实际谈谈你是怎样使用的
递归锁解决死锁问题
10.5、线程死锁
- 队列引起的循环等待
- 主线程中+同步造成死锁
1 | DispatchQueue.main.sync { |
10.6、ABC三个任务并发,完成后执行任务D
1 | let queue = DispatchQueue.init(label: "com.tsn.groupQueue") |
10.7、Operation
需要和OperationQueue
配合使用来实现多线程方案,
- 添加任务依赖或移除依赖 这是GCD和Thread所不具备的
- 任务执行状态控制
- isReady是否可执行,一般用于异步的情况下
- isExexuting标记
Operation
是否正在执行中 - isFinished标记
Operation
是否已经执行完成了,一般用于异步 - isCancelled标记
Operation
是否已经cancel
了
- 可以控制最大并发量
- 如果只重写
main
方法,底层控制变更任务执行完成状态,以及任务退出 - 如果重写了
start
方法,自行控制任务状态
11、内存管理
11.0、常见面试问题:
- ARC
- MRC
- 引用计数机制
- 弱引用表
- AutoReleasePool
- 循环引用
11.1、内存布局
- stack: 栈内存, 方法调用
- heap: 堆内存,通过
alloc
等分配的对象 - bss: 未初始化的全局变量
- data:已初始化的全局变量等
- txt: 程序代码
11.2、内存管理方案
iOS系统会针对不同情况下使用不同的内存管理方法:
- 对于一些小的对象,如
Number
,通过TaggedPointer
来进行内存管理 - 对于64位的
NONPOINTER_ISA
,非指针型的ISA - 散列表 是一种复杂的数据结构,其中包含了弱引用表 和 散列引用计数表
11.3 引用计数
- ARC是
LLVM
和Runtime
协作的结果 - ARC中禁止手动调用
retain/release/retainCount/dealloc
- ARC中新增了
weak 、strong
属性关键字
11.4 weak
- 添加
weak
时实现
一个被申明为__weak
的对象,经过编译器的编译之后回调用objc_initWeak()
方法,然后经过一系列的函数调用栈,最终在weak_reginster_no_lock()
函数中进行弱引用的添加,具体添加的位置通过hash
算法来添加或查找的,如果查找位置中已经存在了一个弱引用数组,那么就把这个新的变量添加到这个弱引用数组中,如果没有弱引用数组那么就创建一个,并把第零个设置这个变量的weak指针,后面都添加为nil
.
- 清除
weak
变量,同时设置指向为nil
当一个对象调用dealloc()
方法中,在dealloc()
实现中会调用弱引用清除的相关函数。在这个函数的内部实现中会根据当前对象指针查找弱引用表,把当前对象相对应的弱引用数组取出,然后遍历数组中所有的弱引用指针,分别置为nil
。
11.5、自动释放池(@autoreleasepool)
@autoreleasepool的实现结构
- 是以栈为节点通过双向链表的形式组合而成
- 是和线程一一对应的
@autoreleasepool的实现原理是怎样的?
以栈为节点通过双向链表的形式组成的数据结构
@autoreleasepool为何可以嵌套使用?
在for循环中alloc图片数据等内存消耗较大场景手动插入**@autoreleasepool**
11.6、循环引用
- 自循环引用
- 相互循环引用
- 多循环引用
11.6.1、产生循环引用的条件
- 代理
- NSTimer
- Block
- 大环引用
11.6.2、如破除循环引用
- 避免产生循环引用
- 在合适的时机解除循环引用
11.6.3、具体的方案有哪些?
- __weak
- __block
- __unsafe_unretained 由这个关键字修饰的关键字也没有增加引用计数
- 修饰对象不会增加其引用计数,避免了循环引用
- 如果被修饰对象在某一时机被释放会产生垂直指针
11.6.4、NSTimer 产生循环引用问题
iOS 10以后通过Block
方式,这时要注意的是和Block
产生的循环引用;
1 | @property (strong, nonatomic) NSTimer *timer; |
一般要处理的是iOS 10之前的系统,方法有多种,可以
模仿系统的采用Block方式
新建
NSTimer
分类
1 | #import "NSTimer+Category.h" |
调用方式:
1 | __weak typeof(self) weakSelf = self; |
Swift代码:
1 | extension Timer { |
1 | if #available(iOS 10.0, *) { |
- 采用中间件
1 | #import <objc/runtime.h> |
12、Block
12.0、常见面试问题
- Block的本质(什么是Block)
- 为什么Block会产生循环引用
- __block修饰符的本质
- Block的内存管理
- 怎样理解Block截获变量的特性
- 截获基本数据类型的变量,是截获其值
- 截获对象类型是截获其修饰符和变量
- 截获静态变量是截获其指针
- 你都遇到过哪些循环引用?你是怎么解决的?
- Block本身是对象的变量,Block又捕获了这个对象的变量,会产生循环引用
12.1、什么是Block
- Block是将函数及其执行上下文封装起来的对象
- Block调用即是函数调用
- Block的底层结构:
1 | struct __block_impl { |
12.2、Block如何截获变量
1 | int multiplier = 6; |
12.2.2、被截获变量的类型分类:
- 局部变量
- 基本数据类型
- 对象类型
- 静态局部变量
- 全局变量
- 静态全局变量
12.2.3、截获变量
- 对于基本数据类型的局部变量截获其值
- 对于对象类型的局部变量连同所有权修饰符一起截获
- 以指针形式截获局部静态变量
- 不截获全局变量、静态全局变量
12.2.4、Block截获变量源码
使用命令将OC代码转成C++代码:
1 | clang -rewrite-objc -fobjc-arc ***.m |
1 | // 全局变量 |
编译结果:
1 | struct __BlockTest__testMethod_block_impl_0 { |
12.2.5、__block修饰符
1 | __block int multiplier = 6; |
- __block修饰的变量变成了对象
12.3、Block内存管理
12.3.1、Block的分类
- 全局类型 _NSConcreteGloabalBlock 存储于已初始化内存区中
- 堆类型 _NSConcreateStackBlock
- 栈类型 _NSConcreateMallocBlock
12.3.2、Block的copy操作
Block类别 | 源 | Copy结果 |
---|---|---|
堆类型 _NSConcreateStackBlock | 栈 | 堆 |
栈类型 _NSConcreateMallocBlock | 数据区 | 什么也不做 |
全局类型 _NSConcreteGloabalBlock | 堆 | 增加引用计数 |
12.3.3 解决循环引用问题
1 | @property (strong, nonatomic) NSArray <NSString *> *array; |
解决方法:
1 | __weak NSArray <NSString *> *weakArray = _array; |
12.3.4、__block造成循环引用
1 | @property (copy, nonatomic) NSString *varNum; |
在MRC下不会产生循环引用
在ARC下,会产生循环引用引起内存泄漏
1 | _numBlock = ^int(int num) { |
13、RunTime
13.1、常见面试问题
- 对象、类对象、元类对象
- 消息传递机制
- 消息转发流程
- 方法缓存
- Method-Swizzling
- 动态添加方法
14、RunLoop
14.0、常见面试问题
- RunLoop的概念
- Mode/Spurce/Timer/Observer
- 事件循环机制
- RunLoop与Timer的关系
- RunLoop和线程之间的关系
- 常驻线程
14.1、RunLoop的概念
RunLoop是通过内部的事件循环来对事件/消息进行管理的一个对象。RunLoop可以不断的接收消息,如点击屏幕、滑动列表,接收到消息之后会对事件进行处理,处理完成之后会进入内核态等待。这里的等待并不是一个简单的等待或者循环,重点是状态的切换。
14.2、事件循环机制
- 没有消息需要处理时,休眠以避免资源占用 (用户态 –>转换时调用
mach_msg()
方法–> 内核态) - 有消息需要处理时,立即被唤醒 (内核态 –> 转换时调用
mach_msg()
方法–> 用户态 )
14.2.1、为什么main函数不会退出
在main函数中维护着一个RunLoop,
14.3、RunLoop与NSTimer
滑动TableView的时候我们的定时器还会生效吗?
- 正常情况下,Timer是运行在RunLoopDefaultMode模式下,当UITableView滑动时会发生一个RunLoop的Mode的切换,会切换到UITrackingRunLoopMode
- 可以使用
addTimer()
同步添加到多个Mode中
1 | let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (time) in |
14.4、RunLoop的Mode
RunLoop有多个Mode,运行到不同的Mode上时,只能接收到当前Mode上的事件,例如UI Mode下只能接收到屏幕点击,上下滑动的事件。一个Timer 要想加入到多个Mode当中呢,
- CommonMode是一个特殊的Mode,CommonMode不是实际存在的一种Mode,
- CommonMode是同步到Timer/Source/Observer到多个Mode的一种技术方案
14.5、RunLoop与多线程
- 线程和RunLoop是一一对应的关系。
- 线程中的RunLoop并没有创建需要手动创建,此处引出常驻线程。
- 为当前线程开启一个RunLoop
- 向当前RunLoop中添加一个Port/Source等位置RunLoop的事件循环
- 启动该RunLoop
14.6、RunLoop数据结构
- RunLoop是CGRunLoop的封装,提供了面向对象的API。
- CFRunLoop
- pthread 线程相关,RunLoop和线程是一一对应的关系
- currentMode
- modes
- commonModes
- commonModeItems
- CGRunLoopMode
- Source/Timer/Observer