diff --git a/Chapter1 - iOS/1.100.md b/Chapter1 - iOS/1.100.md index 89bc206..885f4e1 100644 --- a/Chapter1 - iOS/1.100.md +++ b/Chapter1 - iOS/1.100.md @@ -180,4 +180,34 @@ if (self.socket == NULL) { [_delegates setObject:delegate forKey:task]; [task resume]; } +``` + +### NSURLProtocol 主意事项 +使用 NSURLProtocol 的时候,如果是代理 NSURLSession 的网络请求,则需要重写 protocolClasses 方法。但是在你往给方法设置 protocolClasses 的时候可能全局也有其他 SDK、工具类也做了修改。这样子需要注意不能丢弃别人的,也不能丢弃自己的。参考 OHHTTPStubs 在注册 NSURLProtocol 子类的处理 +``` ++ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig +{ + // Runtime check to make sure the API is available on this version + if ( [sessionConfig respondsToSelector:@selector(protocolClasses)] + && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)]) + { + NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses]; + Class protoCls = HTTPStubsProtocol.class; + if (enable && ![urlProtocolClasses containsObject:protoCls]) + { + [urlProtocolClasses insertObject:protoCls atIndex:0]; + } + else if (!enable && [urlProtocolClasses containsObject:protoCls]) + { + [urlProtocolClasses removeObject:protoCls]; + } + sessionConfig.protocolClasses = urlProtocolClasses; + } + else + { + NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. " + @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call " + @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd)); + } +} ``` \ No newline at end of file diff --git a/Chapter1 - iOS/1.102.md b/Chapter1 - iOS/1.102.md new file mode 100644 index 0000000..0157ba9 --- /dev/null +++ b/Chapter1 - iOS/1.102.md @@ -0,0 +1,102 @@ +# LLVM + +[LLVM](https://llvm.org/) 项目是模块化、可重用的编译器以及工具链技术的集合 + +> The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. + +## 结构 + + + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-segment.png) + +LLVM 由三部分构成: + +- FrontEnd(前端):词法分析、语法分析、语义分析、生成中间代码 + +- Optimizer(优化器):优化中间代码 + +- Backend(后端):生成目标程序(机器码) + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-Structure.png) + +正是由于这样的设计,使得 LLVM 具备很多有点: + +- 不同的前端后端使用统一的中间代码 LLVM Intermediate Representation (LLVM IR) + +- 如果需要支持一种新的编程语言,那么只需要实现一个新的前端 + +- 如果需要支持一种新的硬件设备,那么只需要实现一个新的后端 + +- 优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改 + +- 相比之下,GCC 的前端和后端没分得太开,前端后端耦合在了一起。所以 GCC 为了支持一门新的语言,或者为了支持一个新的目标平台,就 + 变得特别困难 + +LLVM现在被作为实现各种静态和运行时编译语言的通用基础结构(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等) + + + + + +## Clang + +[Clang](http://clang.llvm.org/) 是 LLVM 的一个子项目,基于 LLVM 架构的 c/c++/Objective-C 语言的编译器前端 + +GCC 是 c/c++ 等的编译器 + +Clang 相较于 GCC,具备下面优点: + +- 编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍) + +- 占用内存小:Clang 生成的 AST 所占用的内存是 GCC 的五分之一左右 + +- 模块化设计:Clang 采用基于库的模块化设计,易于 IDE 集成及其他用途的重用 + +- 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告 + +- 设计清晰简单,容易理解,易于扩展增强 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-phase.png) + + + +### 查看编译过程 + +```shell +clang -ccc-print-phases main.m +``` + +对 main.m 文件 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-phase.png) + +可以看到经历了:输入、预处理、编译、LLVM Backend、汇编、链接、绑定架构7个阶段。 + + + +查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入、宏定义替换等。 + +词法分析,生成 Token:`clang -fmodules -E -Xclang -dump-tokens main.m` + +```c +#import +int main(int argc, const char * argv[]) { + int a = 1; + int b =2; + int c = a + b; + return 0; +} +``` + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-analysize.png) + + + +语法分析,生成语法树(AST,Abstract Syntax Tree):`clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-ast.png) + +### LLVM IR + + diff --git a/Chapter1 - iOS/1.103.md b/Chapter1 - iOS/1.103.md new file mode 100644 index 0000000..e69de29 diff --git a/Chapter1 - iOS/1.30.md b/Chapter1 - iOS/1.30.md index 84af5d2..503b541 100644 --- a/Chapter1 - iOS/1.30.md +++ b/Chapter1 - iOS/1.30.md @@ -83,11 +83,11 @@ #pragma clang diagnostic pop 10. Xcode Instruments 内存泄漏检测工具 Leaks 在内存检测后,无法看到具体的堆栈信息。 - ![Leaks](./../assets/2020-11-25-InstrumentMemoryLeaks.jpg) + ![Leaks](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-11-25-InstrumentMemoryLeaks.jpg) 涂上右下方的 `Heaviest Stack Trace` 模块看不到对应的堆栈信息。一番定位问题后发现是工程项目在 debug 阶段,Build Setting 中的 **Debug Information Format** 选项的 debug 条目是没有 dSYM 文件的,我们要想看到堆栈信息,就必须选择 `DWARF with dSYM File` 选项。 - ![](./../assets/2020-11-25-BuildSettingsDebugInformationFormat.png) + ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-11-25-BuildSettingsDebugInformationFormat.png) DWARF,即 ***Debug With Arbitrary Record Format*** ,是一个标准调试信息格式,即调试信息。这部分信息可以查看我的[这篇文章](./1.74.md)中讲 iOS 符号化的部分。 diff --git a/Chapter1 - iOS/1.38.md b/Chapter1 - iOS/1.38.md index b627a04..952001b 100644 --- a/Chapter1 - iOS/1.38.md +++ b/Chapter1 - iOS/1.38.md @@ -1,9 +1,44 @@ -# RunLoop 对象 +# RunLoop 探究 + +## RunLoop 是什么 + +- 运行循环 +- 在程序运行过程中循环做一些事情 + +作用:程序并不会马上退出,而是保持运行状态 + +- 保持程序的持续运行 + +- 处理App中的各种事件(比如触摸事件、定时器事件等) + +- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息 + +- ...... + +场景 + +- 定时器(Timer)、PerformSelector + +- GCD Async Main Queue + +- 事件响应、手势识别、界面刷新 + +- 网络请求 + +- AutoreleasePool 先附上一张总结的非常棒的RunLoop图 ![RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-RunLoop-review.png) +和 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/RunLoop-SourceCode.png) + +## RunLoop API + +### 获取 RunLoop + iOS 中有2套 API 可以访问和使用 RunLoop。分别是 - Foundation:NSRunLoop @@ -14,17 +49,20 @@ iOS 中有2套 API 可以访问和使用 RunLoop。分别是 //Foundation [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象 - + //Core Foundation CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象 CFRunLoopGetMain(); // 获得主线程的RunLoop对象 ``` +NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的内部结果,就需要了解 CFRunLoopRef -NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的内部结果,就需要了解 [CFRunLoopRef](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。 +- RunLoop 保存在一个全局的 Dictionary 里,线程作为 key,RunLoop 作为 value - 每条线程都有与之一一对应的 RunLoop 对象 + - 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要主动创建 + - RunLoop 在第一次获取时创建,在线程结束时消失 ### RunLoop 相关的5个类 @@ -35,13 +73,46 @@ NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的 - CFRunLoopTimerRef - CFRunLoopObserverRef +CFRunLoopRef 是什么? + +查看源码 CFRunLoop 发现是结构体对象别名,`typedef struct __CFRunLoop * CFRunLoopRef;`摘取主要信息如下 + +```c +struct __CFRunLoop { + pthread_t _pthread; + CFMutableSetRef _commonModes; + CFMutableSetRef _commonModeItems; + CFRunLoopModeRef _currentMode; + CFMutableSetRef _modes; +}; +``` + +其中 `_modes` 代表一个 RunLoop 有一个 set 存储运行模式,有多个 Mode。`_currentMode` 表示当前时刻只有一个 Mode。 + +`CFRunLoopModeRef` 是什么?查看发现 + +`typedef struct __CFRunLoopMode *CFRunLoopModeRef;` + +```c +struct __CFRunLoopMode { + CFStringRef _name; + CFMutableSetRef _sources0; + CFMutableSetRef _sources1; + CFMutableArrayRef _observers; + CFMutableArrayRef _timers; +}; +``` ### CFRunLoopModeRef 代表 RunLoop 的运行模式 - 一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个 Source/Timer/Observer - 每次 RunLoop 启动,只能指定一个 Mode,这个 Mode 被叫做 CurrentMode - 如果需要切换 Mode,只能退出 RunLoop,则以一个 Mode 进入 -- 这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响 +- 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出 + +QA:为什么一个 RunLoop 需要创建这么多 Mode? + +这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响(性能、功能)。 系统默认注册了5个Mode @@ -51,120 +122,89 @@ NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的 - GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到 - kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode +### Source0、Source1、Timer、Observers 是什么 -## CFRunLoopSourceRef 事件源(输入源) +```c +struct __CFRunLoopMode { + CFStringRef _name; + CFMutableSetRef _sources0; + CFMutableSetRef _sources1; + CFMutableArrayRef _observers; + CFMutableArrayRef _timers; +}; +``` -- 早期的分法: - - Ported-Based Source - - Custom Input Source - - Cocoa Perform Selector Source -- 现在的分法 - - Source0:非基于 port 的,用户主动触发的事件 - - Source1: 基于 port的,通过内核在线程间相互发送消息 - - -## CFRunLoopTimerRef 是基于时间的触发器 +RunLoop 在各个 Mode 下做事情,其实就是在处理某个 Mode 中的 Source0、Source1、Timer、Observer 事件。 -- 基本上说就是 NSTimer,它会收到 RunLoopMode 的影响 -- GCD 的 timer 不受 RunLoopMode 的影响 +Source0: -## - CFRunLoopObserverRef 观察者,监听 RunLoop 状态的变化 +- 屏幕触摸事件处理 - ```objective-c - /* Run Loop Observer Activities */ - typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { - kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop - kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 NSTimer - kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source - kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 - kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 - kCFRunLoopExit = (1UL << 7), // 退出 RunLoop - kCFRunLoopAllActivities = 0x0FFFFFFFU - }; - ``` +- `performSelector:onThread:` + +Source1: + +- 基于 Port 的线程间通信 + +- 系统事件捕捉(比如屏幕触摸事件,Source1捕捉后,派发给 Source0 处理) + +Timers: + +- NSTimer + +- `performSelector:withObject:afterDelay:` + +Observers: + +- 用于监听 RunLoop 状态 + +- UI刷新(BeforeWaiting) + +- AutoReleasePool 实现(BeforeWaiting) + +CFRunLoopSourceRef 事件源(输入源) + +早期的分法: + +- Ported-Based Source +- Custom Input Source +- Cocoa Perform Selector Source + +现在的分法 + +- Source0:非基于 port 的,用户主动触发的事件 +- Source1: 基于 port的,通过内核在线程间相互发送消息 + +### CFRunLoopObserverRef 监听 RunLoop 状态变化 + +```objective-c +/* Run Loop Observer Activities */ +typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { + kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop + kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 NSTimer + kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source + kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 + kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 + kCFRunLoopExit = (1UL << 7), // 退出 RunLoop + kCFRunLoopAllActivities = 0x0FFFFFFFU +}; +``` 添加 Observer - ```objective-c - //1、获得当前线程下的 RunLoop - CFRunLoopRef runloop = CFRunLoopGetCurrent(); - //2、为 RunLoop 创建观察者 - CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - - - }); - //3、为当前的 RunLoop 添加观察者 - CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode); - //4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release - CFRelease(obersver); - ``` - - - -## NSTimer 经常会不准确,原因是什么? - -NSTimer 在创建的时候经常会指派到特定的 NSRunLoopMode 中去,举个例子,默认创建的NSTimer 是被添加到 NSRunLoopDefaultMode 中去,当你的页面上有 UIScrollView 或者子类的时如果被拖动了,当前 RunLoop 的 NSRunloopMode 会从 NSDefaultRunLoopMode 转变为 UITrackingRunLoopMode 。遇到这种情况你需要精确的 NSTimer 的话,在创建好 NSTimer 之后,设置 RunLoopMod 为 NSRunLoopCommonModes - ```objective-c -NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES]; -[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; +//1、获得当前线程下的 RunLoop +CFRunLoopRef runloop = CFRunLoopGetCurrent(); +//2、为 RunLoop 创建观察者 +CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { +}); +//3、为当前的 RunLoop 添加观察者 +CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode); +//4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release +CFRelease(obersver); ``` -NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 - -```objective-c -#import "ViewController.h" - -@interface ViewController () -@property (nonatomic, strong) dispatch_source_t timer; -@end - - -@implementation ViewController - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ - /* - 只在默认状态下执行的 NSTimer - [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { - NSLog(@"我在执行了"); - }]; - */ - - /* - 指定 NSRunLoopMode 的 NSTimer - NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES]; - [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; - */ - - /* - GCD 的单位是 纳秒. - 使用 GCD 创建的 timer 正常创建后不会执行,因为创建后设置了指定的时间后触发,所以当代码运行到最后一行的时候,Timer 还没执行,就被销毁了。所以我们必须设置一个属性去保存它。 - */ - //1、创建队列 - dispatch_queue_t queue = dispatch_get_main_queue(); - //2、创建 timer - dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); - self.timer = timer; - //3、设置 timer 的参数:精准度、时间间隔 - //第三个参数为 GCD timer 的精准度 - dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); - //4、为 Timer 设置任务 - dispatch_source_set_event_handler(timer, ^{ - NSLog(@"%@",[NSRunLoop currentRunLoop]); - }); - //5、执行任务 - dispatch_resume(timer); -} - -- (void)show{ - NSLog(@"shw-%@",[NSThread currentThread]); - NSLog(@"%@",[NSRunLoop currentRunLoop]); -} -@end -``` - - -## 监听 RunLoop +注意:CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain 等字眼的函数创建出来的对象都需要在最后调用 `release` ```objective-c //给 RunLoop 添加监听者 @@ -180,6 +220,7 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 参数4:优先级 参数5:回调 */ + CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: @@ -188,7 +229,7 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 case kCFRunLoopBeforeTimers: NSLog(@"RunLoop 大哥要处理 Timer 了"); break; - case kCFRunLoopBeforeSources: + case kCF RunLoopBeforeSources: //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息 NSLog(@"RunLoop 大哥要处理 Source 了"); break; @@ -212,7 +253,6 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 参数3:要监听 RunLoop 在哪种运行模式下的状态 */ CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode); - //CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain等字眼的函数创建出来的对象都需要在最后调用 release CFRelease(oberver); [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES]; } @@ -259,7 +299,6 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 ![触摸屏幕事件在 RunLoop 下的 source0](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180801-104553@2x.png) - 上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟 ```objective-c @@ -328,18 +367,10 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 */ ``` - - - - ## RunLoop 内部运行原理 ![RunLoop 运行原理图1](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/image-20180801113342611.png) - - - - - 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。 - Source0:非基于 port 的,用户主动触发的事件。 - Source1:基于 port,通过内核和其它线程互相发送消息 @@ -349,205 +380,161 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 ![转载于网络](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1785352-087fd4b664e0e387.png) - - - - - - ## 底层实现 - - 内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer) 我们来看看苹果官方开源的 [CFRunLoop.c 文件](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。看几个关键函数的实现猜测下 RunLoop 的内部原理 -以下的代码都有注释说明 +但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈 -**__CFRunLoopModeIsEmpty** +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/RunLoop-Specific.png) - 此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop +查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要 - - -```objective-c -// expects rl and rlm locked - static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) { - CHECK_FOR_FORK(); - if (NULL == rlm) return true; - #if DEPLOYMENT_TARGET_WINDOWS - if (0 != rlm->_msgQMask) return false; - #endif - Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); - if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue - // 判断时候有没有_sources0 - if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; - // 判断时候有没有_sources1 - if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; - // 判断时候有没有_timers - if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; - - - struct _block_item *item = rl->_blocks_head; - while (item) { - struct _block_item *curr = item; - item = item->_next; - Boolean doit = false; - if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { - doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); - } else { - doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); - } - if (doit) return false; - } - return true; - } +```c +SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + // ... + // 通知 Observers 进入 RunLoop + __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); + // RunLoop 运行循环主逻辑 + result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); + // 通知 Observers 退出 RunLoop + __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); + return result; +} ``` +我们继续看看 `__CFRunLoopRun` 。源码很多很乱,对无关代码进行裁剪,便于理解流程逻辑 + +```c +static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { + int32_t retVal = 0; + do { + // 通知 Obserers:即将处理 Timers + if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); + // 通知 Obserers:即将处理 Sources + if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); + // 处理 blocks + __CFRunLoopDoBlocks(rl, rlm); + // 处理 Source0 + Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); + if (sourceHandledThisLoop) { + // 处理 blocks + __CFRunLoopDoBlocks(rl, rlm); + } + + Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); + // 判断有无 Source1 + if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { + msg = (mach_msg_header_t *)msg_buffer; + // 如果有 Source1 则跳转到 handle_msg + if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { + goto handle_msg; + } + } + + didDispatchPortLastTime = false; + // 通知 Observers:即将休眠 + if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); + + CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent(); -**CFRunLoopRun、CFRunLoopRunInMode** + do { + if (kCFUseCollectableAllocator) { + // objc_clear_stack(0); + // + memset(msg_buffer, 0, sizeof(msg_buffer)); + } + msg = (mach_msg_header_t *)msg_buffer; + // 等待其他消息来唤醒 RunLoop + __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); - 1、2个函数的作用分别是让 RunLoop 跑在 KCFRunLoopDefaultMode 下和特定的 Mode 下 + if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { + // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer. + while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue)); + if (rlm->_timerFired) { + // Leave livePort as the queue port, and service timers below + rlm->_timerFired = false; + break; + } else { + if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); + } + } else { + // Go ahead and leave the inner loop. + break; + } + } while (1); + // 通知 Observers:结束休眠 + if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); - 2、2个函数本质上都是调用 CFRunLoopRunSpecific + handle_msg:; + __CFRunLoopSetIgnoreWakeUps(rl); -```objective-c - // 用DefaultMode启动 - void CFRunLoopRun(void) { /* DOES CALLOUT */ - int32_t result; - do { - result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); - CHECK_FOR_FORK(); - } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); - } + if (MACH_PORT_NULL == livePort) { + CFRUNLOOP_WAKEUP_FOR_NOTHING(); + // handle nothing + } else if (livePort == rl->_wakeUpPort) { + CFRUNLOOP_WAKEUP_FOR_WAKEUP(); + } + // 被 Timer 唤醒,执行代码。 + else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { + CFRUNLOOP_WAKEUP_FOR_TIMER(); + if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { + // Re-arm the next timer, because we apparently fired early + __CFArmNextTimerInMode(rlm, rl); + } + } + else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { + CFRUNLOOP_WAKEUP_FOR_TIMER(); + // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled. + // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754 + if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { + // Re-arm the next timer + __CFArmNextTimerInMode(rlm, rl); + } + } + // 被 GCD 唤醒 + else if (livePort == dispatchPort) { + // 处理 GCD + __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); + } else { + // 被 Source1 唤醒 + CFRUNLOOP_WAKEUP_FOR_SOURCE(); + // 处理 Source1 + __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; - // 用指定的Mode启动,允许设置RunLoop超时时间 - SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ - CHECK_FOR_FORK(); - return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); - } + } + if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); + // 处理 Blocks + __CFRunLoopDoBlocks(rl, rlm); + + // 设置返回值 + if (sourceHandledThisLoop && stopAfterHandle) { + retVal = kCFRunLoopRunHandledSource; + } else if (timeout_context->termTSR < mach_absolute_time()) { + retVal = kCFRunLoopRunTimedOut; + } else if (__CFRunLoopIsStopped(rl)) { + __CFRunLoopUnsetStopped(rl); + retVal = kCFRunLoopRunStopped; + } else if (rlm->_stopped) { + rlm->_stopped = false; + retVal = kCFRunLoopRunStopped; + } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { + retVal = kCFRunLoopRunFinished; + } + + voucher_mach_msg_revert(voucherState); + os_release(voucherCopy); + + } while (0 == retVal); + return retVal; +} ``` - - -**CFRunLoopRunSpecific** - - 参数1: RunLoop 对象。参数2:运行 Mode 名称。参数3:超时时间。参数4:主_CFRunLoopRun 会用到 - -```objective-c - // RunLoop的实现 - SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ - CHECK_FOR_FORK(); - if (__CFRunLoopIsDeallocating(rl)) - return kCFRunLoopRunFinished; - __CFRunLoopLock(rl); - - - // 根据modeName找到对应mode - CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); - - - // 判断mode里没有source/timer, 没有直接返回。 - if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { - Boolean did = false; - if (currentMode) - __CFRunLoopModeUnlock(currentMode); - __CFRunLoopUnlock(rl); - return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; - } - - - volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); - - - CFRunLoopModeRef previousMode = rl->_currentMode; - rl->_currentMode = currentMode; - int32_t result = kCFRunLoopRunFinished; - - - if (currentMode->_observerMask & kCFRunLoopEntry ) - // 1. 通知 Observers: RunLoop 即将进入 loop - __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); - // 进入loop - result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); - - - if (currentMode->_observerMask & kCFRunLoopExit ) - // 10.通知 Observers: RunLoop 即将退出。 - __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); - - - __CFRunLoopModeUnlock(currentMode); - __CFRunLoopPopPerRunData(rl, previousPerRun); - rl->_currentMode = previousMode; - __CFRunLoopUnlock(rl); - return result; - } -``` - - - -**__CFRunLoopDoObserver** - - - - 调用 Observer 回调 - - 联想给 RunLoop 添加观察者,监听 RunLoop 状态。 - -```objective-c - static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */ - CHECK_FOR_FORK(); - - - CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0; - if (cnt < 1) return; - - - /* Fire the observers */ - STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1); - CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef)); - CFIndex obs_cnt = 0; - //遍历 rlm-> _observers,将元素放到 collectedObservers 数组中 - for (CFIndex idx = 0; idx < cnt; idx++) { - CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx); - if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) { - collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo); - } - } - __CFRunLoopModeUnlock(rlm); - __CFRunLoopUnlock(rl); - for (CFIndex idx = 0; idx < obs_cnt; idx++) { - CFRunLoopObserverRef rlo = collectedObservers[idx]; - __CFRunLoopObserverLock(rlo); - if (__CFIsValid(rlo)) { - Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo); - __CFRunLoopObserverSetFiring(rlo); - __CFRunLoopObserverUnlock(rlo); - __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info); - if (doInvalidate) { - CFRunLoopObserverInvalidate(rlo); - } - __CFRunLoopObserverUnsetFiring(rlo); - } else { - __CFRunLoopObserverUnlock(rlo); - } - CFRelease(rlo); - } - __CFRunLoopLock(rl); - __CFRunLoopModeLock(rlm); - - - if (collectedObservers != buffer) free(collectedObservers); - } -``` - - - -**__CFRunLoopRun** +另一个版本 ```objective-c /* rl, rlm are locked on entrance and exit */ @@ -940,3 +927,765 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 } ``` +__CFRunLoopModeIsEmpty + +此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop + +```objective-c +// expects rl and rlm locked + static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) { + CHECK_FOR_FORK(); + if (NULL == rlm) return true; + #if DEPLOYMENT_TARGET_WINDOWS + if (0 != rlm->_msgQMask) return false; + #endif + Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); + if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue + // 判断时候有没有_sources0 + if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; + // 判断时候有没有_sources1 + if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; + // 判断时候有没有_timers + if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; + + + struct _block_item *item = rl->_blocks_head; + while (item) { + struct _block_item *curr = item; + item = item->_next; + Boolean doit = false; + if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { + doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); + } else { + doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); + } + if (doit) return false; + } + return true; + } +``` + +**CFRunLoopRun、CFRunLoopRunInMode** + +1、2个函数的作用分别是让 RunLoop 跑在 KCFRunLoopDefaultMode 下和特定的 Mode 下 + +2、2个函数本质上都是调用 CFRunLoopRunSpecific + +```objective-c + // 用DefaultMode启动 + void CFRunLoopRun(void) { /* DOES CALLOUT */ + int32_t result; + do { + result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); + CHECK_FOR_FORK(); + } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); + } + + + // 用指定的Mode启动,允许设置RunLoop超时时间 + SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); + } +``` + +## RunLoop 休眠原理 + +本质上就是函数 `__CFRunLoopServiceMachPort`   来控制实现休眠。查看 CFRunLoop.c 源码可以发现,是由 `mach_msg` 实现的。不是平时的在应用层 API 上 sleep 休眠的。比如 while 循环。`mach_msg` 休眠是从用户态切换到内核态,内核 api 控制休眠,做到真正节省影响的作用,等到由新消息来到,继续切换到用户态。 + +```c +static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) { + Boolean originalBuffer = true; + kern_return_t ret = KERN_SUCCESS; + for (;;) { /* In that sleep of death what nightmares may come ... */ + mach_msg_header_t *msg = (mach_msg_header_t *)*buffer; + msg->msgh_bits = 0; + msg->msgh_local_port = port; + msg->msgh_remote_port = MACH_PORT_NULL; + msg->msgh_size = buffer_size; + msg->msgh_id = 0; + if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); } + ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL); + + // Take care of all voucher-related work right after mach_msg. + // If we don't release the previous voucher we're going to leak it. + voucher_mach_msg_revert(*voucherState); + + // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one. + *voucherState = voucher_mach_msg_adopt(msg); + + if (voucherCopy) { + if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) { + // Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy. + // CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged. + *voucherCopy = voucher_copy(); + } else { + *voucherCopy = NULL; + } + } + + CFRUNLOOP_WAKEUP(ret); + if (MACH_MSG_SUCCESS == ret) { + *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL; + return true; + } + if (MACH_RCV_TIMED_OUT == ret) { + if (!originalBuffer) free(msg); + *buffer = NULL; + *livePort = MACH_PORT_NULL; + return false; + } + if (MACH_RCV_TOO_LARGE != ret) break; + buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE); + if (originalBuffer) *buffer = NULL; + originalBuffer = false; + *buffer = realloc(*buffer, buffer_size); + } + HALT; + return false; +} +``` + +## CFRunLoopTimerRef 是基于时间的触发器 + +- 基本上说就是 NSTimer,它会收到 RunLoopMode 的影响 +- GCD 的 timer 不受 RunLoopMode 的影响 + +## 源码解读 + +- performSelector 是在 source0 上实现的 +- RunLoopTimer 外部接口设置的精度,精度大于0,则使用 dispatch_source_set_timer,精度小于0,则使用 mk_timer_arm +- timer_source 使用 dispatch_timer_STRICT 创建,则系统会尽最大努力遵守设置的 leeway 值 +- NSTimer 不准的原因:底层 RunLoop Timer 底层使用的 timer 的精度不高(mk_timer);与 RunLoop 底层的调用机制有关系 +- 那么为什么存在 RunLoopTimer?意义是什么?应用场景 + +```c +// Data structure to hold TSD data, cleanup functions for each +typedef struct __CFTSDTable { + uint32_t destructorCount; + uintptr_t data[CF_TSD_MAX_SLOTS]; + tsdDestructor destructors[CF_TSD_MAX_SLOTS]; +} __CFTSDTable; +``` + +```c +// 主线程 RunLoop +CFRunLoopRef CFRunLoopGetMain(void) { + CHECK_FOR_FORK(); + // 局部静态变量 + static CFRunLoopRef __main = NULL; // no retain needed + // 创建主线程对应的 RunLoop + if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed + return __main; +} + +// 子线程 RunLoop +// 先从 __CFTSDTable 获取 RunLoop,如果有则 return,没有则调用 _CFRunLoopGet0,_CFRunLoopGet0 内部调用 __CFRunLoopCreate 创建 CFRunLoop,然后写入 __CFTSDTable +CFRunLoopRef CFRunLoopGetCurrent(void) { + CHECK_FOR_FORK(); + // __CFTSDTable + CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); + if (rl) return rl; + // 没有则创建 + return _CFRunLoopGet0(pthread_self()); +} + +CF_EXPORT CFRunLoopRef _CFRunLoopGet0(_CFThreadRef t) { + if (pthread_equal(t, kNilPthreadT)) { + t = pthread_main_thread_np(); + } + __CFLock(&loopsLock); + if (!__CFRunLoops) { + CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); + CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); + CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); + if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { + CFRelease(dict); + } + CFRelease(mainLoop); + } + CFRunLoopRef newLoop = NULL; + CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); + if (!loop) { + newLoop = __CFRunLoopCreate(t); + CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); + loop = newLoop; + } + __CFUnlock(&loopsLock); + // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it + if (newLoop) { CFRelease(newLoop); } + + if (pthread_equal(t, pthread_self())) { + _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); + if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { +#if _POSIX_THREADS + _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); +#else + _CFSetTSD(__CFTSDKeyRunLoopCntr, 0, &__CFFinalizeRunLoop); +#endif + } + } + return loop; +} +``` + +为什么只有当 RunLoop 中存在 Timer Sourcrs、Input Sources 时,才能保证 RunLoop 不退出? + +RunLoop 本质就是一个有条件的 do...while 循环。__CFRunLoopModeIsEmpty 里面去判断 source0、source1、timers 不存在则 while 循环条件不满足,RunLoop 退出 + +```c +void CFRunLoopRun(void) { /* DOES CALLOUT */ + int32_t result; + do { + result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); + CHECK_FOR_FORK(); + } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); +} + +// expects rl and rlm locked +static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) { + // ... + if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; + if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; + if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; + // ... + return true; +} +``` + +```c +SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ +// ... +CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); +if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { + Boolean did = false; + if (currentMode) __CFRunLoopModeUnlock(currentMode); + __CFRunLoopUnlock(rl); + return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; +} +// ... +``` + +## Mach Port 跨线程通信 + +1. Mach IPC 基于 Mach 内核实现进程间通讯。 + +2. Mach IPC 被抽象为3种操作:messages、ports、and port sets + +3. Mach port 跨线程通信 + + 线程 B 有个 port 在等待消息,具有消息接收权限,等待消息到来的时候会阻塞当前线程 + + 线程 A 有个 port 在发送消息,具有发送消息权限,要把发送的消息包装成消息,通过消息队列传递,message 包括:header(目的地 port、size)、data。 + + 线程 B 收到消息后,解除 block,线程继续向下运行 + +4. Mach port 如何进行跨线程通信? + + 线程开启一个 port,然后给 port 申请接收、发送的权限。mach_msg 是通信函数,在等待消息的时候不加 timeout 则会一直阻塞,等到消息到来 + +5. 在测试工作中 main.m 文件中打印当前的 RunLoop + + ```objectivec + int main(int argc, char * argv[]) { + NSString * appDelegateClassName; + @autoreleasepool { + appDelegateClassName = NSStringFromClass([AppDelegate class]); + } + NSLog(@"%@", NSRunLoop.currentRunLoop); + return UIApplicationMain(argc, argv, nil, appDelegateClassName); + } + + // + 2020-08-13 09:49:41.326621+0800 ***[52423:2383402] {wakeup port = 0x1103, stopped = false, ignoreWakeUps = true, + current mode = (none), + common modes = {type = mutable set, count = 1, + entries => + 2 : {contents = "kCFRunLoopDefaultMode"} + } + , + common mode items = (null), + modes = {type = mutable set, count = 1, + entries => + 2 : {name = kCFRunLoopDefaultMode, port set = 0x1003, queue = 0x6000007f0100, source = 0x6000007f0280 (not fired), timer port = 0xe03, + sources0 = (null), + sources1 = (null), + observers = (null), + timers = (null), + currently 618976181 (257342842892563) / soft deadline in: 1.84464867e+10 sec (@ -1) / hard deadline in: 1.84464867e+10 sec (@ -1) + }, + } + } + ``` + + 可以看到 main.m 中,还没有 return 的时候当前 RunLoop 的内部结构中存在一个 **wakeup port** 的端口。查看 RunLoop 源代码 **wakeup port** 就是 mach port 的一种。 + + ```c + typedef mach_port_t __CFPort; + + struct __CFRunLoop { + CFRuntimeBase _base; + _CFRecursiveMutex _lock; /* locked for accessing mode list */ + __CFPort _wakeUpPort; // used for CFRunLoopWakeUp + Boolean _unused; + volatile _per_run_data *_perRunData; // reset for runs of the run loop + _CFThreadRef _pthread; + uint32_t _winthread; + CFMutableSetRef _commonModes; + CFMutableSetRef _commonModeItems; + CFRunLoopModeRef _currentMode; + CFMutableSetRef _modes; + struct _block_item *_blocks_head; + struct _block_item *_blocks_tail; + CFAbsoluteTime _runTime; + CFAbsoluteTime _sleepTime; + CFTypeRef _counterpart; + _Atomic(uint8_t) _fromTSD; + CFLock_t _timerTSRLock; + }; + ``` + + ```c + void CFRunLoopWakeUp(CFRunLoopRef rl) { + // ... + kern_return_t ret; + /* We unconditionally try to send the message, since we don't want + * to lose a wakeup, but the send may fail if there is already a + * wakeup pending, since the queue length is 1. */ + ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0); + // ... + int ret; + do { + ret = eventfd_write(rl->_wakeUpPort, 1); + } while (ret == -1 && errno == EINTR); + // ... + SetEvent(rl->_wakeUpPort); + // ... + } + ``` + + `CFRunLoopWakeUp(CFRunLoopGetCurrent());` 可以唤醒 RunLoop,函数的底层实现如上。核心实现就是 `__CFSendTrivialMachMessage` 函数。在iOS 中,除了 source1 可以自己唤醒 RunLoop 之外,其他的事件都需要用户手动唤醒 RunLoop 才可以。RunLoop 提供了专门的方法来实现这个功能。其核心部分就是调用 mach_msg 来向指定的 **_wakeUpPort** 端口发送消息,从而唤醒线程继续工作。 + + 为什么 Source1 可以唤醒 RunLoop?因为 Source1 本质上就是针对 Mach Port 的封装 + +6. 做个实验检测 _wakeUpPort 端口 + + ```objective-c + - (void)viewDidLoad { + [super viewDidLoad]; + [self listenWakeUpPort]; + } + + - (void)listenWakeUpPort + { + NSArray *array = [NSRunLoop.currentRunLoop.description componentsSeparatedByString:@"wakeup port = "]; + NSString *wakeupPort = [array.lastObject substringToIndex:[array.lastObject rangeOfString:@","].location]; + + dispatch_queue_t queue = dispatch_queue_create("com.test.wake_up_port_queue", DISPATCH_QUEUE_CONCURRENT); + dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, [self numberWithHexString:wakeupPort], 0, queue); + dispatch_source_set_event_handler(source, ^{ + mach_port_t port = (mach_port_t)dispatch_source_get_handle(source); + NSLog(@"%u--wakeUp", port); + }); + dispatch_activate(source); + } + + - (NSInteger)numberWithHexString:(NSString *)hexString + { + const char *hexChar = [hexString cStringUsingEncoding:NSUTF8StringEncoding]; + int hexNumber; + sscanf(hexChar, "%x", &hexNumber); + return (NSInteger)hexNumber; + } + + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event + { + CFRunLoopWakeUp(CFRunLoopGetCurrent()); + } + ``` + + 可以看到每次在点击屏幕时调用 `CFRunLoopWakeUp` 尝试唤醒 RunLoop,然后监听 RunLoop 的 _wakeUpPort,都可以在回调中获取到消息。 + +## RunLoop 应用场景 + +- 控制线程生命周期(线程保活) + +- 解决 NSTimer 在滑动时停止工作的问题 + +- APM 卡顿监控 + +- 性能优化 + +## NSTimer 经常会不准确,原因是什么? + +NSTimer 在创建的时候经常会指派到特定的 NSRunLoopMode 中去,举个例子,默认创建的NSTimer 是被添加到 NSRunLoopDefaultMode 中去,当你的页面上有 UIScrollView 或者子类的时如果被拖动了,当前 RunLoop 的 NSRunloopMode 会从 NSDefaultRunLoopMode 转变为 UITrackingRunLoopMode 。遇到这种情况你需要精确的 NSTimer 的话,在创建好 NSTimer 之后,设置 RunLoopMod 为 NSRunLoopCommonModes。 + +注意:NSRunLoopCommonModes 只是一个标识而已,而不是具体的模式。 + +`[NSRunLoop currentRunLoop] addTimer:forMode:` 的作用是告诉 RunLoop 当前 Timer 是可以在 NSRunLoopCommonModes 这个标识的 Mode 下运行。 + +UITrackingRunLoopMode、NSDefaultRunLoopMode 都是属于 NSRunLoopCommonModes 这个标识的。 + +```objective-c +NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES]; +[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; +``` + +NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 + +```objective-c +#import "ViewController.h" + +@interface ViewController () +@property (nonatomic, strong) dispatch_source_t timer; +@end + + +@implementation ViewController + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + /* + 只在默认状态下执行的 NSTimer + [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { + NSLog(@"我在执行了"); + }]; + */ + + /* + 指定 NSRunLoopMode 的 NSTimer + NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES]; + [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; + */ + + /* + GCD 的单位是 纳秒. + 使用 GCD 创建的 timer 正常创建后不会执行,因为创建后设置了指定的时间后触发,所以当代码运行到最后一行的时候,Timer 还没执行,就被销毁了。所以我们必须设置一个属性去保存它。 + */ + //1、创建队列 + dispatch_queue_t queue = dispatch_get_main_queue(); + //2、创建 timer + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + self.timer = timer; + //3、设置 timer 的参数:精准度、时间间隔 + //第三个参数为 GCD timer 的精准度 + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); + //4、为 Timer 设置任务 + dispatch_source_set_event_handler(timer, ^{ + NSLog(@"%@",[NSRunLoop currentRunLoop]); + }); + //5、执行任务 + dispatch_resume(timer); +} + +- (void)show{ + NSLog(@"shw-%@",[NSThread currentThread]); + NSLog(@"%@",[NSRunLoop currentRunLoop]); +} +@end +``` + +### ImageView显示(PerformSelector) + +UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,**FPS** 达不到60。 + +利用 RunLoop 可以实现这个效果,就是给下载并显示图片的方法指定 **NSRunLoopMode**。 + +```objectivec +- (IBAction)clickLoadIMage:(id)sender { + //[self.imageview performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"test"] afterDelay:2]; + [self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]]; +} + +- (void)downloadAndShowImage{ + self.imageview.image = [UIImage imageNamed:@"test"]; +} +``` + +### 自动释放池 + +自动释放池什么时候创建和释放 + +创建时间:第一次进入 RunLoop 的时候 + +释放时间:RunLoop 退出的时候 + +其他情况:当 RunLoop 将要休眠的时候释放,然后创建一个新的 + +**_wrapRunLoopWithAutoreleasePoolHandler** **0x1** + +**_wrapRunLoopWithAutoreleasePoolHandler** **0xa0** + +0x1 和 0xa0 是十六进制的数,对应十进制为1和160。 + +### RunLoop 空闲时做一些任务 + +```objectivec +- (void)print +{ + NSLog(@"test"); +} + +- (void)viewDidLoad +{ + CFRunLoopActivity flags = kCFRunLoopBeforeWaiting; + CFRunLoopObserverRef runloopObserver = CFRunLoopObserverCreateWithHandler( + kCFAllocatorDefault, flags, YES, 0, + ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + [self performSelector:@selector(print) + onThread:[NSThread mainThread] + withObject:nil + waitUntilDone:NO + modes:@[ NSDefaultRunLoopMode ]]; + }); + CFRunLoopAddObserver(CFRunLoopGetCurrent(), runloopObserver, kCFRunLoopDefaultMode); +} +``` + +### 线程保活 + +应用场景:经常在子线程中处理某些逻辑的场景。如果销毁再创建再销毁再创建效率很低,这个情况下就需要线程保活。 + +```objectivec +@interface LBPThread : NSThread +@end +@implementation LBPThread +- (void)dealloc{ + NSLog(@"%s", __func__); +} +@end + +@interface ViewController () +@property (nonatomic, strong) LBPThread *task; +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.task = [[LBPThread alloc] initWithTarget:self selector:@selector(run) object:nil]; + [self.task start]; +} +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + [self performSelector:@selector(test) onThread:self.task withObject:nil waitUntilDone:NO]; +} +- (void)test{ + NSLog(@"沿用保活的线程,做事情:%@", [NSThread currentThread]); +} +- (void)run{ + NSLog(@"%s %@", __func__, [NSThread currentThread]); + // 给 RunLoop 的某个 Mode 里添加 Source/Timer/Observer + [[NSRunLoop currentRunLoop] addPort:[[NSMachPort alloc] init] forMode:NSDefaultRunLoopMode]; + [[NSRunLoop currentRunLoop] run]; + NSLog(@"task finished"); +} +@end +``` + +默认创建的 NSThread 会在 NSDefaultRunLoopMode 模式下运行,当 UI 滑动则进入 UITrackingMode 模式,所以 NSThread 的方法会停止。 + +线程保活就是给方法内部添加 RunLoop,但是新创建的 RunLoop 运行肯定是基于某个 Mode,RunLoop 持续运行(保活)的前提是必须有 Timer、Sources、Observer。所以添加了 NSMachPort。 + +上面的代码存在问题: + +1. ViewController 存在内存泄漏,`initWithTarget:self` 因为线程保活,所以 self 被持有,不会执行 dealloc + +2. LBPThread 线程不会死亡,假如我们需要在某个时机让保活线程销毁,现在是办不到的 + +改进: + +1. Thread 换种 api `-(instancetype)initWithBlock:(void (^)(void))block`,线程不持有 self + +2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop + + ![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/RunLoop-RunIssue.png) + +改进代码如下 + +```objectivec +@interface ViewController () +@property (strong, nonatomic) LBPThread *task; +@property (assign, nonatomic, getter=isStoped) BOOL stopped; +@end + +@implementation ViewController +- (void)viewDidLoad { + [super viewDidLoad]; + __weak typeof(self) weakSelf = self; + self.stopped = NO; + self.task = [[LBPThread alloc] initWithBlock:^{ + NSLog(@"%@----begin----", [NSThread currentThread]); + // 往RunLoop里面添加Source\Timer\Observer + [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; + while (weakSelf && !weakSelf.isStoped) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + NSLog(@"%@----end----", [NSThread currentThread]); + }]; + [self.task start]; +} +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (!self.task) return; + [self performSelector:@selector(test) onThread:self.task withObject:nil waitUntilDone:NO]; +} +// 子线程需要执行的任务 +- (void)test { + NSLog(@"沿用保活的线程,做事情:%@", [NSThread currentThread]); +} +- (IBAction)stop { + if (!self.task) return; + // 在子线程调用stop + [self performSelector:@selector(stopThread) onThread:self.task withObject:nil waitUntilDone:YES]; +} +// 用于停止子线程的RunLoop +- (void)stopThread{ + // 设置标记为NO + self.stopped = YES; + // 停止RunLoop + CFRunLoopStop(CFRunLoopGetCurrent()); + NSLog(@"%s %@", __func__, [NSThread currentThread]); + self.task = nil; +} +- (void)dealloc{ + NSLog(@"%s", __func__); +} +@end +``` + +注意:  + +- 如果 `stop`  方法内部的 `waitUntilDone` 为 NO,则会出现 Crash。因为该参数代表后续代表会不会等该 selector 执行完毕。因为为 NO,所以 ViewController 执行 dealloc 了,所以 `dealloc` 方法和 Thread 内部的 block 同时进行,不能确保在 block 内部执行的时候 dealloc 有没有执行完,访问 weakSelf.isStoped 可能会 crash + +- 线程的 RunLoop 结束了,线程也无法执行任务了,所以需要给线程对象设置为 nil。同时任务派发的地方也需要判断线程是否存在,否则会 crash + +- NSRunLoop 常驻线程可以运行在 NSRunLoopCommonModes 下吗? + + 不可以。因为在跑的时候如果 modeName 等于 kCFRunLoopCommonModes 则直接 kCFRunLoopRunFinished,则 RunLoop 的 while 循环条件失败 + + ```objective-c + SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + CFLog(kCFLogLevelError, CFSTR("invalid mode '%@' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution."), modeName); + _CFRunLoopError_RunCalledWithInvalidMode(); + }); + return kCFRunLoopRunFinished; + } + // ... + return result; + } + ``` + +### 线程封装 + +思考:如何设计一个常驻线程工具类? + +继承自 NSThread 吗?不行,这样的话, api 不够收口,留的口子太多,不方便管控。 + +```objectivec +#import + +typedef void (^LBPPermenantThreadTask)(void); + +@interface LBPPermenantThread : NSObject + +/** + 开启线程 + */ +- (void)run; + +/** + 在当前子线程执行一个任务 + */ +- (void)executeTask:(LBPPermenantThreadTask)task; + +/** + 结束线程 + */ +- (void)stop; + +@end + + +#import "LBPPermenantThread.h" + +/** MJThread **/ +@interface LBPThread : NSThread +@end +@implementation LBPThread +- (void)dealloc +{ + NSLog(@"%s", __func__); +} +@end + +/** MJPermenantThread **/ +@interface LBPPermenantThread() +@property (strong, nonatomic) LBPThread *innerThread; +@property (assign, nonatomic, getter=isStopped) BOOL stopped; +@end + +@implementation LBPPermenantThread +#pragma mark - public methods +- (instancetype)init +{ + if (self = [super init]) { + self.stopped = NO; + + __weak typeof(self) weakSelf = self; + + self.innerThread = [[LBPThread alloc] initWithBlock:^{ + [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; + + while (weakSelf && !weakSelf.isStopped) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + }]; + + [self.innerThread start]; + } + return self; +} + +- (void)run +{ + if (!self.innerThread) return; + [self.innerThread start]; +} + +- (void)executeTask:(LBPPermenantThreadTask)task +{ + if (!self.innerThread || !task) return; + + [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO]; +} + +- (void)stop +{ + if (!self.innerThread) return; + + [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES]; +} + +- (void)dealloc +{ + NSLog(@"%s", __func__); + [self stop]; +} + +#pragma mark - private methods +- (void)__stop +{ + self.stopped = YES; + CFRunLoopStop(CFRunLoopGetCurrent()); + self.innerThread = nil; +} + +- (void)__executeTask:(LBPPermenantThreadTask)task +{ + task(); +} +@end +``` diff --git a/Chapter1 - iOS/1.39.md b/Chapter1 - iOS/1.39.md index 4d6e1d8..f95e4f2 100644 --- a/Chapter1 - iOS/1.39.md +++ b/Chapter1 - iOS/1.39.md @@ -1,1108 +1,1405 @@ -# 监听 RunLoop -```Objective-C -//给 RunLoop 添加监听者 -- (void)testRunLoopObserver{ +# 多线程探究 - //创建监听者 -// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>) - /* - 创建监听对象 - 参数1:分配内存空间 - 参数2:要监听的状态 kCFRunLoopAllActivities :所有状态 - 参数3:是否要持续监听 - 参数4:优先级 - 参数5:回调 - */ - CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - switch (activity) { - case kCFRunLoopEntry: - NSLog(@"RunLoop 闪亮登场"); - break; - case kCFRunLoopBeforeTimers: - NSLog(@"RunLoop 大哥要处理 Timer 了"); - break; - case kCFRunLoopBeforeSources: - //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息 - NSLog(@"RunLoop 大哥要处理 Source 了"); - break; - case kCFRunLoopBeforeWaiting: - NSLog(@"RunLoop 大哥没事干要睡觉了"); - break; - case kCFRunLoopAfterWaiting: - NSLog(@""); - NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了"); - break; - case kCFRunLoopExit: - NSLog(@"RunLoop 大哥要退出离开了"); - break; - default: - break; +> 平时我们经常使用 GCD、锁、队列、block,那这些概念和本质到底是什么?线程安全如何实现?自旋锁、互斥锁区别是什么?本文来一探究竟 + +## 多线程方案 + +| 技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 | +| ----------- | -------------------------------------------------------------- | --- | ------- | ---------- | +| pthread | -一套通用的多线程API
适用于Unix\Linux\Windows等系统
跨平台\可移植
使用难度大 | C | 开发者手动管理 | 很少,底层监控会用到 | +| NSThread | 使用更加面向对象
简单易用,可直接操作线程对象 | OC | 开发者手动管理 | 偶尔 | +| GCD | 旨在替代NSThread等线程技术
充分利用设备的多核 | C | 系统自动管理 | 经常 | +| NSOperation | 基于GCD(底层是GCD)
比GCD多了一些更简单实用的功能
使用更加面向对象 | OC | 系统自动管理 | 经常 | + +| | 并发队列 | 自定义串行队列 | 主队列(串行) | +| --------- | ------------ | ------------ | ------------ | +| 同步(sync) | 不开新线程、串行执行任务 | 不开新线程、串行执行任务 | 不开新线程、串行执行任务 | +| 异步(async) | 开新线程、并发执行任务 | 开新线程、串行执行任务 | 不开新线程、串行执行任务 | + +## 多线程死锁 + +看几个 Demo 观察下死锁情况 + +Demo1 + +```objectivec +NSLog(@"执行任务1"); +dispatch_queue_t queue = dispatch_get_main_queue(); +dispatch_sync(queue, ^{ + NSLog(@"执行任务2"); +}); +NSLog(@"执行任务3"); +// 死锁 +``` + +分析:主队列是一个串行队列,任务3等待 `dispatch_sync` 内的任务执行完毕,可 `dispatch_sync` 内的任务等待任务3执行,互相等待,产生死锁 + +Demo2 + +```objectivec +NSLog(@"执行任务1"); +dispatch_queue_t queue = dispatch_get_main_queue(); +dispatch_async(queue, ^{ + NSLog(@"执行任务2"); +}); +NSLog(@"执行任务3"); +// 1 3 2 +``` + +Demo3 + +```objectivec +NSLog(@"执行任务1"); +dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL); +dispatch_async(queue, ^{ // 0 + NSLog(@"执行任务2") + dispatch_sync(queue, ^{ // 1 + NSLog(@"执行任务3"); + }); + NSLog(@"执行任务4"); +}); +NSLog(@"执行任务5"); +// 1 5 2 Crash +``` + +分析:任务4等待 `dispatch_sync` 内的任务3,`dispatch_sync` 内的任务3等待任务4执行,互相等待 + +Demo4 + +```objectivec +NSLog(@"执行任务1"); +dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL); +dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL); +dispatch_async(queue, ^{ // 0 + NSLog(@"执行任务2"); + dispatch_sync(queue2, ^{ // 1 + NSLog(@"执行任务3"); + }); + NSLog(@"执行任务4"); +}); +NSLog(@"执行任务5"); +// 1 5 2 3 4 +``` + +分析:不会死锁。因为在存在2个任务队列。所以会按照顺序各自从队列上取任务执行。 + +Demo5 + +```objectivec +NSLog(@"执行任务1"); +dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT); +dispatch_async(queue, ^{ // 0 + NSLog(@"执行任务2"); + dispatch_sync(queue, ^{ // 1 + NSLog(@"执行任务3"); + }); + NSLog(@"执行任务4"); +}); +NSLog(@"执行任务5"); +// 1 5 2 3 4 +``` + +总结: + +- 队列决定了任务执行完是否需要等待。任务决定是否可以产生新线程 + +- 死锁:使用`sync` 函数给当前串行队列派发任务,则会卡住当前串行队列,产生死锁 + +Demo6 + +```objectivec +- (void)test{ + NSLog(@"2"); +} +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + dispatch_queue_t queue = dispatch_get_global_queue(0, 0); + dispatch_async(queue, ^{ + NSLog(@"1"); + [self performSelector:@selector(test) withObject:nil afterDelay:3]; + NSLog(@"3"); + }); +} +// 1 3 +``` + +分析:为什么打印1、3,没有打印2。因为 `-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;` 底层是开启了定时器,定时器运行需要添加到 RunLoop。上述代码是在全局并发队列上开启子线程,子线程中没有 RunLoop,所以定时器没有运行。 + +Demo7 + +```objectivec +- (void)test{ + NSLog(@"2"); +} +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + dispatch_queue_t queue = dispatch_get_global_queue(0, 0); + dispatch_async(queue, ^{ + NSLog(@"1"); + [self performSelector:@selector(test) withObject:nil]; + NSLog(@"3"); + }); +} +// 1 3 2 +``` + +分析:为什么现在又执行打印2了?因为 `-(id)performSelector:(SEL)aSelector withObject:(id)object;` 是 Runtime API,本质上就是 `objc_msgSend`,所以不需要 RunLoop 便可运行 + +查看 objc4 `NSObject.m` 即可 + +```c ++ (id)performSelector:(SEL)sel withObject:(id)obj { + if (!sel) [self doesNotRecognizeSelector:sel]; + return ((id(*)(id, SEL, id))objc_msgSend)((id)self, sel, obj); +} +``` + +## iOS 底层研究宝藏 + +如何查看 `-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;` 源码。 + +- 未开源,但是可以设置断点查看汇编分析; + +- Apple 的 XNU 是参考 [GNUstep](http://www.gnustep.org/resources/downloads.php#core),它将 Cocoa 的 OC 库重新实现并开源。虽然不是官方源代码,但是具有研究参考价值 + +查看 GUNStep 源码 + +```c +- (void) performSelector: (SEL)aSelector + withObject: (id)argument + afterDelay: (NSTimeInterval)seconds +{ + NSRunLoop *loop = [NSRunLoop currentRunLoop]; + GSTimedPerformer *item; + + item = [[GSTimedPerformer alloc] initWithSelector: aSelector + target: self + argument: argument + delay: seconds]; + [[loop _timedPerformers] addObject: item]; + RELEASE(item); + [loop addTimer: item->timer forMode: NSDefaultRunLoopMode]; +} +``` + +可以看到底层实现就是 开启一个 Timer 并添加到 RunLoop。但是没有 Run。所以代码改下就可运行。 + +```objectivec +- (void)test{ + NSLog(@"2"); +} +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + dispatch_queue_t queue = dispatch_get_global_queue(0, 0); + dispatch_async(queue, ^{ + NSLog(@"1"); + [self performSelector:@selector(test) withObject:nil afterDelay:3]; + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + NSLog(@"3"); + }); +} +// 1 2 3 +``` + +所以要研究 iOS 底层的同学,看看 **GUNStep 代码吧,这是宝藏** + +## 队列组 + +- 实现异步并发执行任务1、任务2 + +- 等任务1、2都执行完毕,再回到主线程执行任务3 + +```objectivec +dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); +dispatch_group_t group = dispatch_group_create(); +dispatch_group_async(group, queue, ^{ + for (NSInteger index = 0; index< 5; index++) { + NSLog(@"Task1: %@ - index:%zd", [NSThread currentThread], index); + } +}); +dispatch_group_async(group, queue, ^{ + for (NSInteger index = 0; index< 5; index++) { + NSLog(@"Task2: %@ - index:%zd", [NSThread currentThread], index); + } +}); +dispatch_group_notify(group, queue, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + for (NSInteger index = 0; index< 5; index++) { + NSLog(@"Task3: %@ - index:%zd", [NSThread currentThread], index); } }); - /* - 参数1:要监听哪个RunLoop - 参数2:监听者 - 参数3:要监听 RunLoop 在哪种运行模式下的状态 - */ - CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode); - //CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain等字眼的函数创建出来的对象都需要在最后调用 release - CFRelease(oberver); - [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES]; -} - - -//等到 RunLoop 休眠后,5秒钟叫醒 RunLoop -- (void)wakeupRunLoop{ - NSLog(@"%s",__func__); -} -/* -2018-08-01 11:23:49.401626+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 -2018-08-01 11:23:49.401950+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 -2018-08-01 11:23:49.402326+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 -2018-08-01 11:23:49.402509+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 -2018-08-01 11:23:49.402721+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 -2018-08-01 11:23:49.402855+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 -2018-08-01 11:23:49.403080+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 -2018-08-01 11:23:49.459238+0800 RunLoop[38148:1994974] -2018-08-01 11:23:49.459512+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 -2018-08-01 11:23:49.459740+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 -2018-08-01 11:23:49.459932+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 -2018-08-01 11:23:49.460431+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 -2018-08-01 11:23:49.460607+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 -2018-08-01 11:23:49.460775+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 -2018-08-01 11:23:49.880631+0800 RunLoop[38148:1994974] -2018-08-01 11:23:49.880867+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 -2018-08-01 11:23:49.881530+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 -2018-08-01 11:23:49.881699+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 -2018-08-01 11:23:49.881870+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 -2018-08-01 11:23:54.402263+0800 RunLoop[38148:1994974] -2018-08-01 11:23:54.402562+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 -2018-08-01 11:23:54.402773+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop] -2018-08-01 11:23:54.403081+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 -2018-08-01 11:23:54.403245+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 -2018-08-01 11:23:54.403476+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 -2018-08-01 11:23:59.402151+0800 RunLoop[38148:1994974] -2018-08-01 11:23:59.402511+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 -2018-08-01 11:23:59.402687+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop] -2018-08-01 11:23:59.402913+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 -2018-08-01 11:23:59.403037+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 -2018-08-01 11:23:59.403156+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 +}); ``` -![RunLoop 运行原理图1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180801-104553@2x.png) +## 多线程安全问题 +多线程存在资源共享问题。比如1块内存可能会被多个线程共享,同时读或者写,导致不一致,很容易引发数据错乱和数据安全问题。典型的生产者消费者问题 +比如多个线程访问同一个对象、同一个变量、同一个文件 -上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟 +解决方案 +使用线程同步技术(同步,就是协同步调,按预定的先后次序进行) +常见的线程同步技术是:加锁。 -```objective-c -- (void)testRunLoopObserverOnSubThread{ +常见的锁有: - //创建并发队列 - dispatch_queue_t queue = dispatch_queue_create("com.lbp.testRunLoopOnSubThread", DISPATCH_QUEUE_CONCURRENT); - //开启子线程 +- OSSpinLock +- os_unfair_lock +- pthread_mutex +- dispatch_semaphore +- dispatch_queue(DISPATCH_QUEUE_SERIAL) +- NSLock +- NSRecursiveLock +- NSCondition +- NSConditionLock +- @synchronized + +### OSSpinLock + +`OSSpinLock` 叫做”自旋锁”。 + +使用的时候需要导入 `#import ` + +`OSSpinLock lock = OS_SPINLOCK_INIT` 初始化 + +`OSSpinLockLock(&lock);` 加锁 + +`OSSpinLockUnlock(&lock);` 解锁 + +`bool res = OSSpinLockTry(&lock)` 尝试加锁(如果需要等待就不加锁直接返回 false,如果不需等待则加锁,返回 true) + +存在问题: + +- 等待锁的线程会处于忙等(busy-wait)状态,一直占用着 CPU 资源 + +- 不安全,可能会出现优先级反转问题 + +- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁 + +```objectivec +@interface ViewController () +@property (assign, nonatomic) OSSpinLock bankLock; +@property (nonatomic, assign) NSInteger money; +@end + +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + self.bankLock = OS_SPINLOCK_INIT; + self.money = 100; + [self moneyTest]; +} +- (void)moneyTest { + dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ - - //1、获得当前线程下的 RunLoop - CFRunLoopRef runloop = CFRunLoopGetCurrent(); - - - //2、为 RunLoop 创建观察者 - CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - switch (activity) { - case kCFRunLoopEntry: - NSLog(@"RunLoop 闪亮登场"); - break; - case kCFRunLoopBeforeTimers: - NSLog(@"RunLoop 大哥要处理 Timer 了"); - break; - case kCFRunLoopBeforeSources: - //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息 - NSLog(@"RunLoop 大哥要处理 Source 了"); - break; - case kCFRunLoopBeforeWaiting: - NSLog(@"RunLoop 大哥没事干要睡觉了"); - break; - case kCFRunLoopAfterWaiting: - NSLog(@""); - NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了"); - break; - case kCFRunLoopExit: - NSLog(@"RunLoop 大哥要退出离开了"); - break; - default: - break; - } - }); - //为了运行 RunLoop 必须触发事件 - [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(wakeUpRunLoopOnSubThread) userInfo:nil repeats:NO]; - //3、为当前的 RunLoop 添加观察者 - CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode); - //4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release - CFRelease(obersver); - //5、在非主线程创建的 RunLoop 必须触发运行 - [[NSRunLoop currentRunLoop] run]; + for (int i = 0; i < 10; i++) { + [self saveMoney]; + } }); + dispatch_async(queue, ^{ + for (int i = 0; i < 10; i++) { + [self withdrawMoney]; + } + }); + // 100 + 10*50 - 10*10 = 500 } - - -- (void)wakeUpRunLoopOnSubThread{ - NSLog(@"%s",__func__); +- (void)saveMoney { + OSSpinLockLock(&_bankLock); + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney += 50; + self.money = previousMoney; + NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]); + OSSpinLockUnlock(&_bankLock); } -/* -2018-08-01 14:23:06.453282+0800 RunLoop[2376:115968] RunLoop 闪亮登场 -2018-08-01 14:23:06.453608+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Timer 了 -2018-08-01 14:23:06.453781+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Source 了 -2018-08-01 14:23:06.453982+0800 RunLoop[2376:115968] RunLoop 大哥没事干要睡觉了 -2018-08-01 14:23:08.458237+0800 RunLoop[2376:115968] -2018-08-01 14:23:08.458658+0800 RunLoop[2376:115968] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 -2018-08-01 14:23:08.458894+0800 RunLoop[2376:115968] -[ViewController wakeUpRunLoopOnSubThread] -2018-08-01 14:23:08.459082+0800 RunLoop[2376:115968] RunLoop 大哥要退出离开了 -*/ +- (void)withdrawMoney { + OSSpinLockLock(&_bankLock); + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney -= 10; + self.money = previousMoney; + NSLog(@"取20,还剩%zd元 - %@", self.money, [NSThread currentThread]); + OSSpinLockUnlock(&_bankLock); +} +@end ``` +QA:优先级反转是什么? + +线程本质上就是 CPU 高速切换,看上去是同时在做多个线程内的事情。也就是时间片轮转调度算法(进程、线程)。同时不同线程的优先级不一样 + +比如存在 thread1 的优先级最高、thread2 优先级普通。CPU 在调度到 thread2 的时候会加,加锁后 CPU 调度到 thread1,在尝试给 thread1 加锁的时候发现锁被占用,所以此时在 thread1 里面自旋等待锁。thread2 等待 CPU 调度过来,但是因为 thread1 优先级比较高,所以 CPU 优先执行 thread1,可是 thread1 里等待锁,锁此时被 thread2 占用。存在死锁 + +``` +thread2 { + 加锁 +    做事情 +    解锁 +} +thread1 { + 加锁(等待锁) while(未解锁) +    做事情 +    解锁 +} +``` + +上面的代码改进下 + +```objectivec +- (void)saveMoney { + if (OSSpinLockLock(&_bankLock)) { + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney += 50; + self.money = previousMoney; + NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]); + OSSpinLockUnlock(&_bankLock); + } +} +- (void)withdrawMoney { + if (OSSpinLockLock(&_bankLock)) { + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney -= 10; + self.money = previousMoney; + NSLog(@"取20,还剩%zd元 - %@", self.money, [NSThread currentThread]); + OSSpinLockUnlock(&_bankLock); + } +} +``` + +### os_unfair_lock + +`os_unfair_lock` 用于取代不安全的 `OSSpinLock` ,从iOS10开始才支持。使用的时候需要导入头文件 `#import ` + +从底层调用看,等待 `os_unfair_lock` 锁的线程会处于休眠状态,并非忙等(自旋锁会忙等) + +初始化 `os_unfair_lock moneylock = OS_UNFAIR_LOCK_INIT;` + +加锁 `os_unfair_lock_lock(&_moneylock);` + +解锁 `os_unfair_lock_unlock(&_moneylock);` + +尝试加锁 `os_unfair_lock_trylock(&_moneylock)` + +继续对存取钱 Demo 用 `os_unfair_lock` 实现 + +```objectivec +@interface ViewController () +@property (nonatomic, assign) NSInteger money; +@property (nonatomic, assign) os_unfair_lock moneylock; +@end + +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + self.moneylock = OS_UNFAIR_LOCK_INIT; + self.money = 100; + [self moneyTest]; +} +- (void)moneyTest { + dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT); + dispatch_async(queue, ^{ + for (int i = 0; i < 10; i++) { + [self saveMoney]; + } + }); + dispatch_async(queue, ^{ + for (int i = 0; i < 10; i++) { + [self withdrawMoney]; + } + }); + // 100 + 10*50 - 10*10 = 500 +} +int cursorr = 1; +- (void)saveMoney { + NSLog(@"current cursor %d", cursorr); + cursorr++; + os_unfair_lock_lock(&_moneylock); + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney += 50; + self.money = previousMoney; + NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]); + os_unfair_lock_unlock(&_moneylock); +} +- (void)withdrawMoney { + os_unfair_lock_lock(&_moneylock); + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney -= 10; + self.money = previousMoney; + NSLog(@"取20,还剩%zd元 - %@", self.money, [NSThread currentThread]); + os_unfair_lock_unlock(&_moneylock); +} +@end +``` + +假如对存钱过程,忘记解锁怎么办?产生死锁,如下 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Thread-deadlock-unfaillock.png) + +添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。 + +这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Thread-deadlock-unfairTrylock.png) + +### pthread_mutex + +`mutex` 叫做”互斥锁”,等待锁的线程会处于休眠状态。使用时需要引入 `#import ` + +使用: + +```objectivec +// 初始化属性 +pthread_mutexattr_t attr; +pthread_mutexattr_init(&attr); +pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); +// 初始化锁 +pthread_mutex_init(&_moneyLock, &attr); +// 释放属性内存 +pthread_mutexattr_destroy(&attr); +// 加锁 +pthread_mutex_lock(&_moneyLock); +// 解锁 +pthread_mutex_unlock(&_moneyLock); +// 释放锁内存 +pthread_mutex_destroy(&_moneyLock); +``` + +其中 `pthread_mutexattr_settype(pthread_mutexattr_t *, int);` 第二个参数有4个枚举值 + +```objectivec +/* + * Mutex type attributes + */ +#define PTHREAD_MUTEX_NORMAL 0 +#define PTHREAD_MUTEX_ERRORCHECK 1 +#define PTHREAD_MUTEX_RECURSIVE 2 +#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL +``` + +如果类型选 `PTHREAD_MUTEX_DEFAULT` 或者 `PTHREAD_MUTEX_NORMAL` 则可以省略 `pthread_mutexattr_t` 的创建,直接传 NULL,即 `pthread_mutex_init(&_moneyLock, NULL)` + +使用如下 + +```objectivec +- (void)saveMoney { + pthread_mutex_lock(&_moneyLock); + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney += 50; + self.money = previousMoney; + NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]); + pthread_mutex_unlock(&_moneyLock); +} +``` + +如果在某个方法内部递归调用自身怎么实现,好像挺简单的,直接内部调用即可。 + +```objectivec +int cursor = 0; +- (void)sayHi { + if (cursor<10) { + pthread_mutex_lock(&_moneyLock); + cursor++; + NSLog(@"Hi %d", cursor); + [self sayHi]; + pthread_mutex_unlock(&_moneyLock); + } +} +// Hi 1 +``` + +只打印了 1。为什么?因为第一次调用正常加锁,然后递归调用自身,第二次调用的时候尝试加锁,但是这时候第一次调用时候锁还没释放。 + +互斥锁提供 API 实现该功能。只需要在互斥锁初始化地方将属性修改为 `PTHREAD_MUTEX_RECURSIVE`。即 `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);`在**同一个线程中可以多次获取同一把锁。并且不会死锁**。 + + + +### 互斥条件锁 pthread_cond_t + +初始化互斥锁条件 `pthread_cond_init(&_condition, NULL);` + +等待条件进入休眠,放开 mutex 锁,被唤醒后会再次对 mutex 加锁 `pthread_cond_wait(&_condition, &_moneyLock);` + +激活一个等待该条件的线程 `pthread_cond_signal(&_condition)` + +激活所有等待该条件的线程 `pthread_cond_broadcast(&_condition)` + +```objectivec +@interface ViewController () +@property (nonatomic, assign) pthread_mutex_t moneyLock; +@property (nonatomic, assign) pthread_cond_t condition; +@property (nonatomic, strong) NSMutableArray *array; +@end + +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); + // 初始化锁 + pthread_mutex_init(&_moneyLock, &attr); + // 释放属性内存 + pthread_mutexattr_destroy(&attr); + // 加锁 + pthread_mutex_lock(&_moneyLock); + // 解锁 + pthread_mutex_unlock(&_moneyLock); + // 初始化互斥锁条件 + pthread_cond_init(&_condition, NULL); + [self test]; + self.array = [NSMutableArray array]; +} +- (void)test{ + [[[NSThread alloc] initWithTarget:self selector:@selector(delete) object:nil] start]; + [[[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil] start]; +} +- (void)add{ + NSLog(@"add beign"); + pthread_mutex_lock(&_moneyLock); + [self.array addObject:@"bb"]; + NSLog(@"加元素 %zd", self.array.count); + pthread_cond_signal(&_condition); + pthread_mutex_unlock(&_moneyLock); + NSLog(@"add beign"); +} +- (void)delete{ + NSLog(@"delete beign"); + pthread_mutex_lock(&_moneyLock); + if (self.array.count == 0) { + pthread_cond_wait(&_condition, &_moneyLock); + } + [self.array removeLastObject]; + NSLog(@"减元素 %zd", self.array.count); + pthread_mutex_unlock(&_moneyLock); + NSLog(@"delete end"); +} +@end +// delete beign +// add beign +// 加元素 1 +// add beign +// 减元素 0 +// delete end +``` + +可以看到同时调用 delete、add 方法 + +- 执行 delete 方法先加锁,但是由于数组为空,这时候就不需要执行删除元素,然后执行 add 方法 + +- add 方法要加锁,发现锁被 delete 方法占用了 + +- delete 方法为了等有元素再去执行 delete 引入了互斥锁条件 `pthread_cond_t`,调用 `pthread_cond_wait` 。此时线程进入休眠,同时会释放锁。 + +- add 方法内加完元素会调用 `pthread_cond_signal` 来激活等待该条件的线程 -## RunLoop 内部运行原理 -![RunLoop 运行原理图1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/image-20180801113342611.png) +### 从汇编角度分析 os_unfair_lock 属于什么锁(教你如何用汇编分析源码) -- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。 - - Source0:非基于 port 的,用户主动触发的事件。 - - Source1:基于 port,通过内核和其它线程互相发送消息 -- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop) +属于互斥锁。自旋锁是指在等锁的时候通过类似 while 循环的代码,让线程忙碌等到锁的到来。 +> Low-level lock that allows waiters to block efficiently on contention. -运行流程图 +系统说它是低级锁,等不到锁就休眠。 -![运行流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4.png) +测试代码 -运行流程说明 +```objectivec +dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT); +for (int i = 0; i < 10; i++) { + [[[NSThread alloc] initWithTarget:self selector:@selector(saveMoney) object:nil] start]; +} -![流程说明](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/3.png) +- (void)saveMoney { + OSSpinLockLock(&_bankLock); + NSInteger previousMoney = self.money; + sleep(600); + previousMoney += 50; + self.money = previousMoney; + NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]); + OSSpinLockUnlock(&_bankLock); +} +``` -## RunLoopMode 的概念 -![RunLoop 运行原理图1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1785352-087fd4b664e0e387.png) +为了调试方便,开启10个线程去执行 `saveMoney` 方法,为了查看自旋锁的等是什么实现。我们给里面休眠600s。同时 Xcode - Debug - DebugWorkflow - Always Show Disassembly + +lldb 模式下调试汇编有几个指令 + +c: 代表 continue, + +si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看到某个认识或者可疑符号,断点在这一行的时候,在下方 lldb 面板,属于 si,即可进入内部实现。 + +第一步:当第二次调用 saveMoney 方法,开启汇编调试 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble2.png) + +看到可疑方法 `OSSpinLockLock`,给它加断点,看到第10行高亮了。lldb 模式输入 c,敲回车。次数输入 si 即可进入 `OSSpinLockLock`  方法内部调试 + +第二步:继续输入 si,敲回车 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble3.png) + +第三步:看到可疑方法 `_OSSpinLockLockSlow`,给它加断点,lldb 输入 C。此时断点到这一行了,继续输入 si。 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble4.png) + +第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLockAssemble1.png) + +发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,太浪费性能了。 + +同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下 +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble1.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble2.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble3.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble4.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble5.png) +可以看到最后一步调用到了 `syscall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了 + +同样的步骤研究 `pthread_mutex_t` 会发现最后也是调用 `syscall` 做到线程休眠,不像自旋锁一样,在底层实现是 while 循环一样忙等,浪费资源。 -## 底层实现 -内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer) -我们来看看苹果官方开源的 [CFRunLoop.c 文件](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。看几个关键函数的实现猜测下 RunLoop 的内部原理 -以下的代码都有注释说明 +### NSLock、NSRecursiveLock -**__CFRunLoopModeIsEmpty** +NSLock 是对 mutex 普通锁(pthread_mutex_t)的封装 - 此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop +NSRecursiveLock 是对 mutex 递归锁(pthread_mutex_t ,且 attr 为 `PTHREAD_MUTEX_RECURSIVE`)的封装,API 跟 NSLock 基本一致 + +查看 GUN 源码可以看看到底是如何实现的 + +```objectivec ++ (void) initialize{ + static BOOL beenHere = NO; + if (beenHere == NO){ + beenHere = YES; + /* Initialise attributes for the different types of mutex. + * We do it once, since attributes can be shared between multiple + * mutexes. + * If we had a pthread_mutexattr_t instance for each mutex, we would + * either have to store it as an ivar of our NSLock (or similar), or + * we would potentially leak instances as we couldn't destroy them + * when destroying the NSLock. I don't know if any implementation + * of pthreads actually allocates memory when you call the + * pthread_mutexattr_init function, but they are allowed to do so + * (and deallocate the memory in pthread_mutexattr_destroy). + */ + pthread_mutexattr_init(&attr_normal); + pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL); + pthread_mutexattr_init(&attr_reporting); + pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK); + pthread_mutexattr_init(&attr_recursive); + pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE); + + /* To emulate OSX behavior, we need to be able both to detect deadlocks + * (so we can log them), and also hang the thread when one occurs. + * the simple way to do that is to set up a locked mutex we can + * force a deadlock on. + */ + pthread_mutex_init(&deadlock, &attr_normal); + pthread_mutex_lock(&deadlock); + + baseConditionClass = [NSCondition class]; + baseConditionLockClass = [NSConditionLock class]; + baseLockClass = [NSLock class]; + baseRecursiveLockClass = [NSRecursiveLock class]; + + tracedConditionClass = [GSTracedCondition class]; + tracedConditionLockClass = [GSTracedConditionLock class]; + tracedLockClass = [GSTracedLock class]; + tracedRecursiveLockClass = [GSTracedRecursiveLock class]; + + untracedConditionClass = [GSUntracedCondition class]; + untracedConditionLockClass = [GSUntracedConditionLock class]; + untracedLockClass = [GSUntracedLock class]; + untracedRecursiveLockClass = [GSUntracedRecursiveLock class]; + } +} +``` + +可以看到 NSLock 底层就是 pthread_mutex_t。 + +再看看 NSRecursiveLock + +```objectivec +@implementation NSRecursiveLock +- (id) init{ + if (nil != (self = [super init])) { + if (0 != pthread_mutex_init(&_mutex, &attr_recursive)){ + DESTROY(self); + } + } + return self; +} +``` + +底层就是 pthread_mutex_init。参数 `attr_recursive` 其实就是一个递归锁的属性。 + +```objectivec +pthread_mutexattr_init(&attr_recursive); +pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE); +``` -```objective-c -// expects rl and rlm locked - static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) { - CHECK_FOR_FORK(); - if (NULL == rlm) return true; - #if DEPLOYMENT_TARGET_WINDOWS - if (0 != rlm->_msgQMask) return false; - #endif - Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); - if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue - // 判断时候有没有_sources0 - if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; - // 判断时候有没有_sources1 - if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; - // 判断时候有没有_timers - if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; - - - struct _block_item *item = rl->_blocks_head; - while (item) { - struct _block_item *curr = item; - item = item->_next; - Boolean doit = false; - if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { - doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); - } else { - doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); - } - if (doit) return false; - } - return true; - } -``` - - - -**CFRunLoopRun、CFRunLoopRunInMode** - - 1、2个函数的作用分别是让 RunLoop 跑在 KCFRunLoopDefaultMode 下和特定的 Mode 下 - - 2、2个函数本质上都是调用 CFRunLoopRunSpecific - -```objective-c - // 用DefaultMode启动 - void CFRunLoopRun(void) { /* DOES CALLOUT */ - int32_t result; - do { - result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); - CHECK_FOR_FORK(); - } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); - } - - - // 用指定的Mode启动,允许设置RunLoop超时时间 - SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ - CHECK_FOR_FORK(); - return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); - } -``` - - - -**CFRunLoopRunSpecific** - - 参数1: RunLoop 对象。参数2:运行 Mode 名称。参数3:超时时间。参数4:主_CFRunLoopRun 会用到 - -```objective-c - // RunLoop的实现 - SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ - CHECK_FOR_FORK(); - if (__CFRunLoopIsDeallocating(rl)) - return kCFRunLoopRunFinished; - __CFRunLoopLock(rl); - - - // 根据modeName找到对应mode - CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); - - - // 判断mode里没有source/timer, 没有直接返回。 - if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { - Boolean did = false; - if (currentMode) - __CFRunLoopModeUnlock(currentMode); - __CFRunLoopUnlock(rl); - return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; - } - - - volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); - - - CFRunLoopModeRef previousMode = rl->_currentMode; - rl->_currentMode = currentMode; - int32_t result = kCFRunLoopRunFinished; - - - if (currentMode->_observerMask & kCFRunLoopEntry ) - // 1. 通知 Observers: RunLoop 即将进入 loop - __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); - // 进入loop - result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); - - - if (currentMode->_observerMask & kCFRunLoopExit ) - // 10.通知 Observers: RunLoop 即将退出。 - __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); - - - __CFRunLoopModeUnlock(currentMode); - __CFRunLoopPopPerRunData(rl, previousPerRun); - rl->_currentMode = previousMode; - __CFRunLoopUnlock(rl); - return result; - } -``` - - - -**__CFRunLoopDoObserver** - - - - 调用 Observer 回调 - - 联想给 RunLoop 添加观察者,监听 RunLoop 状态。 - -```objective-c - static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */ - CHECK_FOR_FORK(); - - - CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0; - if (cnt < 1) return; - - - /* Fire the observers */ - STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1); - CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef)); - CFIndex obs_cnt = 0; - //遍历 rlm-> _observers,将元素放到 collectedObservers 数组中 - for (CFIndex idx = 0; idx < cnt; idx++) { - CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx); - if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) { - collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo); - } - } - __CFRunLoopModeUnlock(rlm); - __CFRunLoopUnlock(rl); - for (CFIndex idx = 0; idx < obs_cnt; idx++) { - CFRunLoopObserverRef rlo = collectedObservers[idx]; - __CFRunLoopObserverLock(rlo); - if (__CFIsValid(rlo)) { - Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo); - __CFRunLoopObserverSetFiring(rlo); - __CFRunLoopObserverUnlock(rlo); - __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info); - if (doInvalidate) { - CFRunLoopObserverInvalidate(rlo); - } - __CFRunLoopObserverUnsetFiring(rlo); - } else { - __CFRunLoopObserverUnlock(rlo); - } - CFRelease(rlo); - } - __CFRunLoopLock(rl); - __CFRunLoopModeLock(rlm); - - - if (collectedObservers != buffer) free(collectedObservers); - } -``` - - - -**__CFRunLoopRun** - -```objective-c - /* rl, rlm are locked on entrance and exit */ - static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { - - - uint64_t startTSR = mach_absolute_time(); - - - if (__CFRunLoopIsStopped(rl)) { - __CFRunLoopUnsetStopped(rl); - return kCFRunLoopRunStopped; - } else if (rlm->_stopped) { - rlm->_stopped = false; - return kCFRunLoopRunStopped; - } - - - mach_port_name_t dispatchPort = MACH_PORT_NULL; - Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); - if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF(); - - - #if USE_DISPATCH_SOURCE_FOR_TIMERS - mach_port_name_t modeQueuePort = MACH_PORT_NULL; - if (rlm->_queue) { - modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue); - if (!modeQueuePort) { - CRASH("Unable to get port for run loop mode queue (%d)", -1); - } - } - #endif - - - dispatch_source_t timeout_timer = NULL; - struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context)); - if (seconds <= 0.0) { // instant timeout - seconds = 0.0; - timeout_context->termTSR = 0ULL; - } else if (seconds <= TIMER_INTERVAL_LIMIT) { - dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground(); - timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); - dispatch_retain(timeout_timer); - timeout_context->ds = timeout_timer; - timeout_context->rl = (CFRunLoopRef)CFRetain(rl); - timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds); - dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context - dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout); - dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel); - uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL); - dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL); - dispatch_resume(timeout_timer); - } else { - // 设置RunLoop超时时间 - seconds = 9999999999.0; - timeout_context->termTSR = UINT64_MAX; - } - - - Boolean didDispatchPortLastTime = true; - int32_t retVal = 0; - do { - #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI - voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED; - voucher_t voucherCopy = NULL; - #endif - uint8_t msg_buffer[3 * 1024]; - #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI - mach_msg_header_t *msg = NULL; - mach_port_t livePort = MACH_PORT_NULL; - #elif DEPLOYMENT_TARGET_WINDOWS - HANDLE livePort = NULL; - Boolean windowsMessageReceived = false; - #endif - __CFPortSet waitSet = rlm->_portSet; - - - __CFRunLoopUnsetIgnoreWakeUps(rl); - - - if (rlm->_observerMask & kCFRunLoopBeforeTimers) - // 2. 通知 Observers: RunLoop 即将触发 Timer 回调 - __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); - if (rlm->_observerMask & kCFRunLoopBeforeSources) - // 3. 通知 Observers: RunLoop 即将触发 Source 回调 - __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); - // 执行被加入的block - __CFRunLoopDoBlocks(rl, rlm); - - - // 4. RunLoop 触发 Source0 (非port) 回调 - Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); - if (sourceHandledThisLoop) { - // 执行被加入的block - __CFRunLoopDoBlocks(rl, rlm); - } - - - Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); - - - // 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息 - if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { - #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI - msg = (mach_msg_header_t *)msg_buffer; - - - if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { - goto handle_msg; - } - #elif DEPLOYMENT_TARGET_WINDOWS - if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { - goto handle_msg; - } - #endif - } - - - didDispatchPortLastTime = false; - - - // 通知 Observers: RunLoop 的线程即将进入休眠(sleep) - if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); - __CFRunLoopSetSleeping(rl); - // do not do any user callouts after this point (after notifying of sleeping) - - - // Must push the local-to-this-activation ports in on every loop - // iteration, as this mode could be run re-entrantly and we don't - // want these ports to get serviced. - - - __CFPortSetInsert(dispatchPort, waitSet); - - - __CFRunLoopModeUnlock(rlm); - __CFRunLoopUnlock(rl); - - - CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent(); - - - #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI - #if USE_DISPATCH_SOURCE_FOR_TIMERS - do { - if (kCFUseCollectableAllocator) { - // objc_clear_stack(0); - // - memset(msg_buffer, 0, sizeof(msg_buffer)); - } - msg = (mach_msg_header_t *)msg_buffer; - - - __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); - - - if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { - // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer. - while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue)); - if (rlm->_timerFired) { - // Leave livePort as the queue port, and service timers below - rlm->_timerFired = false; - break; - } else { - if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); - } - } else { - // Go ahead and leave the inner loop. - break; - } - } while (1); - #else - if (kCFUseCollectableAllocator) { - // objc_clear_stack(0); - // - memset(msg_buffer, 0, sizeof(msg_buffer)); - } - msg = (mach_msg_header_t *)msg_buffer; - __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); - #endif - - - - - #elif DEPLOYMENT_TARGET_WINDOWS - // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages. - __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived); - #endif - - - __CFRunLoopLock(rl); - __CFRunLoopModeLock(rlm); - - - rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart)); - - - // Must remove the local-to-this-activation ports in on every loop - // iteration, as this mode could be run re-entrantly and we don't - // want these ports to get serviced. Also, we don't want them left - // in there if this function returns. - - - __CFPortSetRemove(dispatchPort, waitSet); - - - __CFRunLoopSetIgnoreWakeUps(rl); - - - // user callouts now OK again - __CFRunLoopUnsetSleeping(rl); - - - // 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了 - if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); - // 处理消息 - handle_msg:; - __CFRunLoopSetIgnoreWakeUps(rl); - - - #if DEPLOYMENT_TARGET_WINDOWS - if (windowsMessageReceived) { - // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after - __CFRunLoopModeUnlock(rlm); - __CFRunLoopUnlock(rl); - - - if (rlm->_msgPump) { - rlm->_msgPump(); - } else { - MSG msg; - if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - } - - - __CFRunLoopLock(rl); - __CFRunLoopModeLock(rlm); - sourceHandledThisLoop = true; - - - // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced - // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later. - // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling. - __CFRunLoopSetSleeping(rl); - __CFRunLoopModeUnlock(rlm); - __CFRunLoopUnlock(rl); - - - __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL); - - - __CFRunLoopLock(rl); - __CFRunLoopModeLock(rlm); - __CFRunLoopUnsetSleeping(rl); - // If we have a new live port then it will be handled below as normal - } - - - - - #endif - if (MACH_PORT_NULL == livePort) { - CFRUNLOOP_WAKEUP_FOR_NOTHING(); - // handle nothing - } else if (livePort == rl->_wakeUpPort) { - CFRUNLOOP_WAKEUP_FOR_WAKEUP(); - // do nothing on Mac OS - #if DEPLOYMENT_TARGET_WINDOWS - // Always reset the wake up port, or risk spinning forever - ResetEvent(rl->_wakeUpPort); - #endif - } - #if USE_DISPATCH_SOURCE_FOR_TIMERS - else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { - CFRUNLOOP_WAKEUP_FOR_TIMER(); - - - if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { - // Re-arm the next timer, because we apparently fired early - __CFArmNextTimerInMode(rlm, rl); - } - } - #endif - #if USE_MK_TIMER_TOO - // 9.1 如果一个 Timer 到时间了,触发这个Timer的回调 - else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { - CFRUNLOOP_WAKEUP_FOR_TIMER(); - // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled. - // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754 - if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { - // Re-arm the next timer - __CFArmNextTimerInMode(rlm, rl); - } - } - #endif - // 9.2 如果有dispatch到main_queue的block,执行block - else if (livePort == dispatchPort) { - CFRUNLOOP_WAKEUP_FOR_DISPATCH(); - __CFRunLoopModeUnlock(rlm); - __CFRunLoopUnlock(rl); - _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL); - #if DEPLOYMENT_TARGET_WINDOWS - void *msg = 0; - #endif - /**/ - __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); - _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL); - __CFRunLoopLock(rl); - __CFRunLoopModeLock(rlm); - sourceHandledThisLoop = true; - didDispatchPortLastTime = true; - } - // 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件 - else { - CFRUNLOOP_WAKEUP_FOR_SOURCE(); - - - // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again. - voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release); - - - /**/ - CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); - if (rls) { - #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI - mach_msg_header_t *reply = NULL; - sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; - if (NULL != reply) { - (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); - CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); - } - #elif DEPLOYMENT_TARGET_WINDOWS - sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop; - #endif - } - - - // Restore the previous voucher - _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release); - - - } - #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI - if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); - #endif - // 执行加入到Loop的block - __CFRunLoopDoBlocks(rl, rlm); - - if (sourceHandledThisLoop && stopAfterHandle) { - // 进入loop时参数说处理完事件就返回 - retVal = kCFRunLoopRunHandledSource; - } else if (timeout_context->termTSR < mach_absolute_time()) { - // 超出传入参数标记的超时时间了 - retVal = kCFRunLoopRunTimedOut; - } else if (__CFRunLoopIsStopped(rl)) { - __CFRunLoopUnsetStopped(rl); - // 被外部调用者强制停止了 - retVal = kCFRunLoopRunStopped; - } else if (rlm->_stopped) { - rlm->_stopped = false; - retVal = kCFRunLoopRunStopped; - } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { - // source/timer一个都没有 - retVal = kCFRunLoopRunFinished; - } - - - #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI - voucher_mach_msg_revert(voucherState); - os_release(voucherCopy); - #endif - // 如果没超时,mode里没空,loop也没被停止,那继续loop - } while (0 == retVal); - - - if (timeout_timer) { - dispatch_source_cancel(timeout_timer); - dispatch_release(timeout_timer); - } else { - free(timeout_context); - } - return retVal; - } -``` - - - -![image-20200812221231993](/Users/lbp/Desktop/resume/2020-08-12-RunLoopStructure1.png) - -- performSelector 是在 source0 上实现的 -- RunLoopTimer 外部接口设置的精度,精度大于0,则使用 dispatch_source_set_timer,精度小于0,则使用 mk_timer_arm -- timer_source 使用 dispatch_timer_STRICT 创建,则系统会尽最大努力遵守设置的 leeway 值 -- NSTimer 不准的原因:底层 RunLoop Timer 底层使用的 timer 的精度不高(mk_timer);与 RunLoop 底层的调用机制有关系 -- 那么为什么存在 RunLoopTimer?意义是什么?应用场景 - - - -![image-20200812125534225](/Users/lbp/Desktop/Github/knowledge-kit/assets/2020-08-12-RunLoopStructure.png) - -```c++ -// Data structure to hold TSD data, cleanup functions for each -typedef struct __CFTSDTable { - uint32_t destructorCount; - uintptr_t data[CF_TSD_MAX_SLOTS]; - tsdDestructor destructors[CF_TSD_MAX_SLOTS]; -} __CFTSDTable; -``` - -```c++ -// 主线程 RunLoop -CFRunLoopRef CFRunLoopGetMain(void) { - CHECK_FOR_FORK(); - // 局部静态变量 - static CFRunLoopRef __main = NULL; // no retain needed - // 创建主线程对应的 RunLoop - if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed - return __main; -} - -// 子线程 RunLoop -// 先从 __CFTSDTable 获取 RunLoop,如果有则 return,没有则调用 _CFRunLoopGet0,_CFRunLoopGet0 内部调用 __CFRunLoopCreate 创建 CFRunLoop,然后写入 __CFTSDTable -CFRunLoopRef CFRunLoopGetCurrent(void) { - CHECK_FOR_FORK(); - // __CFTSDTable - CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); - if (rl) return rl; - // 没有则创建 - return _CFRunLoopGet0(pthread_self()); -} - -CF_EXPORT CFRunLoopRef _CFRunLoopGet0(_CFThreadRef t) { - if (pthread_equal(t, kNilPthreadT)) { - t = pthread_main_thread_np(); - } - __CFLock(&loopsLock); - if (!__CFRunLoops) { - CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); - CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); - CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); - if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { - CFRelease(dict); +### NSCondition + +NSCondition 是对 mutex 和 cond 的封装。 + +```objectivec +- (id) init { + if (nil != (self = [super init])) { + if (0 != pthread_cond_init(&_condition, NULL)){ + DESTROY(self); + } else if (0 != pthread_mutex_init(&_mutex, &attr_reporting)) { + pthread_cond_destroy(&_condition); + DESTROY(self); } - CFRelease(mainLoop); } - CFRunLoopRef newLoop = NULL; - CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); - if (!loop) { - newLoop = __CFRunLoopCreate(t); - CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); - loop = newLoop; + return self; +} +``` + +因为 NSCondtion 已经封装好锁和条件,所以直接使即可。pthread_mutex_t 需要搭配 pthread_cond_t 一起使用 + +Demo + +```objectivec + @interface ViewController () +@property (nonatomic, assign) NSInteger money; +@property (nonatomic, strong) NSCondition *condition; +@property (nonatomic, strong) NSMutableArray *array; +@end + +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + self.condition = [[NSCondition alloc] init]; + [self test]; + self.array = [NSMutableArray array]; +} +- (void)test{ + [[[NSThread alloc] initWithTarget:self selector:@selector(delete) object:nil] start]; + [[[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil] start]; +} +- (void)add{ + NSLog(@"add beign"); + [self.condition lock]; + [self.array addObject:@"bb"]; + NSLog(@"加元素 %zd", self.array.count); + [self.condition unlock]; + [self.condition signal]; + NSLog(@"add beign"); +} +- (void)delete{ + NSLog(@"delete beign"); + [self.condition lock]; + if (self.array.count == 0) { + [self.condition wait]; } - __CFUnlock(&loopsLock); - // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it - if (newLoop) { CFRelease(newLoop); } + [self.array removeLastObject]; + NSLog(@"减元素 %zd", self.array.count); + [self.condition unlock]; + NSLog(@"delete end"); +} +@end +``` + + + +### NSCondtionLock + +`NSConditionLock` 是对 NSCondition 的进一步封装,可以设置具体的条件值。 + +```objectivec +@interface ViewController () +@property (nonatomic, assign) NSInteger money; +@property (nonatomic, strong) NSConditionLock *condition; +@end +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + self.condition = [[NSConditionLock alloc] initWithCondition:1]; + [self test]; +} +- (void)test{ + [[[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil] start]; + [[[NSThread alloc] initWithTarget:self selector:@selector(task2) object:nil] start]; + [[[NSThread alloc] initWithTarget:self selector:@selector(task3) object:nil] start]; +} +- (void)task1{ + [self.condition lockWhenCondition:1]; + NSLog(@"task1"); + [self.condition unlockWithCondition:2]; +} +- (void)task2{ + [self.condition lockWhenCondition:2]; + NSLog(@"task2"); + [self.condition unlockWithCondition:3]; +} +- (void)task3{ + [self.condition lockWhenCondition:3]; + NSLog(@"task3"); + [self.condition unlock]; +} +@end +``` + +通过 NSCondtionLock 可以控制线程的执行顺序。 + + + + + +### dispatch_queue + +使用 GCD 的串行队列,也是可以实现线程同步。 + +线程同步的本质就是多线程的任务是顺序执行 + + + + + +### dispatch_semaphore + +semaphore 叫做”信号量” + +信号量的初始值,可以用来控制线程并发访问的最大数量 + +信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步 + +```objectivec +@interface ViewController () +@property (nonatomic, assign) NSInteger money; +@property (nonatomic, strong) dispatch_semaphore_t semaphore; +@end +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + // 控制线程最大并发量 + self.semaphore = dispatch_semaphore_create(5); + [self test]; +} +- (void)test{ + for (NSInteger index = 0; index<20; index++) { + [[[NSThread alloc] initWithTarget:self selector:@selector(task) object:nil] start]; + } +} +- (void)task{ + dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER); + NSLog(@"task %@", [NSThread currentThread]); + sleep(5); + dispatch_semaphore_signal(self.semaphore); +} +@end +``` + +`dispatch_semaphore_wait` 函数的本质 + +- 如果信号量的值 > 0,则会让信号量的值 -1,然后继续向下执行代码 + +- 如果信号量的值 <= 0,则线程休眠等待。等待多久取决于第二个参数。直到信号量的值 > 0,此时会让信号量的值 -1,然后继续向下执行代码 + +`dispatch_semaphore_signal` 函数的本质:让信号量的值 + 1 + + + +所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行 + + + + + +### @synchronized + +```objectivec +@interface ViewController () +@property (nonatomic, assign) NSInteger money; +@end + +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + self.money = 100; + [self moneyTest]; +} +- (void)moneyTest { + dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT); + dispatch_async(queue, ^{ + for (int i = 0; i < 10; i++) { + [self saveMoney]; + } + }); + dispatch_async(queue, ^{ + for (int i = 0; i < 10; i++) { + [self withdrawMoney]; + } + }); + // 100 + 10*50 - 10*10 = 500 +} +- (void)saveMoney { + @synchronized (self) { + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney += 50; + self.money = previousMoney; + NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]); + } +} +- (void)withdrawMoney { + @synchronized (self) { + NSInteger previousMoney = self.money; + sleep(0.2); + previousMoney -= 10; + self.money = previousMoney; + NSLog(@"取20,还剩%zd元 - %@", self.money, [NSThread currentThread]); + } +} +@end +``` + +`@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装 + +为了探究下实现,开启汇编调试 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/synchronized-asemble.png) + +可以查看 objc4 的源码,查找 `objc_sync_enter` + +```c +int objc_sync_enter(id obj){ + int result = OBJC_SYNC_SUCCESS; + if (obj) { + SyncData* data = id2data(obj, ACQUIRE); + assert(data); + data->mutex.lock(); + } else { + // @synchronized(nil) does nothing + if (DebugNilSync) { + _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug"); + } + objc_sync_nil(); + } + return result; +} + +typedef struct SyncData { + struct SyncData* nextData; + DisguisedPtr object; + int32_t threadCount; // number of THREADS using this block + recursive_mutex_t mutex; +} SyncData; +``` + +可以看到 `@synchronized` 的本质是一个互斥递归锁 `recursive_mutex_t`. + +传递一个参数 obj,经过 `id2data` 方法得到一个结构体对象,访问结构体对象的成员变量 + +`mutex`,然后调用 `lock` 方法。 + +如何根据 obj 获取对象,继续查看 `id2data` 方法。 + +```c +#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock +#define LISTFOR_OBJ(obj) sDataLists[obj].data +static StripedMap sDataLists; + +static SyncData* id2data(id object, enum usage why) +{ + spinlock_t *lockp = &LOCK_FOR_OBJ(object); + SyncData **listp = &LIST_FOR_OBJ(object); + SyncData* result = NULL; + // ... +} + +class recursive_mutex_tt : nocopy_t { + pthread_mutex_t mLock; + + public: + recursive_mutex_tt() : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER) { + lockdebug_remember_recursive_mutex(this); + } + + recursive_mutex_tt(const fork_unsafe_lock_t unsafe) + : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER) + { } + + void lock() + { + lockdebug_recursive_mutex_lock(this); + + int err = pthread_mutex_lock(&mLock); + if (err) _objc_fatal("pthread_mutex_lock failed (%d)", err); + } + + void unlock() + { + lockdebug_recursive_mutex_unlock(this); + + int err = pthread_mutex_unlock(&mLock); + if (err) _objc_fatal("pthread_mutex_unlock failed (%d)", err); + } + + void forceReset() + { + lockdebug_recursive_mutex_unlock(this); + + bzero(&mLock, sizeof(mLock)); + mLock = pthread_mutex_t PTHREAD_RECURSIVE_MUTEX_INITIALIZER; + } + + bool tryUnlock() + { + int err = pthread_mutex_unlock(&mLock); + if (err == 0) { + lockdebug_recursive_mutex_unlock(this); + return true; + } else if (err == EPERM) { + return false; + } else { + _objc_fatal("pthread_mutex_unlock failed (%d)", err); + } + } + + + void assertLocked() { + lockdebug_recursive_mutex_assert_locked(this); + } + + void assertUnlocked() { + lockdebug_recursive_mutex_assert_unlocked(this); + } +}; +``` + +可以看到是一个哈希表 `StripedMap`,哈希表工作原理就是传递一个 key,经过哈希算法生成索引,然后获取对应的值。 + +内部维护了一个哈希表,一个对象一个锁。 + +另外 `recursive_mutex_tt` 在初始化的时候传入 `PTHREAD_RECURSIVE_MUTEX_INITIALIZER`,看起来也支持递归。所以 @synchronized 是一个递归互斥锁的封装。 + + + + + +封装 + +有的时候我们需要在方法内部创建 semaphore ,则可以创建宏 + +```objectivec +#define SemaphoreBegin \ +static dispatch_semaphore_t semaphore; \ +static dispatch_once_t onceToken; \ +dispatch_once(&onceToken, ^{ \ + semaphore = dispatch_semaphore_create(1); \ +}); \ +dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + +#define SemaphoreEnd \ +dispatch_semaphore_signal(semaphore); + +``` + + + +### 自旋锁、互斥锁对比 + +什么情况使用自旋锁比较划算? + +- 预计线程等待锁的时间很短 + +- 加锁的代码(临界区)经常被调用,但竞争情况很少发生 + +- CPU资源不紧张 + +- 多核处理器 + +什么情况使用互斥锁比较划算? + +- 预计线程等待锁的时间较长 + +- 单核处理器 + +- 临界区有IO操作(IO一般占用 CPU 资源较多。互斥锁本身就占用 CPU,所以不适合) + +- 临界区代码复杂或者循环量大 + +- 临界区竞争非常激烈 + + + + + +## atomic + +`atomic` 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁。 + +可以参考源码 objc4 的 `objc-accessors.mm` + +```c +id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { + if (offset == 0) { + return object_getClass(self); + } + + // Retain release world + id *slot = (id*) ((char*)self + offset); + if (!atomic) return *slot; + + // Atomic retain release world + spinlock_t& slotlock = PropertyLocks[slot]; + slotlock.lock(); + id value = objc_retain(*slot); + slotlock.unlock(); - if (pthread_equal(t, pthread_self())) { - _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); - if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { -#if _POSIX_THREADS - _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); -#else - _CFSetTSD(__CFTSDKeyRunLoopCntr, 0, &__CFFinalizeRunLoop); -#endif - } - } - return loop; + // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock. + return objc_autoreleaseReturnValue(value); } ``` -- 为什么只有当 RunLoop 中存在 Timer Sourcrs、Input Sources 时,才能保证 RunLoop 不退出? +可以看到在获取属性值的时候,判断是不是 atomic - RunLoop 本质就是一个有条件的 do...while 循环。__CFRunLoopModeIsEmpty 里面去判断 source0、source1、timers 不存在则 while 循环条件不满足,RunLoop 退出 +- 不是 atomic 则直接 return - ```c++ - void CFRunLoopRun(void) { /* DOES CALLOUT */ - int32_t result; - do { - result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); - CHECK_FOR_FORK(); - } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); - } - - // expects rl and rlm locked - static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) { - // ... - if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; - if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; - if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; - // ... - return true; - } - - - SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ - // ... - CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); - if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { - Boolean did = false; - if (currentMode) __CFRunLoopModeUnlock(currentMode); - __CFRunLoopUnlock(rl); - return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; - } - // ... - result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); - // ... - return result; - } - ``` +- 如果是 atomic,则调用自旋锁 `slotlock` 加锁,取值,解锁,return - ## 线程保活技术: - - ```objective-c - - (void)viewDidLoad { - [super viewDidLoad]; - [self testKeepAliveThread]; - } - - - (void)testKeepAliveThread - { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - self.thread = (ThreadKeepLive *)[[NSThread alloc] initWithTarget:self selector:@selector(threadTask) object:nil]; - [self.thread setName:@"LongLiveThread"]; - [self.thread setQualityOfService:[NSThread mainThread].qualityOfService]; - [self.thread start]; - }); - } - - - (void)threadTask - { - [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { - NSLog(@"come"); - }]; - - [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; - while (true) { - [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; - } - } - ``` - - - come 会打印几次?1次,因为 RunLoop 跑起来之后,后面的代码不会执行,内部不断的 do...while - - - NSRunLoop 常驻线程可以运行在 NSRunLoopCommonModes 下吗? - - 不可以。因为在跑的时候如果 modeName 等于 kCFRunLoopCommonModes 则直接 kCFRunLoopRunFinished,则 RunLoop 的 while 循环条件失败 - - ```objective-c - SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ - CHECK_FOR_FORK(); - if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - CFLog(kCFLogLevelError, CFSTR("invalid mode '%@' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution."), modeName); - _CFRunLoopError_RunCalledWithInvalidMode(); - }); - return kCFRunLoopRunFinished; - } - // ... - return result; +```c +static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) +{ + if (offset == 0) { + object_setClass(self, newValue); + return; } - ``` - + id oldValue; + id *slot = (id*) ((char*)self + offset); - ### Mach Port 跨线程通信 + if (copy) { + newValue = [newValue copyWithZone:nil]; + } else if (mutableCopy) { + newValue = [newValue mutableCopyWithZone:nil]; + } else { + if (*slot == newValue) return; + newValue = objc_retain(newValue); + } - 1. Mach IPC 基于 Mach 内核实现进程间通讯。 + if (!atomic) { + oldValue = *slot; + *slot = newValue; + } else { + spinlock_t& slotlock = PropertyLocks[slot]; + slotlock.lock(); + oldValue = *slot; + *slot = newValue; + slotlock.unlock(); + } - 2. Mach IPC 被抽象为3种操作:messages、ports、and port sets + objc_release(oldValue); +} - 3. Mach port 跨线程通信 +void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) +{ + bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY); + bool mutableCopy = (shouldCopy == MUTABLE_COPY); + reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); +} +``` - 线程 B 有个 port 在等待消息,具有消息接收权限,等待消息到来的时候会阻塞当前线程 +可以看到设置属性的时候会判断是不是 atomic - 线程 A 有个 port 在发送消息,具有发送消息权限,要把发送的消息包装成消息,通过消息队列传递,message 包括:header(目的地 port、size)、data。 +- atomic 类型,则直接赋值 - 线程 B 收到消息后,解除 block,线程继续向下运行 +- 非 atomic 类型,则先自旋锁加锁、赋值、解锁 - ![image-20200813024843611](/Users/lbp/Library/Application Support/typora-user-images/image-20200813024843611.png) +它并不能保证使用属性的过程是线程安全的。 - 4. Mach port 如何进行跨线程通信? - 线程开启一个 port,然后给 port 申请接收、发送的权限。mach_msg 是通信函数,在等待消息的时候不加 timeout 则会一直阻塞,等到消息到来 - ![image-20200813093820911](/Users/lbp/Desktop/Github/knowledge-kit/assets/2020-08-13-MachPort.png) +QA:为什么在 iOS 上几乎没有使用? - 5. 在测试工作中 main.m 文件中打印当前的 RunLoop +因为属性 getter、setter 使用太高频,另外 atomic 内部实现是自旋锁,自旋锁是忙等,所以太耗费性能了。 - ```objective-c - int main(int argc, char * argv[]) { - NSString * appDelegateClassName; - @autoreleasepool { - appDelegateClassName = NSStringFromClass([AppDelegate class]); - } - NSLog(@"%@", NSRunLoop.currentRunLoop); - return UIApplicationMain(argc, argv, nil, appDelegateClassName); - } - - // - 2020-08-13 09:49:41.326621+0800 ***[52423:2383402] {wakeup port = 0x1103, stopped = false, ignoreWakeUps = true, - current mode = (none), - common modes = {type = mutable set, count = 1, - entries => - 2 : {contents = "kCFRunLoopDefaultMode"} - } - , - common mode items = (null), - modes = {type = mutable set, count = 1, - entries => - 2 : {name = kCFRunLoopDefaultMode, port set = 0x1003, queue = 0x6000007f0100, source = 0x6000007f0280 (not fired), timer port = 0xe03, - sources0 = (null), - sources1 = (null), - observers = (null), - timers = (null), - currently 618976181 (257342842892563) / soft deadline in: 1.84464867e+10 sec (@ -1) / hard deadline in: 1.84464867e+10 sec (@ -1) - }, - } - } - ``` - ![Mach Port Test Demo](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-08-13-MachPortTest.png) - 可以看到 main.m 中,还没有 return 的时候当前 RunLoop 的内部结构中存在一个 **wakeup port** 的端口。查看 RunLoop 源代码 **wakeup port** 就是 mach port 的一种。 - ```c++ - typedef mach_port_t __CFPort; - - struct __CFRunLoop { - CFRuntimeBase _base; - _CFRecursiveMutex _lock; /* locked for accessing mode list */ - __CFPort _wakeUpPort; // used for CFRunLoopWakeUp - Boolean _unused; - volatile _per_run_data *_perRunData; // reset for runs of the run loop - _CFThreadRef _pthread; - uint32_t _winthread; - CFMutableSetRef _commonModes; - CFMutableSetRef _commonModeItems; - CFRunLoopModeRef _currentMode; - CFMutableSetRef _modes; - struct _block_item *_blocks_head; - struct _block_item *_blocks_tail; - CFAbsoluteTime _runTime; - CFAbsoluteTime _sleepTime; - CFTypeRef _counterpart; - _Atomic(uint8_t) _fromTSD; - CFLock_t _timerTSRLock; - }; - ``` - ```c++ - void CFRunLoopWakeUp(CFRunLoopRef rl) { - // ... - kern_return_t ret; - /* We unconditionally try to send the message, since we don't want - * to lose a wakeup, but the send may fail if there is already a - * wakeup pending, since the queue length is 1. */ - ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0); - // ... - int ret; - do { - ret = eventfd_write(rl->_wakeUpPort, 1); - } while (ret == -1 && errno == EINTR); - // ... - SetEvent(rl->_wakeUpPort); - // ... - } - ``` +## 读写安全 - `CFRunLoopWakeUp(CFRunLoopGetCurrent());` 可以唤醒 RunLoop,函数的底层实现如上。核心实现就是 `__CFSendTrivialMachMessage` 函数。在iOS 中,除了 source1 可以自己唤醒 RunLoop 之外,其他的事件都需要用户手动唤醒 RunLoop 才可以。RunLoop 提供了专门的方法来实现这个功能。其核心部分就是调用 mach_msg 来向指定的 **_wakeUpPort** 端口发送消息,从而唤醒线程继续工作。 +- 同一时间,只能有1个线程进行写的操作 +- 同一时间,允许有多个线程进行读的操作 +- 同一时间,不允许既有写的操作,又有读的操作 - 为什么 Source1 可以唤醒 RunLoop?因为 Source1 本质上就是针对 Mach Port 的封装 +“多读单写”问题,经常用于文件、数据的读写操作。iOS 主流方案有: - 6. 做个实验检测 _wakeUpPort 端口 +- pthread_rwlock:读写锁 - ```objective-c - - (void)viewDidLoad { - [super viewDidLoad]; - [self listenWakeUpPort]; - } - - - (void)listenWakeUpPort - { - NSArray *array = [NSRunLoop.currentRunLoop.description componentsSeparatedByString:@"wakeup port = "]; - NSString *wakeupPort = [array.lastObject substringToIndex:[array.lastObject rangeOfString:@","].location]; - - dispatch_queue_t queue = dispatch_queue_create("com.test.wake_up_port_queue", DISPATCH_QUEUE_CONCURRENT); - dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, [self numberWithHexString:wakeupPort], 0, queue); - dispatch_source_set_event_handler(source, ^{ - mach_port_t port = (mach_port_t)dispatch_source_get_handle(source); - NSLog(@"%u--wakeUp", port); - }); - dispatch_activate(source); - } - - - (NSInteger)numberWithHexString:(NSString *)hexString - { - const char *hexChar = [hexString cStringUsingEncoding:NSUTF8StringEncoding]; - int hexNumber; - sscanf(hexChar, "%x", &hexNumber); - return (NSInteger)hexNumber; - } - - - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event - { - CFRunLoopWakeUp(CFRunLoopGetCurrent()); - } - ``` +- dispatch_barrier_async:异步栅栏调用 - 可以看到每次在点击屏幕时调用 `CFRunLoopWakeUp` 尝试唤醒 RunLoop,然后监听 RunLoop 的 _wakeUpPort,都可以在回调中获取到消息。 - 7. RunLoop lifecycle - ![RunLoop lifecycle](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-08-13-RunLoopLifeCycle.png) +### pthread_rwlock - 休眠时 RunLoop 被 mach_msg 阻塞,等到消息到来,可以是手动给 RunLoop 的 wakeUpPort 发消息 mach_msg,或者是 timer、Source1。然后继续走到 RunLoop run,不断循环 +初始化 - Timer 不准确的原因:1. RunLoop 底层对 Timer 实现的精度不高,所以需要指定 Timer 的精度;2. RunLoop 在 DoSource0、DoSource1 的时候耗时较多,则导致 Timer 下次执行会不准确 +```objectivec +pthread_rwlock_t lock +pthread_rwlock_init(&_lock, NULL) +``` - +读操作-加锁 `pthread_rwlock_rdlock(&_lock)` - +读操作-尝试加锁 `pthread_rwlock_tryrdlock(&_lock);` +写操作-加锁 `pthread_rwlock_wrlock(&_lock);` + +写操作-尝试加锁 `pthread_rwlock_trywrlock(&_lock);` + +解锁 `pthread_rwlock_unlock(&_lock);` + +销毁 `pthread_rwlock_destroy(&_lock);` + + + +```objectivec +@interface ViewController () +@property (nonatomic, assign) NSInteger money; +@property (nonatomic, assign) pthread_rwlock_t lock; +@end + +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + pthread_rwlock_init(&_lock, NULL); // 初始化读写锁 + self.money = 100; + [self moneyTest]; +} +- (void)moneyTest { + dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT); + dispatch_async(queue, ^{ + for (int i = 0; i < 10; i++) { + [self read]; + } + }); + dispatch_async(queue, ^{ + for (int i = 0; i < 4; i++) { + [self write]; + } + }); +} +- (void)read { + pthread_rwlock_rdlock(&_lock); + NSLog(@"read %zd", self.money); + pthread_rwlock_unlock(&_lock); +} +- (void)write { + pthread_rwlock_wrlock(&_lock); + self.money += 25; + NSLog(@"write %zd", self.money); + pthread_rwlock_unlock(&_lock); +} +@end +2022-04-09 22:25:25.042853+0800 DDD[13652:333135] read 100 +2022-04-09 22:25:25.043058+0800 DDD[13652:333126] write 125 +2022-04-09 22:25:25.043269+0800 DDD[13652:333135] read 125 +2022-04-09 22:25:25.043445+0800 DDD[13652:333126] write 150 +2022-04-09 22:25:25.043586+0800 DDD[13652:333135] read 150 +2022-04-09 22:25:25.043724+0800 DDD[13652:333126] write 175 +2022-04-09 22:25:25.043895+0800 DDD[13652:333135] read 175 +2022-04-09 22:25:25.044037+0800 DDD[13652:333126] write 200 +2022-04-09 22:25:25.044226+0800 DDD[13652:333135] read 200 +2022-04-09 22:25:25.044393+0800 DDD[13652:333135] read 200 +2022-04-09 22:25:25.044557+0800 DDD[13652:333135] read 200 +2022-04-09 22:25:25.044713+0800 DDD[13652:333135] read 200 +2022-04-09 22:25:25.044869+0800 DDD[13652:333135] read 200 +2022-04-09 22:25:25.045020+0800 DDD[13652:333135] read 200 +``` + + + +### dispatch_barrier_async + +```objectivec +// 初始化队列 +self.queue = dispatch_queue_create("rwqueue", DISPATCH_QUEUE_CONCURRENT); +// 读 +dispatch_async(self.queue, ^{ + +}); +// 写 +dispatch_barrier_async(self.queue, ^{ + +}); +``` + +注意: + +- 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的 +- 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果 + + + +上 Demo + +```objectivec +@interface ViewController () +@property (nonatomic, assign) NSInteger money; +@property (nonatomic, strong) dispatch_queue_t queue; +@end + +@implementation ViewController +- (void)viewDidLoad{ + [super viewDidLoad]; + self.queue = dispatch_queue_create("rwqueue", DISPATCH_QUEUE_CONCURRENT); + self.money = 100; + [self moneyTest]; +} +- (void)moneyTest { + dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT); + dispatch_async(queue, ^{ + for (int i = 0; i < 10; i++) { + [self read]; + } + }); + dispatch_async(queue, ^{ + for (int i = 0; i < 4; i++) { + [self write]; + } + }); +} +- (void)read { + dispatch_async(self.queue, ^{ + NSLog(@"read %zd", self.money); + }); +} +- (void)write { + dispatch_barrier_async(self.queue, ^{ + self.money += 25; + NSLog(@"write %zd", self.money); + }); +} +@end +2022-04-09 22:37:26.476134+0800 DDD[14019:343934] read 100 +2022-04-09 22:37:26.476134+0800 DDD[14019:343932] read 100 +2022-04-09 22:37:26.476317+0800 DDD[14019:343932] write 125 +2022-04-09 22:37:26.476489+0800 DDD[14019:343932] read 125 +2022-04-09 22:37:26.476629+0800 DDD[14019:343932] write 150 +2022-04-09 22:37:26.476766+0800 DDD[14019:343932] read 150 +2022-04-09 22:37:26.476905+0800 DDD[14019:343932] write 175 +2022-04-09 22:37:26.477033+0800 DDD[14019:343932] write 200 +2022-04-09 22:37:26.477199+0800 DDD[14019:343932] read 200 +2022-04-09 22:37:26.477216+0800 DDD[14019:343934] read 200 +2022-04-09 22:37:26.477233+0800 DDD[14019:343936] read 200 +2022-04-09 22:37:26.477247+0800 DDD[14019:343937] read 200 +2022-04-09 22:37:26.477267+0800 DDD[14019:343938] read 200 +2022-04-09 22:37:26.477269+0800 DDD[14019:343933] read 200 +``` \ No newline at end of file diff --git a/Chapter1 - iOS/1.40.md b/Chapter1 - iOS/1.40.md index a79ff32..09a375c 100644 --- a/Chapter1 - iOS/1.40.md +++ b/Chapter1 - iOS/1.40.md @@ -1,169 +1,1552 @@ -# RunLoop 的应用 +# 内存问题研究 -> 了解了 RunLoop 的底层原理以及特点后我们有必要想一想它可以应用在什么地方?现在归纳下常见的应用场景 +## 定时器内存泄漏 -## NSTimer +NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题。 -我们常常写的 **NSTimer** 都是在的默认的运行状态下执行的(**NSDefaultRunLoopMode**)。所以我们会经常遇到 NSTimer 执行不准去的问题。因为 RunLoop 是有不同的运行状态的,当我们 UI 滚动的时候从 **NSDefaultRunLoopMode** 切换到 **UITrackingRunLoopMode**,所以添加到 **NSDefaultRunLoopMode** 状态下的事件是不会执行的,为了达到定时器准确的目的有2种方法。方法一:必须根据具体需求给 NSTimer 指定具体的 **CFRunLoopModeRef**。方法二:利用 GCD 的 timer 不会受 **NSDefaultRunLoopMode** 影响的特点。 +定时器内存泄漏原因,解决方案以及高精度定时器,具体可以看这篇 [NSTimer 中的内存泄露](./1.45.md) 。 + +## iOS 内存布局 + +栈、堆、BSS、数据段、代码段 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/iOS-MemoryLayout.png) + +栈(stack):又称作堆栈,用来存储程序的局部变量(但不包括static声明的变量,static修饰的数据存放于数据段中)。除此之外,在函数被调用时,栈用来传递参数和返回值。栈内存地址越来越少 -```objective-c -//默认状态下的 NSTimer -[NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { - NSLog(@"我在执行了"); -}]; ``` - -```objective-c -//方法1:给 NSTimer 指定运行状态 -NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES]; -[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; -``` - -```objective-c - //方法2:GCD 的单位是纳秒。使用 GCD 创建的 timer 正常创建后不会执行,因为创建后设置了指定的时间后触发,所以当代码运行到最后一行的时候,Timer 还没执行,就被销毁了。所以我们必须设置一个属性去保存它。 - - //1、创建队列 - dispatch_queue_t queue = dispatch_get_main_queue(); - //2、创建 timer - dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); - // self.timer = timer; - //3、设置 timer 的参数:精准度、时间间隔 - //第三个参数为 GCD timer 的精准度 - dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); - //4、为 Timer 设置任务 - dispatch_source_set_event_handler(timer, ^{ - NSLog(@"%@",[NSRunLoop currentRunLoop]); - }); - //5、执行任务 - dispatch_resume(timer); -``` - -## ImageView显示\(PerformSelector\) - -UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,**FPS** 达不到60。 - -利用 RunLoop 可以实现这个效果,就是给下载并显示图片的方法指定 **NSRunLoopMode**。 - -```objective-c -- (IBAction)clickLoadIMage:(id)sender { - //[self.imageview performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"test"] afterDelay:2]; - [self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]]; -} - -- (void)downloadAndShowImage{ - self.imageview.image = [UIImage imageNamed:@"test"]; +func a { + 变量 1 地址最大 +    变量 2 地址第二大 +    // ... +    变量n  地址最小 } ``` -## 常驻线程 +堆(heap):用于存储程序运行中被动态分配的内存段,它的大小并不固定,可动态的扩张和缩减。操作函数(malloc/free)。分配的内存空间地址越来越大。 -我们需要常驻线程,那么就需要线程不要销毁,那么一些做法比如设置任务死循环,那么线程就不会销毁;将当前线程强引用不要销毁等都存在问题。最好的方法是为当前线程设置合理的 RunLoop +BSS段(bss segment):通常用来存储程序中未被初始化的全局变量和静态变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段输入静态内存分配 -```objective-c -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ - LBPThread *thred = [[LBPThread alloc] initWithTarget:self selector:@selector(showThreadLife) object:nil]; - self.lbpThread = thred; - [thred start]; -} - -- (void)showThreadLife{ - NSLog(@"---show----"); -} - -- (IBAction)clickLoadIMage:(id)sender { - NSLog(@"%s",__func__); - [self performSelector:@selector(contactWithTwoThread) onThread:self.lbpThread withObject:nil waitUntilDone:YES]; -} - -- (void)contactWithTwoThread{ - NSLog(@"----contactWithTwoThread----"); +数据段(data segment):通常用来存储程序中已被初始化的全局变量和静态变量和字符串的一块内存区域。数据段包含3部分: + +- 字符串常量。比如 `NSString *str = @"杭城小刘";` + +- 已初始化数据:已经初始化的全局变量、静态变量等 + +- 未初始化数据:未初始化的全局变量、静态变量等 + +代码段(code segment):编译之后的代码。通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。 + +![内存](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ram.png) + +上 Demo 验证 + +```objectivec +int a = 10; +static int b; +int main () { + NSString *name = @"杭城小刘"; + int age = 27; + int height = 177; + NSObject *obj = [[NSObject alloc] init]; + NSLog(@"\na: %p\nb: %p\n name: %p\nage: %p\n height: %p\nobj:%p", &a, &b, &name, &age, &height, obj); } +a: 0x107b09b80 +b: 0x107b09c48 +name: 0x7ff7b83fdbc0 +age: 0x7ff7b83fdbbc +height: 0x7ff7b83fdbb8 +obj:0x6000012780e0 ``` -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180806-101917@2x.png) +我们按照内存地址由低到高排个序(如下),发现和我们总结的规律一致。 -我们都知道 RunLoop 存在必须要有一个 Timer 或者 Source。 - -```objective-c -//方法1:添加 Port 的 Source。Sourcr1 -- (void)showThreadLife{ - NSLog(@"---show----"); - //子线程的 RunLoop 是需要自己手动创建并添加;RunLoop 如果不要销毁那么至少存在一个 Timer 或 Source - [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; - [[NSRunLoop currentRunLoop] run]; -} -//方法2 -- (void)showThreadLife{ - NSLog(@"---show----"); - //子线程的 RunLoop 是需要自己手动创建并添加;RunLoop 如果不要销毁那么至少存在一个 Timer 或 Source - - NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(test) userInfo:nil repeats:YES]; - [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; - [[NSRunLoop currentRunLoop] run]; -} +```shell +// 字符串常量 +name: 0x7ff7b83fdbc0 +// 已初始化的全局变量、静态变量 +a: 0x107b09b80 +// 未初始化的全局变量、静态变量 +b: 0x107b09c48 +// 堆 +obj: 0x6000012780e0 +// 栈 +height: 0x7ff7b83fdbb8 +age: 0x7ff7b83fdbbc ``` - -注意:添加 Observer 是没有效果的。 - -注意:直接添加的 Mach Port 后线程确实是常驻线程,但是如果需要让线程停止,`[runLoop run]` 方法不能满足。 -`[runLoop runMode:NSDefualtRunLoopMode beforeDate:[NSDate distantFuture]];` -设计可以停止的常驻线程 - -```Objective-C -@property (assign, nonmatioc) BOOL shouldStopRun; - -self.shouldStopRun = NO; - -- (void) start { - while(!shouldStopRun && [runLoop runMode:NSDefualtRunLoopMode beforeDate:[NSDate distantFuture]]); -} - -- (void)removeSourceOrTimer { - self.shouldStopRun = YES; - CFRunLoopStop(CFRunLoopGetCurrent()); // RunLoop 源代码里面一直循环的条件,得来的停止条件 -} - ``` - -## 自动释放池 - -自动释放池什么时候创建和释放 - -创建时间:第一次进入 RunLoop 的时候 - -释放时间:RunLoop 退出的时候 - -其他情况:当 RunLoop 将要休眠的时候释放,然后创建一个新的 - -**\_wrapRunLoopWithAutoreleasePoolHandler** **0x1** - -**\_wrapRunLoopWithAutoreleasePoolHandler** **0xa0** - -0x1 和 0xa0 是十六进制的数,对应十进制为1和160。 - - -## RunLoop 空闲时做一些任务 +NSObject *obj = [[NSObject alloc] init]; +NSLog(@"%p %p %@", obj, &obj, obj); ``` -- (void)print +分别打印 obj指针指向的堆上的内存地址、obj 指针在栈上的地址、obj 内容 + +## Tagged Pointer + +先来一个 Demo 开启本部分内容(画外音:代码很短,但让我产生了一个大大的问号) + +```objectivec +- (bool)isTaggedPointer:(const void *)ptr { - NSLog(@"test"); + return ((uintptr_t)ptr & (1UL<<63)) == (1UL<<63); } -- (void)viewDidLoad +NSNumber *number = [NSNumber numberWithInt:10]; // 0xb0000000000000a2 b:12 1100 +NSLog(@"%p %d %@", number, [self isTaggedPointer:(__bridge const void *)number], number.class); + +NSString *name1 = [NSString stringWithFormat:@"ss"]; // 0xa000000000073732 a:11 1011 +NSLog(@"%p %d %@", name1, [self isTaggedPointer:(__bridge const void *)name1], name1.class); +``` + +前提说明: + +创建一个 NSNumer 类型的变量 number,NSString 类型的 name1,代码打印地址、类型。产生一个问题:为什么 NSNumber 是 TaggedPointer,但是 class 却显示 __NSCFNumber ? + +```c +static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) { - CFRunLoopActivity flags = kCFRunLoopBeforeWaiting; - CFRunLoopObserverRef runloopObserver = CFRunLoopObserverCreateWithHandler( - kCFAllocatorDefault, flags, YES, 0, - ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - [self performSelector:@selector(print) - onThread:[NSThread mainThread] - withObject:nil - waitUntilDone:NO - modes:@[ NSDefaultRunLoopMode ]]; + return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; +} +``` + +- 通过 objc4 源码研究写了个判断对象是否是 Tagged Pointer 类型的方法。通过系统源码参考写了判断方法 `isTaggedPointer`。调用方法得到 number 对象是 Tagged Pointer 类型 + +- 根据 iOS 平台特性,根据内存地址高位分析确实是 TaggedPointer 类型 ​ + +- 同样的 NSString 指针指向的字符串内容比较少,占用内存没必要开创新的内存时,name1 就是 NSTaggedPointerString,打印出 class 也是 NSTaggedPointerString。调用 `isTaggedPointer` 得到也是 Tageed Pointer 类型 + +带着问题开始吧 + +### 什么是 Tagged Pointer + +iOS 从 64bit 开始引入了Tagged Pointer 技术,用于优化 NSNumber、NSDate、NSString等小对象的存储。 + +在此之前,创建对象需要动态分配内存、维护引用计数等,对象指针存储的是堆中对象的地址值创建一个对象的流程。先在堆上申请一块内存,然后再在栈上增加一个指针类型,指针指向堆上这块内存。假如是 `NSNumber *value = [NSNumber numberWithInt:2]` value 是指针长度为8字节,堆上内存16字节。加起来24字节就存一个int 2。 + +此外还需要维护引用技术,沿用一个真正对象那一套,太大材小用了。 + +Tagged Pointer 格式,对象指针里面存储的数据变成了:`Tag + Data`,将数据直接存储在了指针中。当指针不够存储数据时,才会使用动态分配内存的方式来存储数据 + +objc_msgSend 能识别 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了调用开销。 + +### 经典问题 + +Demo1 + +```objectivec +- (void)test { + dispatch_queue_t queue = dispatch_get_global_queue(0, 0); + for (NSInteger i = 0; i<1000; i++) { + dispatch_async(queue, ^{ + self.name = [NSString stringWithFormat:@"和好多好多好多好多事看看上课上课上课"]; }); - CFRunLoopAddObserver(CFRunLoopGetCurrent(), runloopObserver, kCFRunLoopDefaultMode); + } } ``` +运行该代码会 Crash,报错信息如下 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/TaggedPointerCrash.png) + +说明:一开始的报错信息只说坏内存访问,但是并没有显示具体的方法调用堆。想知道具体 Crash 原因还是需要看看堆栈比较方便。输入 bt 查看最后是由于 `objc_release` 方法造成 crash。 + +小窍门:利用 LLDB 模式下输入 `bt`,可以查看堆栈。也就是 backtrace 的缩写。 + +不仔细想可能发现不了问题,看到 `objc_release` 就会想到是在多线程情况下 NSString 的 setter 方法内,ARC 代码经过编译器最后会按照 MRC 去运行。所以 Setter 类似下面代码。 + +```objectivec +-(void)setName:(NSString *)name +{ + if (_name!=name) { + [_name release]; + _name = [name retain]; + } +} +``` + +Demo + +```objectivec +- (void)test { + dispatch_queue_t queue = dispatch_get_global_queue(0, 0); + for (NSInteger i = 0; i<1000; i++) { + dispatch_async(queue, ^{ + self.name = [NSString stringWithFormat:@"ss"]; + if (i == 100) { + NSLog(@"%p %@", self.name, self.name.class); + } + }); + } +} +// 0xa000000000073732 NSTaggedPointerString +``` + +同样的代码字符串变短居然不 crash 了?因为命中 Tagged Pointer 逻辑了,查看类型是 `NSTaggedPointerString` + +本问题本质是 + +- ARC 代码在编译后真正运行阶段是走 MRC 的,strong、copy 内部都会 release 旧的,copy/retain 新的 + +- 多线程情况下访问 setter 需要加锁 + +- 字符串在 NSTaggedPointerString 情况下不存在像 OC 对象的 setter 方法内的 release、copy 操作 + +### 如何判断一个指针是否为Tagged Pointer + +查看 objc4 源码 + +```c +#if TARGET_OS_OSX && __x86_64__ + // 64-bit Mac - tag bit is LSB +# define OBJC_MSB_TAGGED_POINTERS 0 +#else + // Everything else - tag bit is MSB +# define OBJC_MSB_TAGGED_POINTERS 1 +#endif + +#if OBJC_MSB_TAGGED_POINTERS +# define _OBJC_TAG_MASK (1UL<<63) +# define _OBJC_TAG_INDEX_SHIFT 60 +# define _OBJC_TAG_SLOT_SHIFT 60 +# define _OBJC_TAG_PAYLOAD_LSHIFT 4 +# define _OBJC_TAG_PAYLOAD_RSHIFT 4 +# define _OBJC_TAG_EXT_MASK (0xfUL<<60) +# define _OBJC_TAG_EXT_INDEX_SHIFT 52 +# define _OBJC_TAG_EXT_SLOT_SHIFT 52 +# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12 +# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12 +#else +# define _OBJC_TAG_MASK 1UL +# define _OBJC_TAG_INDEX_SHIFT 1 +# define _OBJC_TAG_SLOT_SHIFT 0 +# define _OBJC_TAG_PAYLOAD_LSHIFT 0 +# define _OBJC_TAG_PAYLOAD_RSHIFT 4 +# define _OBJC_TAG_EXT_MASK 0xfUL +# define _O BJC_TAG_EXT_INDEX_SHIFT 4 +# define _OBJC_TAG_EXT_SLOT_SHIFT 4 +# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0 +# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12 +#endif + +static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) +{ + return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; +} +``` + +可以看到源码通过 `_objc_isTaggedPointer` 方法判断是否是 Tagged Pointer 类型。传入对象地址,内部通过 `_OBJC_TAG_MASK` 按位与运算。 + +其中 _OBJC_TAG_MASK 是一个宏,宏定义外部有个 if 判读,判断是 Mac OS 并且是 x86_64 架构则为0,否则为1。也就是 Mac OS 并且是 x86_64 架构情况下则与 `1UL` 按位与,否则与 `1UL<<63` 按位与。 + +- iOS平台 | Mac 非 x86 平台: 最高有效位是1(第64bit)`1UL<<63` + +- Mac 且 x86平台: 最低有效位是1`1UL` + +比如 iOS 平台下 + +``` +0xb0000000000000a2 b:12 1100 + + 1100 +& 1000 +------- + 1000 +``` + +## OC 对象内存管理 + +iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC 对象引用计数默认是1,当引用计数减为 0,OC 对象就会销毁,释放其占用的内存空间 + +调用 retain/copy 会让 OC 对象的引用计数 +1,调用 release 会让 OC 对象的引用计数 -1。 + +内存管理的经验总结 + +- 当调用 alloc、new、copy、mutableCopy 方法返回了一个对象,在不需要这个对象时,要调用 release 或者 autorelease 来释放它 + +- 想拥有某个对象,就让它的引用计数 +1;不想再拥有某个对象,就让它的引用计数 -1 + +- 可以通过以下私有函数来查看自动释放池的情况`extern void _objc_autoreleasePoolPrint(void);` + +僵尸对象:重复释放内存造成的。一个典型场景是多次 setter。setter 内部实现不合理,比如下面 setter。 + +```objectivec +Person *p = [[Person aloc] init]; // 1 +Cat *cat = [[Cat alloc] init]; // 1 +[p setCat:cat]; // 2 +[cat release]; // 1 +[p setCat:cat]; // 0 +[p setCat:cat]; // badAccess +- (void)setCat:(Cat *)cat +{ + [_cat release]; + _cat = [cat retain]; +} +``` + +改进 + +```objectivec +- (void)setCat:(Cat *)cat +{ + if (_cat != cat) { + [_cat release]; + _cat = [cat retain]; + } +} +``` + +早期在 MRC 时代,在 .h 文件中 @property 只会属性的 getter、setter 声明,`@synthesize` 会自动生成成员变量和属性的 setter、getter 的实现。随着编译器进步,现在 @property 会做完全部的事情。 + +早期 VC 中使用属性 + +```objectivec +@property (nonatomic, strong) NSMutableDictionary *dict; + +NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; +self.dict = dict; +[dict release]; +``` + +通过 Foundation 框架中类方法创建出来的对象,会自动调用 autorelease 方法。 + +简写为 `self.dict = [NSMutableDictionary dictionary];` + +上述可以查看 GUNStep 源码  `NSDictionary.m` + +```objectivec +#define AUTORELEASE(object) [(id)(object) autorelease] ++ (id) dictionary { + return AUTORELEASE([[self allocWithZone: NSDefaultMallocZone()] init]); +} +``` + +QA:ARC 做了什么 + +ARC 其实是 LLVM + Runtime 共同作用的结果。LLVM 编译器自动插入 retain、release 内存管理代码。Runtime 运行时帮我们处理类似 `__weak` 程序运行过程中弱引用清除掉。 + +## copy/mutableCopy + +OC 有2个拷贝方法 + +- copy 不可变拷贝,产生新不可变对象 + +- mutableCopy 可变拷贝,产生新可变对象 + +上个 Demo1 + +```objectivec +NSArray *array1 = [[NSArray alloc] initWithObjects:@"1", @"2", nil]; +NSLog(@"array1 --- %zd", array1.retainCount); +NSArray *array2 = [array1 copy]; +NSLog(@"array1 --- %zd", array1.retainCount); +NSLog(@"array2 --- %zd", array2.retainCount); +NSMutableArray *array3 = [array1 mutableCopy]; +NSLog(@"array1 --- %zd", array1.retainCount); +NSLog(@"array2 --- %zd", array2.retainCount); +NSLog(@"array3 --- %zd" array3.retainCount); + +[array3 release]; +NSLog(@"array3 --- %zd", array3.retainCount); +[array2 release]; +NSLog(@"array2 --- %zd", array2.retainCount); +NSLog(@"array1 --- %zd", array1.retainCount); +[array1 release]; +NSLog(@"array1 --- %zd", array1.retainCount); +2022-04-12 20:50:43.639296+0800 Main[4408:60897] array1 --- 1 +2022-04-12 20:50:43.639715+0800 Main[4408:60897] array1 --- 2 +2022-04-12 20:50:43.639772+0800 Main[4408:60897] array2 --- 2 +2022-04-12 20:50:43.639846+0800 Main[4408:60897] array1 --- 2 +2022-04-12 20:50:43.639899+0800 Main[4408:60897] array2 --- 2 +2022-04-12 20:50:43.639957+0800 Main[4408:60897] array3 --- 1 +2022-04-12 20:50:43.640013+0800 Main[4408:60897] array3 --- 0 +2022-04-12 20:50:43.640059+0800 Main[4408:60897] array2 --- 1 +2022-04-12 20:50:43.640105+0800 Main[4408:60897] array1 --- 1 +2022-04-12 20:50:43.640159+0800 Main[4408:60897] array1 --- 0 +``` + +疑问1: 为什么在 array2 创建之后 array2、array1 的引用技术都是2. + +因为 array1 指针指向堆上一块内存(NSArray 类型),创建好后 array1 引用计数为1。在创建 array2 的时候发现是对 array1 的浅拷贝,系统为了内存的节省优化,array2 的指针也指向堆上的这一块内存,copy 本身会对 array1 引用技术 +1,变为2。所以这时候 array2 指针指向的内存,引用计数也是2. + +基于此,我们稍微修改下,看看 Demo2 + +```objectivec +NSArray *array1 = [[NSArray alloc] initWithObjects:@"1", @"2", nil]; +NSLog(@"array1 --- %zd", array1.retainCount); +NSArray *array2 = [array1 mutableCopy]; +NSLog(@"array1 --- %zd", array1.retainCount); +NSLog(@"array2 --- %zd", array2.retainCount); + +2022-04-12 20:55:36.539060+0800 Main[4576:65031] array1 --- 1 +2022-04-12 20:55:36.539514+0800 Main[4576:65031] array1 --- 1 +2022-04-12 20:55:36.539631+0800 Main[4576:65031] array2 --- 1 +``` + +因为 array1 指针指向堆上一块内存(NSArray 类型),创建好后 array1 引用计数为1。在创建 array2 的时候发现是对 array1 的深拷贝,要产生不可变对象,所以堆上申请内存空间,array2 指针指向这块内存,引用技术为1。 + +此外 mutableCopy 是 Foundation 针对集合类提供的。如果自定义对象需要支持 copy 方法,需遵循对应的`NSCopyint` 协议,实现协议方法 `-(id)copyWithZone:(NSZone *)zone` + +总结: + +| | NSString | NSMutableString | NSArray | NSMutableArray | NSDictionary | NSMutableDictionary | +| ----------- | ------------------- | ------------------- | ------------------ | ------------------ | ----------------------- | ----------------------- | +| copy | NSString 浅拷贝 | NSString 深拷贝 | NSArray 浅拷贝 | NSArray 深拷贝 | NSDictionary 浅拷贝 | NSDictionary 深拷贝 | +| mutableCopy | NSMutableString 深拷贝 | NSMutableString 深拷贝 | NSMutableArray 深拷贝 | NSMutableArray 深拷贝 | NSMutableDictionary 深拷贝 | NSMutableDictionary 深拷贝 | + +## 引用计数 + +```objectivec +union isa_t { + Class cls; + uintptr_t bits; + struct { + uintptr_t nonpointer : 1; + uintptr_t has_assoc : 1; + uintptr_t has_cxx_dtor : 1; + uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 + uintptr_t magic : 6; + uintptr_t weakly_referenced : 1; + uintptr_t deallocating : 1; + uintptr_t has_sidetable_rc : 1; + uintptr_t extra_rc : 19; + }; +} +``` + +iOS 从 64 位开始开始,对 isa 进行了优化,信息存放于 union 结构中 + +- `extra_rc` 存储引用计数信息-1,可以看到是 19位。存储引用计数器 -1 + +- `has_sidetable_rc` 引用计数是否过大无法存储在 isa。当过大无法存储与 isa 中时,`has_sidetable_rc` 这位会变为1,引用计数存储在 SideTable 的类的属性中 + +也就是说,iOS 从64位开始,引用计数存放于 isa 结构体的一个 union 中,字段为 extra_rc,值为对象引用计数值 -1。当引用计数过大无法存放的时候 union 中 has_sidetable_rc 为 1,则引用计数存放于 SideTable 结构体中。 + +SideTable 结构如下 + +```c +struct SideTable { + spinlock_t slock; + RefcountMap refcnts; + weak_table_t weak_table; +}; +``` + +其中 refcnts 是一个存放着对象引用计数的散列表 + +查看 objc4 关于引用计数的实现 + +```c +uintptr_t _objc_rootRetainCount(id obj) { + assert(obj); + return obj->rootRetainCount(); +} + +inline uintptr_t objc_object::rootRetainCount() { + if (isTaggedPointer()) return (uintptr_t)this; + sidetable_lock(); + isa_t bits = LoadExclusive(&isa.bits); + ClearExclusive(&isa.bits); + if (bits.nonpointer) { // 优化过的 isa + uintptr_t rc = 1 + bits.extra_rc; + if (bits.has_sidetable_rc) { // 引用计数不是存储在 isa 中,而是 SideTable + rc += sidetable_getExtraRC_nolock(); + } + sidetable_unlock(); + return rc; + } + sidetable_unlock(); + return sidetable_retainCount(); +} + +size_t objc_object::sidetable_getExtraRC_nolock() { + assert(isa.nonpointer); + SideTable& table = SideTables()[this]; + RefcountMap::iterator it = table.refcnts.find(this); // key 拿值 + if (it == table.refcnts.end()) return 0; + else return it->second >> SIDE_TABLE_RC_SHIFT; +} +``` + +`__unsafe_unretained` 不安全如何体现?上 Demo + +```objectivec +__weak Person *p2; +__unsafe_unretained Person *p3; +{ + Person *p = [[Person alloc] init]; + p2 = p; +} +NSLog(@"%@", p2); +2022-04-12 21:39:30.308917+0800 Main[5307:98296] -[Person dealloc] +2022-04-12 21:39:30.309413+0800 Main[5307:98296] (null) +``` + +可以看到出了代码块,之后 p2 虽然指向 p,但是 p 没有强指针指向,所以回收了,此时打印 p2,是 null。 + +```objectivec +__unsafe_unretained Person *p3; +{ + Person *p = [[Person alloc] init]; + p3 = p; +} +NSLog(@"%@", p3); +2022-04-12 21:40:47.558581+0800 Main[5342:99598] -[Person dealloc] +2022-04-12 21:40:47.559330+0800 Main[5342:99598] +``` + +当用 `__unsafe_unretained` 修饰后,虽然释放了,但是内存还没回收,这时候去使用很容易出错。 + +## dealloc + +查看 objc4 源码 + +```c +- (void)dealloc { + _objc_rootDealloc(self); +} + +void _objc_rootDealloc(id obj) { + assert(obj); + obj->rootDealloc(); +} + +inline void objc_object::rootDealloc() { + if (isTaggedPointer()) return; // fixme necessary? + // fastpath 判断当前对象是否满足条件。 + if (fastpath(isa.nonpointer && // nonpointer + !isa.weakly_referenced && // 是否有弱引用 + !isa.has_assoc && // 关联对象 + !isa.has_cxx_dtor && // c++ 析构函数 + !isa.has_sidetable_rc)) // 是否有 SideTable + { + assert(!sidetable_present()); + free(this); + } else { + object_dispose((id)this); + } +} + +id object_dispose(id obj){ + if (!obj) return nil; + objc_destructInstance(obj); + free(obj); + return nil; +} + +void *objc_destructInstance(id obj) { + if (obj) { + // Read all of the flags at once for performance. + bool cxx = obj->hasCxxDtor(); + bool assoc = obj->hasAssociatedObjects(); + + // This order is important. + if (cxx) object_cxxDestruct(obj); // 清除成员变量 + if (assoc) _object_remove_assocations(obj); + obj->clearDeallocating(); // 将指向当前对象的弱指针置为 nil + } + return obj; +} + +inline void objc_object::clearDeallocating() { + if (slowpath(!isa.nonpointer)) { + // Slow path for raw pointer isa. + sidetable_clearDeallocating(); + } + else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) { + // Slow path for non-pointer isa with weak refs and/or side table data. + clearDeallocating_slow(); + } + assert(!sidetable_present()); +} +void objc_object::sidetable_clearDeallocating(){ + SideTable& table = SideTables()[this]; + // clear any weak table items + // clear extra retain count and deallocating bit + // (fixme warn or abort if extra retain count == 0 ?) + table.lock(); + RefcountMap::iterator it = table.refcnts.find(this); + if (it != table.refcnts.end()) { + if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) { + weak_clear_no_lock(&table.weak_table, (id)this); + } + table.refcnts.erase(it); + } + table.unlock(); +} + +void +weak_clear_no_lock(weak_table_t *weak_table, id referent_id) +{ + objc_object *referent = (objc_object *)referent_id; + + weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); + if (entry == nil) { + /// XXX shouldn't happen, but does with mismatched CF/objc + //printf("XXX no entry for clear deallocating %p\n", referent); + return; + } + + // zero out references + weak_referrer_t *referrers; + size_t count; + + if (entry->out_of_line()) { + referrers = entry->referrers; + count = TABLE_SIZE(entry); + } + else { + referrers = entry->inline_referrers; + count = WEAK_INLINE_COUNT; + } + + for (size_t i = 0; i < count; ++i) { + objc_object **referrer = referrers[i]; + if (referrer) { + if (*referrer == referent) { + *referrer = nil; + } + else if (*referrer) { + _objc_inform("__weak variable at %p holds %p instead of %p. " + "This is probably incorrect use of " + "objc_storeWeak() and objc_loadWeak(). " + "Break on objc_weak_error to debug.\n", + referrer, (void*)*referrer, (void*)referent); + objc_weak_error(); + } + } + } + + weak_entry_remove(weak_table, entry); +} +``` + +## 自动释放池底层原理探索 + +上 Demo + +```objectivec +int main(int argc, const char * argv[]) { + @autoreleasepool { + Person *p = [[[Person alloc] init] autorelease]; + } + return 0; +} +``` + +clang 转为 c++ `xcrun -sdk iphonesimulator clang -rewrite-objc main.m` + +```c +int main(int argc, const char * argv[]) { + /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; + Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease")); + } + return 0; +} +``` + +下面的代码其实就是 objc_msgSend,有效代码是 `__AtAutoreleasePool __autoreleasepool;` + +继续查找 + +```c +struct __AtAutoreleasePool { + __AtAutoreleasePool() { +     atautoreleasepoolobj = objc_autoreleasePoolPush(); + } + ~__AtAutoreleasePool() { +     objc_autoreleasePoolPop(atautoreleasepoolobj); + } + void * atautoreleasepoolobj; +}; +``` + +OC 对象本质就是结构体 + +- `__AtAutoreleasePool` 结构体中 `__AtAutoreleasePool` 是构造方法,在创建结构体的时候调用 + +- `~__AtAutoreleasePool` 是析构函数,在结构体销毁的时候调用 + +main 内的代码作用域,离开代表销毁。所以上面代码等价于 + +```objectivec +atautoreleasepoolobj = objc_autoreleasePoolPush(); +Person *p = [[[Person alloc] init] autorelease]; +objc_autoreleasePoolPop(atautoreleasepoolobj); +``` + +利用关键函数继续查看 objc4 源码 + +```c +void *objc_autoreleasePoolPush(void) { + return AutoreleasePoolPage::push(); +} +void objc_autoreleasePoolPop(void *ctxt) { + AutoreleasePoolPage::pop(ctxt); +} +``` + +**自动释放池的主要实现依靠2个对象:`__AtAutoreleasePool`、`AutoreleasePoolPage`** + +**objc_autoreleasePoolPush、objc_autoreleasePoolPop 底层都是调用了 AutoreleasePoolPage 对象来管理的。** + +查看源码 + +```c +class AutoreleasePoolPage { + magic_t const magic; + id *next; + pthread_t const thread; + AutoreleasePoolPage * const parent; + AutoreleasePoolPage *child; + uint32_t const depth; + uint32_t hiwat; +} +``` + +- 每个 AutoreleasePoolPage 对象占用 4096 字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址 +- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个对象,parent 指向上一个对象 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/autoreleasepool.png) + +```objectivec +id * begin() { + return (id *) ((uint8_t *)this+sizeof(*this)); +} + +id * end() { + return (id *) ((uint8_t *)this+SIZE); +} +``` + +其中 begin 方法返回 autoreleasePoolPage 对象中开始存储 autorelease 对象的开始地址 + +end 方法返回 autoreleasePoolPage 对象中结束存储 autorelease 对象的开始地址 + +调用 `AutoreleasePoolPage::push` 方法会将一个 `POOL_BOUNDARY `入栈,并且返回其存放的内存地址 + +调用 `AutoreleasePoolPage::pop` 方法时传入一个 `POOL_BOUNDARY` 的内存地址,系统会从最后一个入栈的对象开始发送 release消 息,直到遇到这个 `POOL_BOUNDARY` + +`id *next` 指向了下一个能存放 autorelease 对象地址的区域 + +```c +static inline void *push() { + id *dest; + if (DebugPoolAllocation) { + // Each autorelease pool starts on a new pool page. + dest = autoreleaseNewPage(POOL_BOUNDARY); + } else { + dest = autoreleaseFast(POOL_BOUNDARY); + } + assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); + return dest; +} +``` + +来个骚一些的例子 + +```objectivec +int main(int argc, const char * argv[]) { + @autoreleasepool { + Person *p1 = [[[Person alloc] init] autorelease]; + Person *p2 = [[[Person alloc] init] autorelease]; + @autoreleasepool { + Person *p3 = [[[Person alloc] init] autorelease]; + @autoreleasepool { + Person *p4 = [[[Person alloc] init] autorelease]; + } + } + } + return 0; +} +``` + +main 方法内部3个 autoreleasepool 底层怎么样工作的? + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/AutoreleasePoolMoreItem.png) + +3个@auto releasepool, 系统遇到第一个的时候底层就是初始化一个结构体 `__AtAutoreleasePool`,结构体构造方法内部调用 `AutoreleasePoolPage::push` 方法,系统给 AutoreleasePoolPage 真正保存 autorelease 对象的地方存储进一个 `POOL_BOUNDARY` 对象,然后储存 P1、P2 对象地址,遇到第二个则继续初始化结构体,调用 push 方法,存储一个` POOL_BOUNDARY` 对象,继续保存 P3,遇到第三个则继续初始化结构体,调用 push 方法,存储一个 `POOL_BOUNDARY` 对象,继续保存 P4。 + +当结束第三个大括号的时候,第三个结构体对象,调用析构函数,内部调用 `AutoreleasePoolPage::pop` 方法,会从最后一个入栈的对象开始发送 release 消息,直到遇到 `POOL_BOUNDARY` 对象。 + +紧接着第二个大括号结束,第二个结构体对象析构函数执行,内部调用 `AutoreleasePoolPage::pop` 方法,会从最后一个入栈的对象开始发送 release 消息,直到遇到 `POOL_BOUNDARY` 对象。 + +第一个同理。 + +小窍门,对于上述原理的分析可以用源码中看到的 `AutoreleasePoolPage` 对象的 `printAll` 方法。 + +```c +static void printAll() { + _objc_inform("##############"); + _objc_inform("AUTORELEASE POOLS for thread %p", pthread_self()); + + AutoreleasePoolPage *page; + ptrdiff_t objects = 0; + for (page = coldPage(); page; page = page->child) { + objects += page->next - page->begin(); + } + _objc_inform("%llu releases pending.", (unsigned long long)objects); + + if (haveEmptyPoolPlaceholder()) { + _objc_inform("[%p] ................ PAGE (placeholder)", + EMPTY_POOL_PLACEHOLDER); + _objc_inform("[%p] ################ POOL (placeholder)", + EMPTY_POOL_PLACEHOLDER); + } + else { + for (page = coldPage(); page; page = page->child) { + page->print(); + } + } + _objc_inform("##############"); +} +void _objc_autoreleasePoolPrint(void) { + AutoreleasePoolPage::printAll(); +} +``` + +查了下 `printAll` 函数的使用方,就只有 `_objc_autoreleasePoolPrint` 函数。且可以看到在 objc4 `objc-internal.h` 头文件中有将该函数 export 出去,也就是可以在外部链接该符号。 + +```c +OBJC_EXPORT void _objc_autoreleasePoolPrint(void) OBJC_AVAILABLE(10.7, 5.0, 9.0, 1.0, 2.0); +``` + +所以我们在测试 Demo 中将 `_objc_autoreleasePoolPrint` 函数声明下。在打印下 + +```c +extern void _objc_autoreleasePoolPrint(void); +int main(int argc, const char * argv[]) { + @autoreleasepool { + Person *p1 = [[[Person alloc] init] autorelease]; + Person *p2 = [[[Person alloc] init] autorelease]; + @autoreleasepool { + Person *p3 = [[[Person alloc] init] autorelease]; + @autoreleasepool { + Person *p4 = [[[Person alloc] init] autorelease]; + _objc_autoreleasePoolPrint(); + } + } + } + return 0; +} +objc[23132]: ############## +objc[23132]: AUTORELEASE POOLS for thread 0x100094600 +objc[23132]: 7 releases pending. +objc[23132]: [0x10080a000] ................ PAGE (hot) (cold) +objc[23132]: [0x10080a038] ################ POOL 0x10080a038 +objc[23132]: [0x10080a040] 0x10075f060 Person +objc[23132]: [0x10080a048] 0x10075f0c0 Person +objc[23132]: [0x10080a050] ################ POOL 0x10080a050 +objc[23132]: [0x10080a058] 0x10075f0e0 Person +objc[23132]: [0x10080a060] ################ POOL 0x10080a060 +objc[23132]: [0x10080a068] 0x10075f100 Person +objc[23132]: ############## +``` + +可以看到打印结果和上面的分析是一致的(和上面的图片对比看看) + +再来个 Demo,验证下 AutoreleasePoolPage 一页满情况 + +```c +extern void _objc_autoreleasePoolPrint(void); +int main(int argc, const char * argv[]) { + @autoreleasepool { + Person *p1 = [[[Person alloc] init] autorelease]; + Person *p2 = [[[Person alloc] init] autorelease]; + @autoreleasepool { + for (NSInteger index = 0; index<600; index++) { + Person *p3 = [[[Person alloc] init] autorelease]; + } + @autoreleasepool { + Person *p4 = [[[Person alloc] init] autorelease]; + _objc_autoreleasePoolPrint(); + } + } + } + return 0; +} + +objc[23504]: ############## +objc[23504]: AUTORELEASE POOLS for thread 0x100094600 +objc[23504]: 606 releases pending. +objc[23504]: [0x10080d000] ................ PAGE (full) (cold) +objc[23504]: [0x10080d038] ################ POOL 0x10080d038 +objc[23504]: [0x10080d040] 0x1007092f0 Person +objc[23504]: [0x10080d048] 0x100709350 Person +objc[23504]: [0x10080d050] ################ POOL 0x10080d050 +objc[23504]: [0x10080d058] 0x100753250 Person +objc[23504]: [0x10080d060] 0x100753270 Person +objc[23504]: [0x10080d068] 0x100753290 Person +objc[23504]: [0x10080d070] 0x1007532b0 Person +objc[23504]: [0x10080d078] 0x1007532d0 Person +objc[23504]: [0x10080d080] 0x1007532f0 Person +objc[23504]: [0x10080d088] 0x100753310 Person +objc[23504]: [0x10080d090] 0x100753330 Person +objc[23504]: [0x10080d098] 0x100753680 Person +objc[23504]: [0x10080d0a0] 0x1007536a0 Person +objc[23504]: [0x10080d0a8] 0x1007536c0 Person +objc[23504]: [0x10080d0b0] 0x1007536e0 Person +objc[23504]: [0x10080d0b8] 0x100753700 Person +objc[23504]: [0x10080d0c0] 0x100753720 Person +objc[23504]: [0x10080d0c8] 0x100753740 Person +objc[23504]: [0x10080d0d0] 0x100753760 Person +objc[23504]: [0x10080d0d8] 0x100753780 Person +objc[23504]: [0x10080d0e0] 0x1007537a0 Person +objc[23504]: [0x10080d0e8] 0x1007537c0 Person +objc[23504]: [0x10080d0f0] 0x1007537e0 Person +objc[23504]: [0x10080d0f8] 0x100753800 Person +objc[23504]: [0x10080d100] 0x100753820 Person +objc[23504]: [0x10080d108] 0x100753840 Person +objc[23504]: [0x10080d110] 0x100753860 Person +objc[23504]: [0x10080d118] 0x100753880 Person +objc[23504]: [0x10080d120] 0x1007538a0 Person +objc[23504]: [0x10080d128] 0x1007538c0 Person +objc[23504]: [0x10080d130] 0x1007538e0 Person +objc[23504]: [0x10080d138] 0x100753900 Person +objc[23504]: [0x10080d140] 0x100753920 Person +objc[23504]: [0x10080d148] 0x100753940 Person +objc[23504]: [0x10080d150] 0x100753960 Person +objc[23504]: [0x10080d158] 0x100753980 Person +objc[23504]: [0x10080d160] 0x1007539a0 Person +objc[23504]: [0x10080d168] 0x1007539c0 Person +objc[23504]: [0x10080d170] 0x1007539e0 Person +objc[23504]: [0x10080d178] 0x100753a00 Person +objc[23504]: [0x10080d180] 0x100753a20 Person +objc[23504]: [0x10080d188] 0x100753a40 Person +objc[23504]: [0x10080d190] 0x100753a60 Person +objc[23504]: [0x10080d198] 0x100753a80 Person +objc[23504]: [0x10080d1a0] 0x100753aa0 Person +objc[23504]: [0x10080d1a8] 0x100753ac0 Person +objc[23504]: [0x10080d1b0] 0x100753ae0 Person +objc[23504]: [0x10080d1b8] 0x100753b00 Person +objc[23504]: [0x10080d1c0] 0x100753b20 Person +objc[23504]: [0x10080d1c8] 0x100753b40 Person +objc[23504]: [0x10080d1d0] 0x100753b60 Person +objc[23504]: [0x10080d1d8] 0x100753b80 Person +objc[23504]: [0x10080d1e0] 0x100753ba0 Person +objc[23504]: [0x10080d1e8] 0x100753bc0 Person +objc[23504]: [0x10080d1f0] 0x100753be0 Person +objc[23504]: [0x10080d1f8] 0x100753c00 Person +objc[23504]: [0x10080d200] 0x100753c20 Person +objc[23504]: [0x10080d208] 0x100753c40 Person +objc[23504]: [0x10080d210] 0x100753c60 Person +objc[23504]: [0x10080d218] 0x100753c80 Person +objc[23504]: [0x10080d220] 0x100753ca0 Person +objc[23504]: [0x10080d228] 0x100753cc0 Person +objc[23504]: [0x10080d230] 0x100753ce0 Person +objc[23504]: [0x10080d238] 0x100753d00 Person +objc[23504]: [0x10080d240] 0x100753d20 Person +objc[23504]: [0x10080d248] 0x100753d40 Person +objc[23504]: [0x10080d250] 0x100753d60 Person +objc[23504]: [0x10080d258] 0x100753d80 Person +objc[23504]: [0x10080d260] 0x100753da0 Person +objc[23504]: [0x10080d268] 0x100753dc0 Person +objc[23504]: [0x10080d270] 0x100753de0 Person +objc[23504]: [0x10080d278] 0x100753e00 Person +objc[23504]: [0x10080d280] 0x100753e20 Person +objc[23504]: [0x10080d288] 0x100753e40 Person +objc[23504]: [0x10080d290] 0x100753e60 Person +objc[23504]: [0x10080d298] 0x100753e80 Person +objc[23504]: [0x10080d2a0] 0x100753ea0 Person +objc[23504]: [0x10080d2a8] 0x100753ec0 Person +objc[23504]: [0x10080d2b0] 0x100753ee0 Person +objc[23504]: [0x10080d2b8] 0x100753f00 Person +objc[23504]: [0x10080d2c0] 0x100753f20 Person +objc[23504]: [0x10080d2c8] 0x100753f40 Person +objc[23504]: [0x10080d2d0] 0x100753f60 Person +objc[23504]: [0x10080d2d8] 0x100753f80 Person +objc[23504]: [0x10080d2e0] 0x100753fa0 Person +objc[23504]: [0x10080d2e8] 0x100753fc0 Person +objc[23504]: [0x10080d2f0] 0x100753fe0 Person +objc[23504]: [0x10080d2f8] 0x100754000 Person +objc[23504]: [0x10080d300] 0x100754020 Person +objc[23504]: [0x10080d308] 0x100754040 Person +objc[23504]: [0x10080d310] 0x100754060 Person +objc[23504]: [0x10080d318] 0x100754080 Person +objc[23504]: [0x10080d320] 0x1007540a0 Person +objc[23504]: [0x10080d328] 0x1007540c0 Person +objc[23504]: [0x10080d330] 0x1007540e0 Person +objc[23504]: [0x10080d338] 0x100754100 Person +objc[23504]: [0x10080d340] 0x100754120 Person +objc[23504]: [0x10080d348] 0x100754140 Person +objc[23504]: [0x10080d350] 0x100754160 Person +objc[23504]: [0x10080d358] 0x100754180 Person +objc[23504]: [0x10080d360] 0x1007541a0 Person +objc[23504]: [0x10080d368] 0x1007541c0 Person +objc[23504]: [0x10080d370] 0x1007541e0 Person +objc[23504]: [0x10080d378] 0x100754200 Person +objc[23504]: [0x10080d380] 0x100754220 Person +objc[23504]: [0x10080d388] 0x100754240 Person +objc[23504]: [0x10080d390] 0x100754260 Person +objc[23504]: [0x10080d398] 0x100754280 Person +objc[23504]: [0x10080d3a0] 0x1007542a0 Person +objc[23504]: [0x10080d3a8] 0x1007542c0 Person +objc[23504]: [0x10080d3b0] 0x1007542e0 Person +objc[23504]: [0x10080d3b8] 0x100754300 Person +objc[23504]: [0x10080d3c0] 0x100754320 Person +objc[23504]: [0x10080d3c8] 0x100754340 Person +objc[23504]: [0x10080d3d0] 0x100754360 Person +objc[23504]: [0x10080d3d8] 0x100754380 Person +objc[23504]: [0x10080d3e0] 0x1007543a0 Person +objc[23504]: [0x10080d3e8] 0x1007543c0 Person +objc[23504]: [0x10080d3f0] 0x1007543e0 Person +objc[23504]: [0x10080d3f8] 0x100754400 Person +objc[23504]: [0x10080d400] 0x100754420 Person +objc[23504]: [0x10080d408] 0x100754440 Person +objc[23504]: [0x10080d410] 0x100754460 Person +objc[23504]: [0x10080d418] 0x100754480 Person +objc[23504]: [0x10080d420] 0x1007544a0 Person +objc[23504]: [0x10080d428] 0x1007544c0 Person +objc[23504]: [0x10080d430] 0x1007544e0 Person +objc[23504]: [0x10080d438] 0x100754500 Person +objc[23504]: [0x10080d440] 0x100754520 Person +objc[23504]: [0x10080d448] 0x100754540 Person +objc[23504]: [0x10080d450] 0x100754560 Person +objc[23504]: [0x10080d458] 0x100754580 Person +objc[23504]: [0x10080d460] 0x1007545a0 Person +objc[23504]: [0x10080d468] 0x1007545c0 Person +objc[23504]: [0x10080d470] 0x1007545e0 Person +objc[23504]: [0x10080d478] 0x100754600 Person +objc[23504]: [0x10080d480] 0x100754620 Person +objc[23504]: [0x10080d488] 0x100754640 Person +objc[23504]: [0x10080d490] 0x100754660 Person +objc[23504]: [0x10080d498] 0x100754680 Person +objc[23504]: [0x10080d4a0] 0x1007546a0 Person +objc[23504]: [0x10080d4a8] 0x1007546c0 Person +objc[23504]: [0x10080d4b0] 0x1007546e0 Person +objc[23504]: [0x10080d4b8] 0x100754700 Person +objc[23504]: [0x10080d4c0] 0x100754720 Person +objc[23504]: [0x10080d4c8] 0x100754740 Person +objc[23504]: [0x10080d4d0] 0x100754760 Person +objc[23504]: [0x10080d4d8] 0x100754780 Person +objc[23504]: [0x10080d4e0] 0x1007547a0 Person +objc[23504]: [0x10080d4e8] 0x1007547c0 Person +objc[23504]: [0x10080d4f0] 0x1007547e0 Person +objc[23504]: [0x10080d4f8] 0x100754800 Person +objc[23504]: [0x10080d500] 0x100754820 Person +objc[23504]: [0x10080d508] 0x100754840 Person +objc[23504]: [0x10080d510] 0x100754860 Person +objc[23504]: [0x10080d518] 0x100754880 Person +objc[23504]: [0x10080d520] 0x1007548a0 Person +objc[23504]: [0x10080d528] 0x1007548c0 Person +objc[23504]: [0x10080d530] 0x1007548e0 Person +objc[23504]: [0x10080d538] 0x100754900 Person +objc[23504]: [0x10080d540] 0x100754920 Person +objc[23504]: [0x10080d548] 0x100754940 Person +objc[23504]: [0x10080d550] 0x100754960 Person +objc[23504]: [0x10080d558] 0x100754980 Person +objc[23504]: [0x10080d560] 0x1007549a0 Person +objc[23504]: [0x10080d568] 0x1007549c0 Person +objc[23504]: [0x10080d570] 0x1007549e0 Person +objc[23504]: [0x10080d578] 0x100754a00 Person +objc[23504]: [0x10080d580] 0x100754a20 Person +objc[23504]: [0x10080d588] 0x100754a40 Person +objc[23504]: [0x10080d590] 0x100754a60 Person +objc[23504]: [0x10080d598] 0x100754a80 Person +objc[23504]: [0x10080d5a0] 0x100754aa0 Person +objc[23504]: [0x10080d5a8] 0x100754ac0 Person +objc[23504]: [0x10080d5b0] 0x100754ae0 Person +objc[23504]: [0x10080d5b8] 0x100754b00 Person +objc[23504]: [0x10080d5c0] 0x100754b20 Person +objc[23504]: [0x10080d5c8] 0x100754b40 Person +objc[23504]: [0x10080d5d0] 0x100754b60 Person +objc[23504]: [0x10080d5d8] 0x100754b80 Person +objc[23504]: [0x10080d5e0] 0x100754ba0 Person +objc[23504]: [0x10080d5e8] 0x100754bc0 Person +objc[23504]: [0x10080d5f0] 0x100754be0 Person +objc[23504]: [0x10080d5f8] 0x100754c00 Person +objc[23504]: [0x10080d600] 0x100754c20 Person +objc[23504]: [0x10080d608] 0x100754c40 Person +objc[23504]: [0x10080d610] 0x100754c60 Person +objc[23504]: [0x10080d618] 0x100754c80 Person +objc[23504]: [0x10080d620] 0x100754ca0 Person +objc[23504]: [0x10080d628] 0x100754cc0 Person +objc[23504]: [0x10080d630] 0x100754ce0 Person +objc[23504]: [0x10080d638] 0x100754d00 Person +objc[23504]: [0x10080d640] 0x100754d20 Person +objc[23504]: [0x10080d648] 0x100754d40 Person +objc[23504]: [0x10080d650] 0x100754d60 Person +objc[23504]: [0x10080d658] 0x100754d80 Person +objc[23504]: [0x10080d660] 0x100754da0 Person +objc[23504]: [0x10080d668] 0x100754dc0 Person +objc[23504]: [0x10080d670] 0x100754de0 Person +objc[23504]: [0x10080d678] 0x100754e00 Person +objc[23504]: [0x10080d680] 0x10074fa70 Person +objc[23504]: [0x10080d688] 0x10074fa90 Person +objc[23504]: [0x10080d690] 0x10074fab0 Person +objc[23504]: [0x10080d698] 0x10074fad0 Person +objc[23504]: [0x10080d6a0] 0x10074faf0 Person +objc[23504]: [0x10080d6a8] 0x10074fb10 Person +objc[23504]: [0x10080d6b0] 0x10074fb30 Person +objc[23504]: [0x10080d6b8] 0x10074fb50 Person +objc[23504]: [0x10080d6c0] 0x10074fb70 Person +objc[23504]: [0x10080d6c8] 0x10074fb90 Person +objc[23504]: [0x10080d6d0] 0x10074fbb0 Person +objc[23504]: [0x10080d6d8] 0x10074fbd0 Person +objc[23504]: [0x10080d6e0] 0x10074fbf0 Person +objc[23504]: [0x10080d6e8] 0x10074fc10 Person +objc[23504]: [0x10080d6f0] 0x10074fc30 Person +objc[23504]: [0x10080d6f8] 0x10074fc50 Person +objc[23504]: [0x10080d700] 0x10074fc70 Person +objc[23504]: [0x10080d708] 0x10074fc90 Person +objc[23504]: [0x10080d710] 0x10074fcb0 Person +objc[23504]: [0x10080d718] 0x10074fcd0 Person +objc[23504]: [0x10080d720] 0x10074fcf0 Person +objc[23504]: [0x10080d728] 0x10074fd10 Person +objc[23504]: [0x10080d730] 0x10074fd30 Person +objc[23504]: [0x10080d738] 0x10074fd50 Person +objc[23504]: [0x10080d740] 0x10074fd70 Person +objc[23504]: [0x10080d748] 0x10074fd90 Person +objc[23504]: [0x10080d750] 0x10074fdb0 Person +objc[23504]: [0x10080d758] 0x10074fdd0 Person +objc[23504]: [0x10080d760] 0x10074fdf0 Person +objc[23504]: [0x10080d768] 0x10074fe10 Person +objc[23504]: [0x10080d770] 0x10074fe30 Person +objc[23504]: [0x10080d778] 0x10074fe50 Person +objc[23504]: [0x10080d780] 0x10074fe70 Person +objc[23504]: [0x10080d788] 0x10074fe90 Person +objc[23504]: [0x10080d790] 0x10074feb0 Person +objc[23504]: [0x10080d798] 0x10074fed0 Person +objc[23504]: [0x10080d7a0] 0x10074fef0 Person +objc[23504]: [0x10080d7a8] 0x10074ff10 Person +objc[23504]: [0x10080d7b0] 0x10074ff30 Person +objc[23504]: [0x10080d7b8] 0x10074ff50 Person +objc[23504]: [0x10080d7c0] 0x10074ff70 Person +objc[23504]: [0x10080d7c8] 0x10074ff90 Person +objc[23504]: [0x10080d7d0] 0x10074ffb0 Person +objc[23504]: [0x10080d7d8] 0x10074ffd0 Person +objc[23504]: [0x10080d7e0] 0x10074fff0 Person +objc[23504]: [0x10080d7e8] 0x100750010 Person +objc[23504]: [0x10080d7f0] 0x100750030 Person +objc[23504]: [0x10080d7f8] 0x100750050 Person +objc[23504]: [0x10080d800] 0x100750070 Person +objc[23504]: [0x10080d808] 0x100750090 Person +objc[23504]: [0x10080d810] 0x1007500b0 Person +objc[23504]: [0x10080d818] 0x1007500d0 Person +objc[23504]: [0x10080d820] 0x1007500f0 Person +objc[23504]: [0x10080d828] 0x100750110 Person +objc[23504]: [0x10080d830] 0x100750130 Person +objc[23504]: [0x10080d838] 0x100750150 Person +objc[23504]: [0x10080d840] 0x100750170 Person +objc[23504]: [0x10080d848] 0x100750190 Person +objc[23504]: [0x10080d850] 0x1007501b0 Person +objc[23504]: [0x10080d858] 0x1007501d0 Person +objc[23504]: [0x10080d860] 0x1007501f0 Person +objc[23504]: [0x10080d868] 0x100750210 Person +objc[23504]: [0x10080d870] 0x100750230 Person +objc[23504]: [0x10080d878] 0x100750250 Person +objc[23504]: [0x10080d880] 0x100750270 Person +objc[23504]: [0x10080d888] 0x100750290 Person +objc[23504]: [0x10080d890] 0x1007502b0 Person +objc[23504]: [0x10080d898] 0x1007502d0 Person +objc[23504]: [0x10080d8a0] 0x1007502f0 Person +objc[23504]: [0x10080d8a8] 0x100750310 Person +objc[23504]: [0x10080d8b0] 0x100750330 Person +objc[23504]: [0x10080d8b8] 0x100750350 Person +objc[23504]: [0x10080d8c0] 0x100750370 Person +objc[23504]: [0x10080d8c8] 0x100750390 Person +objc[23504]: [0x10080d8d0] 0x1007503b0 Person +objc[23504]: [0x10080d8d8] 0x1007503d0 Person +objc[23504]: [0x10080d8e0] 0x1007503f0 Person +objc[23504]: [0x10080d8e8] 0x100750410 Person +objc[23504]: [0x10080d8f0] 0x100750430 Person +objc[23504]: [0x10080d8f8] 0x100750450 Person +objc[23504]: [0x10080d900] 0x100750470 Person +objc[23504]: [0x10080d908] 0x100750490 Person +objc[23504]: [0x10080d910] 0x1007504b0 Person +objc[23504]: [0x10080d918] 0x1007504d0 Person +objc[23504]: [0x10080d920] 0x1007504f0 Person +objc[23504]: [0x10080d928] 0x100750510 Person +objc[23504]: [0x10080d930] 0x100750530 Person +objc[23504]: [0x10080d938] 0x100750550 Person +objc[23504]: [0x10080d940] 0x100750570 Person +objc[23504]: [0x10080d948] 0x100750590 Person +objc[23504]: [0x10080d950] 0x1007505b0 Person +objc[23504]: [0x10080d958] 0x1007505d0 Person +objc[23504]: [0x10080d960] 0x1007505f0 Person +objc[23504]: [0x10080d968] 0x100750610 Person +objc[23504]: [0x10080d970] 0x100750630 Person +objc[23504]: [0x10080d978] 0x100750650 Person +objc[23504]: [0x10080d980] 0x100750670 Person +objc[23504]: [0x10080d988] 0x100750690 Person +objc[23504]: [0x10080d990] 0x1007506b0 Person +objc[23504]: [0x10080d998] 0x1007506d0 Person +objc[23504]: [0x10080d9a0] 0x1007506f0 Person +objc[23504]: [0x10080d9a8] 0x100750710 Person +objc[23504]: [0x10080d9b0] 0x100750730 Person +objc[23504]: [0x10080d9b8] 0x100750750 Person +objc[23504]: [0x10080d9c0] 0x100750770 Person +objc[23504]: [0x10080d9c8] 0x100750790 Person +objc[23504]: [0x10080d9d0] 0x1007507b0 Person +objc[23504]: [0x10080d9d8] 0x1007507d0 Person +objc[23504]: [0x10080d9e0] 0x1007507f0 Person +objc[23504]: [0x10080d9e8] 0x100750810 Person +objc[23504]: [0x10080d9f0] 0x100750830 Person +objc[23504]: [0x10080d9f8] 0x100750850 Person +objc[23504]: [0x10080da00] 0x100750870 Person +objc[23504]: [0x10080da08] 0x100750890 Person +objc[23504]: [0x10080da10] 0x1007508b0 Person +objc[23504]: [0x10080da18] 0x1007508d0 Person +objc[23504]: [0x10080da20] 0x1007508f0 Person +objc[23504]: [0x10080da28] 0x100750910 Person +objc[23504]: [0x10080da30] 0x100750930 Person +objc[23504]: [0x10080da38] 0x100750950 Person +objc[23504]: [0x10080da40] 0x100750970 Person +objc[23504]: [0x10080da48] 0x100750990 Person +objc[23504]: [0x10080da50] 0x1007509b0 Person +objc[23504]: [0x10080da58] 0x1007509d0 Person +objc[23504]: [0x10080da60] 0x1007509f0 Person +objc[23504]: [0x10080da68] 0x100750a10 Person +objc[23504]: [0x10080da70] 0x100750a30 Person +objc[23504]: [0x10080da78] 0x100750a50 Person +objc[23504]: [0x10080da80] 0x100750a70 Person +objc[23504]: [0x10080da88] 0x100750a90 Person +objc[23504]: [0x10080da90] 0x100750ab0 Person +objc[23504]: [0x10080da98] 0x100750ad0 Person +objc[23504]: [0x10080daa0] 0x100750af0 Person +objc[23504]: [0x10080daa8] 0x100750b10 Person +objc[23504]: [0x10080dab0] 0x100750b30 Person +objc[23504]: [0x10080dab8] 0x100750b50 Person +objc[23504]: [0x10080dac0] 0x100750b70 Person +objc[23504]: [0x10080dac8] 0x100750b90 Person +objc[23504]: [0x10080dad0] 0x100750bb0 Person +objc[23504]: [0x10080dad8] 0x100750bd0 Person +objc[23504]: [0x10080dae0] 0x100750bf0 Person +objc[23504]: [0x10080dae8] 0x100750c10 Person +objc[23504]: [0x10080daf0] 0x100750c30 Person +objc[23504]: [0x10080daf8] 0x100750c50 Person +objc[23504]: [0x10080db00] 0x100750c70 Person +objc[23504]: [0x10080db08] 0x100750c90 Person +objc[23504]: [0x10080db10] 0x100750cb0 Person +objc[23504]: [0x10080db18] 0x100750cd0 Person +objc[23504]: [0x10080db20] 0x100750cf0 Person +objc[23504]: [0x10080db28] 0x100750d10 Person +objc[23504]: [0x10080db30] 0x100750d30 Person +objc[23504]: [0x10080db38] 0x100750d50 Person +objc[23504]: [0x10080db40] 0x100750d70 Person +objc[23504]: [0x10080db48] 0x100750d90 Person +objc[23504]: [0x10080db50] 0x100750db0 Person +objc[23504]: [0x10080db58] 0x100750dd0 Person +objc[23504]: [0x10080db60] 0x100750df0 Person +objc[23504]: [0x10080db68] 0x100750e10 Person +objc[23504]: [0x10080db70] 0x100750e30 Person +objc[23504]: [0x10080db78] 0x100750e50 Person +objc[23504]: [0x10080db80] 0x100750e70 Person +objc[23504]: [0x10080db88] 0x100750e90 Person +objc[23504]: [0x10080db90] 0x100750eb0 Person +objc[23504]: [0x10080db98] 0x100750ed0 Person +objc[23504]: [0x10080dba0] 0x100750ef0 Person +objc[23504]: [0x10080dba8] 0x100750f10 Person +objc[23504]: [0x10080dbb0] 0x100750f30 Person +objc[23504]: [0x10080dbb8] 0x100750f50 Person +objc[23504]: [0x10080dbc0] 0x100750f70 Person +objc[23504]: [0x10080dbc8] 0x100750f90 Person +objc[23504]: [0x10080dbd0] 0x100750fb0 Person +objc[23504]: [0x10080dbd8] 0x100750fd0 Person +objc[23504]: [0x10080dbe0] 0x100750ff0 Person +objc[23504]: [0x10080dbe8] 0x100751010 Person +objc[23504]: [0x10080dbf0] 0x100751030 Person +objc[23504]: [0x10080dbf8] 0x100751050 Person +objc[23504]: [0x10080dc00] 0x100751070 Person +objc[23504]: [0x10080dc08] 0x100751090 Person +objc[23504]: [0x10080dc10] 0x1007510b0 Person +objc[23504]: [0x10080dc18] 0x1007510d0 Person +objc[23504]: [0x10080dc20] 0x1007510f0 Person +objc[23504]: [0x10080dc28] 0x100751110 Person +objc[23504]: [0x10080dc30] 0x100751130 Person +objc[23504]: [0x10080dc38] 0x100751150 Person +objc[23504]: [0x10080dc40] 0x100751170 Person +objc[23504]: [0x10080dc48] 0x100751190 Person +objc[23504]: [0x10080dc50] 0x1007511b0 Person +objc[23504]: [0x10080dc58] 0x1007511d0 Person +objc[23504]: [0x10080dc60] 0x1007511f0 Person +objc[23504]: [0x10080dc68] 0x100751210 Person +objc[23504]: [0x10080dc70] 0x100751230 Person +objc[23504]: [0x10080dc78] 0x100751250 Person +objc[23504]: [0x10080dc80] 0x100751270 Person +objc[23504]: [0x10080dc88] 0x100751290 Person +objc[23504]: [0x10080dc90] 0x1007512b0 Person +objc[23504]: [0x10080dc98] 0x1007512d0 Person +objc[23504]: [0x10080dca0] 0x1007512f0 Person +objc[23504]: [0x10080dca8] 0x100751310 Person +objc[23504]: [0x10080dcb0] 0x100751330 Person +objc[23504]: [0x10080dcb8] 0x100751350 Person +objc[23504]: [0x10080dcc0] 0x100751370 Person +objc[23504]: [0x10080dcc8] 0x100751390 Person +objc[23504]: [0x10080dcd0] 0x1007513b0 Person +objc[23504]: [0x10080dcd8] 0x1007513d0 Person +objc[23504]: [0x10080dce0] 0x1007513f0 Person +objc[23504]: [0x10080dce8] 0x100751410 Person +objc[23504]: [0x10080dcf0] 0x100751430 Person +objc[23504]: [0x10080dcf8] 0x100751450 Person +objc[23504]: [0x10080dd00] 0x100751470 Person +objc[23504]: [0x10080dd08] 0x100751490 Person +objc[23504]: [0x10080dd10] 0x1007514b0 Person +objc[23504]: [0x10080dd18] 0x1007514d0 Person +objc[23504]: [0x10080dd20] 0x1007514f0 Person +objc[23504]: [0x10080dd28] 0x100751510 Person +objc[23504]: [0x10080dd30] 0x100751530 Person +objc[23504]: [0x10080dd38] 0x100751550 Person +objc[23504]: [0x10080dd40] 0x100751570 Person +objc[23504]: [0x10080dd48] 0x100751590 Person +objc[23504]: [0x10080dd50] 0x1007515b0 Person +objc[23504]: [0x10080dd58] 0x1007515d0 Person +objc[23504]: [0x10080dd60] 0x1007515f0 Person +objc[23504]: [0x10080dd68] 0x100751610 Person +objc[23504]: [0x10080dd70] 0x100751630 Person +objc[23504]: [0x10080dd78] 0x100751650 Person +objc[23504]: [0x10080dd80] 0x100751670 Person +objc[23504]: [0x10080dd88] 0x100751690 Person +objc[23504]: [0x10080dd90] 0x1007516b0 Person +objc[23504]: [0x10080dd98] 0x1007516d0 Person +objc[23504]: [0x10080dda0] 0x1007516f0 Person +objc[23504]: [0x10080dda8] 0x100751710 Person +objc[23504]: [0x10080ddb0] 0x100751730 Person +objc[23504]: [0x10080ddb8] 0x100751750 Person +objc[23504]: [0x10080ddc0] 0x100751770 Person +objc[23504]: [0x10080ddc8] 0x100751790 Person +objc[23504]: [0x10080ddd0] 0x1007517b0 Person +objc[23504]: [0x10080ddd8] 0x1007517d0 Person +objc[23504]: [0x10080dde0] 0x1007517f0 Person +objc[23504]: [0x10080dde8] 0x100751810 Person +objc[23504]: [0x10080ddf0] 0x100751830 Person +objc[23504]: [0x10080ddf8] 0x100751850 Person +objc[23504]: [0x10080de00] 0x100751870 Person +objc[23504]: [0x10080de08] 0x100751890 Person +objc[23504]: [0x10080de10] 0x1007518b0 Person +objc[23504]: [0x10080de18] 0x1007518d0 Person +objc[23504]: [0x10080de20] 0x1007518f0 Person +objc[23504]: [0x10080de28] 0x100751910 Person +objc[23504]: [0x10080de30] 0x100751930 Person +objc[23504]: [0x10080de38] 0x100751950 Person +objc[23504]: [0x10080de40] 0x100751970 Person +objc[23504]: [0x10080de48] 0x100751990 Person +objc[23504]: [0x10080de50] 0x1007519b0 Person +objc[23504]: [0x10080de58] 0x1007519d0 Person +objc[23504]: [0x10080de60] 0x1007519f0 Person +objc[23504]: [0x10080de68] 0x100751a10 Person +objc[23504]: [0x10080de70] 0x100751a30 Person +objc[23504]: [0x10080de78] 0x100751a50 Person +objc[23504]: [0x10080de80] 0x100751a70 Person +objc[23504]: [0x10080de88] 0x100751a90 Person +objc[23504]: [0x10080de90] 0x100751ab0 Person +objc[23504]: [0x10080de98] 0x100751ad0 Person +objc[23504]: [0x10080dea0] 0x100751af0 Person +objc[23504]: [0x10080dea8] 0x100751b10 Person +objc[23504]: [0x10080deb0] 0x100751b30 Person +objc[23504]: [0x10080deb8] 0x100751b50 Person +objc[23504]: [0x10080dec0] 0x100751b70 Person +objc[23504]: [0x10080dec8] 0x100751b90 Person +objc[23504]: [0x10080ded0] 0x100751bb0 Person +objc[23504]: [0x10080ded8] 0x100751bd0 Person +objc[23504]: [0x10080dee0] 0x100751bf0 Person +objc[23504]: [0x10080dee8] 0x100751c10 Person +objc[23504]: [0x10080def0] 0x100751c30 Person +objc[23504]: [0x10080def8] 0x100751c50 Person +objc[23504]: [0x10080df00] 0x100751c70 Person +objc[23504]: [0x10080df08] 0x100751c90 Person +objc[23504]: [0x10080df10] 0x100751cb0 Person +objc[23504]: [0x10080df18] 0x100751cd0 Person +objc[23504]: [0x10080df20] 0x100751cf0 Person +objc[23504]: [0x10080df28] 0x100751d10 Person +objc[23504]: [0x10080df30] 0x100751d30 Person +objc[23504]: [0x10080df38] 0x100751d50 Person +objc[23504]: [0x10080df40] 0x100751d70 Person +objc[23504]: [0x10080df48] 0x100751d90 Person +objc[23504]: [0x10080df50] 0x100751db0 Person +objc[23504]: [0x10080df58] 0x100751dd0 Person +objc[23504]: [0x10080df60] 0x100751df0 Person +objc[23504]: [0x10080df68] 0x100751e10 Person +objc[23504]: [0x10080df70] 0x100751e30 Person +objc[23504]: [0x10080df78] 0x100751e50 Person +objc[23504]: [0x10080df80] 0x100751e70 Person +objc[23504]: [0x10080df88] 0x100751e90 Person +objc[23504]: [0x10080df90] 0x100751eb0 Person +objc[23504]: [0x10080df98] 0x100751ed0 Person +objc[23504]: [0x10080dfa0] 0x100751ef0 Person +objc[23504]: [0x10080dfa8] 0x100751f10 Person +objc[23504]: [0x10080dfb0] 0x100751f30 Person +objc[23504]: [0x10080dfb8] 0x100751f50 Person +objc[23504]: [0x10080dfc0] 0x100751f70 Person +objc[23504]: [0x10080dfc8] 0x100751f90 Person +objc[23504]: [0x10080dfd0] 0x100751fb0 Person +objc[23504]: [0x10080dfd8] 0x100751fd0 Person +objc[23504]: [0x10080dfe0] 0x100751ff0 Person +objc[23504]: [0x10080dfe8] 0x100752010 Person +objc[23504]: [0x10080dff0] 0x100752030 Person +objc[23504]: [0x10080dff8] 0x100752050 Person +objc[23504]: [0x100817000] ................ PAGE (hot) +objc[23504]: [0x100817038] 0x100752070 Person +objc[23504]: [0x100817040] 0x100752090 Person +objc[23504]: [0x100817048] 0x1007520b0 Person +objc[23504]: [0x100817050] 0x1007520d0 Person +objc[23504]: [0x100817058] 0x1007520f0 Person +objc[23504]: [0x100817060] 0x100752110 Person +objc[23504]: [0x100817068] 0x100752130 Person +objc[23504]: [0x100817070] 0x100752150 Person +objc[23504]: [0x100817078] 0x100752170 Person +objc[23504]: [0x100817080] 0x100752190 Person +objc[23504]: [0x100817088] 0x1007521b0 Person +objc[23504]: [0x100817090] 0x1007521d0 Person +objc[23504]: [0x100817098] 0x1007521f0 Person +objc[23504]: [0x1008170a0] 0x100752210 Person +objc[23504]: [0x1008170a8] 0x100752230 Person +objc[23504]: [0x1008170b0] 0x100752250 Person +objc[23504]: [0x1008170b8] 0x100752270 Person +objc[23504]: [0x1008170c0] 0x100752290 Person +objc[23504]: [0x1008170c8] 0x1007522b0 Person +objc[23504]: [0x1008170d0] 0x1007522d0 Person +objc[23504]: [0x1008170d8] 0x1007522f0 Person +objc[23504]: [0x1008170e0] 0x100752310 Person +objc[23504]: [0x1008170e8] 0x100752330 Person +objc[23504]: [0x1008170f0] 0x100752350 Person +objc[23504]: [0x1008170f8] 0x100752370 Person +objc[23504]: [0x100817100] 0x100752390 Person +objc[23504]: [0x100817108] 0x1007523b0 Person +objc[23504]: [0x100817110] 0x1007523d0 Person +objc[23504]: [0x100817118] 0x1007523f0 Person +objc[23504]: [0x100817120] 0x100752410 Person +objc[23504]: [0x100817128] 0x100752430 Person +objc[23504]: [0x100817130] 0x100752450 Person +objc[23504]: [0x100817138] 0x100752470 Person +objc[23504]: [0x100817140] 0x100752490 Person +objc[23504]: [0x100817148] 0x1007524b0 Person +objc[23504]: [0x100817150] 0x1007524d0 Person +objc[23504]: [0x100817158] 0x1007524f0 Person +objc[23504]: [0x100817160] 0x100752510 Person +objc[23504]: [0x100817168] 0x100752530 Person +objc[23504]: [0x100817170] 0x100752550 Person +objc[23504]: [0x100817178] 0x1007556d0 Person +objc[23504]: [0x100817180] 0x1007556f0 Person +objc[23504]: [0x100817188] 0x100755710 Person +objc[23504]: [0x100817190] 0x100755730 Person +objc[23504]: [0x100817198] 0x100755750 Person +objc[23504]: [0x1008171a0] 0x100755770 Person +objc[23504]: [0x1008171a8] 0x100755790 Person +objc[23504]: [0x1008171b0] 0x1007557b0 Person +objc[23504]: [0x1008171b8] 0x1007557d0 Person +objc[23504]: [0x1008171c0] 0x1007557f0 Person +objc[23504]: [0x1008171c8] 0x100755810 Person +objc[23504]: [0x1008171d0] 0x100755830 Person +objc[23504]: [0x1008171d8] 0x100755850 Person +objc[23504]: [0x1008171e0] 0x100755870 Person +objc[23504]: [0x1008171e8] 0x100755890 Person +objc[23504]: [0x1008171f0] 0x1007558b0 Person +objc[23504]: [0x1008171f8] 0x1007558d0 Person +objc[23504]: [0x100817200] 0x1007558f0 Person +objc[23504]: [0x100817208] 0x100755910 Person +objc[23504]: [0x100817210] 0x100755930 Person +objc[23504]: [0x100817218] 0x100755950 Person +objc[23504]: [0x100817220] 0x100755970 Person +objc[23504]: [0x100817228] 0x100755990 Person +objc[23504]: [0x100817230] 0x1007559b0 Person +objc[23504]: [0x100817238] 0x1007559d0 Person +objc[23504]: [0x100817240] 0x1007559f0 Person +objc[23504]: [0x100817248] 0x100755a10 Person +objc[23504]: [0x100817250] 0x100755a30 Person +objc[23504]: [0x100817258] 0x100755a50 Person +objc[23504]: [0x100817260] 0x100755a70 Person +objc[23504]: [0x100817268] 0x100755a90 Person +objc[23504]: [0x100817270] 0x100755ab0 Person +objc[23504]: [0x100817278] 0x100755ad0 Person +objc[23504]: [0x100817280] 0x100755af0 Person +objc[23504]: [0x100817288] 0x100755b10 Person +objc[23504]: [0x100817290] 0x100755b30 Person +objc[23504]: [0x100817298] 0x100755b50 Person +objc[23504]: [0x1008172a0] 0x100755b70 Person +objc[23504]: [0x1008172a8] 0x100755b90 Person +objc[23504]: [0x1008172b0] 0x100755bb0 Person +objc[23504]: [0x1008172b8] 0x100755bd0 Person +objc[23504]: [0x1008172c0] 0x100755bf0 Person +objc[23504]: [0x1008172c8] 0x100755c10 Person +objc[23504]: [0x1008172d0] 0x100755c30 Person +objc[23504]: [0x1008172d8] 0x100755c50 Person +objc[23504]: [0x1008172e0] 0x100755c70 Person +objc[23504]: [0x1008172e8] 0x100755c90 Person +objc[23504]: [0x1008172f0] 0x100755cb0 Person +objc[23504]: [0x1008172f8] 0x100755cd0 Person +objc[23504]: [0x100817300] 0x100755cf0 Person +objc[23504]: [0x100817308] 0x100755d10 Person +objc[23504]: [0x100817310] 0x100755d30 Person +objc[23504]: [0x100817318] 0x100755d50 Person +objc[23504]: [0x100817320] 0x100755d70 Person +objc[23504]: [0x100817328] 0x100755d90 Person +objc[23504]: [0x100817330] 0x100755db0 Person +objc[23504]: [0x100817338] 0x100755dd0 Person +objc[23504]: [0x100817340] 0x100755df0 Person +objc[23504]: [0x100817348] 0x100755e10 Person +objc[23504]: [0x100817350] ################ POOL 0x100817350 +objc[23504]: [0x100817358] 0x100755e30 Person +objc[23504]: ############## +``` + +可以看到当600*8=4800字节,所以一页肯定存不下,可以看到 + +`................ PAGE (full) (cold)` page 右边有个 cold、hot。cold 代表不是当前页,hot 代表当前页。 + +继续看看对象调用 `autorelease` 方法做了什么事情? + +```c +- (id)autorelease { + return ((id)self)->rootAutorelease(); +} + +inline id objc_object::rootAutorelease() { + if (isTaggedPointer()) return (id)this; + if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this; + return rootAutorelease2(); +} +_attribute__((noinline,used)) id objc_object::rootAutorelease2() { + assert(!isTaggedPointer()); + return AutoreleasePoolPage::autorelease((id)this); +} + +static inline id autorelease(id obj) { + assert(obj); + assert(!obj->isTaggedPointer()); + id *dest __unused = autoreleaseFast(obj); + assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj); + return obj; +} + +static inline id *autoreleaseFast(id obj) { + AutoreleasePoolPage *page = hotPage(); + if (page && !page->full()) { + return page->add(obj); + } else if (page) { + return autoreleaseFullPage(obj, page); + } else { + return autoreleaseNoPage(obj); + } +} +``` + +查看 NSObject autorelease 方法调用链路可以看到最后还是调用 AutoreleasePoolPage 的 add 方法(会判断有没有页、有没有满) + +## autorelease 对象什么时候调用 release 方法 + + 其实也就是 autorelease 和 RunLoop 的关系。 + +iOS 在主线程的 Runloop 中注册了2个 Observer + +- 第1个 Observer 监听了 `kCFRunLoopEntry` 事件,会调用`objc_autoreleasePoolPush()` + +- 第2个 Observer 监听了 `kCFRunLoopBeforeWaiting` 事件,会调用`objc_autoreleasePoolPop()`、`objc_autoreleasePoolPush()`。还监听了`kCFRunLoopBeforeExit`事件,会调用 `objc_autoreleasePoolPop()` + +结合 RunLoop 运行图 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/RunLoop-SourceCode.png) + +- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush` + +- 做一堆其他事情 + +- 07 在将要休眠的时候先调用 `objc_autoreleasePoolPop`,再调用 `objc_autoreleasePoolPush` + +- 等待唤醒做一堆其他事情,回到第二步 + +- 07 又开始休眠,先调用 `objc_autoreleasePoolPop`,再调用 `objc_autoreleasePoolPush` + +- 11 没任务将要休眠,调用 `objc_autoreleasePoolPop` + +可以看到 objc_autoreleasePoolPush、objc_autoreleasePoolPop 成对调用,贯穿 RunLoop \ No newline at end of file diff --git a/Chapter1 - iOS/1.45.md b/Chapter1 - iOS/1.45.md index f8d1619..97121a2 100644 --- a/Chapter1 - iOS/1.45.md +++ b/Chapter1 - iOS/1.45.md @@ -1,10 +1,6 @@ # NSTimer 中的内存泄露 -- GCD 的 timer -- NSProxy -- 采用 Block 的形式为 NSTimer 增加分类 - - +NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题 ```objective-c @interface ViewController() @@ -50,7 +46,332 @@ 1. 控制器不再强引用定时器 2. 定时器不再保留当前的控制器 -```objective-c + + +## 解决方案: + +### 替换 NSTimer API + +```objectivec +__weak typeof(self) weakSelf = self; +self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { + [weakSelf timerTest]; +}]; +``` + +### GCD Timer + +CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。 + +GCD 的定时器会更加准时,底层依赖系统内核。 + +```objectivec +@property (nonatomic, strong) dispatch_source_t timer; +// 创建队列 +dispatch_queue_t queue = dispatch_get_main_queue(); +// 创建 GCD 定时器 +dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); +uint64_t start = 2.0; +uint64_t interval = 1.0; +// 设置定时器周期 +dispatch_source_set_timer(timerSource, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0); +// 设置定时器任务 +dispatch_source_set_event_handler(timerSource, ^{ + NSLog(@"tick tock"); +}); +// 启动定时器 +dispatch_resume(timerSource); +self.timer = timerSource; +``` + + + +为什么 GCD timer 会更准确?因为普通定时器运行依赖 RunLoop,RunLoop 一个运行周期内的任务繁忙程度是不确定的。当某次任务繁重,那么定时器调度就不准时。 + +GCD timer 不依赖 RunLoop,系统底层驱动,所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动,RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。 + + + +### 打破循环引用,NSTimer target 自定义 + +```objectivec +@interface LBPProxy : NSObject ++ (instancetype)proxyWithObject:(id)target; +@property (nonatomic, weak) id target; +@end + +@implementation LBPProxy ++ (instancetype)proxyWithObject:(id)target{ + LBPProxy *proxy = [[LBPProxy alloc] init]; + proxy.target = target; + return proxy; +} +- (id)forwardingTargetForSelector:(SEL)aSelector{ + return self.target; +} +@end + +self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES]; +``` + +### 高精度定时器封装 + +项目中经常使用定时器,普通定时器存在精度丢失的问题、循环引用的问题,为了使用方法我们封装一个定时器 + +```objectivec +#import + +@interface PreciousTimer : NSObject + ++ (NSString *)execTask:(void(^)(void))task + start:(NSTimeInterval)start + interval:(NSTimeInterval)interval + repeats:(BOOL)repeats + async:(BOOL)async; + ++ (NSString *)execTask:(id)target + selector:(SEL)selector + start:(NSTimeInterval)start + interval:(NSTimeInterval)interval + repeats:(BOOL)repeats + async:(BOOL)async; + ++ (void)cancelTask:(NSString *)name; + +@end + +#import "PreciousTimer.h" + +@implementation PreciousTimer + +static NSMutableDictionary *timers_; +dispatch_semaphore_t semaphore_; ++ (void)initialize +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + timers_ = [NSMutableDictionary dictionary]; + semaphore_ = dispatch_semaphore_create(1); + }); +} + ++ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async +{ + if (!task || start < 0 || (interval <= 0 && repeats)) return nil; + + // 队列 + dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue(); + + // 创建定时器 + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + + // 设置时间 + dispatch_source_set_timer(timer, + dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), + interval * NSEC_PER_SEC, 0); + + + dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER); + // 定时器的唯一标识 + NSString *name = [NSString stringWithFormat:@"%zd", timers_.count]; + // 存放到字典中 + timers_[name] = timer; + dispatch_semaphore_signal(semaphore_); + + // 设置回调 + dispatch_source_set_event_handler(timer, ^{ + task(); + + if (!repeats) { // 不重复的任务 + [self cancelTask:name]; + } + }); + + // 启动定时器 + dispatch_resume(timer); + + return name; +} + ++ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async +{ + if (!target || !selector) return nil; + + return [self execTask:^{ + if ([target respondsToSelector:selector]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [target performSelector:selector]; +#pragma clang diagnostic pop + } + } start:start interval:interval repeats:repeats async:async]; +} + ++ (void)cancelTask:(NSString *)name +{ + if (name.length == 0) return; + + dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER); + + dispatch_source_t timer = timers_[name]; + if (timer) { + dispatch_source_cancel(timer); + [timers_ removeObjectForKey:name]; + } + + dispatch_semaphore_signal(semaphore_); +} + +@end +``` + +使用 Demo + +```objectivec +- (void)viewDidLoad{ + [super viewDidLoad]; + NSLog(@"now"); + self.timerId = [PreciousTimer execTask:^{ + NSLog(@"tick tock %@", [NSThread currentThread]); + } start:2 interval:1 repeats:YES async:YES]; +} +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [PreciousTimer cancelTask:self.timerId]; +} +``` + +说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ignoreXcodewarning.png) + + + +### NSProxy + +```objectivec +#import "LBPProxy.h" +@implementation LBPProxy ++ (instancetype)proxyWithObject:(id)target{ + LBPProxy *proxy = [LBPProxy alloc]; + proxy.target = target; + return proxy; +} +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ + return [self.target methodSignatureForSelector:sel]; +} +- (void)forwardInvocation:(NSInvocation *)invocation{ + // 方法1 + invocation.target = self.target; + [invocation invoke]; + // 方法2 + [invocation invokeWithTarget:self.target]; +} +@end + +self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES]; +``` + +QA: 自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别? + +NSProxy 效率更高。继承自 NSObject 的代理,内部运行的时候还是存在方法查找(isa、superclass、cache、methods)流程。 + + + +看一段神奇的代码 + +`LBPProxy` + +```objectivec +@interface LBPProxy : NSObject ++ (instancetype)proxyWithObject:(id)target; +@property (nonatomic, weak) id target; +@end +@implementation LBPProxy ++ (instancetype)proxyWithObject:(id)target{ + LBPProxy *proxy = [LBPProxy alloc]; + proxy.target = target; + return proxy; +} +- (id)forwardingTargetForSelector:(SEL)aSelector{ + return self.target; +} +@end + +``` + +`LBPProxy2` + +```objectivec +@interface LBPProxy2 : NSProxy ++ (instancetype)proxyWithObject:(id)target; +@property (nonatomic, weak) id target; +@end +@implementation LBPProxy2 ++ (instancetype)proxyWithObject:(id)target{ + LBPProxy2 *proxy = [LBPProxy2 alloc]; + proxy.target = target; + return proxy; +} +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ + return [self.target methodSignatureForSelector:sel]; +} +- (void)forwardInvocation:(NSInvocation *)invocation{ + // 方法1 + invocation.target = self.target; + [invocation invoke]; + // 方法2 + [invocation invokeWithTarget:self.target]; +} +@end +``` + +main.m + +```objectivec +ViewController *vc = [[ViewController alloc] init]; +LBPProxy *p1 = [LBPProxy proxyWithObject:vc]; +LBPProxy2 *p2 = [LBPProxy2 proxyWithObject:vc]; +NSLog(@"%d %d", + [p1 isKindOfClass:[UIViewController class]], + [p2 isKindOfClass:[UIViewController class]]); +appDelegateClassName = NSStringFromClass([AppDelegate class]); +// 0 1 +``` + +为什么打印出 `0 1`。 + +分析: + +- p1 是 LBPProxy 类,继承于 NSObject 所以就不是 UIViewController 类型。 + +- p2 是 LBPProxy2 类,继承自 NSProxy,当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 VC,所以为 1。 + +这一点可以查看 GUN 查看下源码印证。`NSProxy.m` + +```objectivec +- (BOOL) isKindOfClass: (Class)aClass +{ + NSMethodSignature *sig; + NSInvocation *inv; + BOOL ret; + sig = [self methodSignatureForSelector: _cmd]; + inv = [NSInvocation invocationWithMethodSignature: sig]; + [inv setSelector: _cmd]; + [inv setArgument: &aClass atIndex: 2]; + [self forwardInvocation: inv]; + [inv getReturnValue: &ret]; + return ret; +} +``` + +可以看到内部直接调用了消息转发。 + + + +### 采用 Block 的形式为 NSTimer 增加分类 + +```objectivec //.h文件 #import @@ -166,6 +487,4 @@ __strong __typeof(&*weakSelf)self = weakSelf; } ``` - - iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致 \ No newline at end of file diff --git a/Chapter1 - iOS/1.46.md b/Chapter1 - iOS/1.46.md index 291377b..076daa0 100644 --- a/Chapter1 - iOS/1.46.md +++ b/Chapter1 - iOS/1.46.md @@ -1,8 +1,4 @@ -# KVC && KVO - - - - +# KVC && KVO ## 一、基本用法-字典快速赋值 @@ -24,13 +20,12 @@ KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setVal 运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null - 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值 - 运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分 ``` - (void)setValue:(id)value forUndefinedKey:(NSString *)key{ - + } ``` @@ -38,7 +33,6 @@ KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setVal 我们照样可以利用 **setValue:forUndefinedKey:** 去处理 - ```objective-c //model @property (nonatomic,copy)NSString *name; @@ -65,7 +59,6 @@ NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"]; ``` - 情况五: 注册依赖键. - KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类,Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是,Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可” @@ -82,7 +75,7 @@ self.person.dog.weight = 50; // Person.m + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; - + if ([key isEqualToString:@"dog"]) { NSArray *affectingKeys = @[@"name", @"fur", @"weight"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; @@ -91,9 +84,6 @@ self.person.dog.weight = 50; } ``` - - - ## 二、 KVO 的本质 kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。 @@ -109,18 +99,39 @@ kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础 return NO; } ``` + 3. 若类有实例变量 NSString *_foo, 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key ? 都可以 4. KVC 的 keyPath 中的集合运算符如何使用 - - 必须用在 **集合对象** 或者 **普通对象的集合属性** 上 -简单的集合运算符有 @avg、@count、@max、@min、@sum 5. KVO 和 KVC 的 keyPath 一定是属性吗? -可以是成员变量 + 可以是成员变量 + +6. KVO 中 派生类的 setter 方法内部实现调用了 Foundation 框架中的 `_NSSetIntValueAndNotify`. + +7. 直接修改对象的成员变量会触发 KVO 吗? + + 不会。因为成员变量没有 setter. + + ``` + @interface Person: NSObject + { + @public: +     int age; + } + @end + ``` + +8. 手动触发 KVO?调用 willChangeValueForKey、didChangeValueForKey + +9. + + @@ -142,17 +153,13 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间 - 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。 - ![KVO原理图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018_11_12_KVO.png) - 为什么要选择是继承的子类而不是分类呢? 子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃 关于分类与子类的关系可以看看我之前的 [文章](1.50.md). - - ## 四、 模拟实现系统的 KVO 1. 创建被观察对象的子类 @@ -160,6 +167,7 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间 3. 外界改变 isa 指针(class方法重写) 我们用自己的类模拟系统的 KVO。 + ``` //NSObject+LBPKVO.h #import @@ -169,7 +177,7 @@ NS_ASSUME_NONNULL_BEGIN @interface NSObject (LBPKVO) - (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; - + @end NS_ASSUME_NONNULL_END @@ -189,15 +197,15 @@ NS_ASSUME_NONNULL_END Class myclass = objc_allocateClassPair(self.class, [currentClassName UTF8String], 0); // 生成后不能马上使用,必须先注册 objc_registerClassPair(myclass); - + //2. 重写 setter 方法 class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@"); //3. 修改 isa object_setClass(self, myclass); - + //4. 将观察者保存到当前对象里面 objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN); - + //5. 将传递的上下文绑定到当前对象里面 objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN); } @@ -212,7 +220,7 @@ void setName (id self, SEL _cmd, NSString *name) { object_setClass(self, class_getSuperclass(class)); //2. 调用父类的 setName 方法 objc_msgSend(self, @selector(setName:), name); - + //3. 调用观察 id observer = objc_getAssociatedObject(self, "observer"); id context = objc_getAssociatedObject(self, "context"); @@ -235,7 +243,7 @@ void setName (id self, SEL _cmd, NSString *name) { _person.hobbies = [@[@"iOS"] mutableCopy]; NSDictionary *context = @{@"name": @"成吉思汗", @"hobby" : @"弯弓射大雕"}; [_person lbpKVO_addObserver:self forKeyPath:@"hobbies" options:(NSKeyValueObservingOptionNew) context:(__bridge void * _Nullable)(context)]; - + } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ @@ -247,10 +255,8 @@ void setName (id self, SEL _cmd, NSString *name) { - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ NSLog(@"%@",change); } - ``` - KVO 的缺陷: KVO 虽然很强大,你只能重写 `-observeValueForKeyPath:ofObject:change:context: ` 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block 没门儿。感觉如果加入 block 就更棒了。 @@ -261,4 +267,54 @@ KVO 的改装: +## 五、 KVC + +`setValueForKey` 用来设置对象的一层属性值修改。 + +`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改 + + + +KVC 之后会触发 KVO。为什么?探究下 `setValueForKey` + +`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 _key、_isKey、key、isKey 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 NSUnknownKeyException + +整个流程如下 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/KVC-process.png) + +``` +@implementation Person +- (void)setAge:(int)age +{ + _age = age; +} + +- (void)_setAge:(int)age +{ + _age = age; +} + ++ (BOOL)accessInstanceVariableDirectlt +{ + return YES; +} + +@end +``` + +valueForKey 原理 + +- 按照 getKey、key、isKey、_key 的顺序寻找方法实现 + +- 找到则直接调用方法,返回值 + +- 如果没找到则调用 accessInstanceVariableDirectlt 方法,询问是否可以访问成员变量。为 NO 则抛出异常 + +- 为 YES 则按照 __key、isKey、key、isKey 的顺序访问成员变量。找到哪个则返回值 + +- 都没找到则抛出异常 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/KVC-get-process.png) + diff --git a/Chapter1 - iOS/1.48.md b/Chapter1 - iOS/1.48.md index 8522eb2..4e3a30f 100644 --- a/Chapter1 - iOS/1.48.md +++ b/Chapter1 - iOS/1.48.md @@ -1,15 +1,18 @@ -# 类别(Category)、拓展(Extension) +# 类别(Category)、拓展(Extension)、load、initialize > 很多人都知道类别、分类的用法,但是对于一些细节就不是很清楚了,本文主要梳理下这3个概念的细节 ## 类别(Category) ### 文件特征 - - 类别文件有2个,分别为 .h 和 .m - - 命名为: “类名+类别名.h”和“类名+类别名.m” + +- 类别文件有2个,分别为 .h 和 .m +- 命名为: “类名+类别名.h”和“类名+类别名.m” ### 文件内容格式 + .h 文件格式 + ``` #import "类名.h" @@ -19,6 +22,7 @@ ``` .m 文件格式 + ``` #import "类名+类别名.h" @@ -32,14 +36,15 @@ 拓展当前类,为类添加方法 ### 类别的局限性 + - 无法向现有的类添加实例变量(编译器报“instance variables may not be placed in categories”)。Category 一般只为类提供方法的拓展,不提供属性的拓展。但是利用 Runtime 可以在 Category 中添加属性 - 方法名称冲突的情况下,如果 Category 中的方法与当前类的方法名称重名,Category 具有更高的优先级,类别中的方法将完全取代现有类中的方法(调用方法的时候不会去调用现有类里面的方法实现)。 - 当现有类具有多个 Category 的时候,如果每个 Category 都有同名的方法,那么在调用方法的时候肯定不会调用现有类的方法实现。系统根据编译顺序决定调用哪个 Category 下的方法实现。(可以在 Targets -> Build phases -> Compile Sources 下给多个 Category 更换顺序看看到底在执行哪个方法) - ### Category 的使用和注意 + 1. Category 中的方法如果和现有类方法一致,工程中任何调用当前类的方法的时候都会去调用 Category 里面的方法(比如:UIViewCtroller、UITableView这些)的方法时要慎重。因为用Category重写类中的方法会对子类造成很大的影响。比如:用Category 重写了 UIViewCtroller 的方法 A,那么如果你在工程中用到的所有继承自 UIViewCtroller 的子类,去调用方法 A 时,执行的都是 Category 中重写的方法 A,如果不幸的是,你写的方法 A 有 Bug,那么会造成整个工程中调用该方法的所有 UIViewCtroller 子类的不正常。除非你在子类中重写了父类的方法 A,这样子类调用方法 A 时是调用的自己重写的方法 A,消除了父类 Category 中重写方法对自己的影响 2. Category拓展方法按照有没有重写当前类中的方法,分为未重写的拓展方法和重写拓展方法。且类引用自己的 Category 时,只能在 .m 文件中引用(.h 文件引用自己的类别会报错)。子类引用父类的 Category 在 .h 或 .m 都可以。如果类调用 Category 中重写的方法,不用引入 Category 头文件,系统会自动调用 Category 中的重写方法 @@ -50,20 +55,949 @@ 5. Category 的作用是向下有效的。即只会影响到该类的所有子类。比如 A 类和 B 类是继承自 Super 类的2个子类,当给 A 类添加一个 Category sayHello 方法,仅有A 类的子类才可以使用 sayHello 方法 -## 拓展(Extension) - -### 文件特征 -- 只存在一个文件 -- 命名方式:“类名_拓展名.h” -``` -#import "类名.h" +## Category 底层原理 -@interface 类名 () -// 在此添加私有成员变量、属性、声明方法 +### Category 是 category_t 结构体 + +Demo + +```objectivec +@interface Person : NSObject +@property (nonatomic, strong) NSString *name; +- (void)sayHi; +- (void)sleep; +@end +@implementation Person +- (void)sayHi{ + NSLog(@"Person sayHi"); +} +- (void)sleep{ + NSLog(@"人生无常,抓紧睡觉"); +} +@end + + +// Person+Study.h +@interface Person (Study) +@property (nonatomic, strong) NSString *no; +- (void)study; ++ (void)sleep; +@end + +#import "Person+Study.h" +@implementation Person (Study) +- (void)study{ + +} ++ (void)sleep{ + +} +- (void)setNo:(NSString *)no{ + +} +- (NSString *)no{ + return nil; +} +- (id)copyWithZone:(NSZone *)zone{ + return self; +} @end ``` +clang 转为 c++ 代码 + +查看到 Category 本质是一个结构体 + +```c +struct _category_t { + const char *name; + struct _class_t *cls; + const struct _method_list_t *instance_methods; + const struct _method_list_t *class_methods; + const struct _protocol_list_t *protocols; + const struct _prop_list_t *properties; +}; + +struct _class_t { + struct _class_t *isa; + struct _class_t *superclass; + void *cache; + void *vtable; + struct _class_ro_t *ro; +}; + + +static struct /*_method_list_t*/ { + unsigned int entsize; // sizeof(struct _objc_method) + unsigned int method_count; + struct _objc_method method_list[1]; +} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = { + sizeof(_objc_method), + 1, + {{(struct objc_selector *)"sleep", "v16@0:8", (void *)_C_Person_Study_sleep}} +}; + +static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCopying [] __attribute__ ((used, section ("__DATA,__objc_const"))) = +{ + "@24@0:8^{_NSZone=}16" +}; + +static struct /*_method_list_t*/ { + unsigned int entsize; // sizeof(struct _objc_method) + unsigned int method_count; + struct _objc_method method_list[1]; +} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = { + sizeof(_objc_method), + 1, + {{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}} +}; +``` + +可以看到 Person+Study 的Category 底层赋值代码如下,就是结构体对象的初始化。 + +```c +static struct _category_t _OBJC_$_CATEGORY_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = +{ + "Person", + 0, // &OBJC_CLASS_$_Person, + (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Study, + (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Study, + (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Study, + (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Study, +}; +``` + +`_OBJC_CATEGORY_INSTANCE_METHODS_Person__Study` 结构体存放的是对象方法信息,如下 + +```c +static struct /*_method_list_t*/ { + unsigned int entsize; // sizeof(struct _objc_method) + unsigned int method_count; + struct _objc_method method_list[4]; +} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = { + sizeof(_objc_method), + 4, + {{(struct objc_selector *)"study", "v16@0:8", (void *)_I_Person_Study_study}, + {(struct objc_selector *)"setNo:", "v24@0:8@16", (void *)_I_Person_Study_setNo_}, + {(struct objc_selector *)"no", "@16@0:8", (void *)_I_Person_Study_no}, + {(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", (void *)_I_Person_Study_copyWithZone_}} +}; +``` + +`_OBJC_CATEGORY_CLASS_METHODS_Person__Study` 结构体存放的是类方法信息,如下 + +```c +static struct /*_method_list_t*/ { + unsigned int entsize; // sizeof(struct _objc_method) + unsigned int method_count; + struct _objc_method method_list[1]; +} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = { + sizeof(_objc_method), + 1, + {{(struct objc_selector *)"sleep", "v16@0:8", (void *)_C_Person_Study_sleep}} +}; +``` + +`_OBJC_CATEGORY_PROTOCOLS*_Person__Study` 结构体存放的是遵循的协议信息,如下 + +```c +static struct /*_protocol_list_t*/ { + long protocol_count; // Note, this is 32/64 bit + struct _protocol_t *super_protocols[1]; +} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = { + 1, + &_OBJC_PROTOCOL_NSCopying +}; +``` + +`_OBJC_PROP_LIST_Person__Study` 存放的是 Category 中的属性信息,如下 + +```c +static struct /*_prop_list_t*/ { + unsigned int entsize; // sizeof(struct _prop_t) + unsigned int count_of_properties; + struct _prop_t prop_list[1]; +} _OBJC_$_PROP_LIST_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = { + sizeof(_prop_t), + 1, + {{"no","T@\"NSString\",&,N"}} +}; +``` + +查看 Objc 4 源代码,Category 定义如下 + +```c +struct category_t { + const char *name; + classref_t cls; + struct method_list_t *instanceMethods; + struct method_list_t *classMethods; + struct protocol_list_t *protocols; + struct property_list_t *instanceProperties; + // Fields below this point are not always present on disk. + struct property_list_t *_classProperties; + + method_list_t *methodsForMeta(bool isMeta) { + if (isMeta) return classMethods; + else return instanceMethods; + } + + property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); +}; +``` + +### category 中定义的方法,存储在哪? + +查看 objc4 的源代码 `objc-os.mm` 文件中的 `_objc_init` 方法 + +```c +void _objc_init(void) { + static bool initialized = false; + if (initialized) return; + initialized = true; + // fixme defer initialization until an objc-using image is found? + environ_init(); + tls_init(); + static_init(); + lock_init(); + exception_init(); + _dyld_objc_notify_register(&map_images, load_images, unmap_image); +} +``` + +`_objc_init` 内部会调用 `map_images` 方法,其内部如下 + +```c +void map_images(unsigned count, const char * const paths[], + const struct mach_header * const mhdrs[]) { + rwlock_writer_t lock(runtimeLock); + return map_images_nolock(count, paths, mhdrs); +} + +void map_images_nolock(unsigned mhCount, const char * const mhPaths[], + const struct mach_header * const mhdrs[]) { + static bool firstTime = YES; + header_info *hList[mhCount]; + uint32_t hCount; + size_t selrefCount = 0; + + // Perform first-time initialization if necessary. + // This function is called before ordinary library initializers. + // fixme defer initialization until an objc-using image is found? + if (firstTime) { + preopt_init(); + } + + if (PrintImages) { + _objc_inform("IMAGES: processing %u newly-mapped images...\n", mhCount); + } + + // Find all images with Objective-C metadata. + hCount = 0; + + // Count classes. Size various table based on the total. + int totalClasses = 0; + int unoptimizedTotalClasses = 0; + { + uint32_t i = mhCount; + while (i--) { + const headerType *mhdr = (const headerType *)mhdrs[i]; + + auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses); + if (!hi) { + // no objc data in this entry + continue; + } + + if (mhdr->filetype == MH_EXECUTE) { + // Size some data structures based on main executable's size +#if __OBJC2__ + size_t count; + _getObjc2SelectorRefs(hi, &count); + selrefCount += count; + _getObjc2MessageRefs(hi, &count); + selrefCount += count; +#else + _getObjcSelectorRefs(hi, &selrefCount); +#endif + +#if SUPPORT_GC_COMPAT + // Halt if this is a GC app. + if (shouldRejectGCApp(hi)) { + _objc_fatal_with_reason + (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, + OS_REASON_FLAG_CONSISTENT_FAILURE, + "Objective-C garbage collection " + "is no longer supported."); + } +#endif + } + + hList[hCount++] = hi; + + if (PrintImages) { + _objc_inform("IMAGES: loading image for %s%s%s%s%s\n", + hi->fname(), + mhdr->filetype == MH_BUNDLE ? " (bundle)" : "", + hi->info()->isReplacement() ? " (replacement)" : "", + hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "", + hi->info()->optimizedByDyld()?" (preoptimized)":""); + } + } + } + + // Perform one-time runtime initialization that must be deferred until + // the executable itself is found. This needs to be done before + // further initialization. + // (The executable may not be present in this infoList if the + // executable does not contain Objective-C code but Objective-C + // is dynamically loaded later. + if (firstTime) { + sel_init(selrefCount); + arr_init(); + +#if SUPPORT_GC_COMPAT + // Reject any GC images linked to the main executable. + // We already rejected the app itself above. + // Images loaded after launch will be rejected by dyld. + + for (uint32_t i = 0; i < hCount; i++) { + auto hi = hList[i]; + auto mh = hi->mhdr(); + if (mh->filetype != MH_EXECUTE && shouldRejectGCImage(mh)) { + _objc_fatal_with_reason + (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, + OS_REASON_FLAG_CONSISTENT_FAILURE, + "%s requires Objective-C garbage collection " + "which is no longer supported.", hi->fname()); + } + } +#endif + +#if TARGET_OS_OSX + // Disable +initialize fork safety if the app is too old (< 10.13). + // Disable +initialize fork safety if the app has a + // __DATA,__objc_fork_ok section. + + if (dyld_get_program_sdk_version() < DYLD_MACOSX_VERSION_10_13) { + DisableInitializeForkSafety = true; + if (PrintInitializing) { + _objc_inform("INITIALIZE: disabling +initialize fork " + "safety enforcement because the app is " + "too old (SDK version " SDK_FORMAT ")", + FORMAT_SDK(dyld_get_program_sdk_version())); + } + } + + for (uint32_t i = 0; i < hCount; i++) { + auto hi = hList[i]; + auto mh = hi->mhdr(); + if (mh->filetype != MH_EXECUTE) continue; + unsigned long size; + if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) { + DisableInitializeForkSafety = true; + if (PrintInitializing) { + _objc_inform("INITIALIZE: disabling +initialize fork " + "safety enforcement because the app has " + "a __DATA,__objc_fork_ok section"); + } + } + break; // assume only one MH_EXECUTE image + } +#endif + } + + if (hCount > 0) { + _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses); + } + firstTime = NO; +} +``` + +`map_images` 内部会调用 `map_images_nolock`, `map_images_nolock` 会调用 `_read_images` + +```c +void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) +{ + header_info *hi; + uint32_t hIndex; + size_t count; + size_t i; + Class *resolvedFutureClasses = nil; + size_t resolvedFutureClassCount = 0; + static bool doneOnce; + TimeLogger ts(PrintImageTimes); + + runtimeLock.assertWriting(); + +#define EACH_HEADER \ + hIndex = 0; \ + hIndex < hCount && (hi = hList[hIndex]); \ + hIndex++ + + if (!doneOnce) { + doneOnce = YES; + +#if SUPPORT_NONPOINTER_ISA + // Disable non-pointer isa under some conditions. + +# if SUPPORT_INDEXED_ISA + // Disable nonpointer isa if any image contains old Swift code + for (EACH_HEADER) { + if (hi->info()->containsSwift() && + hi->info()->swiftVersion() < objc_image_info::SwiftVersion3) + { + DisableNonpointerIsa = true; + if (PrintRawIsa) { + _objc_inform("RAW ISA: disabling non-pointer isa because " + "the app or a framework contains Swift code " + "older than Swift 3.0"); + } + break; + } + } +# endif + +# if TARGET_OS_OSX + // Disable non-pointer isa if the app is too old + // (linked before OS X 10.11) + if (dyld_get_program_sdk_version() < DYLD_MACOSX_VERSION_10_11) { + DisableNonpointerIsa = true; + if (PrintRawIsa) { + _objc_inform("RAW ISA: disabling non-pointer isa because " + "the app is too old (SDK version " SDK_FORMAT ")", + FORMAT_SDK(dyld_get_program_sdk_version())); + } + } + + // Disable non-pointer isa if the app has a __DATA,__objc_rawisa section + // New apps that load old extensions may need this. + for (EACH_HEADER) { + if (hi->mhdr()->filetype != MH_EXECUTE) continue; + unsigned long size; + if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) { + DisableNonpointerIsa = true; + if (PrintRawIsa) { + _objc_inform("RAW ISA: disabling non-pointer isa because " + "the app has a __DATA,__objc_rawisa section"); + } + } + break; // assume only one MH_EXECUTE image + } +# endif + +#endif + + if (DisableTaggedPointers) { + disableTaggedPointers(); + } + + if (PrintConnecting) { + _objc_inform("CLASS: found %d classes during launch", totalClasses); + } + + // namedClasses + // Preoptimized classes don't go in this table. + // 4/3 is NXMapTable's load factor + int namedClassesSize = + (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3; + gdb_objc_realized_classes = + NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize); + + ts.log("IMAGE TIMES: first time tasks"); + } + + + // Discover classes. Fix up unresolved future classes. Mark bundle classes. + for (EACH_HEADER) { + if (! mustReadClasses(hi)) { + // Image is sufficiently optimized that we need not call readClass() + continue; + } + + bool headerIsBundle = hi->isBundle(); + bool headerIsPreoptimized = hi->isPreoptimized(); + + classref_t *classlist = _getObjc2ClassList(hi, &count); + for (i = 0; i < count; i++) { + Class cls = (Class)classlist[i]; + Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized); + + if (newCls != cls && newCls) { + // Class was moved but not deleted. Currently this occurs + // only when the new class resolved a future class. + // Non-lazily realize the class below. + resolvedFutureClasses = (Class *) + realloc(resolvedFutureClasses, + (resolvedFutureClassCount+1) * sizeof(Class)); + resolvedFutureClasses[resolvedFutureClassCount++] = newCls; + } + } + } + + ts.log("IMAGE TIMES: discover classes"); + + // Fix up remapped classes + // Class list and nonlazy class list remain unremapped. + // Class refs and super refs are remapped for message dispatching. + + if (!noClassesRemapped()) { + for (EACH_HEADER) { + Class *classrefs = _getObjc2ClassRefs(hi, &count); + for (i = 0; i < count; i++) { + remapClassRef(&classrefs[i]); + } + // fixme why doesn't test future1 catch the absence of this? + classrefs = _getObjc2SuperRefs(hi, &count); + for (i = 0; i < count; i++) { + remapClassRef(&classrefs[i]); + } + } + } + + ts.log("IMAGE TIMES: remap classes"); + + // Fix up @selector references + static size_t UnfixedSelectors; + sel_lock(); + for (EACH_HEADER) { + if (hi->isPreoptimized()) continue; + + bool isBundle = hi->isBundle(); + SEL *sels = _getObjc2SelectorRefs(hi, &count); + UnfixedSelectors += count; + for (i = 0; i < count; i++) { + const char *name = sel_cname(sels[i]); + sels[i] = sel_registerNameNoLock(name, isBundle); + } + } + sel_unlock(); + + ts.log("IMAGE TIMES: fix up selector references"); + +#if SUPPORT_FIXUP + // Fix up old objc_msgSend_fixup call sites + for (EACH_HEADER) { + message_ref_t *refs = _getObjc2MessageRefs(hi, &count); + if (count == 0) continue; + + if (PrintVtables) { + _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch " + "call sites in %s", count, hi->fname()); + } + for (i = 0; i < count; i++) { + fixupMessageRef(refs+i); + } + } + + ts.log("IMAGE TIMES: fix up objc_msgSend_fixup"); +#endif + + // Discover protocols. Fix up protocol refs. + for (EACH_HEADER) { + extern objc_class OBJC_CLASS_$_Protocol; + Class cls = (Class)&OBJC_CLASS_$_Protocol; + assert(cls); + NXMapTable *protocol_map = protocols(); + bool isPreoptimized = hi->isPreoptimized(); + bool isBundle = hi->isBundle(); + + protocol_t **protolist = _getObjc2ProtocolList(hi, &count); + for (i = 0; i < count; i++) { + readProtocol(protolist[i], cls, protocol_map, + isPreoptimized, isBundle); + } + } + + ts.log("IMAGE TIMES: discover protocols"); + + // Fix up @protocol references + // Preoptimized images may have the right + // answer already but we don't know for sure. + for (EACH_HEADER) { + protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count); + for (i = 0; i < count; i++) { + remapProtocolRef(&protolist[i]); + } + } + + ts.log("IMAGE TIMES: fix up @protocol references"); + + // Realize non-lazy classes (for +load methods and static instances) + for (EACH_HEADER) { + classref_t *classlist = + _getObjc2NonlazyClassList(hi, &count); + for (i = 0; i < count; i++) { + Class cls = remapClass(classlist[i]); + if (!cls) continue; + + // hack for class __ARCLite__, which didn't get this above +#if TARGET_OS_SIMULATOR + if (cls->cache._buckets == (void*)&_objc_empty_cache && + (cls->cache._mask || cls->cache._occupied)) + { + cls->cache._mask = 0; + cls->cache._occupied = 0; + } + if (cls->ISA()->cache._buckets == (void*)&_objc_empty_cache && + (cls->ISA()->cache._mask || cls->ISA()->cache._occupied)) + { + cls->ISA()->cache._mask = 0; + cls->ISA()->cache._occupied = 0; + } +#endif + + realizeClass(cls); + } + } + + ts.log("IMAGE TIMES: realize non-lazy classes"); + + // Realize newly-resolved future classes, in case CF manipulates them + if (resolvedFutureClasses) { + for (i = 0; i < resolvedFutureClassCount; i++) { + realizeClass(resolvedFutureClasses[i]); + resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/); + } + free(resolvedFutureClasses); + } + + ts.log("IMAGE TIMES: realize future classes"); + + // Discover categories. + for (EACH_HEADER) { + category_t **catlist = + _getObjc2CategoryList(hi, &count); + bool hasClassProperties = hi->info()->hasCategoryClassProperties(); + + for (i = 0; i < count; i++) { + category_t *cat = catlist[i]; + Class cls = remapClass(cat->cls); + + if (!cls) { + // Category's target class is missing (probably weak-linked). + // Disavow any knowledge of this category. + catlist[i] = nil; + if (PrintConnecting) { + _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with " + "missing weak-linked target class", + cat->name, cat); + } + continue; + } + + // Process this category. + // First, register the category with its target class. + // Then, rebuild the class's method lists (etc) if + // the class is realized. + bool classExists = NO; + if (cat->instanceMethods || cat->protocols + || cat->instanceProperties) + { + addUnattachedCategoryForClass(cat, cls, hi); + if (cls->isRealized()) { + remethodizeClass(cls); + classExists = YES; + } + if (PrintConnecting) { + _objc_inform("CLASS: found category -%s(%s) %s", + cls->nameForLogging(), cat->name, + classExists ? "on existing class" : ""); + } + } + + if (cat->classMethods || cat->protocols + || (hasClassProperties && cat->_classProperties)) + { + addUnattachedCategoryForClass(cat, cls->ISA(), hi); + if (cls->ISA()->isRealized()) { + remethodizeClass(cls->ISA()); + } + if (PrintConnecting) { + _objc_inform("CLASS: found category +%s(%s)", + cls->nameForLogging(), cat->name); + } + } + } + } + + ts.log("IMAGE TIMES: discover categories"); + + // Category discovery MUST BE LAST to avoid potential races + // when other threads call the new category code before + // this thread finishes its fixups. + + // +load handled by prepare_load_methods() + + if (DebugNonFragileIvars) { + realizeAllClasses(); + } + + + // Print preoptimization statistics + if (PrintPreopt) { + static unsigned int PreoptTotalMethodLists; + static unsigned int PreoptOptimizedMethodLists; + static unsigned int PreoptTotalClasses; + static unsigned int PreoptOptimizedClasses; + + for (EACH_HEADER) { + if (hi->isPreoptimized()) { + _objc_inform("PREOPTIMIZATION: honoring preoptimized selectors " + "in %s", hi->fname()); + } + else if (hi->info()->optimizedByDyld()) { + _objc_inform("PREOPTIMIZATION: IGNORING preoptimized selectors " + "in %s", hi->fname()); + } + + classref_t *classlist = _getObjc2ClassList(hi, &count); + for (i = 0; i < count; i++) { + Class cls = remapClass(classlist[i]); + if (!cls) continue; + + PreoptTotalClasses++; + if (hi->isPreoptimized()) { + PreoptOptimizedClasses++; + } + + const method_list_t *mlist; + if ((mlist = ((class_ro_t *)cls->data())->baseMethods())) { + PreoptTotalMethodLists++; + if (mlist->isFixedUp()) { + PreoptOptimizedMethodLists++; + } + } + if ((mlist=((class_ro_t *)cls->ISA()->data())->baseMethods())) { + PreoptTotalMethodLists++; + if (mlist->isFixedUp()) { + PreoptOptimizedMethodLists++; + } + } + } + } + + _objc_inform("PREOPTIMIZATION: %zu selector references not " + "pre-optimized", UnfixedSelectors); + _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) method lists pre-sorted", + PreoptOptimizedMethodLists, PreoptTotalMethodLists, + PreoptTotalMethodLists + ? 100.0*PreoptOptimizedMethodLists/PreoptTotalMethodLists + : 0.0); + _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) classes pre-registered", + PreoptOptimizedClasses, PreoptTotalClasses, + PreoptTotalClasses + ? 100.0*PreoptOptimizedClasses/PreoptTotalClasses + : 0.0); + _objc_inform("PREOPTIMIZATION: %zu protocol references not " + "pre-optimized", UnfixedProtocolReferences); + } + +#undef EACH_HEADER +} +``` + +可以看到内部有 `Discover categories` 相关逻辑,里面和 category 方法相关的有 `remethodizeClass`,其实现如下 + +```c +static void remethodizeClass(Class cls){ + category_list *cats; + bool isMeta; + runtimeLock.assertWriting(); + isMeta = cls->isMetaClass(); + // Re-methodizing: check for more categories + if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { + if (PrintConnecting) { + _objc_inform("CLASS: attaching categories to class '%s' %s", + cls->nameForLogging(), isMeta ? "(meta)" : ""); + } + attachCategories(cls, cats, true /*flush caches*/); + free(cats); + } +} +``` + +可以看到内部调用 `attachCategories` 方法。 `attachCategories` 方法传入 3个参数,第一个是类对象(Person),第二个参数是 Category 数组。内部实现如下 + +```c +static void attachCategories(Class cls, category_list *cats, bool flush_caches){ + if (!cats) return; + if (PrintReplacedMethods) printReplacements(cls, cats); + + bool isMeta = cls->isMetaClass(); + // fixme rearrange to remove these intermediate allocations + method_list_t **mlists = (method_list_t **) + malloc(cats->count * sizeof(*mlists)); + property_list_t **proplists = (property_list_t **) + malloc(cats->count * sizeof(*proplists)); + protocol_list_t **protolists = (protocol_list_t **) + malloc(cats->count * sizeof(*protolists)); + + // Count backwards through cats to get newest categories first + int mcount = 0; + int propcount = 0; + int protocount = 0; + int i = cats->count; + bool fromBundle = NO; + while (i--) { + auto& entry = cats->list[i]; + + method_list_t *mlist = entry.cat->methodsForMeta(isMeta); + if (mlist) { + mlists[mcount++] = mlist; + fromBundle |= entry.hi->isBundle(); + } + + property_list_t *proplist = + entry.cat->propertiesForMeta(isMeta, entry.hi); + if (proplist) { + proplists[propcount++] = proplist; + } + + protocol_list_t *protolist = entry.cat->protocols; + if (protolist) { + protolists[protocount++] = protolist; + } + } + + auto rw = cls->data(); + prepareMethodLists(cls, mlists, mcount, NO, fromBundle); + rw->methods.attachLists(mlists, mcount); + free(mlists); + if (flush_caches && mcount > 0) flushCaches(cls); + + rw->properties.attachLists(proplists, propcount); + free(proplists); + + rw->protocols.attachLists(protolists, protocount); + free(protolists); +} +``` + +可以看到通过传入的类对象 `cls` 调用其 `cls->data()` 方法,找到对应的类 `class_rw_t` 信息,里面存放方法、属性、协议信息。 + +```c +// 类对象结构体 +struct objc_class : objc_object { + // Class ISA; + Class superclass; + }cache_t cache; // formerly cache pointer and vtable + class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags + + class_rw_t *data() { + return bits.data(); + } +} + +struct class_rw_t { + uint32_t flags; + uint32_t version; + const class_ro_t *ro; + method_array_t methods; + property_array_t properties; + protocol_array_t protocols; + + class_rw_t* data() { + return (class_rw_t *)(bits & FAST_DATA_MASK); + } +} +``` + +可以看到最后调用 `attachLists`,内部实现如下 + +```c +void attachLists(List* const * addedLists, uint32_t addedCount) { + if (addedCount == 0) return; + + if (hasArray()) { + // many lists -> many lists + uint32_t oldCount = array()->count; + uint32_t newCount = oldCount + addedCount; + setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); + array()->count = newCount; + memmove(array()->lists + addedCount, array()->lists, + oldCount * sizeof(array()->lists[0])); + memcpy(array()->lists, addedLists, + addedCount * sizeof(array()->lists[0])); + } + else if (!list && addedCount == 1) { + // 0 lists -> 1 list + list = addedLists[0]; + } + else { + // 1 list -> many lists + List* oldList = list; + uint32_t oldCount = oldList ? 1 : 0; + uint32_t newCount = oldCount + addedCount; + setArray((array_t *)malloc(array_t::byteSize(newCount))); + array()->count = newCount; + if (oldList) array()->lists[addedCount] = oldList; + memcpy(array()->lists, addedLists, + addedCount * sizeof(array()->lists[0])); + } +} +``` + +其中关键函数 `memmove` 代表将 __src 中的前 __len 个字节长度移动到 __dst 中去。 + +```c +memmove(array()->lists + addedCount, array()->lists, + oldCount * sizeof(array()->lists[0])); +memcpy(array()->lists, addedLists, + addedCount * sizeof(array()->lists[0])); +``` + +其中,`array()->lists` 代表类对象原来的方法列表、`oldCount * sizeof(array()->lists[0])` 代表类对象原来方法列表长度,`addedCount` 代表 category 方法列表长度。 + +c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。 + +`memmove` 效果为将类原方法列表移动到第 n个(n为 category 方法列表长度位置,前面空出n个坑位。 + +`memcopy` 效果将 category 方法列表拷贝到类原方法列表的前面去。位置刚好是 `memmove` 留出的坑位。 + +过程如下 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-categoryattachLists.png) + +QA: + +分类中可以写属性吗? + +不可以,查看分类的 category_t 结构体可以看到没有 `**const** ivar_list_t * ivars;` ,所以 category 声明属性底层只会生成 setter、getter 方法声明,没有实现。需要程序员利用 runtime 关联属性自己实现 + +同理,分类中也不可以添加成员变量,下面代码会报错。 + +``` +@interface Person (Study) +{ + int _age; +} +@end +``` + +总结: + +Category 编译之后 底层结构为 struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息 + +程序运行的时候,runtime 会将 Category 中的数据,合并到类自身信息中(类对象、元类对象) + +## 拓展(Extension) + +### 文件特征 + +- 只存在一个文件 + +- 命名方式:“类名_拓展名.h” + + ``` + #import "类名.h" + @interface 类名 () + // 在此添加私有成员变量、属性、声明方法 + @end + ``` + ### 拓展的作用 + 1. 为类增加额外的属性、成员变量、方法声明 2. 一般将类拓展直接写到当前类的 .m 文件中。不单独创建 @@ -72,41 +1006,44 @@ 4. 和 Category 类似,但是小括号里面没有拓展的名字 +5. 拓展里面的属性和方法,会在编译阶段将相关数据和类本身合并 ### 拓展的局限性 + 1. Extension 中添加的属性、成员变量、方法属于私有(只可以在本类的 .m 文件中访问、调用。在其他类里面是无法访问的,同时子类也是无法继承的)。假如我们有这样一个需求,一个属性对外是只读的,对内是可以读写的,那么我们可以通过 Extension 实现。 2. 通常 Extension 都写在 .m 文件中,不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方,否则编译报错 3. 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话,也需要将类实现文件中包含进类扩展文件,否则会找不到属性的 setter 和 getter 方法。 - -``` -//Web.h -#import "Person.h" -NS_ASSUME_NONNULL_BEGIN -@interface Web : Person -@end -NS_ASSUME_NONNULL_END - -//Web.m -#import "Web.h" -#import "Web+H5.h" -@interface Web () -@property (nonatomic, strong) NSString *skillStacks; -@end -@implementation Web -- (void)test { - self.skills = @"iOS && Web && Node && Hybrid"; - self.skillStacks = @"iOS && Web && Node && Hybrid"; -} -- (void)show { - NSLog(@"%@",self.skillStacks); -} -@end -``` - + + ```objectivec + //Web.h + #import "Person.h" + NS_ASSUME_NONNULL_BEGIN + @interface Web : Person + @end + NS_ASSUME_NONNULL_END + + //Web.m + #import "Web.h" + #import "Web+H5.h" + @interface Web () + @property (nonatomic, strong) NSString *skillStacks; + @end + @implementation Web + + - (void)test { + self.skills = @"iOS && Web && Node && Hybrid"; + self.skillStacks = @"iOS && Web && Node && Hybrid"; + } + - (void)show { + NSLog(@"%@",self.skillStacks); + } + @end + ``` ## 总结 + 1. Category 只能拓充方法,不能拓展属性和成员变量(包含成员变量会报错。属性虽然不可以直接拓展,利用 Runtime 可以实现) 2. 如果 Category 中声明了1个属性,那么 Category 只会生成 setter 和 getter 的声明,不会有实现 @@ -115,9 +1052,177 @@ NS_ASSUME_NONNULL_END 4. 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。 +关于第4点,我们可以查看源代码印证下。去 opensource 下载 objc4 -##「小插曲」:为 Category 实现属性的 Setter 和 Getter +OC 入口函数`_objc_init` + +```objectivec +void _objc_init(void) +{ +    // ... + _dyld_objc_notify_register(&map_images, load_images, unmap_image); +} ``` + +之后注册各种镜像,那么 map_images 哪里来的? + +```objectivec +void +map_images_nolock(unsigned mhCount, const char * const mhPaths[], + const struct mach_header * const mhdrs[]) +{ + // ... + if (hCount > 0) { + _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses); + } + firstTime = NO; +} +``` + +_read_images 方法内部会调用 remethodizeClass + +```objectivec +void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) +{ +// ... +    if (cls->isRealized()) { +        remethodizeClass(cls); +// ... +} +``` + +remethodizeClass 内部会调用 attachCategories + +```objectivec +static void remethodizeClass(Class cls) +{ + category_list *cats; + bool isMeta; + + runtimeLock.assertWriting(); + + isMeta = cls->isMetaClass(); + + // Re-methodizing: check for more categories + if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { + if (PrintConnecting) { + _objc_inform("CLASS: attaching categories to class '%s' %s", + cls->nameForLogging(), isMeta ? "(meta)" : ""); + } + + attachCategories(cls, cats, true /*flush caches*/); + free(cats); + } +} +``` + +attachCategories 会调用 attachLists + +```objectivec +static void +attachCategories(Class cls, category_list *cats, bool flush_caches) +{ + if (!cats) return; + if (PrintReplacedMethods) printReplacements(cls, cats); + + bool isMeta = cls->isMetaClass(); + + // fixme rearrange to remove these intermediate allocations + method_list_t **mlists = (method_list_t **) + malloc(cats->count * sizeof(*mlists)); + property_list_t **proplists = (property_list_t **) + malloc(cats->count * sizeof(*proplists)); + protocol_list_t **protolists = (protocol_list_t **) + malloc(cats->count * sizeof(*protolists)); + + // Count backwards through cats to get newest categories first + int mcount = 0; + int propcount = 0; + int protocount = 0; + int i = cats->count; + bool fromBundle = NO; + while (i--) { + auto& entry = cats->list[i]; + + method_list_t *mlist = entry.cat->methodsForMeta(isMeta); + if (mlist) { + mlists[mcount++] = mlist; + fromBundle |= entry.hi->isBundle(); + } + + property_list_t *proplist = + entry.cat->propertiesForMeta(isMeta, entry.hi); + if (proplist) { + proplists[propcount++] = proplist; + } + + protocol_list_t *protolist = entry.cat->protocols; + if (protolist) { + protolists[protocount++] = protolist; + } + } + + auto rw = cls->data(); + + prepareMethodLists(cls, mlists, mcount, NO, fromBundle); + rw->methods.attachLists(mlists, mcount); + free(mlists); + if (flush_caches && mcount > 0) flushCaches(cls); + + rw->properties.attachLists(proplists, propcount); + free(proplists); + + rw->protocols.attachLists(protolists, protocount); + free(protolists); +} +``` + +attachLists 内部会调用 realloc、memmove、memmcpy + +```objectivec +void attachLists(List* const * addedLists, uint32_t addedCount) { + if (addedCount == 0) return; + + if (hasArray()) { + // many lists -> many lists + uint32_t oldCount = array()->count; + uint32_t newCount = oldCount + addedCount; + setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); + array()->count = newCount; + memmove(array()->lists + addedCount, array()->lists, + oldCount * sizeof(array()->lists[0])); + memcpy(array()->lists, addedLists, + addedCount * sizeof(array()->lists[0])); + } + else if (!list && addedCount == 1) { + // 0 lists -> 1 list + list = addedLists[0]; + } + else { + // 1 list -> many lists + List* oldList = list; + uint32_t oldCount = oldList ? 1 : 0; + uint32_t newCount = oldCount + addedCount; + setArray((array_t *)malloc(array_t::byteSize(newCount))); + array()->count = newCount; + if (oldList) array()->lists[addedCount] = oldList; + memcpy(array()->lists, addedLists, + addedCount * sizeof(array()->lists[0])); + } + } +``` + +最后会把类对象、元类对象、分类二元数组整体处理,结果为最后编译的分类在整合后数组的最前面,也就是为什么说分类和原类存在同名方法时,会被覆盖,且最后编译的分类的方法实现是会被调用的原因。 + +- 通过 Runtime 加载某个类所有的 Category + +- 所有的 Category 方法、属性、协议数据,合并到一个大数组中,后面参与编译的 Category 数据,会放在数组前面 + +- 合并后的 Category 数据(属性、方法、协议)插入到类原来数据的前面(比如class_rw_t->methods) + +## 小插曲:为 Category 实现属性的 Setter 和 Getter + +```objectivec #import "Person.h" NS_ASSUME_NONNULL_BEGIN @@ -151,7 +1256,7 @@ NS_ASSUME_NONNULL_END [_studyNumber release]; [studyNumber retain]; _studyNumber = studyNumber; - + 但是在 Category里面不会生成对应的实例变量,因此我们可以利用 Runtime 为我们的 category 关联属性的值 setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN); getter:objc_getAssociatedObject(self, @selector(firstView)); @@ -169,6 +1274,7 @@ NS_ASSUME_NONNULL_END @end ``` + 说明: `objc_setAssociatedObject` 的第二个参数是`const void * _Nonnull key` 所以可以用 "studyNumber" 或者利用 `@selector()` 的特性返回的数据类型也满足,所以示例代码选用第二种方式 给分类添加属性的时候,为了避免多人开发对于属性添加造成的覆盖,我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程,那么我们可以为属性命名的时候在前面添加当前模块的前缀。 @@ -205,11 +1311,559 @@ NS_ASSUME_NONNULL_END } @end - ``` - +## 底层窥探 load 方法 +Demo 验证。 +```objectivec +@interface Person : NSObject +@end +@interface Student : Person +@end +@interface Student (Good) +@end + +@interface Student (Bad) +@end +// 其中每个类都存在3个方法 ++ (void)load{ + NSLog(@"%s", __func__); +} ++ (void)initialize{ + NSLog(@"%s", __func__); +} +- (void)test{ + NSLog(@"%s", __func__); +} +// Test +Student *st = [[Student alloc] init]; + +2022-04-16 01:35:22.237692+0800 Main[8752:2908124] +[Person load] +2022-04-16 01:35:22.238305+0800 Main[8752:2908124] +[Student load] +2022-04-16 01:35:22.238450+0800 Main[8752:2908124] +[Student(Good) load] +2022-04-16 01:35:22.238562+0800 Main[8752:2908124] +[Student(Bad) load] +2022-04-16 01:35:22.238664+0800 Main[8752:2908124] +[Person initialize] +2022-04-16 01:35:22.238733+0800 Main[8752:2908124] +[Student(Bad) initialize] +2022-04-16 01:35:22.238794+0800 Main[8752:2908124] -[Student(Bad) test] +``` + +QA: + +- 为什么 load 方法打印顺序是这样的? + + 因为调用 student alloc,相当于发送了消息。则肯定先执行 load 方法。类在 Runtime 启动阶段会调用 `schedule_class_load` 方法。方法内部递归调用,如果当前类存在父类则递归调用,否则将当前类加载到 loadable_classes 最后面。load 方法在本质上是执行 `call_load_methods`,方法地址是确定的。不走 objc_msgSend 这套流程。所以先打印父类 load、再打印子类 load、最后打印分类 load。如果存在多个分类,则按照编译顺序打印 load。 + +- 为什么 load 方法不是按照 Category 编译顺序倒序调用 load 方法? + + 看源代码 Objc4 + + ```c + void _objc_init(void){ + static bool initialized = false; + if (initialized) return; + initialized = true; + + // fixme defer initialization until an objc-using image is found? + environ_init(); + tls_init(); + static_init(); + lock_init(); + exception_init(); + + _dyld_objc_notify_register(&map_images, load_images, unmap_image); + } + + void load_images(const char *path __unused, const struct mach_header *mh){ + // Return without taking locks if there are no +load methods here. + if (!hasLoadMethods((const headerType *)mh)) return; + recursive_mutex_locker_t lock(loadMethodLock); + // Discover load methods + { + rwlock_writer_t lock2(runtimeLock); + prepare_load_methods((const headerType *)mh); + } + // Call +load methods (without runtimeLock - re-entrant) + call_load_methods(); + } + + void call_load_methods(void){ + static bool loading = NO; + bool more_categories; + loadMethodLock.assertLocked(); + // Re-entrant calls do nothing; the outermost call will finish the job. + if (loading) return; + loading = YES; + + void *pool = objc_autoreleasePoolPush(); + + do { + // 1. Repeatedly call class +loads until there aren't any more + while (loadable_classes_used > 0) { + call_class_loads(); // 先调用类的 load 方法 + } + + // 2. Call category +loads ONCE + more_categories = call_category_loads(); // 再调用 category 的 load 方法 + + // 3. Run more +loads if there are classes OR more untried categories + } while (loadable_classes_used > 0 || more_categories); + + objc_autoreleasePoolPop(pool); + + loading = NO; + } + + static void call_class_loads(void) { + int i; + // Detach current loadable list. + struct loadable_class *classes = loadable_classes; + int used = loadable_classes_used; + loadable_classes = nil; + loadable_classes_allocated = 0; + loadable_classes_used = 0; + + // Call all +loads for the detached list. + for (i = 0; i < used; i++) { + Class cls = classes[i].cls; + load_method_t load_method = (load_method_t)classes[i].method; + if (!cls) continue; + + if (PrintLoading) { + _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging()); + } + (*load_method)(cls, SEL_load); + } + + // Destroy the detached list. + if (classes) free(classes); + } + + static bool call_category_loads(void) { + int i, shift; + bool new_categories_added = NO; + // Detach current loadable list. + struct loadable_category *cats = loadable_categories; + int used = loadable_categories_used; + int allocated = loadable_categories_allocated; + loadable_categories = nil; + loadable_categories_allocated = 0; + loadable_categories_used = 0; + + // Call all +loads for the detached list. + for (i = 0; i < used; i++) { + Category cat = cats[i].cat; + load_method_t load_method = (load_method_t)cats[i].method; + Class cls; + if (!cat) continue; + + cls = _category_getClass(cat); + if (cls && cls->isLoadable()) { + if (PrintLoading) { + _objc_inform("LOAD: +[%s(%s) load]\n", + cls->nameForLogging(), + _category_getName(cat)); + } + (*load_method)(cls, SEL_load); + cats[i].cat = nil; + } + } + + // Compact detached list (order-preserving) + shift = 0; + for (i = 0; i < used; i++) { + if (cats[i].cat) { + cats[i-shift] = cats[i]; + } else { + shift++; + } + } + used -= shift; + + // Copy any new +load candidates from the new list to the detached list. + new_categories_added = (loadable_categories_used > 0); + for (i = 0; i < loadable_categories_used; i++) { + if (used == allocated) { + allocated = allocated*2 + 16; + cats = (struct loadable_category *) + realloc(cats, allocated * + sizeof(struct loadable_category)); + } + cats[used++] = loadable_categories[i]; + } + + // Destroy the new list. + if (loadable_categories) free(loadable_categories); + + // Reattach the (now augmented) detached list. + // But if there's nothing left to load, destroy the list. + if (used) { + loadable_categories = cats; + loadable_categories_used = used; + loadable_categories_allocated = allocated; + } else { + if (cats) free(cats); + loadable_categories = nil; + loadable_categories_used = 0; + loadable_categories_allocated = 0; + } + + if (PrintLoading) { + if (loadable_categories_used != 0) { + _objc_inform("LOAD: %d categories still waiting for +load\n", + loadable_categories_used); + } + } + + return new_categories_added; + } + ``` + + 会发现源码中先调用类的 load 方法,再调用 category 的 load 方法。 + + 再看看 `call_class_loads`、`call_category_loads` 方法内部实现,是直接找到 `load_method_t load_method = (load_method_t)classes[i].method;` 类对象的 load 方法地址。最后直接调用 `(*load_method)(cls, SEL_load);` 方法本身。 + + test 方法是走消息发送流程 `objc_msgSend()` 所以会走 isa、superclass 这一套流程,test 是对象方法,所以需要根据 isa 找到类对象,从类对象的对象方法列表找到 test 方法,找不到则根据 superclass 找到当前类对象的父类对象,继续查找方法列表。直到 NSObject、nil 对象为止,然后走消息起死回生的阶段。 + + 看2个结构体 + + ```c + struct loadable_class { + Class cls; // may be nil + IMP method; // 指向类的 load 方法 + }; + + struct loadable_category { + Category cat; // may be nil + IMP method; // 指向分类的 load 方法 + }; + ``` + +类、分类的 load 方法调用顺序? + +1. 调用类的 +load 方法顺序 + + - 调用类的 +load + + - 根据编译先后顺序调用 +load(先编译先调用) + + - 存在继承关系的类,会先调用父类的 +load + +2. 调用分类的 +load 方法顺序 + + - 按照编译顺序调用分类的 +load(先编译先调用) + +源代码印证 + +```objectivec +void load_images(const char *path __unused, const struct mach_header *mh) { + // Return without taking locks if there are no +load methods here. + if (!hasLoadMethods((const headerType *)mh)) return; + + recursive_mutex_locker_t lock(loadMethodLock); + + // Discover load methods + { + rwlock_writer_t lock2(runtimeLock); + prepare_load_methods((const headerType *)mh); + } + + // Call +load methods (without runtimeLock - re-entrant) + call_load_methods(); +} + +void prepare_load_methods(const headerType *mhdr){ + size_t count, i; + + runtimeLock.assertWriting(); + + classref_t *classlist = + _getObjc2NonlazyClassList(mhdr, &count); + for (i = 0; i < count; i++) { + schedule_class_load(remapClass(classlist[i])); + } + + category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count); + for (i = 0; i < count; i++) { + category_t *cat = categorylist[i]; + Class cls = remapClass(cat->cls); + if (!cls) continue; // category for ignored weak-linked class + realizeClass(cls); + assert(cls->ISA()->isRealized()); + add_category_to_loadable_list(cat); + } +} +``` + +我们看看 `schedule_class_load` 方法。方法内部递归调用,如果当前类存在父类则递归调用,否则将当前类加载到 loadable_classes 最后面 + +```objectivec +static void schedule_class_load(Class cls) +{ + if (!cls) return; + assert(cls->isRealized()); // _read_images should realize + + if (cls->data()->flags & RW_LOADED) return; + + // Ensure superclass-first ordering + schedule_class_load(cls->superclass); + + add_class_to_loadable_list(cls); + cls->setInfo(RW_LOADED); +} +``` + +```objectivec +void add_class_to_loadable_list(Class cls) +{ + IMP method; + + loadMethodLock.assertLocked(); + + method = cls->getLoadMethod(); + if (!method) return; // Don't bother if cls has no +load method + + if (PrintLoading) { + _objc_inform("LOAD: class '%s' scheduled for +load", + cls->nameForLogging()); + } + + if (loadable_classes_used == loadable_classes_allocated) { + loadable_classes_allocated = loadable_classes_allocated*2 + 16; + loadable_classes = (struct loadable_class *) + realloc(loadable_classes, + loadable_classes_allocated * + sizeof(struct loadable_class)); + } + + loadable_classes[loadable_classes_used].cls = cls; + loadable_classes[loadable_classes_used].method = method; // 加载到最后 + loadable_classes_used++; +} +``` + +`prepare_load_methods` 处理完再执行 `call_load_methods` + +```objectivec +void call_load_methods(void) +{ + static bool loading = NO; + bool more_categories; + + loadMethodLock.assertLocked(); + + // Re-entrant calls do nothing; the outermost call will finish the job. + if (loading) return; + loading = YES; + + void *pool = objc_autoreleasePoolPush(); + + do { + // 1. Repeatedly call class +loads until there aren't any more + while (loadable_classes_used > 0) { + call_class_loads(); + } + + // 2. Call category +loads ONCE + more_categories = call_category_loads(); + + // 3. Run more +loads if there are classes OR more untried categories + } while (loadable_classes_used > 0 || more_categories); + + objc_autoreleasePoolPop(pool); + + loading = NO; +} +这里 +``` + +这里的代码已经看过了,也就先加载类的 +load 方法,加载顺序按照 loadable_classes 中的类顺序进行访问 +load。之后再加载 Catetory 的 +load 方法。 + +在 `prepare_load_methods` 方法内部先给普通类按照编译顺序(谁先编译谁先添加,遇到存在父类的类,递归调用父类对象)添加类信息到 loadable_classes 中,之后给分类按照编译顺序添加到(谁先编译谁先添加) loadable_categories 中。 + ++load 方法在 Runtime 加载类、分类的时候调用。 + +Extension 在编译阶段,数据已经包含在类信息中。 + +Category 是在运行阶段,才会将数据合并到类信息中。 + +## 底层窥探 Initialize 方法 + +上 Demo + +```objectivec +@interface Person : NSObject +@end + +@interface Student : Person +@end + +@interface Student (Good) +@end +``` + +`Person *p1 = [[Person alloc] init];` 这句代码输出什么? 这个比较简单,initialize 方法在类第一次收到消息的时候调用。所以输出 `+[Person initialize]` + +`Student *st = [[Student alloc] init];` 输出什么? + +```objectivec ++[Person initialize] ++[Student(Good) initialize] +``` + +查看分类在 Runtime 加载类信息时候的调用原理可以知道,分类中的类方法、对象方法都会被加载原始类的前面去(initialize 是类方法)如下图: + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-categoryattachLists.png) + + + ++initialize 和 +load 最大区别是 +initialize 是通过 objc_msgSend 进行调用的 + +- 调用方式:load 根据函数地址直接调用,initialize 是根据 objc_msgSend 调用的 + +- 调用时刻:load 是 runtime 加载类、分类的时候调用的。initialize 是在类第一次接收消息的时候调用的。每个类只会 initialize 一次,但是父类的 initialize 可能会调用多次 + +- 调用顺序: + + - load:先调用类的 load(先编译的类优先调用 load、调用子类的 load,会先调用父类的 load)、再调用分类的 load(先编译的分类,优先调用 load ) + +- 如果子类没有实现 +initialize 则会调用父类的 +initialize(所以父类的 +initialize 可能会被调用多次) + +- 如果分类实现了 +initialize,就会覆盖类本身的 +initialize 调用 + +查看源码,伪代码如下: + +``` +if (自己没有初始化) { +    if (父类没有初始化) { +     objc_msgSend(父类,@selector(initializ)) +    } + objc_msgSend(子类,@selector(initializ))     +} +``` + +```objectivec +void _class_initialize(Class cls) +{ + assert(!cls->isMetaClass()); + + Class supercls; + bool reallyInitialize = NO; + + // Make sure super is done initializing BEFORE beginning to initialize cls. + // See note about deadlock above. + supercls = cls->superclass; + if (supercls && !supercls->isInitialized()) { + _class_initialize(supercls); + } + + // Try to atomically set CLS_INITIALIZING. + { + monitor_locker_t lock(classInitLock); + if (!cls->isInitialized() && !cls->isInitializing()) { + cls->setInitializing(); + reallyInitialize = YES; + } + } + + if (reallyInitialize) { + // We successfully set the CLS_INITIALIZING bit. Initialize the class. + + // Record that we're initializing this class so we can message it. + _setThisThreadIsInitializingClass(cls); + + if (MultithreadedForkChild) { + // LOL JK we don't really call +initialize methods after fork(). + performForkChildInitialize(cls, supercls); + return; + } + + // Send the +initialize message. + // Note that +initialize is sent to the superclass (again) if + // this class doesn't implement +initialize. 2157218 + if (PrintInitializing) { + _objc_inform("INITIALIZE: thread %p: calling +[%s initialize]", + pthread_self(), cls->nameForLogging()); + } + + // Exceptions: A +initialize call that throws an exception + // is deemed to be a complete and successful +initialize. + // + // Only __OBJC2__ adds these handlers. !__OBJC2__ has a + // bootstrapping problem of this versus CF's call to + // objc_exception_set_functions(). +#if __OBJC2__ + @try +#endif + { + + callInitialize(cls); + + if (PrintInitializing) { + _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]", + pthread_self(), cls->nameForLogging()); + } + } +#if __OBJC2__ + @catch (...) { + if (PrintInitializing) { + _objc_inform("INITIALIZE: thread %p: +[%s initialize] " + "threw an exception", + pthread_self(), cls->nameForLogging()); + } + @throw; + } + @finally +#endif + { + // Done initializing. + lockAndFinishInitializing(cls, supercls); + } + return; + } + + else if (cls->isInitializing()) { + // We couldn't set INITIALIZING because INITIALIZING was already set. + // If this thread set it earlier, continue normally. + // If some other thread set it, block until initialize is done. + // It's ok if INITIALIZING changes to INITIALIZED while we're here, + // because we safely check for INITIALIZED inside the lock + // before blocking. + if (_thisThreadIsInitializingClass(cls)) { + return; + } else if (!MultithreadedForkChild) { + waitForInitializeToComplete(cls); + return; + } else { + // We're on the child side of fork(), facing a class that + // was initializing by some other thread when fork() was called. + _setThisThreadIsInitializingClass(cls); + performForkChildInitialize(cls, supercls); + } + } + + else if (cls->isInitialized()) { + // Set CLS_INITIALIZING failed because someone else already + // initialized the class. Continue normally. + // NOTE this check must come AFTER the ISINITIALIZING case. + // Otherwise: Another thread is initializing this class. ISINITIALIZED + // is false. Skip this clause. Then the other thread finishes + // initialization and sets INITIALIZING=no and INITIALIZED=yes. + // Skip the ISINITIALIZING clause. Die horribly. + return; + } + + else { + // We shouldn't be here. + _objc_fatal("thread-safe class init in objc runtime is buggy!"); + } +} + +void callInitialize(Class cls) { + ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize); + asm(""); +} +``` diff --git a/Chapter1 - iOS/1.60.md b/Chapter1 - iOS/1.60.md index d3fce4a..abd51ed 100644 --- a/Chapter1 - iOS/1.60.md +++ b/Chapter1 - iOS/1.60.md @@ -1,7 +1,5 @@ # App 瘦身之道 - - App 的包大小做优化的目的就是为了节省用户流量,提高用户的下载速度,也是为了用户手机节省更多的空间。另外 App Store 官方规定 App 安装包如果超过 150MB,那么不可以使 OTA(over-the-air)环境下载,也就是只可以在 WiFi 环境下载,企业或者独立开发者万万不想看到这一点。免得失去大量的用户。 同时如果你的 App 需要适配 iOS7、iOS8 那么官方规定主二进制 text 段的大小不能超过 60MB。如果不能满足这个标准,则无法上架 App Store。 @@ -12,16 +10,26 @@ App 的包大小做优化的目的就是为了节省用户流量,提高用户 +App 瘦身一般指的是安装包(IPA),主要由可执行文件、资源组成。 + +对于产物的分析,可以查看可执行文件的具体组成。 + +Xcode - Build Setting - Write Link Map File 设置为 YES。修改 Path to Link Map File 即可。 + +可借助第三方工具解析LinkMap文件: [GitHub - huanxsd/LinkMap: 检查每个类占用空间大小工具](https://github.com/huanxsd/LinkMap) + + + ## 1. App Thinning App Thinning 是指 iOS9 以后引入的一项优化,官方描述如下 + > The App Store and operating system optimize the installation of iOS, tvOS, and watchOS apps by tailoring app delivery to the capabilities of the user’s particular device, with minimal footprint. This optimization, called app thinning, lets you create apps that use the most device features, occupy minimum disk space, and accommodate future updates that can be applied by Apple. Faster downloads and more space for other apps and content provides a better user experience. Apple 会尽可能,自动降低分发到具体用户时,所需要下载的 App 大小。其中包含三项主要功能:Slicing、Bitcode、On-Demand Resources。 App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术,主要为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户设备存储空间。 - ### 1.1 Slicing ![Slicing](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppSlicing.jpeg) @@ -36,7 +44,6 @@ App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术 ![变体](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppVariant.jpeg) - ### 1.2 Bitcode > Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the App Store. @@ -47,8 +54,8 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App 开启位置:Build Settings -> Enable Bitcode -> 设置为 YES - 开启 Bitcode,有这么2点需要注意: + - 全部都要支持。我们所依赖的静态库、动态库、Cocoapods 管理的第三方库,都需要开启 Bitcode。否则会编译失败 - 奔溃定位。开启 Bitcode 后最终生成的可执行文件是 Apple 自动生成的,同时会产生新的符号表文件,所以我们无法使用自己包生成的 dYSM 符号化文件来进行符号化。 @@ -59,12 +66,10 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App 在上传到 App Store 时需勾选“Includ app symbols for your application...”。勾选之后 Apple 会自动生成对应的 dYSM,然后可以在 Xcode -> Window -> Organizer 中,或者在 Apple Store Connect 中下载对应的 dYSM 来进行符号化 - ![App Connect-dYSM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppConnectYSM.jpeg) ![Xcode-dYSM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-XcodedYSM.jpeg) - 那么 Bitcode 会对 App Thining 有什么作用? 在 New Features in Xcode7 中有这么一段描述: @@ -74,8 +79,7 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App 即,App Store 会再按需将这个 bitcode 编译进 32/64 位的可执行文件。 所以网上铺天盖地地说 Bitcode 完成了具体架构的拆分,从而实现瘦包 - -### 1.3 on-Demand Resources +### 1.3 on-Demand Resources on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,不随着 App 的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。 @@ -85,8 +89,6 @@ on-Demand Resource 即一部分图片可以被放置在苹果的服务器上, 如需支持 iOS9 以下系统,那么无法使用这个功能,否则上传会失败 - - ## 2 包体积 2个概念 @@ -106,21 +108,17 @@ Universal 指通用设备,即未应用 App slicing 优化,同时包含了所 观察 .ipa 的大小和 Universal 对应的包大小相当,稍微小一点,因为 App Store 对 .ipa 做了加密处理 - 有时候下载 App 会提示“此项目大于 150MB,除非此项目支持增量下载,否则您必须连接至 WiFi 才能下载”。150MB 针对的是下载大小。 - - 下载大小:通过 WiFi 下载的压缩 App 大小 - 安装大小:此 App 将在用户设备上占用磁盘空间的大小 所以我们要瘦包,关键在于减小 .app 文件的大小。 - ### 2.1 Architectures 如果不支持32位以及 iOS8 ,去掉 armv7 ,可执行文件以及库会减小,即本地 .ipa 也会减小 - ### 2.2 Resources 资源的优化也就是平时的细心与审查。 @@ -138,23 +136,20 @@ Bundle: 非放在 Asset Catlog 中管理的图片资源。包括 Bundle,散落 - 图片管理方式规范 - on-Demand Resource(游戏的、前置关卡依赖、滤镜App 等的依赖资源,建议用这种方式动态下载图片资源) - #### 2.2.1 无用文件的删除 - 无用文件主要包含:无用图片、无用非图片部分。 非图片部分:资源较少,使用方式固定。比如音频、字体。需要手动排查 图片部分:主要使用一个开源的 Mac App [LSUnusedResources](https://github.com/tinymind/LSUnusedResources) 进行冗余图片的排查。 - 删除无用的图片过程,可以概括为下面6步: + 1. 通过 find 命令获取 App 安装包中的所有资源文件 2. 设置用到的资源类型。比如 gif、jpg、jpeg、png、webp 3. 使用正则匹配出在源码中使用到的资源名,比如 pattern = @"@"(.+?)"" 4. 使用 find 命令找到篇所有资源文件,再去源码中找到使用到的资源文件,2个集合的差集就是无用资源了。 5. 确认无用资源后可以使用 NSFileManager 进行文件的删除。 - 如果不想重新写一个工具,那么可以直接使用开源的工具 LSUnusedResources @@ -168,14 +163,15 @@ Bundle: 非放在 Asset Catlog 中管理的图片资源。包括 Bundle,散落 //... } ``` -源码中的正则表达式处理的情况并不是很准确。可以根据自己的情况修改正则即可 +源码中的正则表达式处理的情况并不是很准确。可以根据自己的情况修改正则即可 #### 2.2.2 图片资源的压缩 删除了无用的资源,那么对于资源这块还是有操作的空间的,比如图片资源的压缩。目前压缩比较好的方案就是 WebP,它是谷歌公司的一个开源项目。 WebP 的优势: + - 压缩率高。支持有损和无损2种方式,比如将 Gif 图可以转换为 Animated WebP,有损模式下可以减小 64%,无损模式下可以减小 19% - WebP 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够出现毛边。 @@ -184,7 +180,6 @@ Google 公司在开源 WebP 的同时,还提供了一个图片压缩工具 [cw 缺点:WebP 在 CUP 消耗和解码时间上会比 PNG 高2倍,所以我们做选择的时候需要取舍。 - #### 2.2.3 重复文件删除 重复文件,即两个内容完全一致的文件。但是文件命名不一样。 @@ -198,7 +193,6 @@ fdupes 是 Linux 下的一个工具,它由 Adrian Lopez 用 C 语言编写并 执行结束后会在命令行展示出来,所以需要我们人工将这些文件确认对比后删除掉。 - #### 2.2.4 大文件压缩 图片本身的压缩,建议使用 ImageOptim。它整合了 Win、Linux 上诸多著名图片处理工具的特色,比如 PNGOUT、AdvPNG、Pngcrush、OptiPNG、JpegOptim、Gifsicle 等。 @@ -209,10 +203,8 @@ Xcode 提供给我们2个编译选项来帮助压缩图像: - Compress PNG Files: 打包的时候自动对图片进行无损压缩。使用的工具为 pngcrush,压缩比蛮高。 - Remove Text Medadata From PNG Files:移除 PNG 资源的文本字符,比如图像名称、作者、版权、创作时间、注释等信息 - #### 2.2.5 图片管理方式规范 - ##### 2.2.5.1 主工程中的图片管理 工程中所有使用的 Asset Catlog 管理的图片(在 .xcassets 文件夹下)最终都会输出到 Asset.car 内。不在 Asset.car 内的都归为 Bundle 管理。 @@ -249,6 +241,7 @@ for (NSInteger index = 0; index < 10; index++) { } self.imageView.image = images.lastObject; ``` + Timeprofile-imageNamedFromAssets @@ -260,52 +253,57 @@ TimeProfile-imageWithContentsOfFile Timeprofile-UIImageNamedFromFolder ![Timeprofile-UIImageNamedFromFolder](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Timeprofile-UIImageNamedFromFolder.png) - Images.xcassets : + - 图片大小要精确,不要出现图片太大的情况 - 不要存放大图,不然会产生缓存 - 不要存 jpg 图片,打包会变大 - 图片不需要额外压缩(有人做过实验,对放入 assets 里面的图片进行压缩后打包发现包体积反而增大,怀疑是 Xcode 的编译选项 Compress PNG Files 自动对图片进行压缩,2种压缩起了冲突反而增大) - ##### 2.2.5.2 各个 pod 库中的图片管理 CocoPods 中两种资源引用方式介绍下: + - resource_bundles - > We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. - 允许定义当前的 pod 库的最远包的名称和文件。用 hash 形式声明,key 是 bundle 的名称,value 是需要包含文件的通配 patterns - CocoPods 官方强烈推荐该方法引用资源,因为 key-value 可以避免相同资源的名称冲突 + + > We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. + > 允许定义当前的 pod 库的最远包的名称和文件。用 hash 形式声明,key 是 bundle 的名称,value 是需要包含文件的通配 patterns + > CocoPods 官方强烈推荐该方法引用资源,因为 key-value 可以避免相同资源的名称冲突 - resources - > We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode. - 使用该方法引用资源,被指定的资源会被拷贝进 target 工程的 main bundle 中。 - - + + > We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode. + > 使用该方法引用资源,被指定的资源会被拷贝进 target 工程的 main bundle 中。 说说项目中的情况吧:在工程中之前是通过 resource_bundles 引用资源的。资源是放在 Resources 目录下的图片引用。查询资料后说「如果图片资源放到 .xcasset 里面 Xcode 会帮我们自动优化、可以使用 Slicing 等(这里不仅仅指的是 resource_bundle 下的 xcassets」。所以动手将各个 Pod 库里面的图片全都通过 Assets Catalog 的方式进行处理。 ![Pod组件库图片处理前后对比](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-08-Cocopod-Assets.png) 步骤: + - 在各个 Pod 组件库里面的 Resources 目录下新建 Asset Catalog 文件,命名为 Images.xcassets + - 将 Resources 里面零散的图片资源拖进 Images.xcassets 里面 + - 修改每个组件库的 podspec 文件 -
+ +
+ + 点击展开 + + ``` + s.resource_bundles = { + 'XQ_UI' => ['XQ_UI/Assets/*.xcassets'] + } +
+ ``` - 点击展开 - - ``` - s.resource_bundles = { - 'XQ_UI' => ['XQ_UI/Assets/*.xcassets'] - } -
- ``` - 主工程执行 pod install 话说 `resources` 和 `resource_bundles` 都可以使用 Asset Catalog,那么有何区别? + - resources 只会将资源文件 copy 到 target 工程,最后和 target 工程的图片资源以及同样使用该方式的 Pod 库的图片资源共同打包到一个 `Assets.car` 中。因此图片资源会有混乱的可能。 - resource_bundles 会生成一个你在 `podspec` 中指定名称的 bundle,且在 bundle 中也会生成一个 Assets.car。所以图片是肯定不会混乱的,但是图片的访问方式需要注意。 - 解决方法:为每个 pod 新建一个图片的分类,比如 UIImage+XQUIModule。然后访问图片的时候通过 `[UIImage xquiModuleImageNamed:@"pull"]` 访问。
@@ -334,18 +332,15 @@ CocoPods 中两种资源引用方式介绍下: } @end ``` +
- - #### 2.2.6 矢量图的使用 事实上,对于 App 里面的单色图标,比如左上角的返回按钮、底部的 tabBar等,只要是单色的纯色图标都是可以使用矢量图代替的,比如 PDF、ttf 字体图标等。这样就不需要添加 @2x、@3x 图标,节省了空间。 iOS 中如何使用 ttf 矢量图,可以查看这个 [Repo](https://github.com/FantasticLBP/IconFont_Demo) - - ## 3. Executable file ### 3.1 编译选项优化 @@ -365,7 +360,6 @@ optimization 选项设置为 space 可以减少包大小 > For statically linked executables, dead-code stripping is the process of removing unreferenced code from the executable file. If the code is unreferenced, it must not be used and therefore is not needed in the executable file. Removing dead code reduces the size of your executable and can help reduce paging. - 删除静态链接的可执行文件中未引用的代码 Debug 设置为 NO, Release 设置为 YES 可减少可执行文件大小。 @@ -394,7 +388,6 @@ Build Settings -> code Generation -> Optimization Level 默认选项,不做修改。 - #### 3.1.5 Swift Compiler - Code Generation Xcode 9.3 版本之后 Swift 编译器提供了新的 Optimization Level 选项来帮助减少 Swift 可执行文件的大小: @@ -409,7 +402,6 @@ Xcode 9.3 版本之后 Swift 编译器提供了新的 Optimization Level 选项 除了 -O 和 -Osize, 还有另外一个概念也值得说一下。 就是 Single File 和 Whole Module 。 在之前的 XCode 版本,这两个选项和 -O 是连在一起设置的,Xcode 9.3 中,将他们分离出来,可以独立设置: - Single File 和 Whole Module 这两个模式分别对应编译器以什么方式处理优化操作。 - Single File:逐个文件进行优化,它的好处是对于增量编译的项目来说,它可以减少编译时间,对没有更改的源文件,不用每次都重新编译。并且可以充分利用多核 CPU,并行优化多个文件,提高编译速度。但它的缺点就是对于一些需要跨文件的优化操作,它没办法处理。如果某个文件被多次引用,那么对这些引用方文件进行优化的时候,会反复的重新处理这个被引用的文件,如果你项目中类似的交叉引用比较多,就会影响性能。 @@ -420,7 +412,6 @@ Single File 和 Whole Module 这两个模式分别对应编译器以什么方式 故,在 Relese 模式下 -Osize 和 Whole Module 同时开启效果会最好! - #### 3.1.6 Strip Symbol Information 1、Deployment Postprocessing @@ -434,7 +425,6 @@ Symbols Hidden by Default 会把所有符号都定义成”private extern”, 故,Release 设置为 YES,Debug 设置为 NO。 - #### 3.1.7 Exceptions 在 iOS微信安装包瘦身 一文中,有提到: @@ -447,7 +437,6 @@ Symbols Hidden by Default 会把所有符号都定义成”private extern”, 可能和项目中用到比较少有关系。故保持开启状态。 - #### 3.1.8 Link-Time Optimization Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码;这个优化也是高效的,因为可以在全局视角下优化代码。 @@ -464,10 +453,8 @@ Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间 在新的版本中,苹果使用了新的优化方式 Incremental,大大减少了链接的时间。建议开启。 - 总结,开启这个优化后,一方面减少了汇编代码的体积,一方面提高了代码的运行效率。 - ### 3.2 代码瘦身 代码的优化,即通过删除无用类、无用方法、重复方法等,来达到可执行文件大小的减小。 @@ -478,35 +465,32 @@ Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间 - 基于 Clang 扫描 - 基于可执行文件扫描 - 基于源码扫描 - 先谈几个概念。 可执行文件就是 **Mach-O** 文件,其大小是油代码量决定的,通常情况下,对可执行文件进行瘦身,就是找到并删除无用代码的过程。找到无用代码的过程类比找到无用图片的思路。 + - 找到类和方法的全集 - 找到使用过的类和方法集合 - 取2者差集得到无用代码集合 - 工程师确认后,删除即可 - LinkMap 文件分为3部分:Object File、Section、Symbols。 ![LinkMap结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Structure.png) + - Object File:包含了代码工程的所有文件 - Section:描述了代码段在生成的 Mach-O 里的偏移位置和大小 - Symbols:会列出每个方法、类、Block,以及它们的大小 先说说如何快速找到方法和类的全集? - 我们可以通过 **LinkMap** 来获得所有的代码类和方法的信息。获取 LinkMap 可以通过将 Build Setting 里面的 **Write Link Map File** 设置为 YES,然后指定 **Path to Link Map File** 的路径就可以得到每次编译后的 LinkMap 文件了。 ![Xcode中设置获取LinkMap](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Xcode.png) - #### 3.2.1 基于 clang 扫描 基本思路是基于 clang AST。追溯到函数的调用层级,记录所有定义的方法/类和所有调用的方法/类,再取差集。具体原理参考 如何使用 Clang Plugin 找到项目中的无用代码,目前只有思路没有现成的工具。 - #### 3.2.2 基于可执行文件扫描(LinkMap 结合 Mach-O 找无用代码) 上面我们得知可以通过 LinkMap 统计出所有的类和方法,还可以清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。 @@ -517,7 +501,6 @@ LinkMap 文件分为3部分:Object File、Section、Symbols。 ![LinkMap-Symbols](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-Symbols.png) - 得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。 Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSend 在 Mach-O 文件里是通过 **_objc_selrefs** 这个 **section** 来获取 selector 这个参数的。 @@ -526,7 +509,6 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe 那么,Mach-O 文件中的 _objc_selrefs、_objc_classrefs、_objc_superrefs 如何查看呢? - 1. 使用 otool 等命令逆向可执行文件中引用到的类/方法和所有定义的类/方法,然后计算差集。具体参考iOS微信安装包瘦身,目前只有思路没有现成的工具。 2. 使用 [MachOView](https://github.com/gdbinit/MachOView) 查看。但是这个项目运行不起来,这个新的 [Repo](https://github.com/fangshufeng/MachOView) 可以运行起来。 @@ -538,14 +520,12 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe 由于 Objective-C 是一门动态语言,所以检测出的结果仍旧需要我们2次确认。 - #### 3.2.3 基于源码扫描 一般都是对源码文件进行字符串匹配。例如将 A *a、[A xxx]、NSStringFromClass("A")、objc_getClass("A") 等归类为使用的类,@interface A : B 归类为定义的类,然后计算差集。 基于源码扫描 有个已经实现的工具 - fui,但是它的实现原理是查找所有 #import "A" 和所有的文件进行比对,所以结果相对于上面的思路来说可能更不准确。 - #### 3.2.4 通过 AppCode 查找无用代码 AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。 @@ -559,7 +539,6 @@ AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码 - 无用宏:Unused macro 是无用的宏。 - 无用全局:Unused global declaration 是无用全局声明。 - #### 3.2.5 运行时真正检测类是否用过 通过上述手段找到并删除了无用代码。App 不断上线迭代蛮多代码都不会被调用了(业务被砍掉了)。这种方式下这些无用的代码也是可以被删除的。 @@ -603,8 +582,6 @@ isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信 既然可以在运行的期间知道类是否初始化了,那么就可以找出哪些类未初始化,即可以找到在真实环境里面没有用到的类并删除掉。 - - ## 4. App Extension App Extension 的占用,都放在 Plugin 文件夹内。它是独立打包签名,然后再拷贝进 Target App Bundle 的。 @@ -616,12 +593,11 @@ App Extension 的占用,都放在 Plugin 文件夹内。它是独立打包签 所以,如果可能的话,把相关的依赖改成动态库方式,达到共享。 - - ## 5. 静态库瘦身 项目中都会引入第三方静态库。通过 lipo 工具可以查看支持的指令集,比如查看微博 SDK 终端切换到微博 SDK 的目录下执行下面命令 + - 静态库指令集信息查看:`lipo -info libname.a(或者libname.framework/libname)` ```Shell @@ -634,7 +610,6 @@ lipo -info libWeiboSDK.a - 静态库拆分:`lipo 静态库文件路径 -thin CPU架构 -output 拆分后的静态库文件路径` - 静态库合并:`lipo -create 静态库1文件路径 静态库2文件路径... 静态库n文件路径 -output 合并后的静态库文件径` - ```Shell lipo libWeiboSDK.a -thin armv7 -output libWeiboSDK-armv7.a lipo libWeiboSDK.a -thin arm64 -output libWeiboSDK-arm64.a @@ -646,35 +621,33 @@ lipo create libWeiboSDK-armv7.a libWeiboSDK-arm64.a -output libWeiboSDK.device.a 1. 平时使用包含模拟器指令集的静态库,在 App 发布的时候去掉 2. 如果使用 Cocoapods 管理可以使用2份 Podfile 文件。一份包含指令集一份不包含,发布的时候切换 Podfile 文件即可。或者一份 Podfile 文件,但是配置不同的环境设置 - 补充2个说明: 1. dSYM 文件 符号表文件 .dSYM 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,实际用于保存调试信息的是 DWARF 文件 - - 自动生成。Xcode 会在工程编译或者归档的时候自动生成 .dSYM 文件,在 Buld setting 设置中有开关可以设置去关掉 .dSYM 文件 -- 手动生成。通过脚本从 Mach-O 文件中提取出来。 -``` -$ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/wangzz/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM -``` -该方式通过 dsymutil 工具,从项目编译结果 .app 目录下的 Mach-O 文件中提取出调试符号表文件。Xcode 在归档的时候是通过它生辰的 .dSYM 文件 +- 手动生成。通过脚本从 Mach-O 文件中提取出来。 + + ``` + $ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/wangzz/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM + ``` + + 该方式通过 dsymutil 工具,从项目编译结果 .app 目录下的 Mach-O 文件中提取出调试符号表文件。Xcode 在归档的时候是通过它生辰的 .dSYM 文件 2. DWARF 文件 DebuggingWith Arbitrary Record Formats 是 ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式,.dSYM 文件中真正保存符号表数据的是 DWARF 文件。DWARF 文件中不同的数据都保存在相应的 section 中。 - 最后的一个对比效果图: ![瘦身效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-AppThinning-Comparation.png) - 总结:瘦身技术常见操作就这些,但是维持应用包体积的瘦身却是一个观念,从日常开发到线上发布都需要有这个意识。这样当你在写代码的时候就会考虑同样一个效果,你的具体实现手段是怎么样的。比如为了一个稍微炫酷的效果就要引入一个很大的三方库,有了“瘦身”的意识,你很大可能就是自己动手撸一个代码。比如一些无用资源的管理方式、有用的图片资源的高效管理方式等等。有了意识,行动自然会往这个方面去靠。(😂大道理一套一套的。我也不想的,毕竟是playboy) 其中遇到了一个神奇的问题。lint 的时候看到一些未使用的依赖库。见 [问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.53.md) - **By the way:** 如果在应用包瘦身方面有其他的做法,请告知,完善文章。 参考文章: + - [Humble Assets Catalog](http://lingyuncxb.com/2019/04/14/HumbleAssetCatalog/) - [关于 Pod 库的资源引用 resource_bundles or resources](http://zhoulingyu.com/2018/02/02/pod-resource-reference/) \ No newline at end of file diff --git a/Chapter1 - iOS/1.61.md b/Chapter1 - iOS/1.61.md index 3b59b04..2283ff6 100644 --- a/Chapter1 - iOS/1.61.md +++ b/Chapter1 - iOS/1.61.md @@ -1,16 +1,382 @@ # App 启动时间优化 +## 启动分类 + +- 冷启动(Cold Launch):点击 App 图标启动前,进程不在系统中。需要系统新创建一个进程并加载 Mach-O 文件,dyld 从 Mach-O 头信息中读取依赖(undefined的动态库库),从动态库共享缓存中读取并链接,经历一次完整的启动过程。 +- 热启动(Warm Launch):App 在冷启动后,用户将 App 退后台。此阶段,App 的进程还在系统中,用户重新启动进入 App 的过程,开发对该阶段能做的事情非常少。 + +所以我们聊启动时间优化,通常是指冷启动。此外启动慢,也就是看到主页面的过程很慢,都是发生在主线程上的。 + + + +为了量化启动时间,要么自定义 APM 监控。要么利用 Xcode 提供的启动时间统计。通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments) + +- `DYLD_PRINT_STATISTICS` 设置为1 + +- 如果需要更详细的信息,那就将 `DYLD_PRINT_STATISTICS_DETAILS` 设置为1 + +## 启动阶段划分 + +App 冷启动可以划分为3大阶段: + +- 第一阶段:进程创建到 main 函数执行(dyld、runtime) + +- 第二阶段:main 函数到 `didFinishLaunchingWithOptions` + +- 第三阶段:`didFinishLaunchingWithOptions` 到业务首页加载完成 + +这里说的阶段都是某一个步骤的最后一步。比如第一阶段的 main 函数执行的结束时刻 + +```shell +xnu_run () { + t1 +    //... +} + +main () { + // .. + // t2 +} +``` + + + +## 第一阶段:进程创建到 main 函数执行(dyld、runtime) + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/AppLaunchingTime.png) + +这一阶段主要包括 dyld 和 Runtime 、main 函数的工作。 + +### dyld + +iOS 的可执行文件都是 Mach-O 格式,所以 App 加载过程就是加载 Mach-O 文件的过程。 + +```c +struct mach_header_64 { + uint32_t magic; // 64位还是32位 + cpu_type_t cputype; // CPU 类型,比如 arm 或 X86 + cpu_subtype_t cpusubtype; // CPU 子类型,比如 armv8 + uint32_t filetype; // 文件类型 + uint32_t ncmds; // load commands 的数量 + uint32_t sizeofcmds; // load commands 大小 + uint32_t flags; // 标签 + uint32_t reserved; // 保留字段 +}; +``` + +加载 Mach-O 文件,内核会先 fork 进程,并为进程分配虚拟内存、为进程创建主线程、代码签名等,用户态 dyld 会对 Mach-O 文件做库加载和符号解析。 + +细节可以查看代码,在 xnu 的 `kern_exec.c` 中 + +```c +int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval) { + // 字段设置 + ... + int is_64 = IS_64BIT_PROCESS(p); + struct vfs_context context; + struct uthread *uthread; // 线程 + task_t new_task = NULL; // Mach Task + ... + + context.vc_thread = current_thread(); + context.vc_ucred = kauth_cred_proc_ref(p); + + // 分配大块内存,不用堆栈是因为 Mach-O 结构很大。 + MALLOC(bufp, char *, (sizeof(*imgp) + sizeof(*vap) + sizeof(*origvap)), M_TEMP, M_WAITOK | M_ZERO); + imgp = (struct image_params *) bufp; + + // 初始化 imgp 结构里的公共数据 + ... + + uthread = get_bsdthread_info(current_thread()); + if (uthread->uu_flag & UT_VFORK) { + imgp->ip_flags |= IMGPF_VFORK_EXEC; + in_vfexec = TRUE; + } else { + // 程序如果是启动态,就需要 fork 新进程 + imgp->ip_flags |= IMGPF_EXEC; + // fork 进程 + imgp->ip_new_thread = fork_create_child(current_task(), + NULL, p, FALSE, p->p_flag & P_LP64, TRUE); + // 异常处理 + ... + + new_task = get_threadtask(imgp->ip_new_thread); + context.vc_thread = imgp->ip_new_thread; + } + + // 加载解析 Mach-O + error = exec_activate_image(imgp); + + if (imgp->ip_new_thread != NULL) { + new_task = get_threadtask(imgp->ip_new_thread); + } + + if (!error && !in_vfexec) { + p = proc_exec_switch_task(p, current_task(), new_task, imgp->ip_new_thread); + + should_release_proc_ref = TRUE; + } + + kauth_cred_unref(&context.vc_ucred); + + if (!error) { + task_bank_init(get_threadtask(imgp->ip_new_thread)); + proc_transend(p, 0); + + thread_affinity_exec(current_thread()); + + // 继承进程处理 + if (!in_vfexec) { + proc_inherit_task_role(get_threadtask(imgp->ip_new_thread), current_task()); + } + + // 设置进程的主线程 + thread_t main_thread = imgp->ip_new_thread; + task_set_main_thread_qos(new_task, main_thread); + } + ... +} +``` + +Mach-O 文件非常大,系统为 Mach-O 分配一块大内存 imgp,接下来会初始化 imgp 里的公共数据。内存处理完,__mac_execve 会通过 fork_create_child 函数 for 出一个新的进程,然后通过 `exec_activate_image` 函数解析加载 Mach-O 文件到内存 imgp 中。最后调用 `task_set_main_thread_qos` 设置新 fork 出来进程的主线程。 + +```c + +struct execsw { + int (*ex_imgact)(struct image_params *); + const char *ex_name; +} execsw[] = { + { exec_mach_imgact, "Mach-o Binary" }, + { exec_fat_imgact, "Fat Binary" }, + { exec_shell_imgact, "Interpreter Script" }, + { NULL, NULL} +}; +``` + +可以看到 Mach-O 文件解析使用 `exec_mach_imgact` 函数。该函数内部调用 `load_machfile` 来加载 Mach-O 文件,解析 Mach-O 文件后得到 load command 信息,通过映射方式加载到内存中。`activate_exec_state()` 函数处理解析加载 Mach-O 后的结构信息,设置执行 App 的入口点。 + +之后会通过 `load_dylinker()` 函数来解析加载 dyld,然后将入口地址改为 dyld 入口地址。至此,内核部分就完成 Mach-O 文件的加载,剩下的就是用户态 dyld 加载 App 了。 + +dyld 入口函数为 `_dyld_start`,dyld 属于用户态进程,不在 xnu 中,具体实现可以查看 [dyld/dyldStartup.s at master · opensource-apple/dyld · GitHub](https://github.com/opensource-apple/dyld/blob/master/src/dyldStartup.s),`_dyld_start` 会加载 App 动态库,处理完成后会返回 App 的入口地址。然后执行 App 的 main 函数。 + + + +dyld(dynamic link editor),Apple的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等) + +启动 APP 时,dyld 所做的事情有 + +- 装载 APP 的可执行文件,同时会递归加载所有依赖的动态库 + +- 当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行下一步的处理。 + 其中包括 ASLR,rebase、bind。 + +QA:这里的通知 Runtime 怎么理解? +查看 objc4 的源代码 `objc-os.mm` 文件中的 `_objc_init` 方法 + +```c +/*********************************************************************** +* _objc_init +* Bootstrap initialization. Registers our image notifier with dyld. +* Called by libSystem BEFORE library initialization time +**********************************************************************/ +void _objc_init(void){ + static bool initialized = false; + if (initialized) return; + initialized = true; + // fixme defer initialization until an objc-using image is found? + environ_init(); + tls_init(); + static_init(); + lock_init(); + exception_init(); + _dyld_objc_notify_register(&map_images, load_images, unmap_image); +} +``` + +方法注释说的很明白,被 dyld 所调用 + +### Runtime + +启动 APP 时,Runtime 所做的事情有 + +- 调用 `map_images` 进行可执行文件内容的解析和处理 + +- 在 `load_images` 中调用 `call_load_methods`,调用所有 Class、Category 的 `+load`方法 + +- 进行各种 objc 结构的初始化(注册 Objc 类 、初始化类对象等等) + +- 调用 C++ 静态初始化器和 `__attribute__((constructor))` 修饰的函数 + +到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 Runtime 所管理 + + + +## 第二阶段:main 函数到 didFinishLaunchingWithOptions + +APP的启动由 dyld 主导,将可执行文件加载到内存,顺便加载所有依赖的动态库。并由Runtime 负责加载成 objc 定义的结构。所有初始化工作结束后,dyld 就会调用 main 函数 + +接下来就是 `UIApplicationMain` 函数。main 函数内部其实没啥逻辑,可能会存在一些防止逆向相关的安全代码。这部分对启动耗时没啥影响,可以忽略先。 + +AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段主要是各种 SDK 的注册、初始化、APM 监控系统的启动、热修复 SDK 的设置、App 初始化数据的加载、IO 等等。 + + + +## 第三阶段:`didFinishLaunchingWithOptions` 到业务首页加载完成 + +这部分主要是首页渲染相关逻辑,不同的场景,首页的定义可能不一样。比如未登录的时候首页就是登陆页、否则就是某个主页 + +- 首屏数据的网络/DB IO 读取 + +- 渲染数据的计算 + + + +## 启动优化 + +### 第一阶段 + +#### dyld + +- 减少动态库加载,合并一些动态库。(定期清理不必要的动态库,iOS 规定开发者写的动态库不能超过6个) + +#### Runtime + +- 用 `+initialize` 方法和 `dispatch_once` 取代所有的 `__attribute__((constructor))`、C++静态构造器、ObjC 的 `+load` + +- +load 方法中的代码可以监控等 App 启动完成后才去执行。或使用 + initialize 方法。一个 +load 方法中如果执行 hook 方法替换,大约影响4ms。 + +- 减少 Objc 类、分类的数量、减少 selector 数量(定期清理不必要的类、分类)。推荐工具 fui。 + +- 减少 C++ 虚函数数量 + +- Swift 尽量使用 struct + +- 控制 C++ 的全局变量的数据 + + + +### 第二阶段 + +- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在 `application:didFinishLaunchingWithOptions` 方法中 +- SDK 初始化遵循规范 +- 任务启动器 +- 二进制重排 +- 方法耗时统计(time profiler、os_signpost) + +AppDelegate 中 `application:didFinishLaunchingWithOptions` 函数阶段一般有很多业务代码介入,大多数启动时间问题都是在此阶段造成的。 + +- 比如二方、三方 SDK 的注册、初始化。这其中有些 SDK 比如 APM 是有必要放在很早的阶段。有些则不需要,比如日志 SDK 的初始化 + + + +### 第三阶段 + +很多时候,需要梳理出那些功能是首屏渲染所需要的初始化功能,那些是非首页需要的功能,按照业务场景梳理并治理。 + + + + + +QA: + +静态库、动态库? + +静态库:.o文件集合。静态库编译、链接后就不存在了,变为可执行文件了 + +动态库:一个已经链接完全的镜像。已经被静态链接过 + +动态库不可以变为静态库。静态库可以变为动态库。 + +静态库缺点:产物体积比较大,影响包大小(大)。链接到 App 之后,App 体积会比较小(??)静态库 strip + +动态库缺点:除了系统动态库之外,没有真正意义上的动态库(不会放到系统的共享缓冲区) + +适用场景: + +静态库不影响启动时间、动态库代码保密性好。 + +Framework 只是打包方式,动态、静态库都可以支持。甚至可以存放资源文件。 + + + +## 二进制重排 + +### 虚拟内存、物理内存、内存分页 + +早期计算机内部都是物理内存。物理内存一个问题就是安全问题,通过地址访问到别的应用程序的数据。进程如果能直接访问物理内存无疑是很不安全的,(诞生了解决方案:虚拟内存)所以操作系统在物理内存的上又建立了一层虚拟内存。 + +一个应用程序在使用的时候并不是全部使用的,往往是使用了一个应用程序的某个或者某几个功能。 + +所以早期工程师的第一个解决方案是将一个应用程序分块,按需加载功能模块的内存地址数据。 + +虚拟内存是间接访问了内存条。 + +内存分页?iOS 一页就是16KB。 + +物理内存如果满了,则将最活跃的内存数据,覆盖掉,最不活跃的内存数据; + +ASLR?为了安全问题诞生。 + +自己的代码中有 NSLog 代码,可是 NSLog 函数的代码实现在 Foundation 动态库中。 + +App 启动则用 dyld 去加载库,共享缓存库。 + +虚拟地址:偏移是编译后就能确定的。 + +内存缺页异常:在使用中,访问虚拟内存的一个 page 而对应的物理内存缺不存在(没有被加载到物理内存中),则发生缺页异常。影响耗时,在几毫秒之内。 + +什么时候发生大量的缺页异常?一个应用程序刚启动的时候。 + +启动时所需要的代码分布在第一页、第二页、第三页...第200页。这样的情况下启动时间会影响较大,所以解决思路就是将应用程序启动刻所需要的代码(二进制优化一下),统一放到某几页,这样就可以避免内存缺页异常,则优化了 App 启动时间。 + +dylib loading time: + +rebase/binding time: 修正符号是由于 ASLR 导致。binding time 是动态库去 bind lazy table、non-lazy table 所占用的时间。rebase 地址偏移,ASLR。 rebase 的时间如何缩小?Mach-O 文件大小变小。 binding time 变小,则需要动态库变小。2者优化手段冲突 + +Objc setup time:Swift 这部分占优势 + +initializer time:load 方法耗时。 + +slowest intializers: + +libS + +libMain + +查看 LinkMap。发现方法展示顺序是按照,写代码的顺序展示的。 + +![image-20200814211215976](/Users/lbp/Library/Application Support/typora-user-images/image-20200814211215976.png) + +### 有没有办法将 App 启动需要的方法集中收拢? + +1. 在 Xcode 的 Build Settings 中设置 **Order File**,Write Link Map Files 设置为 YES(进行观察) + +2. 如果你给 Xcode 工程根目录下指定一个 order 文件,比如 `refine.order`,则 Xcode 会按照指定的文件顺序进行二进制数据重排。分析 App 启动阶段,将优先需要加载的函数、方法,集中合并,利用 Order File,减小缺页异常,从而减小启动时间。 + +### 如何拿到启动时刻所调用的所有方法名称 + +clang 插桩,才可以 hook OC、C、block、Swift 全部。LLVM 官方文档。 + + 二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。 + +一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」,可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。 + +其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC,所以hook 肯定不行、fishhook 也不行,用 clang 插桩可行 + +- https://mp.weixin.qq.com/s/SUHaGD1T2Vce4Ag-qgxtgg + 在看 App 启动时间优化之前先看2个方法: **load** 和 **initialize**。 load + > Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading. -当一个类或者它的分类被加载到 Objective-c 的 runtime 中的时候会出发 load 方法。可以实现这个方法去执行一些类特定的行为。 +> 当一个类或者它的分类被加载到 Objective-c 的 runtime 中的时候会出发 load 方法。可以实现这个方法去执行一些类特定的行为。 initialize + > Initializes the class before it receives its first message. -当一个类接收到第一条消息的时候会初始化。 +> 当一个类接收到第一条消息的时候会初始化。 load 方法会在类被加载到 runtime 的时候调用。且父类的 load 方法比子类先执行。load 方法只会执行1次。 initialize 方法会在第一次收到消息的时候调用。父类的 initialize 方法比子类先执行。假如有 Person 类,还有一个子类 children。子类第一次收到消息的时候会先调用父类的 initialize,然后调用子类的 initialize,如果子类没有实现 initialize 那么父类的 initialize 会执行多次。 - - diff --git a/Chapter1 - iOS/1.7.md b/Chapter1 - iOS/1.7.md index 58cefb0..e8df216 100644 --- a/Chapter1 - iOS/1.7.md +++ b/Chapter1 - iOS/1.7.md @@ -1,6 +1,4 @@ - - -# 对象在内存中的存储 +# 对象在内存中的存储底层原理 ## 一、 栈、堆、BSS、数据段、代码段是什么? @@ -16,9 +14,6 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变 ![内存](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ram.png) - - - ## 二、研究下对象在内存中如何存储? ```Objective-C @@ -28,22 +23,23 @@ Person *p1 = [Person new] 看这行代码,先来看几个注意点: new底层做的事情: - * 在堆内存中申请1块合适大小的空间 - * 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做**isa**,是一个指针,指向对象所属的类在代码段中地址 - * 初始化对象的属性。这里初始化有几个原则:a、如果属性的数据类型是基本数据类型则赋值为0;b、如果属性的数据类型是C语言的指针类型则赋值为NULL;c、如果属性的数据类型为OC的指针类型则赋值为nil。 - * 返回堆空间上对象的地址 + +* 在堆内存中申请1块合适大小的空间 +* 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做**isa**,是一个指针,指向对象所属的类在代码段中地址 +* 初始化对象的属性。这里初始化有几个原则:a、如果属性的数据类型是基本数据类型则赋值为0;b、如果属性的数据类型是C语言的指针类型则赋值为NULL;c、如果属性的数据类型为OC的指针类型则赋值为nil。 +* 返回堆空间上对象的地址 注意: - * 对象只有属性,没有方法。包括类本身的属性和一个指向代码段中的类isa指针 - * 如何访问对象的属性?指针名->属性名;本质:根据指针名找到指针指向的对象,再根据属性名查找来访问对象的属性值 - * 如何调用方法?[指针名 方法];本质:根据指针名找到指针指向的对象,再发现对象需要调用方法,再通过对象的isa指针找到代码段中的类,再调用类里面方法 +* 对象只有属性,没有方法。包括类本身的属性和一个指向代码段中的类isa指针 +* 如何访问对象的属性?指针名->属性名;本质:根据指针名找到指针指向的对象,再根据属性名查找来访问对象的属性值 +* 如何调用方法?[指针名 方法];本质:根据指针名找到指针指向的对象,再发现对象需要调用方法,再通过对象的isa指针找到代码段中的类,再调用类里面方法 为什么不把方法存储在对象中? - * 因为以类为模版创建的对象只有属性可能不相同,而方法相同,如果堆区的对象里面也保存方法的话就会很重复,浪费了堆空间,因此将方法存储与代码段 +* 因为以类为模版创建的对象只有属性可能不相同,而方法相同,如果堆区的对象里面也保存方法的话就会很重复,浪费了堆空间,因此将方法存储与代码段 - * 所以一个类创建的n个对象的isa指针的地址值都相同,都指向代码段中的类地址 +* 所以一个类创建的n个对象的isa指针的地址值都相同,都指向代码段中的类地址 做个小实验 @@ -92,3 +88,451 @@ int main(int argc, const char * argv[]) { ![p2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-15%20下午5.35.34.png) **可以 看到Person类的3个对象p1、p2、p3的isa的值相同。** + +## 三、一个对象占用多少内存空间? + +```objectivec +size_t class_getInstanceSize(Class cls) +{ + if (!cls) return 0; + return cls->alignedInstanceSize(); +} + // Class's ivar size rounded up to a pointer-size boundary. +uint32_t alignedInstanceSize() { + return word_align(unalignedInstanceSize()); +} +``` + +```objectivec +id class_createInstance(Class cls, size_t extraBytes) +{ + return _class_createInstanceFromZone(cls, extraBytes, nil); +} + +static __attribute__((always_inline)) +id +_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, + bool cxxConstruct = true, + size_t *outAllocatedSize = nil) +{ + if (!cls) return nil; + + assert(cls->isRealized()); + + // Read class's info bits all at once for performance + bool hasCxxCtor = cls->hasCxxCtor(); + bool hasCxxDtor = cls->hasCxxDtor(); + bool fast = cls->canAllocNonpointer(); + + size_t size = cls->instanceSize(extraBytes); + if (outAllocatedSize) *outAllocatedSize = size; + + id obj; + if (!zone && fast) { + obj = (id)calloc(1, size); + if (!obj) return nil; + obj->initInstanceIsa(cls, hasCxxDtor); + } + else { + if (zone) { + obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size); + } else { + obj = (id)calloc(1, size); + } + if (!obj) return nil; + + // Use raw pointer isa on the assumption that they might be + // doing something weird with the zone or RR. + obj->initIsa(cls); + } + + if (cxxConstruct && hasCxxCtor) { + obj = _objc_constructOrFree(obj, cls); + } + + return obj; +} + +size_t instanceSize(size_t extraBytes) { + size_t size = alignedInstanceSize() + extraBytes; + // CF requires all objects be at least 16 bytes. + if (size < 16) size = 16; + return size; +} +``` + +用2种方式获取: + +- class_getInstanceSize([NSObject class]):8。返回实例对象的成员变量所占用的内存大小,一个空对象,只有 isa 指针,所以只有8字节 +- malloc_size((__bridge const void *)obj):16。Apple 规定,对象至少16个字节。但是只有一个 isa,所以只占用8个字节。 + 内存对齐:结构体的最终大小必须是最大成员的倍数。比如 + +```c +struct Person_IMPL { +struct NSObject_IMPL NSObject_IVARS; // 8字节 + int age; // 4字节 +}; +``` + + 8*2=16字节 + +## 四、类继承的本质 + +写一个最基础的类 + +```objectivec +@interface Person:NSObject +@end + +@implementation Person +@end +``` + +clang 转为 c 代码看看, `xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp` + +```c +struct NSObject_IMPL { + Class isa; +} + +struct Person_IMPL { + struct NSObject_IMPL NSObject_IVARS; +}; +``` + +如果创建一个继承自 Person 的 Student 类呢 + +```c +struct Student_IMPL { + struct Person_IMPL Person_IVARS; + NSInteger _age; + NSString *_name; +}; +``` + +首先我们可以知道一个 OC 类的属性可以写多种数据类型,那么大概率是 C 结构体实现。用 clang 转为 c 可以得到印证。另外存在类的继承关系的时候,子类结构体中第一个信息是父类结构体对象。其次是当前子类自己的信息。根节点一定是 NSObject_IMPL 结构体,且其中只有 `Class isa` + +观察 clang 转换后的 c 代码,发现 property 没有看到 setter、getter 方法。为什么这么设计? +方法不会存储到实例对象中去的。因为方法在各个对象中是通用的,方法存储在类对象的方法列表中。 + +```objectivec +@interface Person:NSObject +{ + int _height; + int _age; + int _weight; +} +@end + +@implementation Person +@end + +struct NSObject_IMPL { + Class isa; +}; + +struct PersonIMPL { + struct NSObject_IMPL ivars; + int _height; + int _age; + int _weight; +}; + +struct PersonIMPL person = {}; +Person *p = [[Person alloc] init]; +NSLog(@"%zd", class_getInstanceSize([Person class])); // 24,这个数值代表我们这个类,这个结构体,创建出来至少只需要24字节. +NSLog(@"%zd", sizeof(person)); // 24,这个数值代表我们这个类,这个结构体只需要24字节就够 +NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 32。iOS 系统会做优化,比如为了加速访问速度,会按照16的倍数进行分配。 +``` + +`class_getInstanceSize`这个数值代表我们这个类,这个结构体,创建出来至少只需要24字节. `malloc_size` iOS 系统会做优化,比如为了加速访问速度,会按照16的倍数进行分配。 +iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内存的时候也存在内存对齐。 +GUN 都存在内存对齐这个概念。 +sizeof 是运算符。 + +实例对象: +类对象:isa、superclass、属性信息、对象方法信息、协议信息、成员变量信息... +元类对象:存储 isa、superclass、类方法信息... +一个实例对象只有一个类对象,一个实例对象只有一个元类对象。 `class_isMetaClass()`判断一个类是否为元类对象 + +`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/objc-isa.png) + +instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用 +class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/objc-superclass.png) + +当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。 +当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。 + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-metaclass-superclass.png) + +当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。 + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/class-isa-superclass.png) + +```objectivec +@interface Student : NSObject +@end + +@implementation Person +@end + +@interface NSObject(TestMessage) +@end + +@implementation NSObject(TestMessage) + +- (void)test +{ + NSLog(@"%s", __func__); +} +@end +``` + +奇怪的是,我们给 Student 类对象调用 test 方法,`[Student test]` 则调用成功。是不是很奇怪?站在面向对象的角度出发,Student 类对象根据 isa 找到元对象,此时元对象方法列表中没有类对象,所以根据 superclass 找到 NSObject 元类对象,发现元类对象自身也没有类方法。但是为什么调用了 `-(void)test` 对象方法? + +因为NSObject 元类对象的 superClass 继承自 NSObject 的类对象,类对象是存储对象方法的,所以定义在 NSObject 分类中的 `-(void)test` 最终会被调用。 + +从64位开始,iOS 的 isa 需要与 ISA_MASK 进行与位运算。& ISA_MASK 才可以得到真正的类对象地址。 +为了打印和研究类对象中的 superclass、isa + +```objectivec +// 实例对象 +Person *p = [[Person alloc] init]; +Student *s = [[Student alloc] init]; +// 类对象 +class pclass = object_getClass(p); +class sclass = object_getClass(s); +// Mock 系统结构体 object_class +struct mock_object_class { + class isa; + class superclass; +}; +// 转换如下 +struct mock_object_class *person = (__bridge mock_object_class *)[[Person alloc] init]; +struct mock_object_class *student = (__bridge mock_object_class *)[[Student alloc] init]; +``` + +如何查看类真正的结构?在 Xcode 中打印出来 +思路:查看 Class 内部的数据,发现是 struct,所以我们自己定义一个 struct,去承接类对象的元类对象信息 + +```c +#import + +#ifndef MockClassInfo_h +#define MockClassInfo_h + +# if __arm64__ +# define ISA_MASK 0x0000000ffffffff8ULL +# elif __x86_64__ +# define ISA_MASK 0x00007ffffffffff8ULL +# endif + +#if __LP64__ +typedef uint32_t mask_t; +#else +typedef uint16_t mask_t; +#endif +typedef uintptr_t cache_key_t; + +struct bucket_t { + cache_key_t _key; + IMP _imp; +}; + +struct cache_t { + bucket_t *_buckets; + mask_t _mask; + mask_t _occupied; +}; + +struct entsize_list_tt { + uint32_t entsizeAndFlags; + uint32_t count; +}; + +struct method_t { + SEL name; + const char *types; + IMP imp; +}; + +struct method_list_t : entsize_list_tt { + method_t first; +}; + +struct ivar_t { + int32_t *offset; + const char *name; + const char *type; + uint32_t alignment_raw; + uint32_t size; +}; + +struct ivar_list_t : entsize_list_tt { + ivar_t first; +}; + +struct property_t { + const char *name; + const char *attributes; +}; + +struct property_list_t : entsize_list_tt { + property_t first; +}; + +struct chained_property_list { + chained_property_list *next; + uint32_t count; + property_t list[0]; +}; + +typedef uintptr_t protocol_ref_t; +struct protocol_list_t { + uintptr_t count; + protocol_ref_t list[0]; +}; + +struct class_ro_t { + uint32_t flags; + uint32_t instanceStart; + uint32_t instanceSize; // instance对象占用的内存空间 +#ifdef __LP64__ + uint32_t reserved; +#endif + const uint8_t * ivarLayout; + const char * name; // 类名 + method_list_t * baseMethodList; + protocol_list_t * baseProtocols; + const ivar_list_t * ivars; // 成员变量列表 + const uint8_t * weakIvarLayout; + property_list_t *baseProperties; +}; + +struct class_rw_t { + uint32_t flags; + uint32_t version; + const class_ro_t *ro; + method_list_t * methods; // 方法列表 + property_list_t *properties; // 属性列表 + const protocol_list_t * protocols; // 协议列表 + Class firstSubclass; + Class nextSiblingClass; + char *demangledName; +}; + +#define FAST_DATA_MASK 0x00007ffffffffff8UL +struct class_data_bits_t { + uintptr_t bits; +public: + class_rw_t* data() { + return (class_rw_t *)(bits & FAST_DATA_MASK); + } +}; + +/* OC对象 */ +struct mock_objc_object { + void *isa; +}; + +/* 类对象 */ +struct mock_objc_class : mock_objc_object { + Class superclass; + cache_t cache; + class_data_bits_t bits; +public: + class_rw_t* data() { + return bits.data(); + } + + mock_objc_class* metaClass() { + return (mock_objc_class *)((long long)isa & ISA_MASK); + } +}; + +#endif /* MockClassInfo_h */ + +Student *stu = [[Student alloc] init]; +stu->_weight = 10; + +mock_objc_class *studentClass = (__bridge mock_objc_class *)([Student class]); +mock_objc_class *personClass = (__bridge mock_objc_class *)([Person class]); + +class_rw_t *studentClassData = studentClass->data(); +class_rw_t *personClassData = personClass->data(); + +class_rw_t *studentMetaClassData = studentClass->metaClass()->data(); +class_rw_t *personMetaClassData = personClass->metaClass()->data(); +``` + +## 五、 内存对齐 + +Demo1 + +```objectivec +@interface Person : NSObject +{ + int _age; + int _height; +} +@end + +struct Person_IMPL { + Class isa; + int _age; + int _height; +}; + +Person *person = [[Person alloc] init]; +NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 16 +NSLog(@"%zd", sizeof(struct Person_IMPL)); // 16 +``` + +isa 指针8字节 + int _age 4字节 + _hright 字节 = 16 字节 + + Demo2 + +```objectivec +@interface Person : NSObject +{ + int _age; + int _height; + int _no; +} +@end + +struct Person_IMPL { + Class isa; + int _age; + int _height; + int _no; +}; + +Person *person = [[Person alloc] init]; +NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 32 +NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24 +``` + +isa 指针8字节 + int _age 4字节 + _hright 字节 + _no 4 字节 = 20 字节,因为存在内存对齐,因为结构体本身对齐内存对齐,必须为8的倍数,所以占据24个字节的内存。 + +结构体占据24字节,为什么运行起来后通过 `malloc_size` 得到32个字节?这个涉及到运行时内存对齐。规定 **iOS 中内存对齐以 16 的倍数为准**。 + + Demo + +```objectivec +void *temp = malloc(4); +NSLog(@"%zd", malloc_size(temp)); +// 16 +``` + +可以看到 malloc 申请了4个字节,但是打印却看到16个字节。 + +查看源码也可以出来分配内存最小是以16的倍数为基准进行分配的。 + +```c +#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */ +``` \ No newline at end of file diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md index 9f70b75..f81c212 100644 --- a/Chapter1 - iOS/1.74.md +++ b/Chapter1 - iOS/1.74.md @@ -58,7 +58,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di ![卡顿原因](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-ios_frame_drop.png) -VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃,等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。 +VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。 目前 iOS 设备有双缓存机制,也有三缓冲机制,Android 现在主流是三缓冲机制,在早期是单缓冲机制。 [iOS 三缓冲机制例子](https://ios.developreference.com/article/12261072/Metal+newBufferWithBytes+usage) @@ -553,8 +553,6 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod 这样情况下,业务方可以针对特定的业务流程做性能统计分析 - - ## 二、 App 启动时间监控 ### 1. App 启动时间的监控 @@ -773,6 +771,8 @@ for (int i = 0; i < threadCount; i++) { ## 四、 OOM 问题 +大多数情况下 OOM 问题比 crash 问题更严重,线上稳定性问题主要是由于 OOM 造成的,因为线下可以利用 Xcode 的一些工具解决并定位 crash。线上也有类似 KSCrash 这样的优秀监控工具。但是 OOM 方面线下只有一些三方的工具,这些工具大多是基于 OC 对象引用关心实现的,所以只能判断 OC 对象。另外比较耗费性能,所以线上 OOM 问题还是比较多的。 + ### 1. 基础知识准备 硬盘:也叫做磁盘,用于存储数据。你存储的歌曲、图片、视频都是在硬盘里。 @@ -1624,6 +1624,51 @@ for 循环打印出每个进程(也就是 App)的 pid、Priority、User Data ### 4. 如何判定发生了 OOM +首先我们无法通过正常手段监控 OOM,因为 XNU 源码显示发生 OOM 发送的信号为 SIGKILL,该信号无法监控。 + +```c +/* + * The jetsam no frills kill call + * Return: 0 on success + * error code on failure (EINVAL...) + */ +static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason) { + int error = 0; + error = exit_with_reason(p, W_EXITCODE(0, SIGKILL), (int *)NULL, FALSE, FALSE, jetsam_flags, jetsam_reason); + return error; +} +``` + +FacekBook 提出排除法监控 OOM。 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Facebook-OOM.jpeg) + +- App 更新了版本 + +- App 发生了崩溃 + +- 用户手动退出 + +- 操作系统更新了版本 + +- App 切换到后台之后进程终止 + +其实不够全,存在误判,增加下面几种 case + +- 覆盖安装 + +- WatchDog 崩溃 + +- 后台启动 + +- XCTest/UITest 等自动化测试框架驱动 + +- 应用 exit 主动退出 + + + + + OOM 导致 crash 前,app 一定会收到低内存警告吗? 做 2 组对比实验: @@ -2793,49 +2838,47 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 } @end - ``` @implementation NetworkDelegateProxy #pragma mark - life cycle -+ (instancetype)sharedInstance { - static NetworkDelegateProxy *_sharedInstance = nil; + ``` + +- (instancetype)sharedInstance { + static NetworkDelegateProxy *_sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - - _sharedInstance = [NetworkDelegateProxy alloc]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _sharedInstance = [NetworkDelegateProxy alloc]; + }); - + return _sharedInstance; + } - + #pragma mark - public Method -+ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate - { - NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance]; - instance->_originalTarget = originalTarget; - instance->_NewDelegate = newDelegate; - return instance; +- (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate { + NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance]; + instance->_originalTarget = originalTarget; + instance->_NewDelegate = newDelegate; + return instance; } -- (void)forwardInvocation:(NSInvocation *)invocation - { - if ([_originalTarget respondsToSelector:invocation.selector]) { + +- (void)forwardInvocation:(NSInvocation *)invocation { + if ([_originalTarget respondsToSelector:invocation.selector]) { [invocation invokeWithTarget:_originalTarget]; - [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation]; + [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation]; } } -- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel - { - return [_originalTarget methodSignatureForSelector:sel]; +- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ + return [_originalTarget methodSignatureForSelector:sel]; } - @end ``` @@ -6139,7 +6182,7 @@ file_names[ 4]: <起始地址> <结束地址> <函数> [<文件名:行号>] ``` -#### 4.4 **如何获取地址?** +#### 4.4 如何获取地址 image 加载的时候会进行相对基地址进行重定位,并且每次加载的基地址都不一样,函数栈 frame 的地址是重定位后的绝对地址,我们要的是重定位前的相对地址。 @@ -6358,21 +6401,21 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化 Weex 由于历史原因,不做性能监控了,现有业务代码继续跑着,只业务问题和稳定性问题。 -## Weex 异常监控 +### Weex 异常监控 -### 背景 +#### 背景 Weex 由于历史原因,不能很好的统计异常。比如在页面的模版代码中绑定了 data 中的一个对象,此时对象可能并没有值,而是依赖后续的网络请求完成,对象才有了具体的值 data 改变,数据驱动,页面再次 render。所以监控代码会认为第一次 render 的时候访问对象不存在的属性。 真正有问题的代码和不影响业务的异常信息,都会被 Vue 官方认为是一场。基于这样的背景,我们无法 pick 出真正异常或者是开发者判空代码没写好的问题。 -### 解决思路 +#### 解决思路 按照异常的等级,可以划分为影响业务和不影响业务。问题来了,什么叫做“影响业务”?这是我们自己定义的词,也就是影响用户是否正常操作 App。比如说:页面白屏、点击某个按钮无响应等等。定义为 Error。其他不影响业务的定义为 Warning。 -### 技术实现 +#### 技术实现 -#### Warning 异常 +##### Warning 异常 采用主流方案,React、Vue 都提供了框架自己的异常监控方案,由于 Weex 是在 Vue 基础上实现。包括 Native 和 Vue 的 UI 双线程机制、事件机制等等。其余我们不关心,Weex 开发中,开发和都是写 Vue 去实现页面和逻辑,所以我们监控 Vue 的异常就满足了。 @@ -6417,7 +6460,7 @@ Weex 由于历史原因,不能很好的统计异常。比如在页面的模版 componentName = this.fetchComponentName(vm) componentPath = this.fetchComponentPath(vm) } - + let errorInfo = { name: err.name, reason: err.message, @@ -6442,13 +6485,13 @@ export default APM; 由于 Weex 代码是单独可运行和部署的,因此前端没有统一的入口,所以在发布阶段监控代码需要配合打包机,利用脚本动态插入到页面代码中。 -#### Error 监控 +##### Error 监控 根据我们对“影响业务”的定义,Error 包括:页面白屏 + 事件无法响应。 所以我们来拆解下问题。 -##### 页面白屏 +###### 页面白屏 根据 Weex SDK 整个完整流程得出,白屏包括以下几个可能: @@ -6465,14 +6508,13 @@ export default APM; return; } ``` - - + - Weex 资源网络请求成功,但是数据内容为空 ```objectivec _mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) { // ... - + if (!data) { // Weex 资源网络请求成功,但是数据内容为空。也就是下载下来的资源无法消费,页面无法正常渲染 NSString *errorMessage = [NSString stringWithFormat:@"Request to %@ With no data return", WeexSafeString(request.URL)]; @@ -6484,14 +6526,13 @@ export default APM; } } ``` - - + - Weex 资源网络请求成功,但是数据 JSON 序列化失败 ```objectivec _mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) { // ... - + NSString *jsBundleString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; if (!jsBundleString) { // Weex 资源网络请求成功,但是网络数据 JSON 序列化失败,也就是下载下来的资源无法消费,页面无法正常渲染 @@ -6503,8 +6544,7 @@ export default APM; } } ``` - - + - Weex 资源网络请求异常(网络请求 _mainBundleLoader.onFailed) ```objectivec @@ -6518,10 +6558,8 @@ export default APM; // ... }; ``` - - -##### 点击事件无响应 +###### 点击事件无响应 调试 WeexSDK 发现 SDK 内部通过给 JSContext 注册了 `callNativeModule` 方法来实现调用 Native Module 的 Method。 @@ -6555,7 +6593,7 @@ export default APM; }; [[WeexAPM sharedInstance] reportError:errorDict]; } @finally { - + } } } @@ -6604,7 +6642,7 @@ export default APM; } ``` -#### JS ExceptionHandler +##### JS ExceptionHandler JS 引擎中异常回调中上报错误 @@ -6636,6 +6674,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 统计线上数据发现,Weex 页面渲染失败率为4.09%,所以看上去页面渲染失败率还是蛮高的。目前发现失败率最高的场景为 App 启动时候按照功能模块组织的配置清单。 类似于下面的配置: + ``` "//goods/detail": { "configParams": "", @@ -6648,18 +6687,15 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 因此,针对于这样的场景。我们希望针对 Weex 资源的拉取和访问机制做一些优化。 目前有几个流程不太合理: + 1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分,没法实际统计加载页面时有多少JS获取失败的情况。 2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题 3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache,并且会做大量的预加载,但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载,可能还会造成 OOM 问题 -![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeexResourcePull.png) + ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeexResourcePull.png) // todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能,比如某个收银员角色固定的情况下,他日常的操作行为是固定的,商品扫码、开单) - - -### 2. Flutter 异常监控 - - +#### 2. Flutter 异常监控 ## 九、子线程 UI 监控 @@ -6697,8 +6733,6 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 具体可以参考这个 [Demo](https://github.com/FantasticLBP/MainThreadChecker) - - ## 十、页面渲染时长统计 当我们的产品经理、TL、领导或者任何关心我们产品质量的某个角色问你,你们的 App 看上去好像比较卡,很难用,这时候我们心里一阵空虚,卡吗? @@ -6747,10 +6781,8 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 另外随着问题的暴露或者技术研究的渗入,监控方案本身是可以迭代演进的。 - 1. 8060 会有特例,比如8060满足了,且此时主线程空了。因为某个 ImageView 在子线程上根据 URL 异步请求资源,之后会再次触发渲染。所以这个情况下,方案还是存在问题的 - ## 十一、 APM 小结 1. 通常来说各个端的监控能力是不太一致的,技术实现细节也不统一。所以在技术方案评审的时候需要将监控能力对齐统一。每个能力在各个端的数据字段必须对齐(字段个数、名称、数据类型和精度),因为 APM 本身是一个闭环,监控了之后需符号化解析、数据整理,进行产品化开发、最后需要监控大盘展示等 diff --git a/Chapter1 - iOS/1.81.md b/Chapter1 - iOS/1.81.md index 86b2d31..ccce7ed 100644 --- a/Chapter1 - iOS/1.81.md +++ b/Chapter1 - iOS/1.81.md @@ -24,7 +24,7 @@ int main(int argc, const char * argv[]) { 在 foo 方法里面下断点,见下图 -![rename symbol](./../assets/2020-02-25-asm.png) +![rename symbol](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-25-asm.png) 可以看到,`foo` 方法的 symbol 被变为 `@杭城小刘`,变量 `age` 被变为 `objc_age`。 @@ -43,7 +43,7 @@ int main(int argc, char * argv[]) { } ``` -![App main 方法 rename 失败](./../assets/2020-02-25-asm2.png) +![App main 方法 rename 失败](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-25-asm2.png) 可以看到 App 工程主入口函数 `main` 函数,想修改为 `mook_main`。但是报错 `ld: entry point (_main) undefined. fir architecture x86_64` diff --git a/Chapter1 - iOS/1.82.md b/Chapter1 - iOS/1.82.md index 55a6488..1844d91 100644 --- a/Chapter1 - iOS/1.82.md +++ b/Chapter1 - iOS/1.82.md @@ -2,116 +2,2430 @@ > 做很多需求或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写一些场景和源码分析相关的文章。 -## 场景 +## isa 本质 -最简单的一个场景就是防止按钮多次点击吧,比如短时间内点击了多次按钮,可以用「节流」来实现。用到的技术是 runtime。再举一个例子,比如无痕埋点的实现里面对各种控件的点击、页面的跳转等也需要用到 runtime,想看无痕埋点的设计与实现,可以看我这篇[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.55.md)。 +在 arm64 架构之前,isa 就是一个普通的指针,存储着 Class或Meta-Class 对象的内存地址。 -这里不得不提的一个知识点就是为什么给类或者对象进行 hook 的操作,要放到 load 方法中进行了。 +在 arm64 之后,对 isa 进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。 - - -## 一、 load 和 initialize 方法 - -**load 方法** - -对于加入运行期系统中的每个类及分类来说,必定会调用 load 方法,而且仅调用1次。当包含类或分类的程序库载入系统时(通常指应用程序启动的时候),就会执行 load 方法。 - -- 类的 load 方法会在它所有父类 load 方法后调用 -- 分类的 load 方法会在类本身的 load 方法后调用 -- load 方法不遵循继承 -- load 方法内部的实现必须简单,如果逻辑太复杂有可能会导致阻塞 - -load 方法有个需要注意的地方:执行该方法时,运行期系统处于脆弱状态。在执行子类的 load 方法之前,必须先执行完所有的超类的 load 方法,假如代码中还依赖了其他的程序库,那么程序库里的相关 load 方法也会先执行。在开发中,load 方法中使用其他类是不安全的。 - -```objective-c -@implentation ClassB -+ (void)load +```c +union isa_t { - ClassA *classA = [[ClassA alloc] init]; - [classA setUp]; -} -@end + Class cls; + uintptr_t bits; + # if __arm64__ +# define ISA_MASK 0x0000000ffffffff8ULL +# define ISA_MAGIC_MASK 0x000003f000000001ULL +# define ISA_MAGIC_VALUE 0x000001a000000001ULL + struct { + uintptr_t nonpointer : 1; + uintptr_t has_assoc : 1; + uintptr_t has_cxx_dtor : 1; + uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 + uintptr_t magic : 6; + uintptr_t weakly_referenced : 1; + uintptr_t deallocating : 1; + uintptr_t has_sidetable_rc : 1; + uintptr_t extra_rc : 19; +# define RC_ONE (1ULL<<45) +# define RC_HALF (1ULL<<18) + }; +}; ``` -上面的代码不太推荐且不太安全,因为你没办法确定在执行 ClassB 的 load 方法的时候, ClassA 是否已经被加载到系统中。 +struct 内部的成员变量可以指定占用内存位数, `uintptr_t nonpointer : 1` 代表占用1个字节 -load 方法并不像普通方法那样具备继承规则。普通的方法在面向对象程序设计中,父类的方法、属性等都会在子类中存在,假如 Person 类有 eat、sleep 方法,Student 类继承子 Person 类,虽然 Person 类没有重写 eat 方法,但是你给 Student 类发送 eat 消息是可以响应的,因为方法被继承了。 Load 方法就不会,假如子类没有 load 方法,不管超类是否有 load 方法,子类都不会调用 load 方法。 +其中,结构体里面的属于”位域“ -load 方法内代码逻辑必须精简。因为整个应用程序在执行 load 方法的时候会被阻塞。如果 load 方法中包含繁杂的代码,那么应用程序可能会变得无法响应。更加不要使用锁。想通过 load 方法在类加载之前做些操作的,都属于错误的打开方式。它真正的用法应该是 Debug 吧,比如在 Category 中判断当前分类是否被成功 load 进去。 +- nonpointer:0,代表普通的指针,存储着Class、Meta-Class对象的内存地址;1,代表优化过,使用位域存储更多的信息 +- has_assoc:是否有设置过关联对象,如果没有,释放时会更快 +- has_cxx_dtor:是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快 -**inintialize 方法** +- shiftcls:存储着Class、Meta-Class对象的内存地址信息 -对于每个类来说,该方法会在程序首次调用该类之前调用,且只调用一次。它是由运行时系统来调用的,不应该通过代码的方式直接调用。 +- magic:用于在调试时分辨对象是否未完成初始化 +- weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快 +- deallocating:对象是否正在释放 -与 load 的区别: +- extra_rc:里面存储的值是引用计数器减1(刚创建出的对象,查看这个信息位0,因为存储着-1之后的引用计数) -- 惰性调用的。也就是说某个类的 initialize 方法也许永远不会被调用,当且仅当程序用到了该类的时候才会调用。 load 是加载进 runtime 肯定会调用,initizlize 第一次使用前会被调用。对 load 来说,应用程序必须阻塞并且等着所有类 load 方法执行完毕才可以继续。 -- 运行期系统在执行 initialize 方法时,是处于正常状态的,因此从运行期完整度方面来讲,此时可以安全使用并调用任何类的任意方法,而且 runtime 保证了在 initizlize 方法时期一定会在一个线程安全的环境中执行,也就是说执行 initialize 方法的这个线程可以操作类或者实例,其他线程先阻塞,等待执行完毕 -- initialize 同其他方法一样,如果某个类并未实现它,而其实现了,那么就会运行超类实现的代码。 +- has_sidetable_rc:引用计数器是否过大无法存储在isa中;如果为1,那么引用计数会存储在一个叫SideTable的类的属性中 -```objective-c -@implentation AClass -+ (void)initialize +上面说的更快,是如何得出结论的? + +查看 objc4 源代码看到对象执行销毁函数的时候会判断对象是否有关联对象、析构函数,有的话分别调用析构函数、移除关联对象等逻辑。 + +```c +/*********************************************************************** +* objc_destructInstance +* Destroys an instance without freeing memory. +* Calls C++ destructors. +* Calls ARC ivar cleanup. +* Removes associative references. +* Returns `obj`. Does nothing if `obj` is nil. +**********************************************************************/ +void *objc_destructInstance(id obj) { - NSLog(@"%@ initialize", self); + if (obj) { + // Read all of the flags at once for performance. + bool cxx = obj->hasCxxDtor(); + bool assoc = obj->hasAssociatedObjects(); + + // This order is important. + if (cxx) object_cxxDestruct(obj); + if (assoc) _object_remove_assocations(obj); + obj->clearDeallocating(); + } + + return obj; } -@end - -@interface BClass:AClass +``` -@end -@implentation BClass +isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class(类对象、元类对象) 真正的地址 -@end +`0x0000000ffffffff8ULL` 用程序员模式打开计算器 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/objc-isa-mask.png) + +其中,结构体中的数据存放大体是下面的结构: + +extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer + +知道结构体可以指定存储大小这个功能后,可以看到 isa_t 联合体与 ISA_MASK 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象) + +如果要找出下面中间的 `1010` 如何实现?按位与即可,且要找的位置补充位1,其他位置为0 + +```shell +0b0010 1000 + +0b0011 1100 +----------- +0b0010 1000 +``` + +结论:**根据按位与的效果。`ISA_MASK` 的后3位都是0,所以我们找到的类地址二进制表示时后3位一定为0** + +我们可以验证下 + +```objectivec +Person *p = [[Person alloc] init]; +NSLog(@"%p", [p class]); // 0x1000081d8 +NSLog(@"%p", object_getClass([Person class])); // 0x100008200 +NSLog(@"%p", object_getClass([NSObject class])); // 0x7ff84cb29fe0 +NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28 +``` + +为什么有的结尾是8? + +16进制的8转为二进制,`0x1000` + +关于这部分的调试,需要在真机上运行,真机上 arm64,拷贝对象地址到系统自带的运算器(程序员模式),查看64位地址。按照下面的顺序一一查看 + +`extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer` + +所以可以根据 isa 信息查看对象是否创建过关联对象、有没有设置弱引用、 + +## 模仿系统位运算设计 API + +系统很多 API 都有位或运算。比如 KVO 中的 options,可以传递 `NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld` ,那么系统是如何知道我到底传递了哪几个值? + +按位或运算 + +```shell +0b0000 0001 // 1 +0b0000 0010 // 2 +0b0000 0100 // 4 +------------ +0b0000 0111 // 7 +``` + +可以看到上面3个数,按位或之后的结果为 `0b0000 0111` + +按位与运算。 + +```shell +0b0000 0111 +0b0000 0001 +----------- +0b0000 0001 + +0b0000 0111 +0b0000 0010 +----------- +0b0000 0010 + +0b0000 0111 +0b0000 0100 +----------- +0b0000 0100 + +0b0000 0111 +0b0000 1000 +----------- +0b0000 0000 +``` + +我们发现上面3个数按位或之后的数字,分别与每个数按位与,得到的结果就是数据本身。 + +与一个不是3个数之一的数按位与,得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值 + +```objectivec +typedef enum { + OptionsEast = 1<<0, // 0b0001 + OptionsSouth = 1<<1, // 0b0010 + OptionsWest = 1<<2, // 0b0100 + OptionsNorth = 1<<3 // 0b1000 +} Options; + +- (void)setOptions:(Options)options +{ + if (options & OptionsEast) { + NSLog(@"我自东边来"); + } + + if (options & OptionsSouth) { + NSLog(@"我自南边来"); + } + + if (options & OptionsWest) { + NSLog(@"我自西边来"); + } + + if (options & OptionsNorth) { + NSLog(@"我自北边来"); + } +} +[self setOptions: OptionsWest | OptionsNorth]; +// 我自西边来 +// 我自北边来 +``` + +## 类对象 Class 的结构 + +查看 objc4 源代码看看 + +```c +struct objc_object { +private: + isa_t isa; +} + +struct objc_class : objc_object { + // Class ISA; + Class superclass; + cache_t cache; // formerly cache pointer and vtable + class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags +}; +``` + +结构体继承于 `objc_object` 等同于下面代码 + +```c +struct objc_class : objc_object { + isa_t isa; + Class superclass; + cache_t cache; // 方法缓存 + class_data_bits_t bits; // 用于获取具体的类信息 +}; +``` + +```c +struct class_rw_t { + // Be warned that Symbolication knows the layout of this structure. + uint32_t flags; + uint32_t version; + + const class_ro_t *ro; + + method_array_t methods; // 方法列表 + property_array_t properties; // 属性列表 + protocol_array_t protocols; // 协议列表 + + Class firstSubclass; + Class nextSiblingClass; + + char *demangledName; +}; + +struct class_data_bits_t { + // Values are the FAST_ flags above. + uintptr_t bits; +public: + + class_rw_t* data() { + return (class_rw_t *)(bits & FAST_DATA_MASK); + } +} +``` + +可以看到 `objc_class` 获取 bits 里的真实数据需要经过按位与 `FAST_DATA_MASK` + +```c +struct class_ro_t { + uint32_t flags; + uint32_t instanceStart; + uint32_t instanceSize; // instance 对象占用的内存空间 +#ifdef __LP64__ + uint32_t reserved; +#endif + + const uint8_t * ivarLayout; + + const char * name; // 类名 + method_list_t * baseMethodList; + protocol_list_t * baseProtocols; + const ivar_list_t * ivars; // 成员变量列表 + + const uint8_t * weakIvarLayout; + property_list_t *baseProperties; + + method_list_t *baseMethods() const { + return baseMethodList; + } +}; +``` + +具体关系整理如下图 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-class.png) + +说明: + +- `class_rw_t`里面的 methods、properties、protocols 是数组(数组元素是也是方法组成的 Array),是可读可写的,包含了类的初始内容、分类的内容。 -// log -AClass initialize -BClass initialize -``` - -基于上述特点,我们一般需要 initialize 方法中做些判断,如下 - -```objective-c -+ (void)initialize -{ - if (self == [ACLass class]) { - // 确定了我是 AClass 的 initialize 方法执行时期 + 为什么不是二维数组?因为Array 中的子 Array长度不一致,且不能补空 + + ![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-class-rw-t.png) + + ```c + static void remethodizeClass(Class cls) + { + category_list *cats; + bool isMeta; + + runtimeLock.assertWriting(); + + isMeta = cls->isMetaClass(); + + // Re-methodizing: check for more categories + if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { + if (PrintConnecting) { + _objc_inform("CLASS: attaching categories to class '%s' %s", + cls->nameForLogging(), isMeta ? "(meta)" : ""); + } + + attachCategories(cls, cats, true /*flush caches*/); + free(cats); + } } + static void + attachCategories(Class cls, category_list *cats, bool flush_caches) + { + if (!cats) return; + if (PrintReplacedMethods) printReplacements(cls, cats); + + bool isMeta = cls->isMetaClass(); + + // fixme rearrange to remove these intermediate allocations + method_list_t **mlists = (method_list_t **) + malloc(cats->count * sizeof(*mlists)); + property_list_t **proplists = (property_list_t **) + malloc(cats->count * sizeof(*proplists)); + protocol_list_t **protolists = (protocol_list_t **) + malloc(cats->count * sizeof(*protolists)); + + // Count backwards through cats to get newest categories first + int mcount = 0; + int propcount = 0; + int protocount = 0; + int i = cats->count; + bool fromBundle = NO; + while (i--) { + auto& entry = cats->list[i]; + + method_list_t *mlist = entry.cat->methodsForMeta(isMeta); + if (mlist) { + mlists[mcount++] = mlist; + fromBundle |= entry.hi->isBundle(); + } + + property_list_t *proplist = + entry.cat->propertiesForMeta(isMeta, entry.hi); + if (proplist) { + proplists[propcount++] = proplist; + } + + protocol_list_t *protolist = entry.cat->protocols; + if (protolist) { + protolists[protocount++] = protolist; + } + } + + auto rw = cls->data(); + + prepareMethodLists(cls, mlists, mcount, NO, fromBundle); + rw->methods.attachLists(mlists, mcount); + free(mlists); + if (flush_caches && mcount > 0) flushCaches(cls); + + rw->properties.attachLists(proplists, propcount); + free(proplists); + + rw->protocols.attachLists(protolists, protocount); + free(protolists); + } + ``` + + 查看 objc4 源码发现针对类自身信息、Category 信息会进行组合。 + +- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容 + + ![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-class-ro-t.png) + +## Method_t + +`method_t` 是对方法\函数的封装 + +```c +struct method_t { + SEL name; // 函数名、方法名 + const char *types; // 编码(返回值类型、参数类型) + IMP imp; // 指向函数的指针(函数地址) } ``` +`IMP` 代表函数的具体实现 - -经常说不要在 App 中写太多的 load 方法,会影响 App 的启动时间。原因就是 load 方法的执行特点决定的。某个类的 load 方法执行是在它所有父类 load 方法执行后执行的,该类的分类的 load 方法执行是在当前类 load 方法之后执行的。如果某个类的 load 方法中引用了其他的类或者其他库的代码,则该类的 load 方法必须是其他类或者其他库中类的 load 方法执行后执行,所以类的 load 方法中最好做本类相关的逻辑,比如 runtime method swizzling。 - - -TagPointerString 不走消息转发 -CFString 走消息转发 - -Objective-C 方法调用则先通过对象的 isa 找到类对象,然后根据类对象的 cache_t 查找方法缓存列表,根据 sel mask 去计算 index,这个 index 代表当前方法缓存在哈希表中的下标索引。 sel 比较,如果没命中,则继续走 objc_msgSend_uncached 流程。 - -1ookUpImpOrForward : 1. 当前类对象中的方法列表中遍历方法列表;2. 继承链中 superClass 遍历查找,一直到根部 NSObject;3. 动态特性: -- 动态方法解析,动态的添加一个方法,在方法列表中新建一个 SEL 和对应的 IMP (resolveInstanceMethod、resolveClassMethod) - -- 重定向 ` - (id)forwardingTargetForSelector:(SEL)aSelector` - +```c +typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); ``` + +`SEL` 代表方法、函数名,一般叫做选择器,底层结构跟 `char *` 类似 + +```c +typedef struct objc_selector *SEL; +``` + +- 可以通过 `@selector()` 和 `sel_registerName()` 获得 + +- 可以通过 `sel_getName()` 和 `NSStringFromSelector()` 转成字符串 + +- 不同类中相同名字的方法,所对应的方法选择器是相同的 + +`types` 包含了函数返回值、参数编码的字符串。`返回值|参数1|参数2| ... | 参数n` + +**Type Encoding** + +iOS 中提供了一个叫做 `@encode` 的指令,可以将具体的类型表示成字符串编码 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-method-encoding.png) + +```objectivec +- (int)calcuate:(int)age heigith:(float)height; +``` + +比如这个方法的 type encoding 为 `i24@0:8i16f20` + +解读下,上面的方法其实携带了2个基础参数。 + +`(id)self _cmd:(SEL)_cmd` + +`i` 代表方法返回值为 int + +`24` 代表参数共占24个字节大小。4个参数分别为 id 类型的 `self`、`SEL` 类型的 `_cmd`, int 类型的 age、float 类型的 height。8+8+4+4 共24个字节(id、SEL 都为指针,长度为8) + +`@` 代表第一个参数为 object 类型,从第0个字节开始 + +`:`代表第二个参数为 SEL,从第8个字节开始 + +`i` 代表第三个参数为 int,从第16个字节开始 + +`f` 代表第四个参数为 float,从第20个字节开始 + +## 方法缓存 + +调用方法的本质,比如说对象方法,先根据对象的 isa 找到类对象,在类对象的 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 methods 方法数组(Array 中的元素是方法 Array),效率较低,所以为了方便,给类设置了方法缓存。比如调用 Student 对象的 eat 方法,eat 在 student 中不存在,通过 isa 不断找,在 Person 类中找到了,则将 Person 类中的 eat 方法缓存在 Student 的 `cache_t` 类型的 cache 中。 + +`Class` 内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度 + +所以完整结构为:先根据对象的 isa 找到类对象,在类对象的 cache 列表中查找方法实现,如果找不到,则去 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 cache 中查找,找到则调用方法,同时将父类 cache 缓存中的方法,在子类的 cache 中缓存一边。父类 cache 没找到,则在 methods 方法数组(Array 中的元素是方法 Array)查找,找到则调用,同时在子类 cache 中缓存一份。父类 methods 方法数组(Array 中的元素是方法 Array)没找到则继续调用 superclass,依次类推 + +```c +struct cache_t { + struct bucket_t *_buckets; // 散列表 + mask_t _mask; // 散列表的参数 -1 + mask_t _occupied; // 已经缓存的方法数量 +} +``` + +```c +struct bucket_t { +private: + cache_key_t _key; // SEL 作为 key + IMP _imp; // 函数的内存地址 +} +``` + +`_buckets` -> | bucket_t |bucket_t |bucket_t |bucket_t |... + +方法缓存查找原理,散列表查找 + +objc4 源码 `objc-cache.mm` + +```c +bucket_t * cache_t::find(cache_key_t k, id receiver) +{ + assert(k != 0); + + bucket_t *b = buckets(); + mask_t m = mask(); + mask_t begin = cache_hash(k, m); + mask_t i = begin; + do { + if (b[i].key() == 0 || b[i].key() == k) { + return &b[i]; + } + } while ((i = cache_next(i, m)) != begin); + + // hack + Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); + cache_t::bad_cache(receiver, (SEL)k, cls); +} +``` + +散列表不够了,则会哈希拓容,此时缓存会释放 `cache_collect_free` + +```c +void cache_t::expand() +{ + cacheUpdateLock.assertLocked(); + uint32_t oldCapacity = capacity(); + uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; + if ((uint32_t)(mask_t)newCapacity != newCapacity) { + // mask overflow - can't grow further + // fixme this wastes one bit of mask + newCapacity = oldCapacity; + } + reallocate(oldCapacity, newCapacity); +} + +void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) +{ + bool freeOld = canBeFreed(); + bucket_t *oldBuckets = buckets(); + bucket_t *newBuckets = allocateBuckets(newCapacity); + // Cache's old contents are not propagated. + // This is thought to save cache memory at the cost of extra cache fills. + // fixme re-measure this + assert(newCapacity > 0); + assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1); + setBucketsAndMask(newBuckets, newCapacity - 1); + if (freeOld) { + cache_collect_free(oldBuckets, oldCapacity); + cache_collect(false); + } +} +``` + +哈希查找元素核心是一个求 key 的过程,Java 中是求余,iOS 中是按位与 `key & mask`。 + +```c +static inline mask_t cache_hash(cache_key_t key, mask_t mask) +{ + return (mask_t)(key & mask); +} +``` + +空间换时间的一个实现。 + +查找类的方法缓存 Demo + +```objectivec +#import + +#ifndef MockClassInfo_h +#define MockClassInfo_h + +# if __arm64__ +# define ISA_MASK 0x0000000ffffffff8ULL +# elif __x86_64__ +# define ISA_MASK 0x00007ffffffffff8ULL +# endif + +#if __LP64__ +typedef uint32_t mask_t; +#else +typedef uint16_t mask_t; +#endif +typedef uintptr_t cache_key_t; + +struct bucket_t { + cache_key_t _key; + IMP _imp; +}; + +struct cache_t { + bucket_t *_buckets; + mask_t _mask; + mask_t _occupied; +}; + +struct eint main () { + GoodStudent *goodStudent = [[GoodStudent alloc] init]; + mock_objc_class *goodStudentClass = (__bridge mj_objc_class *)[GoodStudent class]; + [goodStudent goodStudentTest]; + [goodStudent studentTest]; + [goodStudent personTest]; + return 0; +}ntsize_list_tt { + uint32_t entsizeAndFlags; + uint32_t count; +}; + +struct method_t { + SEL name; + const char *types; + IMP imp; +}; + +struct method_list_t : entsize_list_tt { + method_t first; +}; + +struct ivar_t { + int32_t *offset; + const char *name; + const char *type; + uint32_t alignment_raw; + uint32_t size; +}; + +struct ivar_list_t : entsize_list_tt { + ivar_t first; +}; + +struct property_t { + const char *name; + const char *attributes; +}; + +struct property_list_t : entsize_list_tt { + property_t first; +}; + +struct chained_property_list { + chained_property_list *next; + uint32_t count; + property_t list[0]; +}; + +typedef uintptr_t protocol_ref_t; +struct protocol_list_t { + uintptr_t count; + protocol_ref_t list[0]; +}; + +struct class_ro_t { + uint32_t flags; + uint32_t instanceStart; + uint32_t instanceSize; // instance对象占用的内存空间 +#ifdef __LP64__ + uint32_t reserved; +#endif + const uint8_t * ivarLayout; + const char * name; // 类名 + method_list_t * baseMethodList; + protocol_list_t * baseProtocols; + const ivar_list_t * ivars; // 成员变量列表 + const uint8_t * weakIvarLayout; + property_list_t *baseProperties; +}; + +struct class_rw_t { + uint32_t flags; + uint32_t version; + const class_ro_t *ro; + method_list_t * methods; // 方法列表 + property_list_t *properties; // 属性列表 + const protocol_list_t * protocols; // 协议列表 + Class firstSubclass; + Class nextSiblingClass; + char *demangledName; +}; + +#define FAST_DATA_MASK 0x00007ffffffffff8UL +struct class_data_bits_t { + uintptr_t bits; +public: + class_rw_t* data() { + return (class_rw_t *)(bits & FAST_DATA_MASK); + } +}; + +/* OC对象 */ +struct mock_objc_object { + void *isa; +}; + +/* 类对象 */ +struct mock_objc_class : mock_objc_object { + Class superclass; + cache_t cache; + class_data_bits_t bits; +public: + class_rw_t* data() { + return bits.data(); + } + + mock_objc_class* metaClass() { + return (mock_objc_class *)((long long)isa & ISA_MASK); + } +}; + +#endif /* MockClassInfo_h */ + +@interface Person : NSObject +- (void)personSay; +@end + +@interface Student : Person +- (void)studentSay; +@end + +@interface GoodStudent : Student +- (void)goodStudentSay; +@end + +int main () { + GoodStudent *goodStudent = [[GoodStudent alloc] init]; + mock_objc_class *goodStudentClass = (__bridge mj_objc_class *)[GoodStudent class]; + // breakpoints1 + [goodStudent goodStudentSay]; + // breakpoints2 + [goodStudent studentSay]; + // breakpoints3 + [goodStudent personSay]; + // breakpoints4 + [goodStudent goodStudentSay]; + // breakpoints5 + [goodStudent studentSay]; + // breakpoints6 + NSLog(@"well donw"); + return 0; +} +``` + +流程: + +断点1的地方可以看到 `mock_objc_class` 结构体 `cache` 的 `_occupied` 为1,`_mask` 为3,初始化哈希表长度为4 + +在断点1的地方,`_occupied` 为1则代表只有 init 方法被缓存,本行代码执行完,`_occupied` 为2. + +在断点2的地方,`_occupied` 为2则代表只有 init、goodStudentSay 方法被缓存。本行代码执行完,`_occupied` 为3 + +在断点3的地方,`_occupied` 为3则代表只有 init 、goodStudentSay 、studentSay方法被缓存。本行代码执行完,`_occupied` 为1,且 `_mask` 为7。 + +奇了怪了,为什么 `_occupied`为1,且`_mask` 为7? + +因为哈希表长度为4,缓存3个方法后,到第4个方法需要缓存的时候会执行哈希表拓容,缓存会失效。拓容策略为乘以2 即 `uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;` 所以长度为8,mask 为`长度-1` ,则为7,第4个方法刚好被缓存下来,`_occupied` 为1。 + +```c +void cache_t::expand() +{ + cacheUpdateLock.assertLocked(); + uint32_t oldCapacity = capacity(); + uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; + if ((uint32_t)(mask_t)newCapacity != newCapacity) { + // mask overflow - can't grow further + // fixme this wastes one bit of mask + newCapacity = oldCapacity; + } + reallocate(oldCapacity, newCapacity); +} +``` + +继续运行 + +在断点4的地方,`_occupied` 为1则代表只有 personSay方法被缓存。本行代码执行完,`_occupied` 为2,且 `_mask` 为7。 + +在断点5的地方,`_occupied` 为2则代表只有 personSay、goodStudentSay 方法被缓存。本行代码执行完,`_occupied` 为3,且 `_mask` 为7。 + +在断点6的地方,`_occupied` 为3则代表只有 personSay、goodStudentSay、studentSay 方法被缓存, `_mask` 为7。 + +如何根据方法散列表查找某个方法 + +```objectivec +GoodStudent *student = [[GoodStudent alloc] init]; +mock_objc_class *studentClass = (__bridge mock_objc_class *)[GoodStudent class]; +[student goodStudentSay]; +[student studentSay]; +[student personSay]; +NSLog(@"Well done"); + +cache_t cache = studentClass->cache; +bucket_t *buckets = cache._buckets; + +bucket_t bucket = buckets[(long long)@selector(personSay) & cache._mask]; +NSLog(@"%s %p", bucket._key, bucket._imp); +// personSay 0xbec8 +``` + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-method-find.png) + +原理就是根据类对象结构体找到 cache 结构体,cache 结构体内部的 `_buckets` 是一个方法散列表,查看源代码,根据散列表的哈希寻找策略 `(key & mask)` 找到哈希索引,然后找到方法对象 bucket,其中寻找方法索引的 key 就是 方法 selector。 + +```c +static inline mask_t cache_hash(cache_key_t key, mask_t mask) +{ + return (mask_t)(key & mask); +} +``` + +## objc_msgSend + +oc 方法(对象方法、类方法)调用本质就是 `objc_msgSend` + +```objectivec +[person eat]; +objc_msgSend(person, sel_registerName("eat")); +[Person initialize]; +objc_msgSend([Person class], sel_registerName("initialize")); +``` + +`objc_msgSend` 可以分为3个阶段: + +- 消息发送 + +- 动态方法解析 + +- 消息转发 + +查看源码 `objc-msg-arm64.s` + +```shell +ENTRY _objc_msgSend + UNWIND _objc_msgSend, NoFrame + MESSENGER_START +    // x0 寄存器代表消息接受者,receiver。objc_msgSend(person, sel_registerName("eat")) 的 person + cmp x0, #0 // nil check and tagged pointer check + // b 代表指令跳转。le 代表 小于等于。<=0则跳转到 LNilOrTagged + b.le LNilOrTagged // (MSB tagged pointer looks negative) + ldr x13, [x0] // x13 = isa // ldr 代表加载指令。这里的意思是将 x0 寄存器信息写入到 x13中 + and x16, x13, #ISA_MASK // x16 = class // 这里就是将 x13 与 ISA_MASK 按位与,然后得到真实的 isa 信息,然后写入到 x16 中 +LGetIsaDone: + CacheLookup NORMAL // calls imp or objc_msgSend_uncached // 这里执行 objc_msgSend_uncached 逻辑,CacheLookup 是一个汇编宏,看下面的说明 + +LNilOrTagged: + // 判断为 nil 则跳转到 LReturnZero + b.eq LReturnZero // nil check + + // tagged + mov x10, #0xf000000000000000 + cmp x0, x10 + b.hs LExtTag + adrp x10, _objc_debug_taggedpointer_classes@PAGE + add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF + ubfx x11, x0, #60, #4 + ldr x16, [x10, x11, LSL #3] + b LGetIsaDone + +LExtTag: + // ext tagged + adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE + add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF + ubfx x11, x0, #52, #8 + ldr x16, [x10, x11, LSL #3] + b LGetIsaDone + +LReturnZero: + // x0 is already zero + mov x1, #0 + movi d0, #0 + movi d1, #0 + movi d2, #0 + movi d3, #0 + MESSENGER_END_NIL + // 汇编中 ret 代表 return + ret + + END_ENTRY _objc_msgSend + + +.macro CacheLookup // 汇编宏,可以看到根据 (SEL & mask) 来寻找真正的方法地址 + // x1 = SEL, x16 = isa + ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask + and w12, w1, w11 // x12 = _cmd & mask + add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4) + + ldp x9, x17, [x12] // {x9, x17} = *bucket +1: cmp x9, x1 // if (bucket->sel != _cmd) + b.ne 2f // scan more + CacheHit $0 // call or return imp + +2: // not hit: x12 = not-hit bucket + CheckMiss $0 // miss if bucket->sel == 0 + cmp x12, x10 // wrap if bucket == buckets + b.eq 3f + ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket + b 1b // loop + +3: // wrap: x12 = first bucket, w11 = mask + add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4) + + // Clone scanning loop to miss instead of hang when cache is corrupt. + // The slow path may detect any corruption and halt later. + + ldp x9, x17, [x12] // {x9, x17} = *bucket +1: cmp x9, x1 // if (bucket->sel != _cmd) + b.ne 2f // scan more + CacheHit $0 // call or return imp + +2: // not hit: x12 = not-hit bucket + // 这里是方法查找失败,则走 checkMiss 逻辑,具体看下面 + CheckMiss $0 // miss if bucket->sel == 0 + cmp x12, x10 // wrap if bucket == buckets + b.eq 3f + ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket + b 1b // loop + +3: // double wrap + JumpMiss $0 + +.endmacro + +// CheckMiss 汇编宏,上面走 Normal 逻辑,内部走 __objc_msgSend_uncached 流程 +.macro CheckMiss + // miss if bucket->sel == 0 +.if $0 == GETIMP + cbz x9, LGetImpMiss +.elseif $0 == NORMAL + cbz x9, __objc_msgSend_uncached +.elseif $0 == LOOKUP + cbz x9, __objc_msgLookup_uncached +.else +.abort oops +.endif +.endmacro + + +// __objc_msgSend_uncached 内部其实走 MethodTableLookup 逻辑 +STATIC_ENTRY __objc_msgSend_uncached +UNWIND __objc_msgSend_uncached, FrameWithNoSaves + +// THIS IS NOT A CALLABLE C FUNCTION +// Out-of-band x16 is the class to search + +MethodTableLookup +br x17 + +END_ENTRY __objc_msgSend_uncached + +// MethodTableLookup 是一个汇编宏,内部指令跳转到 __class_lookupMethodAndLoadCache3。 +.macro MethodTableLookup + + // push frame + stp fp, lr, [sp, #-16]! + mov fp, sp + + // save parameter registers: x0..x8, q0..q7 + sub sp, sp, #(10*8 + 8*16) + stp q0, q1, [sp, #(0*16)] + stp q2, q3, [sp, #(2*16)] + stp q4, q5, [sp, #(4*16)] + stp q6, q7, [sp, #(6*16)] + stp x0, x1, [sp, #(8*16+0*8)] + stp x2, x3, [sp, #(8*16+2*8)] + stp x4, x5, [sp, #(8*16+4*8)] + stp x6, x7, [sp, #(8*16+6*8)] + str x8, [sp, #(8*16+8*8)] + + // receiver and selector already in x0 and x1 + mov x2, x16 + bl __class_lookupMethodAndLoadCache3 + + // imp in x0 + mov x17, x0 + + // restore registers and return + ldp q0, q1, [sp, #(0*16)] + ldp q2, q3, [sp, #(2*16)] + ldp q4, q5, [sp, #(4*16)] + ldp q6, q7, [sp, #(6*16)] + ldp x0, x1, [sp, #(8*16+0*8)] + ldp x2, x3, [sp, #(8*16+2*8)] + ldp x4, x5, [sp, #(8*16+4*8)] + ldp x6, x7, [sp, #(8*16+6*8)] + ldr x8, [sp, #(8*16+8*8)] + + mov sp, fp + ldp fp, lr, [sp], #16 + +.endmacro +``` + +Tips:c 方法在汇编中使用的时候,需要在方法名前加 `_` 。所以在汇编中某个方法为 `_xxx`,则在其他地方查找实现,需要去掉 `_` +此时 `__class_lookupMethodAndLoadCache3` 在汇编中没有实现,则按照 `_class_lookupMethodAndLoadCache3` 查找 + +```c +IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) +{ + return lookUpImpOrForward(cls, sel, obj, + YES/*initialize*/, NO/*cache*/, YES/*resolver*/); +} + +IMP lookUpImpOrForward(Class cls, SEL sel, id inst, + bool initialize, bool cache, bool resolver) +{ + IMP imp = nil; + bool triedResolver = NO; + + runtimeLock.assertUnlocked(); + + // Optimistic cache lookup + if (cache) { + imp = cache_getImp(cls, sel); + if (imp) return imp; + } + + // runtimeLock is held during isRealized and isInitialized checking + // to prevent races against concurrent realization. + + // runtimeLock is held during method search to make + // method-lookup + cache-fill atomic with respect to method addition. + // Otherwise, a category could be added but ignored indefinitely because + // the cache was re-filled with the old value after the cache flush on + // behalf of the category. + + runtimeLock.read(); + + if (!cls->isRealized()) { + // Drop the read-lock and acquire the write-lock. + // realizeClass() checks isRealized() again to prevent + // a race while the lock is down. + runtimeLock.unlockRead(); + runtimeLock.write(); + + realizeClass(cls); + + runtimeLock.unlockWrite(); + runtimeLock.read(); + } + + if (initialize && !cls->isInitialized()) { + runtimeLock.unlockRead(); + _class_initialize (_class_getNonMetaClass(cls, inst)); + runtimeLock.read(); + // If sel == initialize, _class_initialize will send +initialize and + // then the messenger will send +initialize again after this + // procedure finishes. Of course, if this is not being called + // from the messenger then it won't happen. 2778172 + } + + + retry: + runtimeLock.assertReading(); + + // Try this class's cache. + imp = cache_getImp(cls, sel); + if (imp) goto done; + + // Try this class's method lists. + { + Method meth = getMethodNoSuper_nolock(cls, sel); + if (meth) { + log_and_fill_cache(cls, meth->imp, sel, inst, cls); + imp = meth->imp; + goto done; + } + } + + // Try superclass caches and method lists. + { + unsigned attempts = unreasonableClassCount(); + for (Class curClass = cls->superclass; + curClass != nil; + curClass = curClass->superclass) + { + // Halt if there is a cycle in the superclass chain. + if (--attempts == 0) { + _objc_fatal("Memory corruption in class list."); + } + + // Superclass cache. + imp = cache_getImp(curClass, sel); + if (imp) { + if (imp != (IMP)_objc_msgForward_impcache) { + // Found the method in a superclass. Cache it in this class. + log_and_fill_cache(cls, imp, sel, inst, curClass); + goto done; + } + else { + // Found a forward:: entry in a superclass. + // Stop searching, but don't cache yet; call method + // resolver for this class first. + break; + } + } + + // Superclass method list. + Method meth = getMethodNoSuper_nolock(curClass, sel); + if (meth) { + log_and_fill_cache(cls, meth->imp, sel, inst, curClass); + imp = meth->imp; + goto done; + } + } + } + + // No implementation found. Try method resolver once. + + if (resolver && !triedResolver) { + runtimeLock.unlockRead(); + _class_resolveMethod(cls, sel, inst); + runtimeLock.read(); + // Don't cache the result; we don't hold the lock so it may have + // changed already. Re-do the search from scratch instead. + triedResolver = YES; + goto retry; + } + + // No implementation found, and method resolver didn't help. + // Use forwarding. + + imp = (IMP)_objc_msgForward_impcache; + cache_fill(cls, sel, imp, inst); + + done: + runtimeLock.unlockRead(); + + return imp; +} +``` + +### 消息发送阶段 + +上面的代码走到 `getMethodNoSuper_nolock` 寻找类里的方法 + +```c +static method_t * +getMethodNoSuper_nolock(Class cls, SEL sel) +{ + runtimeLock.assertLocked(); + + assert(cls->isRealized()); + // fixme nil cls? + // fixme nil sel? + // 这里根据类结构体找到 data(),然后找到 methods (Array 数组,数组元素是方法 Array) + /* + data() 其实就是 class_rw_t* data() { + return (class_rw_t *)(bits & FAST_DATA_MASK); + } + */ + for (auto mlists = cls->data()->methods.beginLists(), + end = cls->data()->methods.endLists(); + mlists != end; + ++mlists) + { + method_t *m = search_method_list(*mlists, sel); + if (m) return m; + } + + return nil; +} + +static method_t *search_method_list(const method_list_t *mlist, SEL sel) +{ + int methodListIsFixedUp = mlist->isFixedUp(); + int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t); + // 排好序则调用 `findMethodInSortedMethodList`,其内部是二分查找实现。 + if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) { + return findMethodInSortedMethodList(sel, mlist); + } else { + // 没排序则线性查找 + // Linear search of unsorted method list + for (auto& meth : *mlist) { + if (meth.name == sel) return &meth; + } + } + +#if DEBUG + // sanity-check negative results + if (mlist->isFixedUp()) { + for (auto& meth : *mlist) { + if (meth.name == sel) { + _objc_fatal("linear search worked when binary search did not"); + } + } + } +#endif + + return nil; +} +static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list) +{ + assert(list); + + const method_t * const first = &list->first; + const method_t *base = first; + const method_t *probe; + uintptr_t keyValue = (uintptr_t)key; + uint32_t count; + + for (count = list->count; count != 0; count >>= 1) { + probe = base + (count >> 1); + + uintptr_t probeValue = (uintptr_t)probe->name; + + if (keyValue == probeValue) { + // `probe` is a match. + // Rewind looking for the *first* occurrence of this value. + // This is required for correct category overrides. + while (probe > first && keyValue == (uintptr_t)probe[-1].name) { + probe--; + } + return (method_t *)probe; + } + + if (keyValue > probeValue) { + base = probe + 1; + count--; + } + } + + return nil; +} +``` + +`cls->data()->methods.beginLists` 这里根据类结构体调用到 data() 方法,获取到 `class_rw_t` + +```c +class_rw_t *data() { + return bits.data(); +} +``` + +然后通过 `class_rw_t` 找到 methods (Array 数组,数组元素是方法 Array)。内部调用 `search_method_list` 方法。 + +`search_method_list` 方法内部判断方法数组是否排好序 + +- 排好序则调用 `findMethodInSortedMethodList`,其内部是二分查找实现。 + +- 没排序,则线性查找 (Linear search of unsorted method list) + +`getMethodNoSuper_nolock` 执行完则会将方法写入到当前类对象的缓存中。 + +```c +static void +log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) +{ +#if SUPPORT_MESSAGE_LOGGING + if (objcMsgLogEnabled) { + bool cacheIt = logMessageSend(implementer->isMetaClass(), + cls->nameForLogging(), + implementer->nameForLogging(), + sel); + if (!cacheIt) return; + } +#endif + cache_fill (cls, sel, imp, receiver); +} + +void cache_fill(Class cls, SEL sel, IMP imp, id receiver) +{ +#if !DEBUG_TASK_THREADS + mutex_locker_t lock(cacheUpdateLock); + cache_fill_nolock(cls, sel, imp, receiver); +#else + _collecting_in_critical(); + return; +#endif +} + +static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) +{ + cacheUpdateLock.assertLocked(); + + // Never cache before +initialize is done + if (!cls->isInitialized()) return; + + // Make sure the entry wasn't added to the cache by some other thread + // before we grabbed the cacheUpdateLock. + if (cache_getImp(cls, sel)) return; + + cache_t *cache = getCache(cls); + cache_key_t key = getKey(sel); + + // Use the cache as-is if it is less than 3/4 full + mask_t newOccupied = cache->occupied() + 1; + mask_t capacity = cache->capacity(); + if (cache->isConstantEmptyCache()) { + // Cache is read-only. Replace it. + cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); + } + else if (newOccupied <= capacity / 4 * 3) { + // Cache is less than 3/4 full. Use it as-is. + } + else { + // Cache is too full. Expand it. + cache->expand(); + } + + // Scan for the first unused slot and insert there. + // There is guaranteed to be an empty slot because the + // minimum size is 4 and we resized at 3/4 full. + bucket_t *bucket = cache->find(key, receiver); + if (bucket->key() == 0) cache->incrementOccupied(); + bucket->set(key, imp); +} +``` + +摘出 `lookUpImpOrForward` 方法中的一段代码 + +```c +// Try this class's cache. +imp = cache_getImp(cls, sel); +if (imp) goto done; +// Try this class's method lists. +{ + Method meth = getMethodNoSuper_nolock(cls, sel); + if (meth) { + log_and_fill_cache(cls, meth->imp, sel, inst, cls); + imp = meth->imp; + goto done; + } +} +// Try superclass caches and method lists. +``` + +如果代码没有找到,则不会 `goto` 到 `done`,开始走父类缓存查找逻辑 + +```c +// Try superclass caches and method lists. +{ + unsigned attempts = unreasonableClassCount(); + // for 循环不断查找,找当前类的父类,直到当前类为 nil。 + for (Class curClass = cls->superclass; + curClass != nil; + curClass = curClass->superclass) + { + // Halt if there is a cycle in the superclass chain. + if (--attempts == 0) { + _objc_fatal("Memory corruption in class list."); + } + + // Superclass cache. + // 先在父类的方法缓存中查找(根据 sel & mask)`cache_getImp` ,找到则将方法写入到自身类的方法缓存中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);` + imp = cache_getImp(curClass, sel); + if (imp) { + if (imp != (IMP)_objc_msgForward_impcache) { + // Found the method in a superclass. Cache it in this class. + log_and_fill_cache(cls, imp, sel, inst, curClass); + goto done; + } + else { + // Found a forward:: entry in a superclass. + // Stop searching, but don't cache yet; call method + // resolver for this class first. + break; + } + } + + // Superclass method list. + // 如果在父类的方法缓存中没找到,则调用 `getMethodNoSuper_nolock` 父类的 方法数组(Array 元素为方法数组),按照排序好和没排序好分别走二分查找和线性查找。 + Method meth = getMethodNoSuper_nolock(curClass, sel); + if (meth) { + // 如果找到则继续填充到当前类的方法缓存中去 + log_and_fill_cache(cls, meth->imp, sel, inst, curClass); + imp = meth->imp; + goto done; + } + } +} +``` + +for 循环不断查找,找当前类的父类,直到当前类为 nil。 + +先在父类的方法缓存中查找(根据 sel & mask)`cache_getImp` ,找到则将方法写入到自身类的方法缓存中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);` + +比如 Person 类有 eat 方法,Student 类有 stduy 方法,调用 Student 对象的 eat 方法,则会走到这里,从父类找到方法后写入到 Student 类的方法缓存中去。 + +如果在父类的方法缓存中没找到,则调用 `getMethodNoSuper_nolock` 父类的 方法数组(Array 元素为方法数组),按照排序好和没排序好分别走二分查找和线性查找。 + +如果找到则继续填充到当前类的方法缓存中去 `log_and_fill_cache(cls, meth->imp, sel, inst, curClass);`,最后 goto done + +上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-objc_msgSend-messageSend.png) + +### 动态方法解析阶段 + +接着查看源码 + +```c +IMP lookUpImpOrForward(Class cls, SEL sel, id inst, + bool initialize, bool cache, bool resolver) +{ + //... + // No implementation found. Try method resolver once. + if (resolver && !triedResolver) { + runtimeLock.unlockRead(); + _class_resolveMethod(cls, sel, inst); + runtimeLock.read(); + // Don't cache the result; we don't hold the lock so it may have + // changed already. Re-do the search from scratch instead. + triedResolver = YES; + goto retry; + } + // ... +} + +void _class_resolveMethod(Class cls, SEL sel, id inst) +{ + if (! cls->isMetaClass()) { + // try [cls resolveInstanceMethod:sel] + _class_resolveInstanceMethod(cls, sel, inst); + } + else { + // try [nonMetaClass resolveClassMethod:sel] + // and [cls resolveInstanceMethod:sel] + _class_resolveClassMethod(cls, sel, inst); + if (!lookUpImpOrNil(cls, sel, inst, + NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) + { + _class_resolveInstanceMethod(cls, sel, inst); + } + } +} +``` + +判断当前类没有走过动态方法解析阶段,则走动态方法解析阶段,调用 `_class_resolveMethod` 方法。 + +内部会判断但前类是不是元类对象、还是类对象走不同逻辑。 + +类对象走 `_class_resolveInstanceMethod` 逻辑 + +```c +static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) +{ + if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, + NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) + { + // Resolver not implemented. + return; + } + BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; + bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); + // Cache the result (good or bad) so the resolver doesn't fire next time. + // +resolveInstanceMethod adds to self a.k.a. cls + IMP imp = lookUpImpOrNil(cls, sel, inst, + NO/*initialize*/, YES/*cache*/, NO/*resolver*/); + + if (resolved && PrintResolving) { + if (imp) { + _objc_inform("RESOLVE: method %c[%s %s] " + "dynamically resolved to %p", + cls->isMetaClass() ? '+' : '-', + cls->nameForLogging(), sel_getName(sel), imp); + } + else { + // Method resolver didn't add anything? + _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES" + ", but no new implementation of %c[%s %s] was found", + cls->nameForLogging(), sel_getName(sel), + cls->isMetaClass() ? '+' : '-', + cls->nameForLogging(), sel_getName(sel)); + } + } +} +``` + +核心就调用 `bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);` 运行 `resolveInstanceMethod` 方法。 + +元类对象走 `_class_resolveClassMethod` 逻辑 + +```c +static void _class_resolveClassMethod(Class cls, SEL sel, id inst) +{ + assert(cls->isMetaClass()); + + if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, + NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) + { + // Resolver not implemented. + return; + } + + BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; + bool resolved = msg(_class_getNonMetaClass(cls, inst), + SEL_resolveClassMethod, sel); + + // Cache the result (good or bad) so the resolver doesn't fire next time. + // +resolveClassMethod adds to self->ISA() a.k.a. cls + IMP imp = lookUpImpOrNil(cls, sel, inst, + NO/*initialize*/, YES/*cache*/, NO/*resolver*/); + + if (resolved && PrintResolving) { + if (imp) { + _objc_inform("RESOLVE: method %c[%s %s] " + "dynamically resolved to %p", + cls->isMetaClass() ? '+' : '-', + cls->nameForLogging(), sel_getName(sel), imp); + } + else { + // Method resolver didn't add anything? + _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES" + ", but no new implementation of %c[%s %s] was found", + cls->nameForLogging(), sel_getName(sel), + cls->isMetaClass() ? '+' : '-', + cls->nameForLogging(), sel_getName(sel)); + } + } +} +``` + +其实就是调用 `bool resolved = msg(_class_getNonMetaClass(cls, inst), +SEL_resolveClassMethod, sel);` + +最后还是走到了 `goto retry;` 继续走完整的消息发送流程(因为添加了方法,所以会按照方法查找再去执行的逻辑) + +完整流程如下 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-objc_msgSend-ResolveMethod.png) + +上 Demo + +```objectivec +Person *person = [[Person alloc] init]; +[person eat]; +``` + +调用不存在方法则报错 `***** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person eat]: unrecognized selector sent to instance 0x101b2d900'**` + +因为调用对象不存在的方法,所以会 Crash + +知道 `objc_msgSend` 的流程,我们尝试给它修正下 + +```objectivec +- (void)customEat { + NSLog(@"我的假的 eat 方法,为了解决奔溃问题"); +} + ++ (BOOL)resolveInstanceMethod:(SEL)sel { + if (sel == @selector(eat)) { + // 对象方法,存在于对象上。 + Method method = class_getInstanceMethod(self, @selector(customEat)); + class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method)); + return YES; + } + return [super resolveInstanceMethod:sel]; +} +``` + +也可以添加 c 语音方法 + +```objectivec +void customEat (id self, SEL _cmd) { + NSLog(@"%@-%s-%s", self, sel_getName(_cmd), __func__); +} + ++ (BOOL)resolveInstanceMethod:(SEL)sel +{ + if (sel == @selector(eat)) { + // 对象方法,存在于对象上。 + class_addMethod(self, sel, (IMP)customEat, "v16@0:8"); + return YES; + } + return [super resolveInstanceMethod:sel]; +} +``` + +因为 c 语言方法名就是函数地址,所以不需要直接传递即可,需要做下类型转换 `(IMP)customEat` + +也可以给类方法做动态方法解析。需要注意的是类方法。 + +- 调用 `-(BOOL)resolveClassMethod:(SEL)sel` + +- `class_addMethod` 方法中的第一个参数,需要加到类的元类对象中,所以是 `object_getClass` + +```objectivec +Person *person = [[Person alloc] init]; +[Person drink]; +void customDrink (id self, SEL _cmd) { + NSLog(@"假喝水"); +} + ++ (BOOL)resolveClassMethod:(SEL)sel +{ + if (sel == @selector(drink)) { + // 类方法,存在于元类对象上。 + class_addMethod(object_getClass(self), sel, (IMP)customDrink, "v16@0:8"); + return YES; + } + return [super resolveClassMethod:sel]; +} +``` + +### 消息转发阶段 + +能走到消息转发,说明 + +1. 类自身没有该方法(`objc_msgSend` 的消息发送) + +2. `objc_msgSend` 动态方法解析失败或者没有做 + +说明类自身和父类没有可以处理该消息的能力,此时应该将该消息转发给其他对象。 + +查看 objc4 的源码 + +```c +IMP lookUpImpOrForward(Class cls, SEL sel, id inst, + bool initialize, bool cache, bool resolver) +{ + //... + // No implementation found, and method resolver didn't help. + // Use forwarding. + imp = (IMP)_objc_msgForward_impcache; + cache_fill(cls, sel, imp, inst); + // ... +} +``` + +继续查找 `_objc_msgForward_impcache` + +```shell +STATIC_ENTRY __objc_msgForward_impcache + +MESSENGER_START +nop +MESSENGER_END_SLOW + +// No stret specialization. +b __objc_msgForward +END_ENTRY __objc_msgForward_impcache + +ENTRY __objc_msgForward + +adrp x17, __objc_forward_handler@PAGE +ldr x17, [x17, __objc_forward_handler@PAGEOFF] +br x1 + +END_ENTRY __objc_msgForward +``` + +查找 `__objc_forward_handler` 没有找到,可以猜想是一个 c 方法,去掉最前面的 `_`,按照 `_objc_forward_handler` 查找得到 + +```c +__attribute__((noreturn)) void +objc_defaultForwardHandler(id self, SEL sel) +{ + _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p " + "(no message forward handler is installed)", + class_isMetaClass(object_getClass(self)) ? '+' : '-', + object_getClassName(self), sel_getName(sel), self); +} +void *_objc_forward_handler = (void*)objc_defaultForwardHandler; +``` + +消息转发的代码是不开源的,查找资料找到一份靠谱的 `__forwarding `方法实现 + +为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-forwardingFailed.png) + +```c +int __forwarding__(void *frameStackPointer, int isStret) { + id receiver = *(id *)frameStackPointer; + SEL sel = *(SEL *)(frameStackPointer + 8); + const char *selName = sel_getName(sel); + Class receiverClass = object_getClass(receiver); + + // 调用 forwardingTargetForSelector: + if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) { + id forwardingTarget = [receiver forwardingTargetForSelector:sel]; + if (forwardingTarget && forwardingTarget != receiver) { + return objc_msgSend(forwardingTarget, sel, ...); + } + } + + // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation + if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) { + NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel]; + if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) { + NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; + + [receiver forwardInvocation:invocation]; + + void *returnValue = NULL; + [invocation getReturnValue:&value]; + return returnValue; + } + } + + if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) { + [receiver doesNotRecognizeSelector:sel]; + } + + // The point of no return. + kill(getpid(), 9); +} +``` + +具体地址可以参考 [__frowarding](../assets/__forwarding__clean.c) + +完整流程如下 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-forwarding.png) + +上 Demo + +Person 类不存在 drink 方法,Bird 类存在 + +```objectivec +@implementation Bird +- (void)drink +{ + NSLog(@"一只鸟儿在喝水"); +} +@end + +Person *person = [[Person alloc] init]; +[person drink]; +``` + +方法1 + +```objectivec +@implementation Person - (id)forwardingTargetForSelector:(SEL)aSelector { - if(aSelector == @selector(mysteriousMethod:)){ - return alternateObject; + if (aSelector == @selector(drink)) { + return [[Bird alloc] init]; + } + return [super forwardingTargetForSelector:aSelector]; +} +@end +``` + +方法2 + +```objectivec +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + if (aSelector == @selector(drink)) { + return nil; + } + return [super forwardingTargetForSelector:aSelector]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v16@0:8"]; + return signature; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + [anInvocation invokeWithTarget:[[Bird alloc] init]]; +} +``` + +注意:`methodSignatureForSelector` 如果返回 nil,则 `forwardInvocation` 不会执行 + +给 Person 类方法进行消息转发处理 + +方法1 + +```objectivec ++ (id)forwardingTargetForSelector:(SEL)aSelector +{ + if (aSelector == @selector(drink)) { + return [Bird class]; } return [super forwardingTargetForSelector:aSelector]; } ``` -如果此方法返回 nil 或者 self,则会进入下一步 -- 消息重定向:`methodSignatureForSelector` 获取函数的参数和返回值类型 -如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。 +方法2 -如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。 +```objectivec ++ (id)forwardingTargetForSelector:(SEL)aSelector +{ + if (aSelector == @selector(drink)) { + return nil; + } + return [super forwardingTargetForSelector:aSelector]; +} ++ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + if (aSelector == @selector(drink)) { + return [[Bird class] methodSignatureForSelector:@selector(drink)]; + } + return [super methodSignatureForSelector:aSelector]; +} + ++ (void)forwardInvocation:(NSInvocation *)anInvocation +{ + [anInvocation invokeWithTarget:[Bird class]]; +} +``` + +#### 方法签名的获取 + +方法1: 自己根据方法的返回值类型,方法2个基础参数参数:`id self`、`SEL _cdm`,其他参数类型按照 Encoding 自己拼。 类似 `v16@0:8` + +方法2 :根据某个类的对象,去调用 `methodSignatureForSelector ` 方法获取。 + + `[[[Bird alloc] init] methodSignatureForSelector:**@selector**(drink)];` + +```objectivec +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + if (aSelector == @selector(drink)) { + return [[[Bird alloc] init] methodSignatureForSelector:@selector(drink)]; + } + return [super methodSignatureForSelector:aSelector]; +} +``` + +## Super 原理 + +```objectivec +@implementation Person +@end + +@implementation Student +- (instancetype)init +{ + if (self = [super init]) { + NSLog(@"%@", [self class]); // Student + NSLog(@"%@", [self superclass]); // Person + NSLog(@"%@", [super class]); // Student + NSLog(@"%@", [super superclass]); // Person + } + return self; +} +@end +``` + +后面2个的打印似乎不符合预期?转成 c++ 代码看看 + +```c +static instancetype _I_Student_init(Student * self, SEL _cmd) { + if (self = ((Student *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init"))) { + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))); + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass"))); + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"))); + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_3, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("superclass"))); + } + return self; +} +``` + +`[super class]` 这句代码底层实现为 `objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"));` + +`__rw_objc_super` 是什么? + +```c +struct objc_super { + __unsafe_unretained _Nonnull id receiver; + __unsafe_unretained _Nonnull Class super_class; +}; +``` + +`objc_msgSendSuper` 如下 + +```c +/** + * Sends a message with a simple return value to the superclass of an instance of a class. + * + * @param super A pointer to an \c objc_super data structure. Pass values identifying the + * context the message was sent to, including the instance of the class that is to receive the + * message and the superclass at which to start searching for the method implementation. + * @param op A pointer of type SEL. Pass the selector of the method that will handle the message. + * @param ... + * A variable argument list containing the arguments to the method. + * + * @return The return value of the method identified by \e op. + * + * @see objc_msgSend + */ +objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...) +``` + +所以 `objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"));` 等同于下面代码 + +```c +struct objc_super arg = {self, class_getSuperclass(self)}; +objc_msgSendSuper(arg, sel_registerName("class")) +``` + +`[super class]` super 调用的 receiver 还是 self + +结构体的目的是为了在类对象查找的过程中,直接从当前类的父类中查找,而不是本类(比如 Student 类的 [super init] 会直接从 Person 的类对象中查找 init,找不到则通过 superclass 向上查找) + +大致推测系统的 class、superclass 方法实现如下 + +```c +@implementation Person +- (Class)class{ + return object_getClass(self); +} +- (Class)superclass { + return class_getSuperclass(object_getClass(self)); +} +@end +``` + +`class` 方法是在 NSObject 类对象的方法列表中的。所以 + + `[self class]` 等价于 `objc_msgSend(self, sel_registerName("class"))` + +`[super class]` 等价于 `objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("class"))` + +其实2个方法本质上消息 receiver 都是 self,也就是当前的 Student,所以打印都是 Student + +结论:`[super message]` 有2个特征 + +- super 消息的调用者还是 self + +- 方法查找是根据当前 self 的父类开始查找 + +通过将代码转为 c++ 发现,super 调用本质就是 `objc_msgSendSuper`,实际不然 + +我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2` + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-super.png) + +查看 objc4 源代码发现是一段汇编实现。 + +```shell +ENTRY _objc_msgSendSuper2 +UNWIND _objc_msgSendSuper2, NoFrame +MESSENGER_START + +ldp x0, x16, [x0] // x0 = real receiver, x16 = class +ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass +CacheLookup NORMAL + +END_ENTRY _objc_msgSendSuper2 +``` + +所以 `super viewDidLoad`本质上就是 + +```objectivec +struct objc_super arg = { + self, + [UIViewController class] +}; +objc_msgSendSuper2(arg, sel_registerName("viewDidLoad")); +``` + +objc_msgSendSuper2 和 objc_msgSendSuper 区别在于第二个参数 + +objc_msgSendSuper2 底层源码(汇编代码 objc-msg-arm64.s 422 行)会将第二个参数找到父类,然后进行方法缓存查找 + +objc_msgSendSuper 直接从第二个参数查找方法。 + +总结:clang 转 c++ 可以窥探系统实现,可以作为研究参考。super 本质上就是 `objc_msgSendSuper2`,传递2个参数,第一个参数为结构体,第二个参数是sel。 + +为什么转为 c++ 和真正实现不一样?思考下 + +源代码变为机器码之前,会经过 LLVM 编译器转换为中间代码(Intermediate Representation),最后转为汇编、机器码 + +我们来验证下 super 在中间码上是什么 + +```shell +clang -emit-llvm -S Student.m +``` + +llvm 中间码如下,可以看到确实内部是 `objc_msgSendSuper2` + +```shell +; Function Attrs: noinline optnone ssp uwtable +define internal void @"\01-[Student sayHi]"(%0* %0, i8* %1) #1 { + %3 = alloca %0*, align 8 + %4 = alloca i8*, align 8 + %5 = alloca %struct._objc_super, align 8 + store %0* %0, %0** %3, align 8 + store i8* %1, i8** %4, align 8 + %6 = load %0*, %0** %3, align 8 + %7 = bitcast %0* %6 to i8* + %8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0 + store i8* %7, i8** %8, align 8 + %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8 + %10 = bitcast %struct._class_t* %9 to i8* + %11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1 + store i8* %10, i8** %11, align 8 + %12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.6, align 8, !invariant.load !12 + call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %12) + notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.8 to i8*)) + ret void +} +``` + +指令介绍 + +```shell +@ - 全局变量 +% - 局部变量 +alloca - 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存 +i32 - 32位4字节的整数 +align - 对齐 +load - 读出,store 写入 +icmp - 两个整数值比较,返回布尔值 +br - 选择分支,根据条件来转向label,不根据条件跳转的话类似 goto +label - 代码标签 +call - 调用函数 +``` + +## isKindOfClass、isMemberOfClass + +Demo + +```c +Student *student = [[Student alloc] init]; +NSLog(@"%hhd", [student isMemberOfClass:[Student class]]); // 1 +NSLog(@"%hhd", [student isKindOfClass:[Person class]]); // 1 +NSLog(@"%hhd", [Student isMemberOfClass:[Student class]]); // 0 +NSLog(@"%hhd", [Student isKindOfClass:[Student class]]); // 0 +``` + +有些人答对了,有些人错了。 + +上面2个判断都是调用对象方法的 `isMemberOfClass` 、`isKindOfClass` + +由于 objc4 是开源的,查看 `object.mm` + +```c +- (BOOL)isKindOfClass:(Class)cls { + for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { + if (tcls == cls) return YES; + } + return NO; +} +- (BOOL)isMemberOfClass:(Class)cls { + return [self class] == cls; +} +``` + +`isMemberOfClass` 判断当前对象是不是传递进来的对象 + +`isKindOfClass` 内部是一个 for 循环,第一次循环先拿当前类的类对象,判断是不是和传递进来的对象一样,一样则 return YES,否则先给 tlcs 赋值当前类的父类,然后走第二次判断,直到 cls 不存在位置(NSObject 的父类为 nil)。所以 `isKindOfClass` 其实判断的是当前类是传递进来的类,或者传递进来类的子类 + +下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass` + +```c ++ (BOOL)isMemberOfClass:(Class)cls { + return object_getClass((id)self) == cls; +} ++ (BOOL)isKindOfClass:(Class)cls { + for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { + if (tcls == cls) return YES; + } + return NO; +} +``` + +可以看到 `+(BOOL)isMemberOfClass:(Class)cls` 方法内部就是对当前类获取类对象,然后与传递进来的 cls 判断是否相等。由于是 `[Student isMemberOfClass:[Student class]])` `Student` 类调用类方法 `+isMemberOfClass` 所以类对象的类对象也就是元类对象,cls 参数也就是 `[Student class]` 是一个类对象,元类对象等于类对象吗?显然不是 + +想让判断成立,可以改为 `[Student isMemberOfClass:object_getClass([Student class])]` 或者 `[[Student **class**] isMemberOfClass:object_getClass([Student class])]` + +`+(BOOL)isKindOfClass:(Class)cls` 同理分析。作用是当前类的元类,是否是右边传入对象的元类或者元类的子类。 + +来个特殊 case + +```c +NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); // NO +``` + +输出 1。为什么? + +看坐右边的部分,调用 `isKindOfClass` 方法,本质上就是 Student 类的类对象,也就是 Student 元类,和传入的右边 `[NSObject class]`判断是否想通过 + +第一次 for 循环当然不同,所以不能 return,会将 `tcls ` 走步长改变逻辑 `tcls = tcls->superclass`,也就是找到当前 Student 元类对象的父类。 + +第二次 for 循环也一样不相等,Person 元类不等于 `[NSObject class]` 继续向上,直到 tcls = NSObject。此时还是不等,这时候 tcls  走步长改变逻辑,`tcls = tcls->superclass` NSObject 元类的 superclass 还是 NSObject。所以 for 循环内部的判断编委 ` [NSObject class] == [NSObject class]`,return YES。 + +**tips:基类的元类对象指向基类的类对象。** + +```c ++ (BOOL)isKindOfClass:(Class)cls { + for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { + if (tcls == cls) return YES; + } + return NO; +} +``` + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/class-isa-superclass.png) + +Quiz + +```objectivec +NSLog(@"%hhd", [NSObject isKindOfClass:[NSObject class]]); // 1 +NSLog(@"%hhd", [NSObject isMemberOfClass:[NSObject class]]); //0 +NSLog(@"%hhd", [Person isKindOfClass:[Person class]]); // 0 +NSLog(@"%hhd", [Person isMemberOfClass:[Person class]]); //0 +``` + +## Runtime 刁钻题 + +```objectivec +@interface Person : NSObject +@property (nonatomic, strong) NSString *name; +- (void)sayHi; +@end +@implementation Person +- (void)sayHi{ + NSLog(@"hi,my name is %@", self->_name); // hi,my name is 杭城小刘 +} +@end + +int main(int argc, const char * argv[]) { + @autoreleasepool { + NSString *temp = @"杭城小刘"; + id obj = [Person class]; + void *p = &obj; + [(__bridge id)p sayHi]; + + test(); + } + return 0; +} +``` + +程序运行什么结果? + +``` +hi,my name is 杭城小刘 +``` + +为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘" + +我们来分析下: + +1.**方法调用本质就是寻找 isa 进行消息发送** + +```objectivec +Person *person = [[Person alloc] init]; +[person sayHi]; +``` + +`[[Person alloc] init]`在内存中分配一块内存,然后 isa 指向这块内存,然后 person 指针,指向结构体,结构体的第一个成员。 + +2.**栈空间数据内存向下生长。第一个变量地址高,其次降低。且每个变量的内存地址是连续的。** + +这个流程其实和上面的代码一样的。所以可以正常调用 + +```c +void test () { + long long a = 4; // 0x7ff7bfeff2d8 + long long b = 5; // 0x7ff7bfeff2d0 + long long c = 6; // 0x7ff7bfeff2c8 + NSLog(@"%p %p %p", &a, &b, &c); +} +``` + +方法内的变量存储在栈上,堆向上增长,栈向下增长。 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-isa-demo.png) + +3.**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程(此时就是偏移8个字节)** + +上面代码可以类比类调用方法的流程。 obj 指针指向 Person 这块内存,给类对象发送 `sayHi` 消息也就是通过 obj 指针找到 isa,恰好 obj 指针指向的地址就是类对象的类结构体的地址,结构体成员变量第一个就是 isa 指针,结构体的其他成员变量就是类的其他属性,这里也就是 `_name`,所以我们给自定义的指针 `void *p` 调用 sayHi 方法,系统 runtime 在打印 name 的时候,会在 p 附近(下8个字节,因为 isa 是指针,长度为8)找 `_name` 属性,此时也就找到了 temp 字符串。 + +```c +struct Person_IMPL { + Class isa; // 8字节 + NSString *_name; // 8字节 +} +``` + +再看一个变体1 + +```c +NSObject *temp = [[NSObject alloc] init]; +id obj = [Person class]; +void *p = &obj; +[(__bridge id)p sayHi]; +// hi,my name is +``` + +再看一个变体2(将代码放在 ViewController中) + +```objectivec +- (void)viewDidLoad { + [super viewDidLoad]; + id obj = [Person class]; + void *p = &class; + NSObject *temp = [[NSObject alloc] init]; + [(__bridge id)p sayHi]; +} +// hi,my name is +``` + +搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。 + +再强调一句,根据指针寻找成员变量 _name 的过程其实就是根据内存偏移找对象的过程。在变体2中,isa 地址就是 class 的地址,所以按照`地址 +8` 的策略,其实前一个局部变量。 + +`[super viewDidLoad];` 本质就是 `objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("viewDidLoad"))` + +```c +struct objc_super arg = {self, class_getSuperclass(self)}; +objc_msgSendSuper(arg, sel_registerName("viewDidLoad")); +``` + +所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self,所以“前一个局部变量” 也就是 self(ViewController) + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-super-isa-demo.png) + +## 应用场景 + +1.统计 App 中未响应的方法。给 NSObject 添加分类 + +```objectivec +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + // 本来能调用的方法 + if ([self respondsToSelector:aSelector]) { + return [super methodSignatureForSelector:aSelector]; + } + // 找不到的方法 + return [NSMethodSignature signatureWithObjCTypes:"v@:"]; +} + +// 找不到的方法,都会来到这里 +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + NSLog(@"找不到%@方法", NSStringFromSelector(anInvocation.selector)); +} +@end +``` + +2.修改类的 isa + +`object_setClass` 实现 + +```objectivec +Person *p = [Person new]; +object_setClass(p, [Student class]); +``` + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-changeisa-demo.png) + +3.动态创建类 + +`objc_allocateClassPair`、`objc_registerClassPair` 成对存在 + +动态创建类、添加属性、方法 + +```objectivec +void study (id self, SEL _cmd) { + NSLog(@"在学习了"); +} + +void createClass (void) { + Class newClass = objc_allocateClassPair([NSObject class], "GoodStudent", 0); + class_addIvar(newClass, "_score", 4, 1, "i"); + class_addIvar(newClass, "_height", 4, 1, "i"); + class_addMethod(newClass, @selector(study), (IMP)study, "v16@0:8"); + objc_registerClassPair(newClass); + id student = [[newClass alloc] init]; + [student setValue:@100 forKey:@"_score"]; + [student setValue:@177 forKey:@"_height"]; + [student performSelector:@selector(study)]; + NSLog(@"%@ %@", [student valueForKey:@"_score"], [student valueForKey:@"_height"]); +} +``` + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-dynamicCreateClass-demo.png) + +runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)` + +4.访问成员变量信息 + +```objectivec +void ivarInfo (void) { + Ivar nameIvar = class_getInstanceVariable([Person class], "_name"); + NSLog(@"%s %s", ivar_getName(nameIvar), ivar_getTypeEncoding(nameIvar)); //_name @"NSString" + // 设置、获取成员变量 + Person *p = [[Person alloc] init]; + Ivar ageIvar = class_getInstanceVariable([Person class], "_age"); + object_setIvar(p, ageIvar, (__bridge id)(void *)27); + NSLog(@"%d", p.age); +} +``` + +runtime 设置值 api `object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value)` 第三个参数要求为 id 类型,但是我们给 int 类型的属性设置值,怎么办?可以将27这个数字的地址传进去,同时需要类型转换为 id `(__bridge id)(void *)27)` + +KVC 可以根据具体的值,去取出 NSNumber ,然后调用 intValue + +`[p setValue:@27 forKey:@"_age"];` + +5.访问对象的所有成员变量信息 + +```objectivec +@property (nonatomic, strong) NSString *name; +@property (nonatomic, assign) int age; +@end + +unsigned int count; +// 数组指针 +Ivar *properties = class_copyIvarList([Person class], &count); +for (int i =0 ; iimp; + m1->imp = m2->imp; + m2->imp = m1_imp; + + // RR/AWZ updates are slow because class is unknown + // Cache updates are slow because class is unknown + // fixme build list of classes whose Methods are known externally? + flushCaches(nil); + updateCustomRR_AWZ(nil, m1); + updateCustomRR_AWZ(nil, m2); +} + +static void flushCaches(Class cls) +{ + runtimeLock.assertWriting(); + mutex_locker_t lock(cacheUpdateLock); + if (cls) { + foreach_realized_class_and_subclass(cls, ^(Class c){ + cache_erase_nolock(c); + }); + } + else { + foreach_realized_class_and_metaclass(^(Class c){ + cache_erase_nolock(c); + }); + } +} +``` + +总结: + +OC 是一门动态性很强的编程语言,允许很多操作推迟到程序运行时决定。OC 动态性其实就是由 Runtime 来实现的,Runtime 是一套 c 语言 api,封装了很多动态性相关函数。平时写的 oc 代码,底层大多都是转换为 Runtime api 进行调用的。 + +- 关联对象 + +- 遍历类的所有成员变量(可以访问私有变量,比如修改 UITextFiled 的 placeholder 颜色、字典转模型、自动归档接档) + +- 交换方法实现 + +- 扩大点击区域 + +- 利用消息转发机制,解决消息找不到的问题 + +- 无痕埋点 + +- 热修复 diff --git a/Chapter1 - iOS/1.83.md b/Chapter1 - iOS/1.83.md index b695298..b0aefea 100644 --- a/Chapter1 - iOS/1.83.md +++ b/Chapter1 - iOS/1.83.md @@ -16,14 +16,12 @@ NSURLProtocol 是 Foundation 框架中 URL Loading System 的一部分。它可 答案就是通过子类化来定义新的或是已经存在的 URL 加载行为。如果当前的网络请求是可以被拦截的,那么开发者只需要将一个自定义的 NSURLProtocol 子类注册到 App 中,在这个子类中就可以拦截到所有请求并进行修改。 - - - ## 二、NSURLProtocol 使用场景 ### 1. 技术层面 NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有基于 URL Loading System 的网络请求: + - NSURLSession - NSURLConnection - NSURLDownload @@ -31,9 +29,10 @@ NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有 - NSHTTPURLResponse - NSURLRequest - NSMutableURLRequest -所以,基础这些基础技术开发的网络框架比如 AFNetworking、Alamofire 也可以拦截。 + 所以,基础这些基础技术开发的网络框架比如 AFNetworking、Alamofire 也可以拦截。 想到了2种场景不能拦截: + - 早期使用 CFNetwork 实现的 ASIHTTPRequest 框架就无法拦截 - UIWebView 也是可以被拦截的。但是 WKWebView 是基于 webkit,不走底层 c socket。 @@ -54,9 +53,6 @@ NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有 - 自定义网络请求,过滤垃圾内容 - H5 加速,请求走本地离线包 - - - ## 三、NSURLProtocol 的相关方法 创建协议对象 @@ -69,6 +65,7 @@ NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有 ``` 注册和注销协议类 + ```Objective-c // 尝试注册 NSURLProtocol 的子类,使之在 URL 加载系统中可见 + (BOOL)registerClass:(Class)protocolClass; @@ -103,6 +100,7 @@ NSURLProtocol 允许开发者去获取、添加、删除 request 对象的任意 提供请求的规范版本 如果你想要用特定的某个方式来修改请求,可以用下面这个方法。 + ```Objective-c // 返回指定请求的规范版本 + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request; @@ -118,6 +116,7 @@ NSURLProtocol 允许开发者去获取、添加、删除 request 对象的任意 启动和停止加载 这是子类中最重要的两个方法,不同的自定义子类在调用这两个方法时会传入不同的内容,但共同点都是围绕 protocol 客户端进行操作。 + ```Objective-c // 开始加载 - (void)startLoading; @@ -138,24 +137,19 @@ NSURLProtocol 允许开发者去获取、添加、删除 request 对象的任意 - (NSURLSessionTask *)task; ``` - - - ## 四、 如何利用 NSProtocol 拦截网络请求 + NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL 转发。先来看如何拦截网络请求。 - - **创建 NSURLProtocol 子类** 这里创建一个名为 HTCustomURLProtocol 的子类。 + ```Objective-c @interface HTCustomURLProtocol : NSURLProtocol @end ``` - - **注册 NSURLProtocol 的子类** 在合适的位置注册这个子类。对基于 NSURLConnection 或者使用 [NSURLSession sharedSession] 初始化对象创建的网络请求,调用 registerClass 方法即可。 @@ -166,16 +160,12 @@ NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL // [NSURLProtocol registerClass:[HTCustomURLProtocol class]]; ``` - 如果需要全局监听,可以设置在 `AppDelegate.m` 的 `didFinishLaunchingWithOptions` 方法中。如果只需要在单个 UIViewController 中使用,记得在合适的时机注销监听: ```objective-c [NSURLProtocol unregisterClass:[NSClassFromString(@"HTCustomURLProtocol") class]]; ``` - - - 如果是基于 `NSURLSession` 的网络请求,且不是通过 `[NSURLSession sharedSession]` 方式创建的,就得配置 `NSURLSessionConfiguration` 对象的 `protocolClasses` 属性。 ```objective-c @@ -184,16 +174,10 @@ NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; ``` - - **实现 NSURLProtocol 子类** - - > 注册 → 拦截 → 转发 → 回调 → 结束 - - 以拦截 UIWebView 为例,这里需要重写父类的这五个核心方法。 ```objective-c @@ -323,11 +307,8 @@ didCancelAuthenticationChallenge:challenge]; } ``` - 注意:NSURLConnection 已经被废弃,推荐使用 NSURLSession 进行网络请求,它好处多多,具体的自行查阅官方介绍。 - - ## 五、 读源码,学习 NSURLProtocol iOS 中网络测试框架 [ OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs)的实现就是利用了 NSURLProtocol 实现的。 @@ -349,15 +330,8 @@ HTTPStubsProtocol 继承自 NSURLProtocol,可以在 HTTP 请求发送之前对 `firstStubPassingTestForRequest` 方法内部会判断请求是否需要被当前对象处理 - - 紧接着开始发送网络请求。实际上在 `- (void)startLoading` 方法中可以用任何网络能力去完成请求,比如 NSURLSession、NSURLConnection、AFNetworking 或其他网络框架。OHHTTPStubs 的做法是获取 request、client 对象。如果 HTTPStubs 单例中包含 `onStubActivationBlock` 对象,则执行该 block,然后利用 responseBlock 对象返回一个 HTTPStubsResponse 响应对象。 - - - - - ## 六、 补充内容 ### 1. 使用 NSURLSession 时的注意事项 @@ -368,16 +342,12 @@ HTTPStubsProtocol 继承自 NSURLProtocol,可以在 HTTP 请求发送之前对 • 如果要用 registerClass 注册,只能通过 ` [NSURLSession sharedSession] `的方式创建网络请求。 - - ### 2. 注册多个 NSURLProtocol 子类 当有多个自定义 NSURLProtocol 子类注册到系统中的话,会按照他们注册的反向顺序依次调用 URL 加载流程,也就是最后注册的 NSURLProtocol 会被优先判断。 对于通过配置 NSURLSessionConfiguration 对象的 protocolClasses 属性来注册的情况,protocolClasses 数组中只有第一个 NSURLProtocol 会起作用,后续的 NSURLProtocol 就无法拦截到了。 - - ### 3. 如何拦截 WKWebview WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 webKit2 源码暴露了 私有API : @@ -402,15 +372,15 @@ if ([cls respondsToSelector:sel]) { 该方案还存在2个严重缺陷: 1. post 请求 body 数据被清空 - + 由于 WKWebview 在独立的进程中执行网络请求,一旦注册 registerSchemeForCustomProtocol http(https) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。在 Webkit2 的设计里使用 **MessageQueue** 进行进程间通信, Network Process 会将请求 encode 成一个 message,然后通过 IPC 发送给 App Process,出于性能角度的考虑,encode 的时候 HTTPBody 和 HTTPBodyStream 这2个字段被丢弃掉了。可以查看 [webkit2 源码](https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88)。 2. 对 ATS 支持不足 - + info.plist 中打开 ATS 开关,设置 Allow Arbitrary Loads 选项为 NO,设置 registerSchemeForCustomProtocol 注册了 http(https) scheme,WKWebView 发起的所有 http 网络请求将被阻塞(即使 Allow Arbitrary Loads in Web Content 选项为 YES)。 - + WKWebView 可以注册 customScheme,比如自定义 scheme:`Hybrid://`,因此使用离线包但不使用 post 方式的请求可以通过 customScheme 发起。比如 `Hybrid://www.xxx.com/`,然后在 App 进程被 NSURLProtocol 拦截这个请求,然后加载离线包资源。 - + 不足:使用 post 方式的请求需要修改 h5 侧代码(scheme)。 ### 4. WKWebView loadRequest 问题 @@ -440,34 +410,9 @@ if ([cls respondsToSelector:sel]) { 6. 网络请求完成后,通过 NetworkProtocolClient 将请求结果返回给 WKWebView。 - ### 5. 拦截 WebView 内 Ajax 请求 其实上述的方法也是可行,不过使用私有 API 的方式不是很推荐,一般在穷途末路的时候才选择私有 API,所以另一种思路是 hook Web 端的 ajax 请求。在执行 hook 后的 ajax 请求的时候将 ajax 的请求相关信息(请求方式、header、body 等)以 messageHandler 的方式告诉 Native,然后起到监控的效果。 参考: https://www.jianshu.com/p/7337ac624b8e;https://github.com/wendux/Ajax-hook - - - - - - - - - - - - - - - - - - - - - - -1. -关于 WKWebview 的各种问题可以查看这篇[文章](https://www.tuicool.com/articles/QbE3Mb7)。 - +1. 关于 WKWebview 的各种问题可以查看这篇[文章](https://www.tuicool.com/articles/QbE3Mb7)。 diff --git a/Chapter1 - iOS/1.88.md b/Chapter1 - iOS/1.88.md index 12374d9..ec68b18 100644 --- a/Chapter1 - iOS/1.88.md +++ b/Chapter1 - iOS/1.88.md @@ -1,71 +1,113 @@ # fishhook 原理 -1. image: 代码编译后的可执行文件,被加载到内存中,就叫做镜像文件。 -2. MachO 可执行文件被 dyld 加载到内存中,加载时并不是所有的符号都可以确定地址,有些是通过 lazy bind 在真正调用的时候绑定的。 -3. iOS 代码在编译时没有办法确定方法的实现地址。**动态库共享缓存**,里面有动态库。NSLog 属于 Foundation 框架,每个手机内部中的地址不一定。 +## 先看看怎么用 - ![image-20200810182447587](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/image-20200810182447587.png) +经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了,有了 fishhook 神器,hook “c 函数”已不是难题。 -4. DYLD 动态链接器,负责将可执行文件加载到内存中。App 启动后,DYLD 将 Foundation、UIKit 等加载进动态库共享缓存中,但是加载到的位置不确定。 +为什么对 “c 函数”加了引号,带着问题往下看 -5. 可执行文件头有 **Load Commands**:加载命令。告诉 DYLD 依赖了什么。 +Hook NSLog,上 Demo -6. 从 NSLog 找到代码实现,经历过2种方式。早期:重定向。现在:PIC 技术(位置代码独立)。 +```objectivec +static void (*SystemLog)(NSString *format, ...); +- (void)viewDidLoad { + [super viewDidLoad]; + struct rebinding NSLogRebinding = { + "NSLog", + lbpLog, + (void *)&SystemLog + }; + struct rebinding rebs[1] = {NSLogRebinding}; + rebind_symbols(rebs, 1); + NSLog(@"沙沙"); +} +void lbpLog(NSString *format, ...) { + format = [NSString stringWithFormat:@"fishhook 探索 - %@", format]; + SystemLog(format); +} +@end +// fishhook 探索 - 沙沙 +``` -7. 可执行文件: +可以看到 hook 成功了。 - - 代码段:可读可执行 - - 数据段:可读可写 - -8. 写的业务代码里面假如某一行调用了 `NSLog`,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址,它指向了一个表(类似一个应用程序的外部函数名,函数真正实现地址),去这个表里面找地址,这个表叫做**符号表**。 - - 当 DYLD 加载当前可执行文件的时候,才将这个表每个编号对应的函数地址去填上去,这个动作叫做**符号绑定**。 - - 当真正去调用 NSLog 函数的时候才去这个符号表中去寻找函数地址,去调用实现。 - - | 编号(符号) | 地址 | | - | ------------ | -------- | ---- | - | NSLog | 0xaabbcc | | - | ... | ... | | - - ![image-20200810201822593](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/mage-20200810201822593.png) - -9. fishhook 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于**动态调用**。 - - https://www.bilibili.com/video/BV1UZ4y1u7Ba?from=search&seid=14997461811427810898 +```c +struct rebinding { + const char *name; // 需要 hook 的函数名称,c 字符串 + void *replacement; // 新函数地址 + void **replaced; // 原始函数地址的指针 +}; +``` -fishhook去 hook c 函数的原理。 +## 原理窥探 -假如我们的代码中调用了 NSLog 函数,因为 NSLog 的实现是在 Foundation 库中,动态库在内存中的地址是不固定的, ASLR 机制下, -所以在编译阶段是没办法确定 +我们知道 NSLog 的函数实现在 Foundation 库中,而我们开发自己写的其他函数则在自身可执行文件中,也就是 Mach-O。 + +这里稍微展开谈谈静态链接和动态链接。 + +链接分为静态链接和动态链接。早期计算机都是采用静态链接这种方式的。静态链接存在缺点: + +- 对于计算机内存和磁盘浪费很严重。想象下,每个程序内部保留了 printf、scanf 等公用库函数,还有很多其他库函数和所需要的数据结构。 + +- 程序的开发和发布很不方便。比如应用 A,使用的 Lib.o 是一个第三方厂商提供的,当 lib.o 修复 bug 或者升级,开发都需要将应用 A 重新链接再发布,整个周期很不方便。 + +要解决空间浪费和更新困难最简单的办法就是把程序的模块拆分,形成独立文件,而不再将他们静态地链接在一起。而是等到程序运行起来才进行链接,也就是动态链接。 + +动态链接涉及运行时的链接以及多个文件的装载,必须有操作系统级别的支持。此时还有个角色叫做动态链接库。所有应用都可以在运行时使用它。 + +程序与 lib 动态库之间的链接工作是由动态链接器完成的,而不是静态链接器 ld 完成的。也就是动态链接是把链接这个过程由程序装载前被推迟到了装载的时候。 + +但也带来了坏处,因为都是程序每次装载的时候进行重新链接。有解决方案,叫做延迟绑定(Lazy binding),可使得动态链接对性能的影响减的最小。据估算,动态链接相比静态链接,存在大约5%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。 -## fishHook 不能 hook 自定义函数 +地址无关代码(PIC) -可执行文件、动态链接库,加载到内存中的时候,会存在多种文件格式,系统为了统一标准,让加载到内存中的文件必须是 Mach-O 文件格式。 - -Hopper Disassembler v4 - -- Mach-O 的定义?结构组成 -- +装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码(Position Independent Code)PIC 技术。 -fishhook 可以 hook c 函数的原因? +写的业务代码里面假如某一行调用了 `NSLog`,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址。这个底层是如何工作的呢? -1. 函数符号 数据段 被修改 -2. 函数符号为何位于数据段? -3. 动态库每次被加载到内存中,地址都是随机不确定的。所以需要符号地址的修正。符号重定位、重绑定 -4. Lazy Symbol Pointers 懒汉模式;Non-Lazy Symbol Pointers 启动就去绑定 +在工程编译阶段,所产生的 Mach-O 可执行文件中会预留出一段空间,这个空间被叫做符号表,存放在 `_DATA` 数据段中,且数据段是可读可写的。 + +工程中所有引用了动态库共享缓存区中的系统符号,其指向的地址设置成符号地址。比如工程中 NSLog,那么编译时就会在 Mach-O 中创建一个 NSLog 符号,工程中的 NSLog 就指向这个符号 + +当 dyld 将 Mach-O 加载到内存中时,读取 header 中 load command 信息,找出需要加载哪些库文件,去做绑定的操作。比如 dyld 会找到 Foundation 中的 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 对应的符号上。 + +当 DYLD 加载当前可执行文件的时候,才将这个表每个编号对应的函数地址去填上去,这个动作叫做**符号绑定**。 + +它指向了一个表(类似一个应用程序的外部函数名,函数真正实现地址),去这个表里面找地址,这个表叫做**符号表**。 + + + + + +当真正去调用 NSLog 函数的时候才去这个符号表中去寻找函数地址,去调用实现。 + +调用外部函数(在内部找不到方法实现)的时候,在 Mach-O 的数据段生成一个区域,叫做符号表。符号表的 key 就是方法名,比如 NSLog。 + +fishHook 做的事情就是 rebind,将 NSLog 真正实现的地址,指向到其他我们生成的函数地址去(hook)。 + +PIC 技术。 + +| 编号(符号) | 地址 | | +| ------ | -------- | --- | +| NSLog | 0xaabbcc | | +| ... | ... | | + +![image-20200810201822593](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/mage-20200810201822593.png) + +fishhook 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于**动态调用**。 + + + +知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。可以通过 `rebind_symbols` 这个名称得以印证。 -1. 可以 hook c++吗?为什么 -2. linux 平台下能否 hook c/c++? -3. diff --git a/Chapter1 - iOS/1.89.md b/Chapter1 - iOS/1.89.md index 65d6022..acbca8e 100644 --- a/Chapter1 - iOS/1.89.md +++ b/Chapter1 - iOS/1.89.md @@ -1,21 +1,902 @@ -# block 原理 +# block 底层原理 -1. 解决循环引用不应该使用 weakself,而是使用 strong-weak -```Objective-c -__weak typeof(self) Weakself = self; -self.block = ^ { - __strong typeof(Weakself) Strongself = Weakself; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ { - NSLog(@"%@", Strongself.name); - }); +## block 本质 + +block 本质上就是一个 oc 对象,也有 isa 指针 + +block 是封装了函数调用和函数调用环境的 OC 对象 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-structure.png) + +```objectivec +int age = 27; +void(^block)(void) = ^(){ +    NSLog(@"age:%d", age); }; -self.block(); +block(); ``` -![image-20200810214301251](/Users/lbp/Library/Application Support/typora-user-images/image-20200810214301251.png) +`xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp` + +转换为 c++ 如下 + +```c +int age = 27; +void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); +((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); + + +struct __main_block_impl_0 { + struct __block_impl impl; + struct __main_block_desc_0* Desc; + int age; + __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { + impl.isa = &_NSConcreteStackBlock; + impl.Flags = flags; + impl.FuncPtr = fp; + Desc = desc; + } +}; +``` + +可以看到 `__main_block_impl_0` 是一个结构体,有3个变量,其中 `__main_block_impl_0` 是一个构造方法,接收4个参数,第四个参数 flags 是非必须的。 + +`__main_block_func_0` 参数是一个方法实现。 + +```c +static void __main_block_func_0(struct __main_block_impl_0 *__cself) { + int age = __cself->age; // bound by copy + + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_eb3c55_mi_0, age); + } +``` + +```c +static struct __main_block_desc_0 { + size_t reserved; + size_t Block_size; +} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; +``` + +`__main_block_desc_0` 是一个 block 信息的描述,占用了 `sizeof(struct __main_block_impl_0)` 大小的空间。 + +`void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));` 第一行代码也就是将构造一个 struct 给 block 变量。 + +`((**void** (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);` 第二行代码其实就是 `block->FuncPtr(block)` 根据 block 内部 FuncPtr 方法并调用。 + +```c +static struct __main_block_desc_0 { + size_t reserved; + size_t Block_size; +} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; +``` + +为什么 block->FuncPtr 可以直接访问,而不是 block 先访问 impl,再访问 FuncPtr?因为 __block_impl 就是 __main_block_impl_0 这个结构体的第一个变量地址(结构体特性) + +```c +struct __block_impl { + void *isa; + int Flags; + int Reserved; + void *FuncPtr; +}; +``` + +类似于下面代码 + +``` +struct __main_block_impl_0 { + void *isa; + int Flags; + int Reserved; + void *FuncPtr; + struct __main_block_desc_0* Desc; + int age; + __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { + impl.isa = &_NSConcreteStackBlock; + impl.Flags = flags; + impl.FuncPtr = fp; + Desc = desc; + } +}; +``` + +## block 变量捕获 + +```objectivec +int age = 27; +void(^block)(void) = ^(){ + NSLog(@"age:%d", age); +}; +age = 30; +block(); +``` + +输出27。因为 Block 会对变量进行捕获。 + +```c +int age = 27; +void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); +age = 30; + ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); +``` + +变量分为:static、auto、register。 + +static:表示作为静态变量存储在数据区。 + +auto:一般的变量不加修饰词则默认为 auto,auto 表示作为自动变量存储在栈上。意味着离开作用域变量会自动销毁。 + +register:这个关键字告诉编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。是尽可能,不是绝对。如果定义了很多 register 变量,可能会超过CPU 的寄存器个数,超过容量。所以只是可能。 + +| 作用域 | 捕获到 block 内部 | 访问方式 | +| ---------- | ------------ | ---- | +| 局部变量 auto | YES | 值传递 | +| 局部变量static | YES | 指针传递 | +| 全局变量 | NO | 直接访问 | + +Demo2 + +```objectivec +auto int age = 27; +static int height = 176; +void(^block)(void) = ^(){ + NSLog(@"age:%d, height: %d", age, height); +}; +age = 30; +height = 177; +block(); +// age:27, height: 177 +``` + +clang 转为 c++ + +```c +auto int age = 27; +static int height = 176; +void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height)); +age = 30; +height = 177; +((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); + + +struct __main_block_impl_0 { + struct __block_impl impl; + struct __main_block_desc_0* Desc; + int age; + int *height; + __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) { + impl.isa = &_NSConcreteStackBlock; + impl.Flags = flags; + impl.FuncPtr = fp; + Desc = desc; + } +}; +``` + +可以看到 static 修饰的 height 在 c++ 代码底层用指针被 block 捕获,所以值修改后,是最终的 177,static 修饰的 age,被 block 捕获是值传递方式,所以还是27 + +Demo3 + +```objectivec +int age = 27; +static int height = 176; +int main(int argc, const char * argv[]) { + @autoreleasepool { + void(^block)(void) = ^(){ + NSLog(@"age:%d, height: %d", age, height); + }; + age = 26; + height = 177; + block(); + } + return 0; +} +``` + +转为 c++ + +```c +void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); +age = 26; +height = 177; +((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); + + +struct __main_block_impl_0 { + struct __block_impl impl; + struct __main_block_desc_0* Desc; + __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { + impl.isa = &_NSConcreteStackBlock; + impl.Flags = flags; + impl.FuncPtr = fp; + Desc = desc; + } +}; +static void __main_block_func_0(struct __main_block_impl_0 *__cself) { + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_65da50_mi_0, age, height); +} +``` + +可以看到全局变量是直接访问的,不进行拷贝。 + +为什么这么设计? + +因为 auto 修饰的变量一出作用域马上回收,所以 block 为了自身运行信息的完整性,所以会捕获。static 修饰的变量,数据一直在内存中,所以执行 block 代码的时候是用指针获取内存中最新的数据。全局变量不会捕获。 + +self 会捕获吗? + +会,因为 self 就是局部变量。 一个 oc 方法转换为 `void test(Person *self, SEL _cmd)` 形式,所以 self 也是局部变量,会被捕获 + +## block 类型 + +block 的类型可以通过 isa 或者 class 方法查看,最终都是继承自 NSBlock 类型 + +`__NSGlobalBlock__` (`_NSConcreteGlobalBlock`):程序的数据区域(.data 区) + +`__NSStackBlock__` (`_NSConcreteStackBlock`) + +`__NSMallocBlock__`(`_NSConcreteMallockBlock`) + +```objectivec +void(^block)(void) = ^(){ + NSLog(@"Hello block"); +}; +NSLog(@"%@", [block class]); // __NSGlobalBlock__ +NSLog(@"%@", [[block class] superclass]); // NSBlock +NSLog(@"%@", [[[block class] superclass] superclass]); // NSObjec +``` + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-memorylayout.png) + +代码存放在 text 段,static 修饰的数据存放在 data 区,程序员手动申请的内存存放在堆,局部变量存放在栈区 + +### 如何判断 block 属于什么类型 + +Demo1 + +```objectivec +void(^block)(void) = ^{ + NSLog(@""); +}; +int main(int argc, const char * argv[]) { + @autoreleasepool { + NSLog(@"%@", [block class]); + } + return 0; +} +``` + +`__NSGlobalBlock__` ,此**类型的 block 用结构体实例的内容不依赖于执行时的状态,所以整个程序只需要1个实例。因此存放于全局变量相同的数据区域即可。** + +```objectivec +void(^block)(void); +void test() +{ + int age = 22; + block = ^ { + NSLog(@"ag:%d", age); + }; +} +int main(int argc, const char * argv[]) { + @autoreleasepool { + test(); + block(); // age:-1074793800 + } + return 0; +} +``` + +为什么会打印出 `age:-1074793800`。 因为 block 访问了auto 变量,所以是 `__NSStackBlock__`。那么 block 内部的数据在栈上维护,当出了 test 方法后,block 内部变量的地址到底指向什么是不确定的,可能会出现异常。 + +| block 类型 | 环境 | +| ------------------- | ------------------------------ | +| `__NSGlobalBlock__` | 没有访问 auto 变量 | +| `__NSStackBlock__` | 访问了 auto 变量 | +| `__NSMallocBlock__` | `__NSStackBlock__` 调用了 copy 方法 | + +Demo1: + +```objectivec +MyBlock block; +{ + Person *person = [[Personalloc] init]; + block = ^{ + NSLog(@"block called"); + }; + NSLog(@"%@", [block class]); +}; +``` + +MRC 环境: 如果 block 不访问外部局部变量,则`__NSGlobalBlock__` + +ARC 环境:如果 block 不访问外部局部变量,则`__NSGlobalBlock__` + +Demo2: + +```objectivec +typedef void(^MyBlock)(void); +int main(int argc, const char * argv[]) { + @autoreleasepool { + MyBlock block; + { + auto Person *person = [[Person alloc] init]; + person.age = 10; + block = ^{ + NSLog(@"age:%zd", person.age); + }; + NSLog(@"%@", [block class]); // __NSStackBlock__ + }; + } + return 0; +} +``` + +MRC 环境下:如果访问了 auto 变量,则为 `__NSStackBlock__` + +ARC 环境下:**ARC 下面比较特殊,默认局部变量对象都是强指针,存放在堆里面。所以 block 为 `__NSMallocStack__`** + +Demo3: + +```objectivec +typedef void(^MyBlock)(void); +int main(int argc, const char * argv[]) { + @autoreleasepool { + MyBlock block; + { + auto Person *person = [[Person alloc] init]; + person.age = 10; + block = [^{ + NSLog(@"age:%zd", person.age); + } copy]; + NSLog(@"%@", [block class]); // __NSMallocBlock__ + }; + } + return 0; +} +``` + +MRC 下:如果 block 调用 copy 方法,则 block 为 `__NSMallocStck__` + +ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 `__NSMallocBlock__`。`__NSMallocBlock__` 调用 copy 仍旧为 `__NSMallocBlock__` + +在 ARC 下,如果有一个强指针引用 block,则 block 会被拷贝到堆上,成为 `__NSMallocStck` -字节对齐:k值:8的倍数。 +| Block 类 | 原本位置 | 复制效果 | | +| --------------------------- | ------ | ------ | --- | +| `__NSConcreteStackBlock__` | 栈 | 栈复制到堆 | | +| `__NSConcreteGlobalBlock__` | 程序的数据段 | 什么也不做 | | +| `__NSConcreteMallocBlock__` | 堆 | 引用计数+1 | | -字节对齐的原因: \ No newline at end of file +### 内存管理 + +### 思考题 + +查看 block 编译成 c++ 代码的源码可以发现 `__main_block_desc_0` 结构体内部是变化的。什么意思呢?reserved、Block_size 是一直有的,void (*copy)、void (*dispose) 只有在修饰对象的时候才有。为什么这么设计? + +```c +static struct __main_block_desc_0 { + size_t reserved; + size_t Block_size; + void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); + void (*dispose)(struct __main_block_impl_0*); +} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; +``` + +因为 block 会对变量进行内存管理。`void *copy`、`void *dispose` 都是内存管理的方法。 + +如果 block 访问的不是对象,则结构体没有 `void *copy`、`void *dispose` + +Demo1: + +```objectivec +{ + Person *person = [[Person alloc] init]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"1---%@", person); + }); +} +``` + +1s 后 person 执行了 dealloc 方法 + +Demo2 + +```objectivec +{ + __weak Person *person = [[Person alloc] init]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"1---%@", person); + }); +} +``` + +马上执行了 Person 的 dealloc 方法。因为 `__weak` 修饰,block 内部的 `_Block_object_assign` 会根据 `__strong` 为对象引用计数 +1,`__weak` 则引用计数不变。所以是 `__weak` 修饰,出离作用域则立马会释放 Person 对象。 + +`_Block_object_assign` 会根据内存修饰符来对内存进行操作。 + +Demo3 + +```objectivec +{ + Person *person = [[Person alloc] init]; + __weak Person *weakP = person; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"1---%@", person); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"2---%@", person); + }); + }); +} +``` + +3s 后执行 Person 的 dealloc + +Demo4 + +```objectivec +{ + Person *person = [[Person alloc] init]; + __weak Person *weakP = person; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"1---%@", weakP); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"2---%@", person); + }); + }); +} +``` + +3s 后执行 Person 的 dealloc 方法 + +Demo5 + +```objectivec +{ + Person *person = [[Person alloc] init]; + __weak Person *weakP = person; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"1---%@", person); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"2---%@", weakP); + }); + }); +} +``` + +1s 后执行 Person 的 dealloc 方法。 + +结论:block 作为 GCD API 的方法参数时,如果 block 内部访问了对象,对象的生命周期结束需要查看强引用结束时刻。 + +### ARC 环境下编译器会自动会 block copy 复制到堆上 + +1. block 作为函数返回值时(如果栈 block,离开方法作用域之后,return 给新的变量区使用,由于栈变化了,所以不安全。比如访问 auto 变量的栈 block,可能某个变量已经不是之前的某个值了) + +2. 将 block 赋值给 __strong 指针时 + +3. block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时 + +4. block 作为 GCD API 的方法参数时 + +栈空间的 block 不会对变量进行 copy 操作 + +堆空间的 block 会堆变量自动进行 copy 操作 + +`__NSStackBlock__` 内部访问了对象,默认是 `__strong` 修饰。如果对象是 `__weak` 则 block 转换 c++ 内部捕获的对象,也用 weak 修饰 + +```c +typedef void(^MyBlock)(void); +int main(int argc, const char * argv[]) { + @autoreleasepool { + MyBlock block; + { + __weak Person *person = [[Person alloc] init]; + person.age = 27; + block = ^{ + NSLog(@"age:%zd", person.age); + }; + person.age = 28; + }; + } + return 0; +} + +struct __main_block_impl_0 { + struct __block_impl impl; + struct __main_block_desc_0* Desc; + Person *__weak person; + __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _person, int flags=0) : person(_person) { + impl.isa = &_NSConcreteStackBlock; + impl.Flags = flags; + impl.FuncPtr = fp; + Desc = desc; + } +}; +``` + +## block 修改变量 + +Demo1 + +```objectivec +typedef void(^MyBlock)(void); +int main(int argc, const char * argv[]) { + @autoreleasepool { + int age = 27; + MyBlock block = ^{ + age = 28; + }; + NSLog(@"%zd", age); + } + return 0; +} +``` + +运行会报错 `// Variable is not assignable (missing __block type specifier)` 为什么不能修改?继续查看 c++ 源代码 + +```c +static void __main_block_func_0(struct __main_block_impl_0 *__cself) { + int age = __cself->age; // bound by copy + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f31a48_mi_0, age); +} + +static struct __main_block_desc_0 { + size_t reserved; + size_t Block_size; +} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; +int main(int argc, const char * argv[]) { + /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; + int age = 27; + MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f31a48_mi_1, age); + } + return 0; +} +``` + +可以看到在 block 内部修改外部变量也就是在新创建的函数内部,修改 main 函数内部的变量 😂 这怎么可能? + +全局变量、static 变量在 block 内部可以修改。 + +- `__block` 用于解决 block 内部无法修改 auto 变量的问题。 + +- `__block` 不能修饰 static、全局变量 + +- 编译器会将 `__block` 修饰的变量包装为一个对象(后续修改则通过指针找到结构体对象,结构体对象再修改里面的值) + +Demo + +```objectivec +__block int age = 27; +MyBlock block = ^{ + age = 28; +}; +``` + +转为 C++ + +```c +__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27}; + MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344)); + NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f0f60a_mi_0, (age.__forwarding->age)); + +struct __Block_byref_age_0 { + void *__isa; +__Block_byref_age_0 *__forwarding; + int __flags; + int __size; + int age; +}; + +static void __main_block_func_0(struct __main_block_impl_0 *__cself) { + __Block_byref_age_0 *age = __cself->age; // bound by ref + (age->__forwarding->age) = 28; +} +``` + +可以看到 `__block int age = 27;` 变为了 `__Block_byref_age_0 age` 结构体。block 内部的函数在修改 age 的时候其实就是通过 `__main_block_impl_0` 结构体的 age 找到 `__Block_byref_age_0`,然后访问 `__Block_byref_age_0` 中的成员变量 `__forwarding` 访问成员变量 age,并修改值。 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-forwarding.png) + + `__block` 修饰基本数据类型和对象,对于生成的结构体也不一样。 + +QA:为什么`__block` 变量的 `__Block_byref_age_0` 结构体并不在 block 结构体 `__main_block_impl_0` 中? + +因为这样做可以在多个 block 中使用 `__block` 变量。 + +Demo + +```objectivec +struct __Block_byref_age_0 { + void *__isa; +__Block_byref_age_0 *__forwarding; + int __flags; + int __size; + int age; +}; + +struct __Block_byref_p_1 { + void *__isa; +__Block_byref_p_1 *__forwarding; + int __flags; + int __size; + void (*__Block_byref_id_object_copy)(void*, void*); + void (*__Block_byref_id_object_dispose)(void*); + NSObject *p; +}; +``` + +block 只要和对象打交道,结构体里面要管理内存,所以会有 `void *copy` ,`void *dispose` + +block 内部操作数组等类型,不需要加 `__block` + + + +Demo:知道 `__block` 的本之后,下面打印的 age 的地址是 struct 里面哪个的值? + +```objectivec +__block int age = 27; +MyBlock block = ^{ + age = 28; +}; +NSLog(@"%p", &age); +``` + +知道转换为c++后的效果,我们可以在代码中按照结构体,自己定义并转接到 block + +```objectivec +struct __Block_byref_age_0 { + void *__isa; // 0x0000000105231f70 +8 + struct __Block_byref_age_0 *__forwarding; // 0x0000000105231f78 + 8 + int __flags; // 0x0000000105231f80 +4 + int __size; // 0x0000000105231f84 + 4 + int age; // 0x0000000105231f88 +}; + +struct __block_impl { + void *isa; + int Flags; + int Reserved; + void *FuncPtr; +}; + +struct __main_block_desc_0 { + size_t reserved; + size_t Block_size; + void (*copy)(void); + void (*dispose)(void); +}; + +struct __main_block_impl_0 { + struct __block_impl impl; + struct __main_block_desc_0* Desc; + struct __Block_byref_age_0 *age; // by ref +}; + + +typedef void(^MyBlock)(void); +int main(int argc, const char * argv[]) { + @autoreleasepool { + __block int age = 27; + MyBlock block = ^{ + age = 28; + }; + struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block; + NSLog(@"%p", &age); + } + return 0; +} +``` + +我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Block-variableAddress.png) + +```c +// 0x0000000105231f70 +struct __Block_byref_age_0 { + void *__isa; // 地址:0x0000000105231f70 长度:+8 + struct __Block_byref_age_0 *__forwarding; // 地址:0x0000000105231f78 长度:+8 + int __flags; // 地址:0x0000000105231f80 长度:+4 + int __size; // 地址:0x0000000105231f84 长度:+4 + int age; // 地址:0x0000000105231f88 +}; + +``` + +将地址打印出来。该地址就是 `__Block_byref_age_0` 结构体的地址,也就是结构体内第一个 `isa` 的地址。我们计算下,规则如下: + +- 指针长度8个字节 + +- int 长度4个字节 + +算出来 age 的地址为 `0x0000000105231f88` ,此时 Xcode 打印出的地址也是 `0x105231f88`。其实也就是 `blockImple->age->age` 的地址 + +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` 函数会自动释放 `__block` 修饰的变量(release) + + + +## `__forwarding` 的设计 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block_forwarding.png) + +当block在栈中时,`__Block_byref_age_0`结构体内的`__forwarding`指针指向结构体自己。 + +而当block被复制到堆中时,栈中的`__Block_byref_age_0`结构体也会被复制到堆中一份,而此时栈中的`__Block_byref_age_0`结构体中的`__forwarding`指针指向的就是堆中的`__Block_byref_age_0`结构体,堆中`__Block_byref_age_0`结构体内的`__forwarding`指针依然指向自己。 + + + + + +## Block 内存引用 + +被 `__block ` 修饰符修饰的对象在内存中如下 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-object-memoery.png) + +```c + +int main(int argc, const char * argv[]) { + /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; + __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {(void*)0,(__Block_byref_p_0 *)&p, 33554432, sizeof(__Block_byref_p_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))}; + void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_p_0 *)&p, 570425344)); + } + return 0; +} + + +static void __Block_byref_id_object_copy_131(void *dst, void *src) { + _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); +} + +static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { + _Block_object_assign((void*)&dst->p, (void*)src->p, 8/*BLOCK_FIELD_IS_BYREF*/); +} +``` + + + +如果 `__block` 修饰 `__strong` 则表示 block_impl 结构体中的 person 成员变量指向一个新的结构体 `__Block_byref_person_0`。这个线是强引用。 + +`__Block_byref_person_0` 结构体成员变量 person 真正的 Person 对象的引用关系要看 block 外部 person 的修饰是 `__strong` 还是 `__weak`,因为从栈上拷贝到堆上,会调用 block 的 desc 的 `__main_block_copy_0`,本质上调用的是 `_Block_object_assign` + +`__Block_byref_id_object_copy_131` 方法里的 40 代表什么? + +```c + +struct __Block_byref_p_0 { + void *__isa; 8 +__Block_byref_p_0 *__forwarding; 8 + int __flags; 4 + int __size; 4 + void (*__Block_byref_id_object_copy)(void*, void*); 8 + void (*__Block_byref_id_object_dispose)(void*); 8 + Person *p; +}; + + +__attribute__((__blocks__(byref))) __Block_byref_p_0 p = { + 0, + &p, + 33554432, + sizeof(__Block_byref_p_0), + __Block_byref_id_object_copy_131, + __Block_byref_id_object_dispose_131, + ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")) + +}; +``` + +`__Block_byref_p_0` 结构体地址上偏移40就是 p 对象。 + + + + + +## 循环引用 + + self 是一个局部变量,block 访问 self,即存在捕获变量的效果。 + +### ARC 下 + + `__weak`、`__unsafe_unretained`、`__block`` + +区别在于:`__weak` 不会产生强引用,指向的对象销毁时,会自动给指针置为 nil + +`__unsafe_retained` 不会产生强引用,不安全。当指向的对象销毁时,指针地址值不变。 + + + +```objectivec +@interface Person : NSObject +@property (nonatomic, assign) NSInteger age; +@property (nonatomic, copy) void (^block)(void); +- (void)test; +@end + +@implementation Person +- (void)dealloc +{ + NSLog(@"%s", __func__); +} +- (void)test +{ +     + __weak typeof(self) weakself = self; + self.block = ^{ + weakself.age = 23; + }; + self.block(); + NSLog(@"age:%ld", (long)self.age); +} +@end + +Person *p = [[Person alloc] init]; +[p test]; +``` + +方法1: `__weak` 修饰。`__weak typeof(self) weakself = self;` + +方法2: `__unsafe_retained` 修饰。`**__unsafe_unretained** **typeof**(**self**) weakself = **self**;` + +方法3: `__block` 修饰。因为此时会构成3角关系。所以需要调用 block。block 内部需要将对象设置为 nil。 + +```objectivec +__block Person *weakself = [[Person alloc] init]; +p.block = ^{ + weakself.age = 23; + NSLog(@"%ld", weakself.age); + weakself = nil; +}; +p.block(); +``` + +`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存,所以不推荐。ARC 下最佳用 `__weak` + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block_object_cycle.png) + +### MRC 下 + +方法1: `__unsafe_retained` 修饰。`**__unsafe_unretained** **typeof**(**self**) weakself = **self**;` + +方法2: `__block` 修饰。MRC 下不会对 block 内部的对象引用计数 +1 + + + + + +## 总结 + +block 本质是什么? + +封装了函数调用及其调用环境的 OC 对象 + + + +`__block` 的作用是什么? + +可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如`__weak`、`__unsafe_unretained`、`__block` + + + +修改 NSMutableArray 不需要加 `__block`? + +是的 \ No newline at end of file diff --git a/Chapter1 - iOS/1.91.md b/Chapter1 - iOS/1.91.md index 13c9bbd..e52a28b 100644 --- a/Chapter1 - iOS/1.91.md +++ b/Chapter1 - iOS/1.91.md @@ -1,99 +1,204 @@ -# 二进制重排 +# DYLD + +dynamic loader,动态加载器。在 MacOS/iOS 中,使用 `/usr/lib/dyld` 程序来加载动态库的。 + +## DYLD(dyld shared cache) 动态库共享缓存 + +UIKit、CoreGraphics 等。从 iOS13 开始,为了提高性能,绝大部分的系统动态库文件都被打包存放到了一个缓存文件中(dyld shared cache)中。 +存放路径为:`/System/Libarey/Caches/com.apple.dyld/dyld_shared_cache_armX` +其中,`X` 代表 ARM 处理器的指令集架构。V6、V7、V7s、arm64、arm64e。不同架构,对应的动态库缓存不一致。 + +所有指令集原则是向下兼容的。动态库共享缓存一个非常明显的好处是节省内存。 + +具体底层源码可以查看,dyld 源码中 `dyld2.cpp` 文件,函数入口为 load 方法。 + +```c +ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex); +``` + +某个 App 被第一次打开时候, dyld 根据动态库的依赖,循环加载动态库到动态库共享缓存中(内存),后续其他 App 第一次打开发现使用了动态库A已经在动态库共享缓存中存在,则不需要加载。这一步调用方法为 `findInSharedCacheImage` + +## dyld 应用 + +窥探系统库底层实现的时候可能需要从动态库共享缓存中提取出某个 Framework,比如 UIKit。这时候要么用第三方工具,要么用 dyld 的能力。 + +查看 `dsc_extractor.cpp` 代码可以看到是具备抽取能力的。修改源码,将 if 宏定义去掉。如下 + +```c +#include +#include +#include -## 启动检测 +typedef int (*extractor_proc)(const char* shared_cache_file_path, const char* extraction_root_path, + void (^progress)(unsigned current, unsigned total)); -- App 动态库不要超过6个。 +int main(int argc, const char* argv[]) +{ + if ( argc != 3 ) { + fprintf(stderr, "usage: dsc_extractor \n"); + return 1; + } -- 静态库:影响 Mach-O 文件。 + //void* handle = dlopen("/Volumes/my/src/dyld/build/Debug/dsc_extractor.bundle", RTLD_LAZY); + void* handle = dlopen("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/usr/lib/dsc_extractor.bundle", RTLD_LAZY); + if ( handle == NULL ) { + fprintf(stderr, "dsc_extractor.bundle could not be loaded\n"); + return 1; + } -- 动态库:影响 App 启动时间。 + extractor_proc proc = (extractor_proc)dlsym(handle, "dyld_shared_cache_extract_dylibs_progress"); + if ( proc == NULL ) { + fprintf(stderr, "dsc_extractor.bundle did not have dyld_shared_cache_extract_dylibs_progress symbol\n"); + return 1; + } + int result = (*proc)(argv[1], argv[2], ^(unsigned c, unsigned total) { printf("%d/%d\n", c, total); } ); + fprintf(stderr, "dyld_shared_cache_extract_dylibs_progress() => %d\n", result); + return 0; +} +``` +然后用 clang++ 编译,命令为 `clang++ dsc_extractor.cpp` -## 虚拟内存、物理内存、内存分页 +将编译后的产物复制到动态库共享缓存目录下去。然后执行命令`./dsc_extractor dyld_shared_cache_armv7s armv7s`,代表将动态库提取到 armv7s 目录下。 -早期计算机内部都是物理内存。物理内存一个问题就是安全问题,通过地址访问到别的应用程序的数据。进程如果能直接访问物理内存无疑是很不安全的,(诞生了解决方案:虚拟内存)所以操作系统在物理内存的上又建立了一层虚拟内存。 +## Mach-O -一个应用程序在使用的时候并不是全部使用的,往往是使用了一个应用程序的某个或者某几个功能。 +Mach Object 的缩写,是 iOS/MacOS 上用于存储程序、库的标准格式 -所以早期工程师的第一个解决方案是将一个应用程序分块,按需加载功能模块的内存地址数据。 +在 XNU 源码中可以查看 Mach-O 的定义。`loader.h` +属于 Mach-O 格式的文件类型有: +```c +#define MH_OBJECT 0x1 /* relocatable object file */ +#define MH_EXECUTE 0x2 /* demand paged executable file */ +#define MH_FVMLIB 0x3 /* fixed VM shared library file */ +#define MH_CORE 0x4 /* core file */ +#define MH_PRELOAD 0x5 /* preloaded executable file */ +#define MH_DYLIB 0x6 /* dynamically bound shared library */ +#define MH_DYLINKER 0x7 /* dynamic link editor */ +#define MH_BUNDLE 0x8 /* dynamically bound bundle file */ +#define MH_DYLIB_STUB 0x9 /* shared library stub for static */ + /* linking only, no section contents */ +#define MH_DSYM 0xa /* companion file with only debug */ + /* sections */ +#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */ +``` -虚拟内存是间接访问了内存条。 +### 常见的 Mach-O 文件类型 -内存分页?iOS 一页就是16KB。 +MH_OBJECT -物理内存如果满了,则将最活跃的内存数据,覆盖掉,最不活跃的内存数据; +- 目标文件(.o) -ASLR?为了安全问题诞生。 +- 静态库文件(.a),静态库其实就是 N 个 `.o` 合并在一起 -自己的代码中有 NSLog 代码,可是 NSLog 函数的代码实现在 Foundation 动态库中。 +MH_EXECUTE:可执行文件 -App 启动则用 dyld 去加载库,共享缓存库。 +MH_DYLIB:动态库文件 +- dylib +- .framework -虚拟地址:偏移是编译后就能确定的。 +MY_DYLINKER:动态链接编辑器 (/usr/lib/dyld) +MH_DSYM:存储着二进制文件符号。`.dSYM/Contents/Resource/DWARF/xx` 常用于还原堆栈 +Xcode 中也可以查看 Mach-O 文件类型 -内存缺页异常:在使用中,访问虚拟内存的一个 page 而对应的物理内存缺不存在(没有被加载到物理内存中),则发生缺页异常。影响耗时,在几毫秒之内。 +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOFileType.png) -什么时候发生大量的缺页异常?一个应用程序刚启动的时候。 +### Universal Binary -启动时所需要的代码分布在第一页、第二页、第三页...第200页。这样的情况下启动时间会影响较大,所以解决思路就是将应用程序启动刻所需要的代码(二进制优化一下),统一放到某几页,这样就可以避免内存缺页异常,则优化了 App 启动时间。 +通用二进制文件,可以同时适用于多种架构的二进制文件,包含多种不同架构的独立的二进制文件。 +因为要存储多种架构的代码,所以通用二进制文件通常比单一平台的二进制程序更大 +由于两种架构有共同的一些资源,所以并不会达到单一版本的2倍多。 +执行的过程中,只调用一部分代码,运行起来不需要额外的内存。 +因为通用二进制文件比原来的大,所以被成为“胖二进制文件”(Fat Binary) -dylib loading time: + 查看某可执行文件(Test)支持的架构指令集 -rebase/binding time: 修正符号是由于 ASLR 导致。binding time 是动态库去 bind lazy table、non-lazy table 所占用的时间。rebase 地址偏移,ASLR。 rebase 的时间如何缩小?Mach-O 文件大小变小。 binding time 变小,则需要动态库变小。2者优化手段冲突 +`lipo -info Test` -Objc setup time:Swift 这部分占优势 +将某个指令集拆出来比如 arm64 -initializer time:load 方法耗时。 +`lipo Test -thin arm64 -o Test_arm64` -slowest intializers: +也可以将多个指令集合并 -libS +`lipo -create Test_arm64 Test_armv7 -output Test_universal` -libMain +## Mach-O 结构 +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Mach-OStructure.png) +一个 Mach-O 文件包含3块 -查看 LinkMap。发现方法展示顺序是按照,写代码的顺序展示的。 +- Header:文件类型、目标架构类型信息 -![image-20200814211215976](/Users/lbp/Library/Application Support/typora-user-images/image-20200814211215976.png) +- Load commands:描述文件在虚拟内存中的逻辑结构、布局 +- Raw segment data:在 Load Commands 中定义的 segment 的原始数据 +可以用 [GitHub - fangshufeng/MachOView: 分析Macho必备工具](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息 -## 有没有办法将 App 启动需要的方法集中收拢? +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/otoolhelp.png) -1. 在 Xcode 的 Build Settings 中设置 **Order File**,Write Link Map Files 设置为 YES(进行观察) +用 MachOView 查看 DDD Mach-O 文件 -2. 如果你给 Xcode 工程根目录下指定一个 order 文件,比如 `refine.order`,则 Xcode 会按照指定的文件顺序进行二进制数据重排。分析 App 启动阶段,将优先需要加载的函数、方法,集中合并,利用 Order File,减小缺页异常,从而减小启动时间。 +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOPageZero.png) +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOText.png) +可以看到在 Mach-O 文件上,`__PAGEZERO` 的 `VM Size` 有值,但是 File Size 为0,也就是说 `__PAGEZERO` 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0(如果前面的 `__PAGEZERO` 的 File size 有值,这里的 File Offset 就不为0)。 -## 如何拿到启动时刻所调用的所有方法名称 +**在没有 ASLR 的时候:__TEXT 代码段地址 = __PAGEZEROR 地址** -clang 插桩,才可以 hook OC、C、block、Swift 全部。LLVM 官方文档。 +## ASLR +### 未使用 ASLR 的问题 +- 函数代码存放在 `__TEXT` 段 +- 全局变量存放在 `__DATA` 段 - 二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。 +- 可执行文件的内存地址为 `0x0` -一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」,可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。 +- 可执行文件 Header 的内存地址,就是 `LC_SEGMENT(__TEXT)` 中的 VM Address + + - arm64 :0x100000000 + + - 非 arm64:0x4000 -其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC,所以hook 肯定不行、fishhook 也不行,用 clang 插桩可行 +也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布 +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOInsepect.png) +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ASLRDemo.png) +我们会发现根据 Mach-O 文件中的信息,File Size、File Offset、VM Address、VM Size 可以判断出 `__TEXT` 段内函数信息,这样子不够安全 +### ASLR 诞生 -- https://mp.weixin.qq.com/s/SUHaGD1T2Vce4Ag-qgxtgg \ No newline at end of file +Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。 + +![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ASLROffset.png) + +- LC_SEGMENT (__TEXT) 的 VM Address 0x10005000 + +- ASLR 随机偏移 0x5000,也就是可执行文件的内存地址 + +在有 ASLR 的时候:__TEXT 代码段地址 = __PAGEZEROR 地址 + +在 Mach-O 文件中的地址是原始地址 + +```shell +代码运行起来函数真实地址 = ASLR-Offset + __PAGEZERO(arm64:0x100000000,其他:0x4000)+ 函数基于 Mach-O 的地址 +``` \ No newline at end of file diff --git a/Chapter1 - iOS/1.98.md b/Chapter1 - iOS/1.98.md index 2848a37..289b8b1 100644 --- a/Chapter1 - iOS/1.98.md +++ b/Chapter1 - iOS/1.98.md @@ -58,7 +58,7 @@ Vue 支持单向绑定和双向绑定。单向绑定其实就是 Model 到 View 使用 React Redux 一个典型的流程图如下 -![React-Redux](./../assets/2019-06-26-Redux-Structures.png) +![React-Redux](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-26-Redux-Structures.png) 假如我们把 action 和 dispatcher 的实现隐藏在框架内部,这个图可以简化为 diff --git a/Chapter1 - iOS/chapter1.md b/Chapter1 - iOS/chapter1.md index 3c57c24..e9a4ae6 100644 --- a/Chapter1 - iOS/chapter1.md +++ b/Chapter1 - iOS/chapter1.md @@ -9,7 +9,7 @@ * [4、如何优雅地调试手机网页?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.4.md) * [5、事件响应者链](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.5.md) * [6、外卖App双列表联动](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.6.md) - * [7、在内存剖析对象](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md) + * [7、对象在内存中的存储底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md) * [8、教你实现微信公众号效果:长按图片保存到相册](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.8.md) * [9、hitTest和pointInside方法你真的熟吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.9.md) * [10、HyBrid探索(一)](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.10.md) @@ -41,9 +41,9 @@ * [35、一些布局小知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.35.md) * [36、iOS数值计算精度丢失问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.36.md) * [37、一些看到但未尝试的知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.37.md) - * [38、RunLoop上](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md) - * [39、RunLoop下](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md) - * [40、RunLoop的应用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md) + * [38、RunLoop探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md) + * [39、多线程探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md) + * [40、内存问题研究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md) * [41、iOS 应用启动性能优化资料汇总](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.41.md) * [42、App security](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.42.md) * [43、奇技淫巧调试篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.43.md) @@ -51,7 +51,7 @@ * [45、NSTimer 的内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md) * [46、KVC && KVO](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.46.md) * [47、金额格式化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.47.md) - * [48、OC类别(Catrgory)和拓展(Extension)](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md) + * [48、类别(Category)、拓展(Extension)、load、initialize](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md) * [49、MVC、MVP、MVVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.49.md) * [50、“静态库”和“动态库”](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.50.md) * [51、cocoapods 相关小技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.51.md) @@ -92,9 +92,9 @@ * [86、GCD 源码探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.86.md) * [87、Objective-C 底层探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.87.md) * [88、fishhook 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.88.md) - * [89、block 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md) + * [89、block 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md) * [90、YYImage 框架原理,探索图片高效加载原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.90.md) - * [91、二进制重排](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md) + * [91、DYLD](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md) * [92、flutter 无痕埋点](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.92.md) * [93、flutter 新功能引导](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.93.md) * [94、APM-Wake Up](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.94.md) @@ -103,4 +103,6 @@ * [97、__attribute__ 的骚操作](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.97.md) * [98、前端、BFF、后端一些常见的设计模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.98.md) * [99、客户端质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.md) - * [100、iOS 端底层网络错误](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.100.md) \ No newline at end of file + * [100、iOS 端底层网络错误](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.100.md) + * [101、离屏渲染](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.101.md) + * [102、LLVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md) \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.40.md b/Chapter2 - Web FrontEnd/2.40.md index 0dd1746..aea2ce9 100644 --- a/Chapter2 - Web FrontEnd/2.40.md +++ b/Chapter2 - Web FrontEnd/2.40.md @@ -398,7 +398,7 @@ Electron 架构和 Chromium 架构类似,也是具有1个主进程和多个渲 - ![chrome inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electronChromeInspect.png) - 重新开启服务 `npm start`,在 chrome inspect 面板的 `Target` 节点中选择需要调试的页面 - 在面板中可以看到主进程执行的 `main.js`。可以加断点进行调试 - ![chrome inspect](./../assets/2020-04-21-Electron-MainProcessInspect.png) + ![chrome inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-Electron-MainProcessInspect.png) 方法二:利用 VS Code 调试 Electron 主进程。 @@ -846,6 +846,6 @@ app.listen(33855) 1. App 包体积大小是一个工程治理的一个永恒话题,伴随着 App 每一次版本发布的生命周期,App 包大小的意义就不再赘述,这里讲讲【App 包体积】这个命题如何与 Electron 结合起来。 - App 包体积的治理方案可以查看 [App瘦身之道](./../ Chapter1\ -\ iOS/1.60.md) 这篇文章。目的是通过 Electron 这个技术打造有赞自己的移动潘多拉魔盒,囊括必要的各种能力,所以【App 包体积】这个命题可以结合 Electron,将包大小检测能力作为魔盒的能力之一。 + App 包体积的治理方案可以查看 [App瘦身之道](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/ Chapter1\ -\ iOS/1.60.md) 这篇文章。目的是通过 Electron 这个技术打造有赞自己的移动潘多拉魔盒,囊括必要的各种能力,所以【App 包体积】这个命题可以结合 Electron,将包大小检测能力作为魔盒的能力之一。 2. 桌面端技术选型的时候现在多了一些选择:Electron、[Tauri](https://github.com/tauri-apps/tauri)、ImGui。其中 Tauri 就是 WebView + Rust 的实现。ImGui 是一个 C/C++ 实现的即时渲染框架。 \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md index 0da3c5e..5b28e1c 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -9,7 +9,7 @@ * [4、如何优雅地调试手机网页?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.4.md) * [5、事件响应者链](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.5.md) * [6、外卖App双列表联动](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.6.md) - * [7、在内存剖析对象](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md) + * [7、对象在内存中的存储底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md) * [8、教你实现微信公众号效果:长按图片保存到相册](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.8.md) * [9、hitTest和pointInside方法你真的熟吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.9.md) * [10、HyBrid探索(一)](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.10.md) @@ -40,9 +40,9 @@ * [35、一些布局小知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.35.md) * [36、iOS数值计算精度丢失问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.36.md) * [37、一些看到但未尝试的知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.37.md) - * [38、RunLoop上](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md) - * [39、RunLoop下](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md) - * [40、RunLoop的应用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md) + * [38、RunLoop探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md) + * [39、多线程探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md) + * [40、内存问题研究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md) * [41、iOS 应用启动性能优化资料汇总](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.41.md) * [42、App security](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.42.md) * [43、奇技淫巧调试篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.43.md) @@ -50,7 +50,7 @@ * [45、NSTimer 的内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md) * [46、KVC && KVO](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.46.md) * [47、金额格式化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.47.md) - * [48、OC类别(Catrgory)和拓展(Extension)](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md) + * [48、类别(Category)、拓展(Extension)、load、initialize](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md) * [49、MVC、MVP、MVVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.49.md) * [50、“静态库”和“动态库”](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.50.md) * [51、cocoapods 相关小技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.51.md) @@ -91,9 +91,9 @@ * [86、GCD 源码探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.86.md) * [87、Objective-C 底层探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.87.md) * [88、fishhook 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.88.md) - * [89、block 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md) + * [89、block 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md) * [90、YYImage 框架原理,探索图片高效加载原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.90.md) - * [91、二进制重排](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md) + * [91、DYLD](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md) * [92、flutter 无痕埋点技术方案](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.92.md) * [93、flutter 新功能引导](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.93.md) * [94、APM-Wake Up](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.94.md) @@ -103,6 +103,8 @@ * [98、前端、BFF、后端一些常见的设计模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.98.md) * [99、客户端质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.md) * [100、iOS 端底层网络错误](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.100.md) + * [101、离屏渲染](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.101.md) + * [102、LLVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md) * [Chapter2 - Web FrontEnd](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/chapter2.md) * [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.1.md) * [2、正则表达式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.2.md) diff --git a/assets/ASLRDemo.png b/assets/ASLRDemo.png new file mode 100644 index 0000000..0fbf125 Binary files /dev/null and b/assets/ASLRDemo.png differ diff --git a/assets/ASLROffset.png b/assets/ASLROffset.png new file mode 100644 index 0000000..228bac3 Binary files /dev/null and b/assets/ASLROffset.png differ diff --git a/assets/AppLaunchingTime.png b/assets/AppLaunchingTime.png new file mode 100644 index 0000000..7a5c92a Binary files /dev/null and b/assets/AppLaunchingTime.png differ diff --git a/assets/AutoreleasePoolMoreItem.png b/assets/AutoreleasePoolMoreItem.png new file mode 100644 index 0000000..77b4c99 Binary files /dev/null and b/assets/AutoreleasePoolMoreItem.png differ diff --git a/assets/Block-variableAddress.png b/assets/Block-variableAddress.png new file mode 100644 index 0000000..d8a7ee2 Binary files /dev/null and b/assets/Block-variableAddress.png differ diff --git a/assets/Facebook-OOM.jpeg b/assets/Facebook-OOM.jpeg new file mode 100644 index 0000000..de9f988 Binary files /dev/null and b/assets/Facebook-OOM.jpeg differ diff --git a/assets/KVC-get-process.png b/assets/KVC-get-process.png new file mode 100644 index 0000000..5d75d14 Binary files /dev/null and b/assets/KVC-get-process.png differ diff --git a/assets/KVC-process.png b/assets/KVC-process.png new file mode 100644 index 0000000..c066d88 Binary files /dev/null and b/assets/KVC-process.png differ diff --git a/assets/LLVM-Structure.png b/assets/LLVM-Structure.png new file mode 100644 index 0000000..719b1e7 Binary files /dev/null and b/assets/LLVM-Structure.png differ diff --git a/assets/LLVM-phase.png b/assets/LLVM-phase.png new file mode 100644 index 0000000..5b6ba7f Binary files /dev/null and b/assets/LLVM-phase.png differ diff --git a/assets/LLVM-segment.png b/assets/LLVM-segment.png new file mode 100644 index 0000000..152ea36 Binary files /dev/null and b/assets/LLVM-segment.png differ diff --git a/assets/Mach-OStructure.png b/assets/Mach-OStructure.png new file mode 100644 index 0000000..3f0ab47 Binary files /dev/null and b/assets/Mach-OStructure.png differ diff --git a/assets/MachOFileType.png b/assets/MachOFileType.png new file mode 100644 index 0000000..3a1d48c Binary files /dev/null and b/assets/MachOFileType.png differ diff --git a/assets/MachOInsepect.png b/assets/MachOInsepect.png new file mode 100644 index 0000000..674530d Binary files /dev/null and b/assets/MachOInsepect.png differ diff --git a/assets/MachOPageZero.png b/assets/MachOPageZero.png new file mode 100644 index 0000000..46cf395 Binary files /dev/null and b/assets/MachOPageZero.png differ diff --git a/assets/MachOText.png b/assets/MachOText.png new file mode 100644 index 0000000..bed5228 Binary files /dev/null and b/assets/MachOText.png differ diff --git a/assets/MockClassInfo.h b/assets/MockClassInfo.h new file mode 100644 index 0000000..efe55b7 --- /dev/null +++ b/assets/MockClassInfo.h @@ -0,0 +1,143 @@ +// +// MockClassInfo.h +// TestClass +// +// Created by MJ Lee on 2018/3/8. +// Copyright © 2018年 MJ Lee. All rights reserved. +// + +#import + +#ifndef MockClassInfo_h +#define MockClassInfo_h + +# if __arm64__ +# define ISA_MASK 0x0000000ffffffff8ULL +# elif __x86_64__ +# define ISA_MASK 0x00007ffffffffff8ULL +# endif + +#if __LP64__ +typedef uint32_t mask_t; +#else +typedef uint16_t mask_t; +#endif +typedef uintptr_t cache_key_t; + +struct bucket_t { + cache_key_t _key; + IMP _imp; +}; + +struct cache_t { + bucket_t *_buckets; + mask_t _mask; + mask_t _occupied; +}; + +struct entsize_list_tt { + uint32_t entsizeAndFlags; + uint32_t count; +}; + +struct method_t { + SEL name; + const char *types; + IMP imp; +}; + +struct method_list_t : entsize_list_tt { + method_t first; +}; + +struct ivar_t { + int32_t *offset; + const char *name; + const char *type; + uint32_t alignment_raw; + uint32_t size; +}; + +struct ivar_list_t : entsize_list_tt { + ivar_t first; +}; + +struct property_t { + const char *name; + const char *attributes; +}; + +struct property_list_t : entsize_list_tt { + property_t first; +}; + +struct chained_property_list { + chained_property_list *next; + uint32_t count; + property_t list[0]; +}; + +typedef uintptr_t protocol_ref_t; +struct protocol_list_t { + uintptr_t count; + protocol_ref_t list[0]; +}; + +struct class_ro_t { + uint32_t flags; + uint32_t instanceStart; + uint32_t instanceSize; // instance对象占用的内存空间 +#ifdef __LP64__ + uint32_t reserved; +#endif + const uint8_t * ivarLayout; + const char * name; // 类名 + method_list_t * baseMethodList; + protocol_list_t * baseProtocols; + const ivar_list_t * ivars; // 成员变量列表 + const uint8_t * weakIvarLayout; + property_list_t *baseProperties; +}; + +struct class_rw_t { + uint32_t flags; + uint32_t version; + const class_ro_t *ro; + method_list_t * methods; // 方法列表 + property_list_t *properties; // 属性列表 + const protocol_list_t * protocols; // 协议列表 + Class firstSubclass; + Class nextSiblingClass; + char *demangledName; +}; + +#define FAST_DATA_MASK 0x00007ffffffffff8UL +struct class_data_bits_t { + uintptr_t bits; +public: + class_rw_t* data() { + return (class_rw_t *)(bits & FAST_DATA_MASK); + } +}; + +/* OC对象 */ +struct mock_objc_object { + void *isa; +}; + +/* 类对象 */ +struct mock_objc_class : mock_objc_object { + Class superclass; + cache_t cache; + class_data_bits_t bits; +public: + class_rw_t* data() { + return bits.data(); + } + + mock_objc_class* metaClass() { + return (mock_objc_class *)((long long)isa & ISA_MASK); + } +}; + +#endif /* MockClassInfo_h */ diff --git a/assets/OSSpinLock-Assemble2.png b/assets/OSSpinLock-Assemble2.png new file mode 100644 index 0000000..3143caa Binary files /dev/null and b/assets/OSSpinLock-Assemble2.png differ diff --git a/assets/OSSpinLock-Assemble3.png b/assets/OSSpinLock-Assemble3.png new file mode 100644 index 0000000..f1afb33 Binary files /dev/null and b/assets/OSSpinLock-Assemble3.png differ diff --git a/assets/OSSpinLock-Assemble4.png b/assets/OSSpinLock-Assemble4.png new file mode 100644 index 0000000..74411ae Binary files /dev/null and b/assets/OSSpinLock-Assemble4.png differ diff --git a/assets/OSSpinLockAssemble1.png b/assets/OSSpinLockAssemble1.png new file mode 100644 index 0000000..0e085bf Binary files /dev/null and b/assets/OSSpinLockAssemble1.png differ diff --git a/assets/RunLoop-RunIssue.png b/assets/RunLoop-RunIssue.png new file mode 100644 index 0000000..f615e00 Binary files /dev/null and b/assets/RunLoop-RunIssue.png differ diff --git a/assets/RunLoop-SourceCode.png b/assets/RunLoop-SourceCode.png new file mode 100644 index 0000000..97896ae Binary files /dev/null and b/assets/RunLoop-SourceCode.png differ diff --git a/assets/RunLoop-Specific.png b/assets/RunLoop-Specific.png new file mode 100644 index 0000000..c25d6c3 Binary files /dev/null and b/assets/RunLoop-Specific.png differ diff --git a/assets/Runtime-AssociatedProperty.png b/assets/Runtime-AssociatedProperty.png new file mode 100644 index 0000000..1febd9a Binary files /dev/null and b/assets/Runtime-AssociatedProperty.png differ diff --git a/assets/TaggedPointerCrash.png b/assets/TaggedPointerCrash.png new file mode 100644 index 0000000..93b15a5 Binary files /dev/null and b/assets/TaggedPointerCrash.png differ diff --git a/assets/Thread-deadlock-unfaillock.png b/assets/Thread-deadlock-unfaillock.png new file mode 100644 index 0000000..cbfeeba Binary files /dev/null and b/assets/Thread-deadlock-unfaillock.png differ diff --git a/assets/Thread-deadlock-unfairTrylock.png b/assets/Thread-deadlock-unfairTrylock.png new file mode 100644 index 0000000..d047a53 Binary files /dev/null and b/assets/Thread-deadlock-unfairTrylock.png differ diff --git a/assets/__forwarding__clean.c b/assets/__forwarding__clean.c new file mode 100644 index 0000000..d1bbbbe --- /dev/null +++ b/assets/__forwarding__clean.c @@ -0,0 +1,35 @@ +int __forwarding__(void *frameStackPointer, int isStret) { + id receiver = *(id *)frameStackPointer; + SEL sel = *(SEL *)(frameStackPointer + 8); + const char *selName = sel_getName(sel); + Class receiverClass = object_getClass(receiver); + + // 调用 forwardingTargetForSelector: + if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) { + id forwardingTarget = [receiver forwardingTargetForSelector:sel]; + if (forwardingTarget && forwardingTarget != receiver) { + return objc_msgSend(forwardingTarget, sel, ...); + } + } + + // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation + if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) { + NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel]; + if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) { + NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; + + [receiver forwardInvocation:invocation]; + + void *returnValue = NULL; + [invocation getReturnValue:&value]; + return returnValue; + } + } + + if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) { + [receiver doesNotRecognizeSelector:sel]; + } + + // The point of no return. + kill(getpid(), 9); +} diff --git a/assets/autoreleasepool.png b/assets/autoreleasepool.png new file mode 100644 index 0000000..17eab29 Binary files /dev/null and b/assets/autoreleasepool.png differ diff --git a/assets/block-forwarding.png b/assets/block-forwarding.png new file mode 100644 index 0000000..3c0b433 Binary files /dev/null and b/assets/block-forwarding.png differ diff --git a/assets/block-memorylayout.png b/assets/block-memorylayout.png new file mode 100644 index 0000000..1f1e0c8 Binary files /dev/null and b/assets/block-memorylayout.png differ diff --git a/assets/block-object-memoery.png b/assets/block-object-memoery.png new file mode 100644 index 0000000..daaa711 Binary files /dev/null and b/assets/block-object-memoery.png differ diff --git a/assets/block-structure.png b/assets/block-structure.png new file mode 100644 index 0000000..0749283 Binary files /dev/null and b/assets/block-structure.png differ diff --git a/assets/block_forwarding.png b/assets/block_forwarding.png new file mode 100644 index 0000000..3f160e2 Binary files /dev/null and b/assets/block_forwarding.png differ diff --git a/assets/block_object_cycle.png b/assets/block_object_cycle.png new file mode 100644 index 0000000..db40949 Binary files /dev/null and b/assets/block_object_cycle.png differ diff --git a/assets/clang-analysize.png b/assets/clang-analysize.png new file mode 100644 index 0000000..9b0494a Binary files /dev/null and b/assets/clang-analysize.png differ diff --git a/assets/clang-ast.png b/assets/clang-ast.png new file mode 100644 index 0000000..cb65976 Binary files /dev/null and b/assets/clang-ast.png differ diff --git a/assets/clang-phase.png b/assets/clang-phase.png new file mode 100644 index 0000000..01ab9d6 Binary files /dev/null and b/assets/clang-phase.png differ diff --git a/assets/class-isa-superclass.png b/assets/class-isa-superclass.png new file mode 100644 index 0000000..66649d2 Binary files /dev/null and b/assets/class-isa-superclass.png differ diff --git a/assets/iOS-MemoryLayout.png b/assets/iOS-MemoryLayout.png new file mode 100644 index 0000000..43b52fe Binary files /dev/null and b/assets/iOS-MemoryLayout.png differ diff --git a/assets/ignoreXcodewarning.png b/assets/ignoreXcodewarning.png new file mode 100644 index 0000000..1b6cfe4 Binary files /dev/null and b/assets/ignoreXcodewarning.png differ diff --git a/assets/objc-isa-mask.png b/assets/objc-isa-mask.png new file mode 100644 index 0000000..0c21822 Binary files /dev/null and b/assets/objc-isa-mask.png differ diff --git a/assets/objc-isa.png b/assets/objc-isa.png new file mode 100644 index 0000000..eb72d11 Binary files /dev/null and b/assets/objc-isa.png differ diff --git a/assets/objc-metaclass-superclass.png b/assets/objc-metaclass-superclass.png new file mode 100644 index 0000000..a46f193 Binary files /dev/null and b/assets/objc-metaclass-superclass.png differ diff --git a/assets/objc-superclass.png b/assets/objc-superclass.png new file mode 100644 index 0000000..af02308 Binary files /dev/null and b/assets/objc-superclass.png differ diff --git a/assets/osunfairlock-assemble1.png b/assets/osunfairlock-assemble1.png new file mode 100644 index 0000000..d8b3dea Binary files /dev/null and b/assets/osunfairlock-assemble1.png differ diff --git a/assets/osunfairlock-assemble2.png b/assets/osunfairlock-assemble2.png new file mode 100644 index 0000000..8eb4786 Binary files /dev/null and b/assets/osunfairlock-assemble2.png differ diff --git a/assets/osunfairlock-assemble3.png b/assets/osunfairlock-assemble3.png new file mode 100644 index 0000000..854db56 Binary files /dev/null and b/assets/osunfairlock-assemble3.png differ diff --git a/assets/osunfairlock-assemble4.png b/assets/osunfairlock-assemble4.png new file mode 100644 index 0000000..21114fd Binary files /dev/null and b/assets/osunfairlock-assemble4.png differ diff --git a/assets/osunfairlock-assemble5.png b/assets/osunfairlock-assemble5.png new file mode 100644 index 0000000..132e775 Binary files /dev/null and b/assets/osunfairlock-assemble5.png differ diff --git a/assets/otoolhelp.png b/assets/otoolhelp.png new file mode 100644 index 0000000..fac4df0 Binary files /dev/null and b/assets/otoolhelp.png differ diff --git a/assets/runtime-categoryattachLists.png b/assets/runtime-categoryattachLists.png new file mode 100644 index 0000000..0717afd Binary files /dev/null and b/assets/runtime-categoryattachLists.png differ diff --git a/assets/runtime-changeisa-demo.png b/assets/runtime-changeisa-demo.png new file mode 100644 index 0000000..8f9d7c2 Binary files /dev/null and b/assets/runtime-changeisa-demo.png differ diff --git a/assets/runtime-class-ro-t.png b/assets/runtime-class-ro-t.png new file mode 100644 index 0000000..bd47b26 Binary files /dev/null and b/assets/runtime-class-ro-t.png differ diff --git a/assets/runtime-class-rw-t.png b/assets/runtime-class-rw-t.png new file mode 100644 index 0000000..c42bcae Binary files /dev/null and b/assets/runtime-class-rw-t.png differ diff --git a/assets/runtime-class.png b/assets/runtime-class.png new file mode 100644 index 0000000..3899fda Binary files /dev/null and b/assets/runtime-class.png differ diff --git a/assets/runtime-dynamicCreateClass-demo.png b/assets/runtime-dynamicCreateClass-demo.png new file mode 100644 index 0000000..3d886c5 Binary files /dev/null and b/assets/runtime-dynamicCreateClass-demo.png differ diff --git a/assets/runtime-forwarding.png b/assets/runtime-forwarding.png new file mode 100644 index 0000000..bdde97c Binary files /dev/null and b/assets/runtime-forwarding.png differ diff --git a/assets/runtime-forwardingFailed.png b/assets/runtime-forwardingFailed.png new file mode 100644 index 0000000..3fcaaa6 Binary files /dev/null and b/assets/runtime-forwardingFailed.png differ diff --git a/assets/runtime-isa-demo.png b/assets/runtime-isa-demo.png new file mode 100644 index 0000000..70ff867 Binary files /dev/null and b/assets/runtime-isa-demo.png differ diff --git a/assets/runtime-method-encoding.png b/assets/runtime-method-encoding.png new file mode 100644 index 0000000..d280259 Binary files /dev/null and b/assets/runtime-method-encoding.png differ diff --git a/assets/runtime-method-find.png b/assets/runtime-method-find.png new file mode 100644 index 0000000..1e16c1a Binary files /dev/null and b/assets/runtime-method-find.png differ diff --git a/assets/runtime-objc_msgSend-ResolveMethod.png b/assets/runtime-objc_msgSend-ResolveMethod.png new file mode 100644 index 0000000..824becf Binary files /dev/null and b/assets/runtime-objc_msgSend-ResolveMethod.png differ diff --git a/assets/runtime-objc_msgSend-messageSend.png b/assets/runtime-objc_msgSend-messageSend.png new file mode 100644 index 0000000..26b2d12 Binary files /dev/null and b/assets/runtime-objc_msgSend-messageSend.png differ diff --git a/assets/runtime-super-isa-demo.png b/assets/runtime-super-isa-demo.png new file mode 100644 index 0000000..21f1a01 Binary files /dev/null and b/assets/runtime-super-isa-demo.png differ diff --git a/assets/runtime-super.png b/assets/runtime-super.png new file mode 100644 index 0000000..ccf520a Binary files /dev/null and b/assets/runtime-super.png differ diff --git a/assets/synchronized-asemble.png b/assets/synchronized-asemble.png new file mode 100644 index 0000000..ac4f92f Binary files /dev/null and b/assets/synchronized-asemble.png differ