前言: 这篇文章是我看李明杰老师的 iOS 底层原理班(下)/OC 对象 / 关联对象 / 多线程 / 内存管理 / 性能优化 https://ke.qq.com/course/314526 总结所得, 断断续续历时 3 个月左右, 把课堂听的东西给做了一下笔记.
1, 一个 NSObject 对象占用多少内存 https://github.com/SunshineBrother/JHBlog
2,OC 对象的分类 https://github.com/SunshineBrother/JHBlog
3,KVO 实现原理 https://github.com/SunshineBrother/JHBlog
4,KVC 实现原理 https://github.com/SunshineBrother/JHBlog
5, 分类 https://github.com/SunshineBrother/JHBlog
5.1, 分类的实现原理 https://github.com/SunshineBrother/JHBlog
5.2,Load 和 Initialize 实现原理 https://github.com/SunshineBrother/JHBlog
6,Block 底层解密 https://github.com/SunshineBrother/JHBlog
7,RunLoop 实现原理 https://github.com/SunshineBrother/JHBlog
8,RunTime 实现原理 https://github.com/SunshineBrother/JHBlog
8.1,isa 解析 https://github.com/SunshineBrother/JHBlog
8.2, 方法缓存 https://github.com/SunshineBrother/JHBlog
8.3,objc_msgSend 执行流程 https://github.com/SunshineBrother/JHBlog
8.4,@dynamic 关键字 https://github.com/SunshineBrother/JHBlog
8.5,Class 和 SuperClass 区别 https://github.com/SunshineBrother/JHBlog
8.6,isKindOfClass 和 isMemberOfClass 区别 https://github.com/SunshineBrother/JHBlog
8.7,RunTime 的相关 API https://github.com/SunshineBrother/JHBlog
9, 多线程 https://github.com/SunshineBrother/JHBlog
9.1, 多线程面试题 https://github.com/SunshineBrother/JHBlog
9.2, 多线程之 NSThread https://github.com/SunshineBrother/JHBlog
9.3, 多线程之 GCD https://github.com/SunshineBrother/JHBlog
9.4, 多线程之 NSOperation https://github.com/SunshineBrother/JHBlog
9.5, 多线程之线程安全 https://github.com/SunshineBrother/JHBlog
9.6, 死锁 https://github.com/SunshineBrother/JHBlog
9.7,GCD 高级用法 https://github.com/SunshineBrother/JHBlog
9.8, 线程之间的通讯 https://github.com/SunshineBrother/JHBlog
10, 内存管理
10.1, 定时器 target 的内存泄漏 https://github.com/SunshineBrother/JHBlog
10.2,Tagged Pointer https://github.com/SunshineBrother/JHBlog
10.3,copy&retain&strong 原理
10.4,weak&assign 原理 https://github.com/SunshineBrother/JHBlog
10.5,@property 的本质是什么 https://github.com/SunshineBrother/JHBlog
10.6,autorelease 原理 https://github.com/SunshineBrother/JHBlog
10.7,atomic 一定是线程安全的吗 https://github.com/SunshineBrother/JHBlog
我们经常会看一些面试题, 但是好多面试题我们都是知其然不知其所以然, 你如果认真的看了我上面总结的几十篇文章, 那么你也会知其所以然.
OC 对象本质
1, 一个 NSObject 对象占用多少内存?
系统分配了 16 个字节给 NSObject 对象(通过 malloc_size 函数获得), 但 NSObject 对象内部只使用了 8 个字节的空间(64bit 环境下, 可以通过 class_getInstanceSize 函数获得)
2, 对象的 isa 指针指向哪里?
instance 对象的 isa 指向 class 对象
class 对象的 isa 指向 meta-class 对象
meta-class 对象的 isa 指向基类的 meta-class 对象
3,OC 的类信息存放在哪里?
对象方法, 属性, 成员变量, 协议信息, 存放在 class 对象中
类方法, 存放在 meta-class 对象中
成员变量的具体值, 存放在 instance 对象
具体实现请参考: 1, 一个 NSObject 对象占用多少内存 2,OC 对象的分类
KVO
1,iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么?)
利用 RuntimeAPI 动态生成一个子类, 并且让 instance 对象的 isa 指向这个全新的子类
当修改 instance 对象的属性时, 会调用 Foundation 的
_NSSetXXXValueAndNotify
函数
1, 调用
willChangeValueForKey
方法
2, 调用 setAge 方法
3, 调用
didChangeValueForKey
方法
- 4,
- didChangeValueForKey
方法内部调用 oberser 的
observeValueForKeyPath:ofObject:change:context:
方法
2, 如何手动触发 KVO?
手动调用 willChangeValueForKey: 和 didChangeValueForKey:
3, 直接修改成员变量会触发 KVO 么?
不会触发 KVO
具体实现请参考: 3,KVO 实现原理
KVC
1, 通过 KVC 修改属性会触发 KVO 么?
会触发 KVO, 因为 KVC 是调用 set 方法, KVO 就是监听 set 方法
2,KVC 的赋值和取值过程是怎样的? 原理是什么?
KVO 的 setValue:forKey 原理
1, 按照 setKey,_setKey 的顺序查找成员方法, 如果找到方法, 传递参数, 调用方法
2, 如果没有找到, 查看 accessInstanceVariablesDirectly 的返回值(accessInstanceVariablesDirectly 的返回值默认是 YES),
返回值为 YES, 按照_Key,_isKey,Key,isKey 的顺序查找成员变量, 如果找到, 直接赋值, 如果没有找到, 调用 setValue:forUndefinedKey:, 抛出异常
返回 NO, 直接调用 setValue:forUndefinedKey:, 抛出异常
KVO 的 ValueforKey 原理
1, 按照 getKey,key,isKey,_key 的顺序查找成员方法, 如果找到直接调用取值
2, 如果没有找到, 查看 accessInstanceVariablesDirectly 的返回值
返回值为 YES, 按照_Key,_isKey,Key,isKey 的顺序查找成员变量, 如果找到, 直接取值, 如果没有找到, 调用 setValue:forUndefinedKey:, 抛出异常
返回 NO, 直接调用 setValue:forUndefinedKey:, 抛出异常
具体实现请参考: 4,KVC 实现原理
Category
1,Category 的实现原理
Category 编译之后的底层结构是 struct category_t, 里面存储着分类的对象方法, 类方法, 属性, 协议信息
在程序运行的时候, runtime 会将 Category 的数据, 合并到类信息中(类对象, 元类对象中)
2,Category 和 Class Extension 的区别是什么?
Class Extension 在编译的时候, 它的数据就已经包含在类信息中
Category 是在运行时, 才会将数据合并到类信息中
3,load,initialize 方法的区别什么?
1. 调用方式
1> load 是根据函数地址直接调用
2> initialize 是通过 objc_msgSend 调用
2. 调用时刻
1> load 是 runtime 加载类, 分类的时候调用(只会调用 1 次 )
2> initialize 是类第一次接收到消息的时候调用, 每一个类只会 initialize 一次(父类的 initialize 方法可能会被调用多次)
4,load,initialize 的调用顺序
1.load
1> 先调用类的 load
a) 先编译的类, 优先调用 load
b) 调用子类的 load 之前, 会先调用父类的 load
2> 再调用分类的 load
a) 先编译的分类, 优先调用 load
2.initialize
1> 先初始化父类
2> 再初始化子类(可能最终调用的是父类的 initialize 方法)
5, 如何实现给分类 "添加成员变量"?
默认情况下, 因为分类底层结构的限制, 不能添加成员变量到分类中. 但可以通过关联对象来间接实现
关联对象提供了以下 API
添加关联对象
- void objc_setAssociatedObject(id object, const void * key,
- id value, objc_AssociationPolicy policy)
获得关联对象
id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象
void objc_removeAssociatedObjects(id object)
具体实现请参考: 5.1, 分类的实现原理 5.2,Load 和 Initialize 实现原理
Block
1,block 的原理是怎样的? 本质是什么?
block 本质上也是一个 OC 对象, 它内部也有个 isa 指针
block 是封装了函数调用以及函数调用环境的 OC 对象
2,block 的(capture)
为了保证 block 内部能够正常访问外部的变量, block 有个变量捕获机制
3,Block 类型有哪几种 block 有 3 种类型, 可以通过调用 class 方法或者 isa 指针查看具体类型, 最终都是继承自 NSBlock 类型
- ,NSGlobalBlock ( _NSConcreteGlobalBlock
- ,NSStackBlock ( _NSConcreteStackBlock )
- ,NSMallocBlock ( _NSConcreteMallocBlock )
4,block 的 copy
在 ARC 环境下, 编译器会根据情况自动将栈上的 block 复制到堆上, 比如以下情况
1,block 作为函数返回值时
2, 将 block 赋值给__strong 指针时
3,block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
4,block 作为 GCD API 的方法参数时
MRC 下 block 属性的建议写法
@property (copy, nonatomic) void (^block)(void);
ARC 下 block 属性的建议写法
- @property (strong, nonatomic) void (^block)(void);
- @property (copy, nonatomic) void (^block)(void);
5,__block 修饰符
__block 可以用于解决 block 内部无法修改 auto 变量值的问题
__block 不能修饰全局变量, 静态变量(static)
编译器会将__block 变量包装成一个对象
当__block 变量在栈上时, 不会对指向的对象产生强引用
当__block 变量被 copy 到堆时
会调用__block 变量内部的 copy 函数
copy 函数内部会调用_Block_object_assign 函数
_Block_object_assign 函数会根据所指向对象的修饰符 (__strong,__weak,__unsafe_unretained) 做出相应的操作, 形成强引用 (retain) 或者弱引用(注意: 这里仅限于 ARC 时会 retain,MRC 时不会 retain)
如果__block 变量从堆上移除
会调用__block 变量内部的 dispose 函数
dispose 函数内部会调用_Block_object_dispose 函数
_Block_object_dispose 函数会自动释放指向的对象(release)
6, 循环引用
用__weak,__unsafe_unretained 解决
- __unsafe_unretained typeof(self) weakSelf = self;
- self.block = ^{
- print(@"%p", weakSelf);
- }
- __weak typeof(self) weakSelf = self;
- self.block = ^{
- print(@"%p", weakSelf);
- }
用__block 解决(必须要调用 block)
- __block id weakSelf = self;
- self.block = ^{
- weakSelf = nil;
- }
- self.block();
具体实现请参考: 6,Block 底层解密
RunTime
1, 讲一下 OC 的消息机制
OC 中的方法调用其实都是转成了 objc_msgSend 函数的调用, 给 receiver(方法调用者)发送了一条消息(selector 方法名)
objc_msgSend 底层有 3 大阶段: 消息发送(当前类, 父类中查找), 动态方法解析, 消息转发
2, 消息转发机制流程
1, 消息发送
2, 动态方法解析
3, 消息转发
消息发送阶段
消息发送流程是我们平时最经常使用的流程, 其他的像动态方法解析和消息转发其实是补救措施. 具体流程如下
1, 首先判断消息接受者 receiver 是否为 nil, 如果为 nil 直接退出消息发送
2, 如果存在消息接受者 receiverClass, 首先在消息接受者 receiverClass 的 cache 中查找方法, 如果找到方法, 直接调用. 如果找不到, 往下进行
3, 没有在消息接受者 receiverClass 的 cache 中找到方法, 则从 receiverClass 的 class_rw_t 中查找方法, 如果找到方法, 执行方法, 并把该方法缓存到 receiverClass 的 cache 中; 如果没有找到, 往下进行
4, 没有在 receiverClass 中找到方法, 则通过 superClass 指针找到 superClass, 也是现在缓存中查找, 如果找到, 执行方法, 并把该方法缓存到 receiverClass 的 cache 中; 如果没有找到, 往下进行
5, 没有在消息接受者 superClass 的 cache 中找到方法, 则从 superClass 的 class_rw_t 中查找方法, 如果找到方法, 执行方法, 并把该方法缓存到 receiverClass 的 cache 中; 如果没有找到, 重复 4,5 步骤. 如果找不到了 superClass 了, 往下进行
6, 如果在最底层的 superClass 也找不到该方法, 则要转到动态方法解析
动态方法解析
开发者可以实现以下方法, 来动态添加方法实现
- +resolveInstanceMethod:
- +resolveClassMethod:
动态解析过后, 会重新走 "消息发送" 的流程, 从 receiverClass 的 cache 中查找方法这一步开始执行
消息转发
如果方法一个方法在消息发送阶段没有找到相关方法, 也没有进行动态方法解析, 这个时候就会走到消息转发阶段了.
调用
forwardingTargetForSelector
, 返回值不为 nil 时, 会调用 objc_msgSend(返回值, SEL)
调用
methodSignatureForSelector
, 返回值不为 nil, 调用 forwardInvocation: 方法; 返回值为 nil 时, 调用
doesNotRecognizeSelector:
方法
开发者可以在 forwardInvocation: 方法中自定义任何逻辑
以上方法都有对象方法, 类方法 2 个版本(前面可以是加号 +, 也可以是减号 -)
3, 什么是 Runtime? 平时项目中有用过么?
OC 是一门动态性比较强的编程语言, 允许很多操作推迟到程序运行时再进行
OC 的动态性就是由 Runtime 来支撑和实现的, Runtime 是一套 C 语言的 API, 封装了很多动态性相关的函数
平时编写的 OC 代码, 底层都是转换成了 Runtime API 进行调用
具体应用
利用关联对象 (AssociatedObject) 给分类添加属性
遍历类的所有成员变量(修改 textfield 的占位文字颜色, 字典转模型, 自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题
4,super 的本质
super 调用, 底层会转换为 objc_msgSendSuper2 函数的调用, 接收 2 个参数
- struct objc_super2
- SEL
receiver 是消息接收者
current_class 是 receiver 的 Class 对象
具体实现请参考:
8.1,isa 解析
8.2, 方法缓存
8.3,objc_msgSend 执行流程
8.4,@dynamic 关键字
8.5,Class 和 SuperClass 区别
8.6,isKindOfClass 和 isMemberOfClass 区别
8.7,RunTime 的相关 API
RunLoop
1, 讲讲 RunLoop, 项目中有用到吗? 1, 定时器切换的时候, 为了保证定时器的准确性, 需要添加 runLoop 2, 在聊天界面, 我们需要持续的把聊天信息存到数据库中, 这个时候需要开启一个保活线程, 在这个线程中处理
2,runloop 内部实现逻辑
每次运行 RunLoop, 线程的 RunLoop 会自动处理之前未处理的消息, 并通知相关的观察者. 具体顺序
1, 通知观察者(observers)RunLoop 即将启动
2, 通知观察者 (observers) 任何即将要开始的定时器
3, 通知观察者 (observers) 即将处理 source0 事件
4, 处理 source0
5, 如果有 source1, 跳到第 9 步
6, 通知观察者 (observers) 线程即将进入休眠
7, 将线程置于休眠知道任一下面的事件发生
1,source0 事件触发
2, 定时器启动
3, 外部手动唤醒
8, 通知观察者 (observers) 线程即将唤醒
9, 处理唤醒时收到的时间, 之后跳回 2
1, 如果用户定义的定时器启动, 处理定时器事件
2, 如果 source0 启动, 传递相应的消息
10, 通知观察者 RunLoop 结束
3,RunLoop 与线程
每条线程都有唯一的一个与之对应的 RunLoop 对象
RunLoop 保存在一个全局的 Dictionary 里, 线程作为 key,RunLoop 作为 value
线程刚创建时并没有 RunLoop 对象, RunLoop 会在第一次获取它时创建
RunLoop 会在线程结束时销毁
主线程的 RunLoop 已经自动获取(创建), 子线程默认没有开启 RunLoop
4,timer 与 runloop 的关系?
一个 RunLoop 包含若干个 Mode, 每个 Mode 又包含若干个 Source0/Source1/Timer/Observer
RunLoop 启动时只能选择其中一个 Mode, 作为 currentMode
如果需要切换 Mode, 只能退出当前 Loop, 再重新选择一个 Mode 进入
不同组的 Source0/Source1/Timer/Observer 能分隔开来, 互不影响
如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出
解决定时器在滚动视图上面失效问题 NSTimer 添加到两种 RunLoop 中
- [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
- [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
5,RunLoop 有几种状态
- kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
- kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
- kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
- kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
- kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
- kCFRunLoopExit = (1UL << 7),// 即将退出 RunLoop
**6,RunLoop 的 mode 的作用 **
RunLoop 的 mode 的作用 系统注册了 5 中 mode
- kCFRunLoopDefaultMode //App 的默认 Mode, 通常主线程是在这个 Mode 下运行
- UITrackingRunLoopMode // 界面跟踪 Mode, 用于 ScrollView 追踪触摸滑动, 保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode // 在刚启动 App 时第进入的第一个 Mode, 启动完成后就不再使用
- GSEventReceiveRunLoopMode // 接受系统事件的内部 Mode, 通常用不到
- kCFRunLoopCommonModes // 这是一个占位用的 Mode, 不是一种真正的 Mode
但是我们只能使用两种 mode
- kCFRunLoopDefaultMode //App 的默认 Mode, 通常主线程是在这个 Mode 下运行
- UITrackingRunLoopMode // 界面跟踪 Mode, 用于 ScrollView 追踪触摸滑动, 保证界面滑动时不受其他 Mode 影响
具体实现请参考: 7,RunLoop 实现原理
多线程
1, 你理解的多线程? 2,iOS 的多线程方案有哪几种? 你更倾向于哪一种? 3, 你在项目中用过 GCD 吗? 4,GCD 的队列类型 5, 说一下 OperationQueue 和 GCD 的区别, 以及各自的优势 6, 线程安全的处理手段有哪些? 使用线程锁
- ,OSSpinLock
- ,os_unfair_lock
- ,pthread_mutex
- ,dispatch_semaphore
- ,dispatch_queue(DISPATCH_QUEUE_SERIAL)
- ,NSLock
- ,NSRecursiveLock
- ,NSCondition
- ,NSConditionLock
- ,@synchronized
- ,pthread_rwlock
- ,dispatch_barrier_async
- ,atomic
7, 线程通讯 线程间通信的体现
1, 一个线程传递数据给另一个线程
2, 在一个线程中执行完特定任务后, 转到另一个线程继续执行任务
1,NSThread 可以先将自己的当前线程对象注册到某个全局的对象中去, 这样相互之间就可以获取对方的线程对象, 然后就可以使用下面的方法进行线程间的通信了, 由于主线程比较特殊, 所以框架直接提供了在主线程执行的方法
- - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
- 2,GCD
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- });
内存管理
1, 使用 CADisplayLink,NSTimer 有什么注意点? CADisplayLink,NSTimer 会对 target 产生强引用, 如果 target 又对它们产生强引用, 那么就会引发循环引用
2, 介绍下内存的几大区域
代码段: 编译之后的代码
数据段
字符串常量: 比如 NSString *str = @"123"
已初始化数据: 已初始化的全局变量, 静态变量等
未初始化数据: 未初始化的全局变量, 静态变量等
栈: 函数调用开销, 比如局部变量. 分配的内存空间地址越来越小
堆: 通过 alloc,malloc,calloc 等动态分配的空间, 分配的内存空间地址越来越大
3, 讲一下你对 iOS 内存管理的理解 在 iOS 中, 使用引用计数来管理 OC 对象的内存
一个新创建的 OC 对象引用计数默认是 1, 当引用计数减为 0,OC 对象就会销毁, 释放其占用的内存空间
调用 retain 会让 OC 对象的引用计数 + 1, 调用 release 会让 OC 对象的引用计数 - 1
内存管理的经验总结
当调用 alloc,new,copy,mutableCopy 方法返回了一个对象, 在不需要这个对象时, 要调用 release 或者 autorelease 来释放它
想拥有某个对象, 就让它的引用计数 + 1; 不想再拥有某个对象, 就让它的引用计数 - 1
可以通过以下私有函数来查看自动释放池的情况 extern void _objc_autoreleasePoolPrint(void);
4,ARC 都帮我们做了什么 LLVM + Runtime
LVVM 生成 release 代码
RunTime 负责执行
5,weak 指针的实现原理 runtime 维护了一个 weak 表, 用于存储指向某个对象的所有 weak 指针. weak 表其实是一个 hash(哈希)表, key 是所指对象的地址, Value 是 weak 指针的地址 (这个地址的值是所指对象指针的地址) 数组
1, 初始化时: runtime 会调用 objc_initWeak 函数, 初始化一个新的 weak 指针指向对象的地址
2, 添加引用时: objc_initWeak 函数会调用 storeWeak() 函数, storeWeak() 的作用是更新指针指向, 创建对应的弱引用表
3, 释放时, 调用 clearDeallocating 函数. clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组, 然后遍历这个数组把其中的数据设为 nil, 最后把这个 entry 从 weak 表中删除, 最后清理对象的记录
6,autorelease 对象在什么时机会被调用 release
1,iOS 在主线程的 Runloop 中注册了 2 个 Observer
2, 第 1 个 Observer 监听了 kCFRunLoopEntry 事件, 会调用 objc_autoreleasePoolPush()
3, 第 2 个 Observer 监听了 kCFRunLoopBeforeWaiting 事件, 会调用 objc_autoreleasePoolPop(),objc_autoreleasePoolPush() 监听了 kCFRunLoopBeforeExit 事件, 会调用 objc_autoreleasePoolPop()
autoreleased 对象是在 runloop 的即将进入休眠时进行释放的
7, 方法里有局部对象, 出了方法后会立即释放吗 在 ARC 情况下会立即释放 在 MRC 情况下, 对象是在 runloop 的即将进入休眠时进行释放的
来源: https://juejin.im/post/5bfcdce0e51d4503fb4474ff