├── .gitignore ├── C ├── .DS_Store ├── fishhook │ ├── 巧用符号表 - 探求 fishhook 原理(一).md │ └── 验证试验 - 探求 fishhook 原理(二).md └── mach-o │ └── Mach-O 文件格式探索.md ├── LICENSE ├── Objective-C ├── Foundation │ ├── CFArray 的历史渊源及实现原理.md │ ├── Run Loop 记录与源码注释.md │ ├── 从经典问题来看 Copy 方法.md │ └── 复用的精妙 - UITableView 复用技术原理分析.md ├── Runtime │ ├── load 方法全程跟踪.md │ ├── objc_msgSend消息传递学习笔记 - 对象方法消息传递流程.md │ ├── objc_msgSend消息传递学习笔记 - 消息转发.md │ ├── weak 弱引用的实现方式.md │ ├── 浅谈 block(1) - clang 改写后的 block 结构.md │ ├── 浅谈 block(2) - 截获变量方式.md │ ├── 浅谈Associated Objects.md │ └── 用 isa 承载对象的类信息.md ├── SDWebImage │ ├── SDWebImage Source Probe - Downloader.md │ ├── SDWebImage Source Probe - Manager.md │ ├── SDWebImage Source Probe - Operation.md │ └── SDWebImage Source Probe - WebCache.md └── UIKit │ └── AutoLayout 中的线性规划 - Simplex 算法.md ├── Python └── Shadowsocks │ ├── Shadowsocks Probe I - Socks5 与 EventLoop 事件分发.md │ ├── Shadowsocks Probe II - TCP 代理过程.md │ └── media │ ├── 15024138759233 │ ├── IO-muti-road.png │ ├── eventloop-flow.png │ ├── handshake-time.png │ ├── patriotic-networ-2.png │ ├── visit-apple.com.re.png │ └── whats-shadowsocks-041.png │ └── 15027549268791 │ ├── 15033138825661.jpg │ ├── 15033138971502.jpg │ ├── 15033139083269.jpg │ ├── Agreement.png │ ├── STAGE.png │ ├── TCPRelay.description.png │ ├── agreement_new.png │ ├── handle_addr.png │ ├── handle_addr_new.png │ ├── handle_event.png │ └── ss-event-stage-relationship.svg ├── README.md ├── SUMMARY.md ├── Swift └── Swift Probe - Optional.md ├── banner-logo.jpg ├── book.json ├── image ├── 15058343519881 │ ├── 15073563641200.jpg │ ├── 15074291855917.jpg │ ├── 15074302081133.jpg │ ├── 15074350810612.jpg │ ├── 15074371126384.jpg │ ├── 15074373726740.jpg │ ├── 15074375282210.jpg │ ├── 15074383379688.jpg │ ├── 15074385597132.jpg │ ├── 15074386833986.jpg │ └── mach-o.png ├── 15101394649922 │ ├── 15119364721450.jpg │ ├── 15119414520860.jpg │ ├── 15119418259666.jpg │ ├── 15119440533652.jpg │ ├── 15119461061753.jpg │ ├── 15119572694685.jpg │ ├── 15133014200273.jpg │ ├── 15133015712894.jpg │ ├── 15133019900468.jpg │ ├── 15133214554985.jpg │ ├── 15134932813922.jpg │ ├── 15134938187077.jpg │ └── 15134988564426.jpg ├── 15272074383889 │ ├── 15297228541122.jpg │ ├── 15305805997704.jpg │ └── 15306636723931.jpg └── Objective │ ├── Foundation │ └── 从经典问题来看 Copy 方法 │ │ └── img_1.jpg │ ├── Runtime │ ├── objc_msgSend消息传递学习笔记 - 对象方法消息传递流程 │ │ ├── img_1.jpg │ │ ├── img_2.jpg │ │ ├── img_3.jpg │ │ └── img_4.jpg │ ├── objc_msgSend消息传递学习笔记 - 消息转发 │ │ └── img.sketch │ ├── weak 弱引用的实现方式 │ │ └── sidetable.sketch │ ├── 浅谈 block(1) - clang 改写后的 block 结构 │ │ ├── img.sketch │ │ ├── img_1.jpg │ │ └── img_2.jpg │ ├── 浅谈 block(2) - 截获变量方式 │ │ ├── block.sketch │ │ └── block_forwarding.sketch │ ├── 浅谈Associated Objects │ │ ├── img_1.jpg │ │ ├── img_2.png │ │ ├── img_3.jpg │ │ └── img_4.jpg │ └── 用 isa 承载对象的类信息 │ │ └── isa.sketch │ └── SDWebImage │ ├── SDWebImage Source Probe Manager │ └── Manager.sketch │ └── SDWebImage Source Probe WebCache │ └── UIImageVIew-WebCache.sketch ├── logo.jpg ├── package.json └── release.sh /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | _book/ 3 | 4 | node_modules 5 | node_modules/ 6 | 7 | .DS_Store 8 | package-lock.json 9 | 10 | venv 11 | venv/ 12 | 13 | .idea 14 | .idea/ 15 | -------------------------------------------------------------------------------- /C/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/C/.DS_Store -------------------------------------------------------------------------------- /C/fishhook/验证试验 - 探求 fishhook 原理(二).md: -------------------------------------------------------------------------------- 1 | # 验证试验 - 探求 fishhook 原理(二) 2 | 3 | > 作者:冬瓜 4 | 5 | > 原文链接:[Guardia · 瓜地](http://www.desgard.com/fishhook-2/) 6 | 7 | ## 示例 Demo Code 8 | 9 | 继续使用上一篇文中的代码示例: 10 | 11 | ```C 12 | #include 13 | #include "fishhook.h" 14 | static int (*original_strlen)(const char *_s); 15 | int new_strlen(const char *_s) { 16 | return 666; 17 | } 18 | 19 | int main(int argc, const char * argv[]) { 20 | struct rebinding strlen_rebinding = { "strlen", new_strlen, 21 | (void *)&original_strlen }; 22 | rebind_symbols((struct rebinding[1]){ strlen_rebinding }, 1); 23 | char *str = "hellolazy"; 24 | printf("%d\n", strlen(str)); 25 | return 0; 26 | } 27 | ``` 28 | 29 | 运行之后将其生成的执行文件拖入 *MachOView* 中。对 Mach-O 进行解析。 30 | 31 | ## Linkedit Base Address 计算 32 | 33 | 34 | 从 *MachOView* 中 *Load Commands -> LC_SEGMENT_64(__LINKEDIT)* 中获取到以下值: 35 | 36 | ![](https://diycode.b0.upaiyun.com/photo/2018/f86ae9bc39e16551633a17b329ba49f2.jpg) 37 | 38 | 进行下列*式(1)*和*式(2)*的计算: 39 | 40 | 41 | $$ 42 | \begin{equation} 43 | \left\{ 44 | \begin{array}{l@{\;=\;}l} 45 | vmaddr=1000030000_{(hex)}\\ 46 | file\ offset=30000_{(hex)} 47 | \end{array} 48 | \right. 49 | \end{equation} 50 | $$ 51 | 52 | $$ 53 | \begin{equation} 54 | \begin{aligned} 55 | base\_address&=slide+vmaddr-file\ offset \\ 56 | &=slide+1000000000_{(hex)} 57 | \end{aligned} 58 | \end{equation} 59 | $$ 60 | 61 | ## 符号表、字符表和跳转表 62 | 63 | 继续看上文的这张图片: 64 | 65 | ![](https://diycode.b0.upaiyun.com/photo/2018/76aab6244569b2be67b64c3c4b88b45b.jpg) 66 | 67 | 此时,我们已经获取到了 *Base Address*,根据之前的流程,需要找到 *__LINKEDIT Section* 中的 Symbols 表、Indirect Symbols 表和 String 字符串表,这些地址我们需要在 Load Commands 中来获取。找到 *LC_SYMTAB* 和 *LC_DYSYMTAB* 这两个 Commaneds。 68 | 69 | ![](https://diycode.b0.upaiyun.com/photo/2018/1b7a95521a9c8c8cf679d397f9dff302.jpg) 70 | 71 | 72 | ![](https://diycode.b0.upaiyun.com/photo/2018/78d3a4b9139be62b6a25369470ea4d59.jpg) 73 | 74 | 75 | $$ 76 | \begin{equation} 77 | \left\{ 78 | \begin{array}{l@{\;=\;}l} 79 | symbol\_offset=symtab\_cmd\to symoff=12680_{(oct)}=3188_{(hex)}\\ 80 | indirect\_symbol\_offset=dysymtab\_cmd\to symoff =14008_{(oct)}=35B8_{(hex)}\\ 81 | string\_table\_offset=symtab\_cmd\to stringoff=13864_{(oct)}=3628_{(hex)} 82 | \end{array} 83 | \right. 84 | \end{equation} 85 | $$ 86 | 87 | $$ 88 | \begin{equation} 89 | \left\{ 90 | \begin{array}{l@{\;=\;}l} 91 | symbol\_base=base\_address+symbol\_offset=slide+100003188_{(hex)}\\ 92 | indirect\_symbol\_base=base\_address+indirect\_symbol\_offset=slide+1000035B8_{(hex)}\\ 93 | string\_table\_base=base\_address+string\_table\_offset=slide+100003628_{(hex)} 94 | \end{array} 95 | \right. 96 | \end{equation} 97 | $$ 98 | 99 | 100 | ![](https://diycode.b0.upaiyun.com/photo/2018/f3846df367c1c69f25eebe7ab79d4101.jpg) 101 | 102 | ![](https://diycode.b0.upaiyun.com/photo/2018/d3c25d286d0bf7ab3baf07acabe6d28b.jpg) 103 | 104 | ![](https://diycode.b0.upaiyun.com/photo/2018/2e24a8f940d9c299a016226953fd9c0f.jpg) 105 | 106 | 107 | ## 跳转表 nl 和 la 绑定符号基址 108 | 109 | 我们在 MachOView 中验证了三个表的位置。之后继续跟随着 fishhook 的思路来进行验证。下面将进入二尺遍历 Load Command 流程。这一次需要拿出的 Command 是 *LC_SEGMENT(__DATA)*,目的是为了找到 `__nl_symbol_ptr` 和 `__la_symbol_ptr` 这两个 Section 的位置。 110 | 111 | $$ 112 | \begin{equation} 113 | \left\{ 114 | \begin{array}{l@{\;=\;}l} 115 | nl\_indirect\_sym\_index=13_{(oct)}\\ 116 | la\_indirect\_sym\_index=15_{(oct)}\\ 117 | \end{array} 118 | \right. 119 | \end{equation} 120 | $$ 121 | 122 | $$ 123 | \begin{equation} 124 | \left\{ 125 | \begin{array}{l@{\;=\;}l} 126 | nl\_sym\_base\_addr&=nl\_indirect\_sym\_index\times size+indirect\_symbol\_base\\ 127 | &=13_{(oct)}\times 4 + (1000035B8_{(hex)}+slide)=1000035EC_{(hex)}+C\\ 128 | la\_sym\_base\_addr&=la\_indirect\_sym\_index\times size+indirect\_symbol\_base\\ 129 | &=15_{(oct)}\times 4 + (1000035B8_{(hex)}+slide)=1000035F4_{(hex)}+C\\ 130 | \end{array} 131 | \right. 132 | \end{equation} 133 | $$ 134 | 135 | 这里的 C 其实是上方 slide 常量的运算结果,由于在这个场景下 MachOView 验证时发现 `slide = 0`,但不意味着 `slide` 是一个恒为 0 的值。`vmaddr_slide` 的取值其实是**地址空间布局随机化(ASLR)**机制的结果,这是一种针对缓冲区溢出的安全保护技术,这里不再赘述。 136 | 137 | ![](https://diycode.b0.upaiyun.com/photo/2018/9d136ba97927863fa3e2a675e8ee8d95.jpg) 138 | 139 | 140 | 根据 *Indirect Symbols* 中的描述,发现下标标识的区域的起始位置与我们的计算相吻合,此处也再次验证成功。在 *Indirect Symbols* 的结构中,我们可以找到其 *Lazy Symbol Pointer*。例如,我们在代码中出现的 `strlen` 方法: 141 | 142 | ![](https://diycode.b0.upaiyun.com/photo/2018/09ab382c46526f986d37b25ec27185cb.jpg) 143 | 144 | ![](https://diycode.b0.upaiyun.com/photo/2018/2cf3651bb443e051f7fa3ca950b2b889.jpg) 145 | 146 | ## 在符号表中获取全部信息 147 | 148 | 在 `Lazy Symbol Pointers` 这里,我们最终获取到了 `0x10002068 -> _strlen` 这个值。它起始是对于当前 `_strlen` 方法在符号表中的一个映射,我们可以简单的理解为**它就是在符号表中对应方法的下标**。 149 | 150 | 这个结论虽然至今笔者无法跟踪代码,将其结构实例化,但是可以通过 fishhook 的源码进行分析得出结论。 151 | 152 | ```C 153 | ... 154 | // 这里直接将 Lazy Symbol Point 的一个内容强转为二阶指针 155 | // 也就是说明每个 Lazy Symbol Point 也是一个指针(当然从命名中也能猜出) 156 | void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); 157 | ... 158 | // 在遍历每一个内容的时候,先拿出其对应的 index 159 | uint32_t symtab_index = indirect_symbol_indices[i]; 160 | // 之后直接从符号表中用下标访问的方法来获取信息 161 | uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; 162 | ``` 163 | 164 | `symtab[symtab_index].n_un.n_strx` 从这个获取的方式来看,`symtab_index` 处的位置对应的是某个符号表的基址位,因此可以将类型转成 `nlist_64`。 165 | 166 | ```C 167 | struct nlist_64 { 168 | union { 169 | uint32_t n_strx; /* 符号表中的位置 */ 170 | } n_un; 171 | uint8_t n_type; /* 经过掩码处理,表示重定义符号不同的种类 */ 172 | uint8_t n_sect; /* section 的编号 */ 173 | uint16_t n_desc; /* 类似于 n_type 经过掩码处理,用来定义一些性质 */ 174 | uint64_t n_value; /* 记录信息的特殊值 */ 175 | }; 176 | ``` 177 | 178 | 为什么在 fishhook 源码中可以将符号名称直接通过 `char *symbol_name = strtab + strtab_offset;` 直接取得呢?因为在 String Table 存储过程中会在每一个符号名的末尾增加不确定个 `\0`,而在 C 中一次的字符数组获取会以 `\0` 为结束。在 MachOView 中已经将符号名进行了解析,我们可以清晰的在解析后的 `nlist` 结构中发现符号名: 179 | 180 | ![](https://diycode.b0.upaiyun.com/photo/2018/a4c5c512f0ac0e645865950cfb4cc41e.jpg) 181 | 182 | 此时我们已经找到了改符号表的位置,剩下的工作仅需将 `Lazy Symbol Pointers` 中对应的指针进行指向变更即可完成操作。我在 fishhook 中加入了部分输出验证代码,可以大致验证这一地址的更变,但是由于在运行时有着 ASLR 的参与,是无法精准的将地址完全与 MachOView 的解析结果精准比对,但是从地址的大致区域中可以看出这个操作的成功性,以下是加入代码和输出的 log: 183 | 184 | ```C 185 | static void perform_rebinding_with_section(struct rebindings_entry *rebindings, 186 | section_t *section, 187 | intptr_t slide, 188 | nlist_t *symtab, 189 | char *strtab, 190 | uint32_t *indirect_symtab) { 191 | // 在 Indirect Symbol 表中检索到对应位置 192 | uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; 193 | // 获取 _DATA.__nl_symbol_ptr(或__la_symbol_ptr) Section 194 | // 已知其 value 是一个指针类型,整段区域用二阶指针来获取 195 | void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); 196 | // 用 size / 一阶指针来计算个数,遍历整个 Section 197 | for (uint i = 0; i < section->size / sizeof(void *); i++) { 198 | // 通过下标来获取每一个 Indirect Address 的 Value 199 | // 这个 Value 也是外层寻址时需要的下标 200 | uint32_t symtab_index = indirect_symbol_indices[i]; 201 | if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || 202 | symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) { 203 | continue; 204 | } 205 | // 获取符号名在字符表中的偏移地址 206 | uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; 207 | // 获取符号名 208 | char *symbol_name = strtab + strtab_offset; 209 | // 过滤掉符号名小于 4 位的符号 210 | if (strnlen(symbol_name, 2) < 2) { 211 | continue; 212 | } 213 | // 取出 rebindings 结构体实例数组,开始遍历链表 214 | struct rebindings_entry *cur = rebindings; 215 | while (cur) { 216 | // 对于链表中每一个 rebindings 数组的每一个 rebinding 实例 217 | // 依次在 String Table 匹配符号名 218 | for (uint j = 0; j < cur->rebindings_nel; j++) { 219 | // 符号名与方法名匹配 220 | if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { 221 | // 如果是第一次对跳转地址进行重写 222 | if (cur->rebindings[j].replaced != NULL && 223 | indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { 224 | // 记录原始跳转地址 225 | *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; 226 | } 227 | // 重写跳转地址 228 | indirect_symbol_bindings[i] = cur->rebindings[j].replacement; 229 | // 完成后不再对当前 Indirect Symbol 处理 230 | // 继续迭代到下一个 Indirect Symbol 231 | 232 | /** 加入调试代码 **/ 233 | printf("\n\nSymbol Name: %s\n", &symbol_name[1]); 234 | printf("Rebinding Name: %s\n", cur->rebindings[j].name); 235 | printf("Origin Addr: 0x%X\n", cur->rebindings[j].replaced); 236 | printf("Rebinding Addr: 0x%X\n", cur->rebindings[j].replacement); 237 | /** 调试代码 END **/ 238 | goto symbol_loop; 239 | } 240 | } 241 | // 链表遍历 242 | cur = cur->next; 243 | } 244 | symbol_loop:; 245 | } 246 | } 247 | ``` 248 | 249 | ```shell 250 | Symbol Name: strlen 251 | Rebinding Name: strlen 252 | Origin Addr: 0x20A0 253 | Rebinding Addr: 0x1DA0 254 | 666 255 | Program ended with exit code: 0 256 | ``` 257 | 258 | `0x1DA0` 这个地址在原始的间接表指向的位置之前,所以我们大致断定它处在 `_TEXT` 段。并且`__stub_helper`, `__cstring`, `__unwoind_info` 这些 Session 是我们无法直接干预的位置,所以可以猜测 `0x1DA0` 落在我们的 `__text` Session 中。我们掏出 Hopper 来验证一下这个猜想,发现正是如此: 259 | 260 | ![](https://diycode.b0.upaiyun.com/photo/2018/6eb46dbda7214a22ad5e2e3c39bbd28b.jpg) 261 | 262 | 263 | ## 尾声 264 | 265 | fishhook 的原理探究至此就告一段落。通过这个源码探求,强化了对于 Mach-O 的学习和认识,也在之中学习到了 Facebook 对于 Hook C 方法这个很妙的技巧。最后不得不再惊叹一句,FB 真的是令技术人向往的地方。 266 | 267 | ## 参考 268 | 269 | * [nlist-Mach-O文件重定向信息数据结构分析](http://turingh.github.io/2016/05/24/nlist-Mach-O%E6%96%87%E4%BB%B6%E9%87%8D%E5%AE%9A%E5%90%91%E4%BF%A1%E6%81%AF%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%88%86%E6%9E%90/) 270 | * [地址空间布局随机化(ASLR)机制的分析与绕过](http://blog.c0smic.cn/2017/04/18/aslr/) 271 | 272 | 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /C/mach-o/Mach-O 文件格式探索.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](http://www.desgard.com/iosre-1/) 4 | 5 | # Mach-O 文件格式探索 6 | 7 | 最近开始研究 iOS 逆向的相关知识,并且使用 [MonkeyDev](https://github.com/AloneMonkey/MonkeyDev/) 对 WeChat 进行了实战。这里我放出后期会持续更新的个人项目 [WeCheat](https://github.com/Desgard/WeCheat)。在逆向专题真正开始之前,需要系统的学习一些软件内幕知识。这篇文章将从二进制格式开始讲起,并探秘 Mach-O 文件格式内容。 8 | 9 | ## 进程与二进制格式 10 | 11 | 进程在众多操作系统中都有提及,它是作为一个正在执行的程序的实例,这是 UNIX 的一个基本概念。而进程的出现是特殊文件在内从中加载得到的结果,这种文件必须使用操作系统可以认知的格式,这样才对该文件引入依赖库,初始化运行环境以及顺利地执行创造条件。 12 | 13 | **Mach-O**(Mach Object File Format)是 macOS 上的可执行文件格式,类似于 Linux 和大部分 UNIX 的原生格式 **ELF**(Extensible Firmware Interface)。为了更加全面的了解这块内容,我们看一下 macOS 支持的三种可执行格式:解释器脚本格式、通用二进制格式和 Mach-O 格式。 14 | 15 | | 可执行格式 | magic | 用途 | 16 | | ------| ------ | ------ | 17 | | 脚本 | `\x7FELF` | 主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 `#!` 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令 | 18 | | 通用二进制格式 | `0xcafebabe`
`0xbebafeca` | 包含多种架构支持的二进制格式,只在 macOS 上支持 | 19 | | Mach-O | `0xfeedface`(32 位)
`0xfeedfacf`(64 位) | macOS 的原生二进制格式 | 20 | 21 | 22 | ### 通用二进制格式(Universal Binary) 23 | 24 | 这个格式在有些资料中也叫胖二进制格式(Fat Binary),Apple 提出这个概念是为了解决一些历史原因,macOS(更确切的应该说是 OS X)最早是构建于 PPC 架构智商,后来才移植到 Intel 架构(从 Mac OS X Tiger 10.4.7 开始),通用二进制格式的二进制文件可以在 PPC 和 x86 两种处理器上执行。 25 | 26 | 说到底,通用二进制格式只不过是对多架构的二进制文件的打包集合文件,而 macOS 中的多架构二进制文件也就是适配不同架构的 Mach-O 文件。 27 | 28 | **Fat Header** 的数据结构在 `` 头文件中有定义,可以参看 `/usr/include/mach-o/fat.h` 找到定义头: 29 | 30 | ```c 31 | #define FAT_MAGIC 0xcafebabe 32 | #define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */ 33 | 34 | struct fat_header { 35 | uint32_t magic; /* FAT_MAGIC 或 FAT_MAGIC_64 */ 36 | uint32_t nfat_arch; /* 结构体实例的个数 */ 37 | }; 38 | 39 | struct fat_arch { 40 | cpu_type_t cputype; /* cpu 说明符 (int) */ 41 | cpu_subtype_t cpusubtype; /* 指定 cpu 确切型号的整数 (int) */ 42 | uint32_t offset; /* CPU 架构数据相对于当前文件开头的偏移值 */ 43 | uint32_t size; /* 数据大小 */ 44 | uint32_t align; /* 数据内润对其边界,取值为 2 的幂 */ 45 | }; 46 | ``` 47 | 48 | 对于 `cputype` 和 `cpusubtype` 两个字段这里不讲述,可以参看 `/usr/include/mach/machine.h` 头中对其的定义,另外 [Apple 官方文档](https://developer.apple.com/documentation/kernel/mach_header?language=objc)中也有简单的描述。 49 | 50 | 在 `fat_header` 中,`magic` 也就是我们之前在表中罗列的 *magic* 标识符,也可以类比成 UNIX 中 ELF 文件的 *magic* 标识。加载器会通过这个符号来判断这是什么文件,通用二进制的 *magic* 为 `0xcafebabe`。`nfat_arch` 字段指明当前的通用二进制文件中包含了多少个不同架构的 Mach-O 文件。`fat_header` 后会跟着多个 `fat_arch`,并与多个 Mach-O 文件及其描述信息(文件大小、CPU 架构、CPU 型号、内存对齐方式)相关联。 51 | 52 | 这里可以通过 `file` 命令来查看简要的架构信息,这里以 iOS 平台 WeChat 4.5.1 版本为例: 53 | 54 | ``` 55 | ~ file Desktop/WeChat.app/WeChat 56 | Desktop/WeChat.app/WeChat: Mach-O universal binary with 2 architectures: [arm_v7: Mach-O executable arm_v7] [arm64] 57 | Desktop/WeChat.app/WeChat (for architecture armv7): Mach-O executable arm_v7 58 | Desktop/WeChat.app/WeChat (for architecture arm64): Mach-O 64-bit executable arm64 59 | ``` 60 | 61 | 进一步,也可以使用 `otool` 工具来打印其 `fat_header` 详细信息: 62 | 63 | ``` 64 | ~ otool -f -V Desktop/WeChat.app/WeChat 65 | Fat headers 66 | fat_magic FAT_MAGIC 67 | nfat_arch 2 68 | architecture armv7 69 | cputype CPU_TYPE_ARM 70 | cpusubtype CPU_SUBTYPE_ARM_V7 71 | capabilities 0x0 72 | offset 16384 73 | size 56450224 74 | align 2^14 (16384) 75 | architecture arm64 76 | cputype CPU_TYPE_ARM64 77 | cpusubtype CPU_SUBTYPE_ARM64_ALL 78 | capabilities 0x0 79 | offset 56475648 80 | size 64571648 81 | align 2^14 (16384) 82 | ``` 83 | 84 | 之后我们用 *Synalyze It!* 来查看 WeChat 的 Mach64 Header 的效果: 85 | 86 | ![](../../image/15058343519881/15073563641200.jpg) 87 | 88 | * 从第一个段中得到 `magic = 0xcafebabe` ,说明是 `FAT_MAGIC`。 89 | * 第二段中所存储的字段为 `nfat_arch = 0x00000002`,说明该 App 中包含了两种 CPU 架构。 90 | * 后续的则是 `fat_arch` 结构体中的内容,`cputype(0x0000000c)`、`cpusubtype(0x00000009)`、`offset(0x00004000)`、`size(0x03505C00)` 等等。需要臧帅闯是如果只含有一种 CPU 架构,是没有 fat 头定义的,这部分则可跳过,从而直接过去 `arch` 数据。 91 | 92 | ## Mach-O 文件格式 93 | 94 | 由上所知一个通用二进制格式包含了很多个 Mach-O 文件格式,下面我们来具体说说这个格式。Mach-O 文件格式在官方文档中有一个描述图,是很多教程中都引用到的,我重新绘制了一版更清晰的: 95 | 96 | ![mach-o](../../image/15058343519881/mach-o.png) 97 | 可以看的出 Mach-O 主要由 3 部分组成: 98 | 99 | * Mach-O 头(Mach Header):这里描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息; 100 | * 加载命令(Load Command):描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示; 101 | * 数据区(Data):Data 中每一个段(Segment)的数据都保存在此,段的概念和 ELF 文件中段的概念类似,都拥有一个或多个 Section ,用来存放数据和代码。 102 | 103 | ### Mach-O 头 104 | 105 | 与 Mach-O 文件格式有关的结构体定义都可以从 `/usr/include/mach-o/loader.h` 中找到,也就是 `` 头。以下只给出 64 位定义的代码,因为 32 位的区别是缺少了一个预留字段: 106 | 107 | ```c 108 | #define MH_MAGIC 0xfeedface /* the mach magic number */ 109 | #define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */ 110 | 111 | struct mach_header_64 { 112 | uint32_t magic; /* mach magic 标识符 */ 113 | cpu_type_t cputype; /* CPU 类型标识符,同通用二进制格式中的定义 */ 114 | cpu_subtype_t cpusubtype; /* CPU 子类型标识符,同通用二级制格式中的定义 */ 115 | uint32_t filetype; /* 文件类型 */ 116 | uint32_t ncmds; /* 加载器中加载命令的条数 */ 117 | uint32_t sizeofcmds; /* 加载器中加载命令的总大小 */ 118 | uint32_t flags; /* dyld 的标志 */ 119 | uint32_t reserved; /* 64 位的保留字段 */ 120 | }; 121 | ``` 122 | 123 | 由于 Mach-O 支持多种类型文件,所以此处引入了 `filetype` 字段来标明,这些文件类型定义在 `loader.h` 文件中同样可以找到。 124 | 125 | ```c 126 | #define MH_OBJECT 0x1 /* Target 文件:编译器对源码编译后得到的中间结果 */ 127 | #define MH_EXECUTE 0x2 /* 可执行二进制文件 */ 128 | #define MH_FVMLIB 0x3 /* VM 共享库文件(还不清楚是什么东西) */ 129 | #define MH_CORE 0x4 /* Core 文件,一般在 App Crash 产生 */ 130 | #define MH_PRELOAD 0x5 /* preloaded executable file */ 131 | #define MH_DYLIB 0x6 /* 动态库 */ 132 | #define MH_DYLINKER 0x7 /* 动态连接器 /usr/lib/dyld */ 133 | #define MH_BUNDLE 0x8 /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */ 134 | #define MH_DYLIB_STUB 0x9 /* 静态链接文件(还不清楚是什么东西) */ 135 | #define MH_DSYM 0xa /* 符号文件以及调试信息,在解析堆栈符号中常用 */ 136 | #define MH_KEXT_BUNDLE 0xb /* x86_64 内核扩展 */ 137 | ``` 138 | 139 | 另外在 `loader.h` 中还可以找到 `flags` 中所取值的全部定义,这里只介绍常用的: 140 | 141 | ```c 142 | #define MH_NOUNDEFS 0x1 /* Target 文件中没有带未定义的符号,常为静态二进制文件 */ 143 | #define MH_SPLIT_SEGS 0x20 /* Target 文件中的只读 Segment 和可读写 Segment 分开 */ 144 | #define MH_TWOLEVEL 0x80 /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */ 145 | #define MH_FORCE_FLAT 0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */ 146 | #define MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */ 147 | #define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */ 148 | #define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */ 149 | #define MH_PIE 0x200000 /* 对可执行的文件类型启用地址空间 layout 随机化 */ 150 | #define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */ 151 | ``` 152 | 153 | Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且 `ncmds` 和 `sizeofcmds` 来能个字段将会用在加载命令的过程中。 154 | 155 | ### Mach-O Data 156 | 157 | 加载命令在 Mach-O 文件加载解析时,会被内核加载器或者动态链接器调用。这些指令都采用 `Type-Size-Value` 这种格式,即:32 位的 `cmd` 值(表示类型),32 位的 `cmdsize` 值(32 位二级制位 4 的倍数,64 位位 8 的倍数),以及命令本身(由 `cmdsize` 指定的长度)。内核加载器使用的命令可以参看 [xnu 源码](http://unix.superglobalmegacorp.com/xnu/newsrc/bsd/kern/mach_loader.c.html)来学习,其他命令则是由动态链接器处理的。 158 | 159 | 在正式进入加载命令这一过程之前,先来学习一下 Mach-O 的 Data 区域,其中由 Segment 段和 Section 节组成。先来说 Segment 的组成,以下代码仍旧来自 `loader.h`: 160 | 161 | ```c 162 | #define SEG_PAGEZERO "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,捕获到空指针 */ 163 | #define SEG_TEXT "__TEXT" /* 代码/只读数据段 */ 164 | #define SEG_DATA "__DATA" /* 数据段 */ 165 | #define SEG_OBJC "__OBJC" /* Objective-C runtime 段 */ 166 | #define SEG_LINKEDIT "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */ 167 | ``` 168 | 169 | 进而来看一下 Segment 的数据结构具体是什么样的(同样这里也只放出 64 位的代码,与 32 位的区别就是其中 `uint64_t` 类型的几个字段取代了原先 32 位类型字段): 170 | 171 | ```c 172 | struct segment_command_64 { 173 | uint32_t cmd; /* LC_SEGMENT_64 */ 174 | uint32_t cmdsize; /* section_64 结构体所需要的空间 */ 175 | char segname[16]; /* segment 名字,上述宏中的定义 */ 176 | uint64_t vmaddr; /* 所描述段的虚拟内存地址 */ 177 | uint64_t vmsize; /* 为当前段分配的虚拟内存大小 */ 178 | uint64_t fileoff; /* 当前段在文件中的偏移量 */ 179 | uint64_t filesize; /* 当前段在文件中占用的字节 */ 180 | vm_prot_t maxprot; /* 段所在页所需要的最高内存保护,用八进制表示 */ 181 | vm_prot_t initprot; /* 段所在页原始内存保护 */ 182 | uint32_t nsects; /* 段中 Section 数量 */ 183 | uint32_t flags; /* 标识符 */ 184 | }; 185 | ``` 186 | 187 | 部分的 Segment (主要指的 `__TEXT` 和 `__DATA`)可以进一步分解为 Section。之所以按照 Segment -> Section 的结构组织方式,是因为在同一个 Segment 下的 Section,可以控制相同的权限,也可以不完全按照 Page 的大小进行内存对其,节省内存的空间。而 Segment 对外整体暴露,在程序载入阶段映射成一个完整的虚拟内存,更好的做到内存对齐(可以继续参考 *OS X & iOS Kernel Programming* 一书的第一章内容)。下面给出 Section 具体的数据结构: 188 | 189 | ```c 190 | struct section_64 { 191 | char sectname[16]; /* Section 名字 */ 192 | char segname[16]; /* Section 所在的 Segment 名称 */ 193 | uint64_t addr; /* Section 所在的内存地址 */ 194 | uint64_t size; /* Section 的大小 */ 195 | uint32_t offset; /* Section 所在的文件偏移 */ 196 | uint32_t align; /* Section 的内存对齐边界 (2 的次幂) */ 197 | uint32_t reloff; /* 重定位信息的文件偏移 */ 198 | uint32_t nreloc; /* 重定位条目的数目 */ 199 | uint32_t flags; /* 标志属性 */ 200 | uint32_t reserved1; /* 保留字段1 (for offset or index) */ 201 | uint32_t reserved2; /* 保留字段2 (for count or sizeof) */ 202 | uint32_t reserved3; /* 保留字段3 */ 203 | }; 204 | ``` 205 | 206 | 下面列举一些常见的 Section。 207 | 208 | | Section | 用途 | 209 | | ------| ------ | 210 | | `__TEXT.__text` | 主程序代码 | 211 | | `__TEXT.__cstring` | C 语言字符串 | 212 | | `__TEXT.__const` | `const` 关键字修饰的常量 | 213 | | `__TEXT.__stubs ` | 用于 Stub 的占位代码,很多地方称之为*桩代码*。 | 214 | | `__TEXT.__stubs_helper` | 当 Stub 无法找到真正的符号地址后的最终指向 | 215 | | `__TEXT.__objc_methname` | Objective-C 方法名称 | 216 | | `__TEXT.__objc_methtype` | Objective-C 方法类型 | 217 | | `__TEXT.__objc_classname` | Objective-C 类名称 | 218 | | `__DATA.__data` | 初始化过的可变数据 | 219 | | `__DATA.__la_symbol_ptr` | lazy binding 的指针表,表中的指针一开始都指向 `__stub_helper` | 220 | | `__DATA.nl_symbol_ptr` | 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 | 221 | | `__DATA.__const` | 没有初始化过的常量 | 222 | | `__DATA.__cfstring` | 程序中使用的 Core Foundation 字符串(`CFStringRefs`) | 223 | | `__DATA.__bss` | BSS,存放为初始化的全局变量,即常说的静态内存分配 | 224 | | `__DATA.__common` | 没有初始化过的符号声明 | 225 | | `__DATA.__objc_classlist` | Objective-C 类列表 | 226 | | `__DATA.__objc_protolist` | Objective-C 原型 | 227 | | `__DATA.__objc_imginfo` | Objective-C 镜像信息 | 228 | | `__DATA.__objc_selfrefs` | Objective-C `self` 引用| 229 | | `__DATA.__objc_protorefs` | Objective-C 原型引用 | 230 | | `__DATA.__objc_superrefs` | Objective-C 超类引用 | 231 | 232 | ## 验证实验 233 | 234 | 当了解了 Segment 和 Section 的定义之后,我们可以简单的探索一下 `LC_SEGMENT` 这个命令的过程。用 helloworld 来做个试验: 235 | 236 | ```c++ 237 | /// main.cpp 238 | #import 239 | 240 | int main() { 241 | printf("hello"); 242 | return 0; 243 | } 244 | ``` 245 | 246 | 使用 `clang -g main.cpp -o main` 生成执行文件。然后拖入到 *MachOView* 中来查看一下加载 Segment 的结构(当然使用 *Synalyze It!* 也能捕捉到这些信息的,但是 *MachOView* 更对结构的分层更加一目了然): 247 | 248 | ![MachOView](../../image/15058343519881/15074302081133.jpg) 249 | 250 | 251 | ![Synalyze It! 分析结果](../../image/15058343519881/15074291855917.jpg) 252 | 253 | 在 `LC_SEGMENT_64` 中有四个元素,分别是 `__PAGEZERO`、`__TEXT`、`__DATA`、`__LINKEDIT` 这四个 Segment。其中,`__TEXT` 的 `__text` Section 的加载是可以验证到的,我们从 Section 的实例中取出其 `addr` 来对比汇编之后代码的起始地址即可。使用 `otool -vt main` 来获取其汇编代码: 254 | 255 | ``` 256 | ~ otool -vt main 257 | main: 258 | (__TEXT,__text) section 259 | _main: 260 | 0000000100000f60 pushq %rbp 261 | 0000000100000f61 movq %rsp, %rbp 262 | 0000000100000f64 subq $0x10, %rsp 263 | 0000000100000f68 leaq 0x3b(%rip), %rdi 264 | 0000000100000f6f movl $0x0, -0x4(%rbp) 265 | 0000000100000f76 movb $0x0, %al 266 | 0000000100000f78 callq 0x100000f8a 267 | 0000000100000f7d xorl %ecx, %ecx 268 | 0000000100000f7f movl %eax, -0x8(%rbp) 269 | 0000000100000f82 movl %ecx, %eax 270 | 0000000100000f84 addq $0x10, %rsp 271 | 0000000100000f88 popq %rbp 272 | 0000000100000f89 retq 273 | ``` 274 | 275 | 对比 *Synalyze It!* 的分析结果中 `SEG__TEXT.__text` 中的 `addr` 观察: 276 | 277 | ![](../../image/15058343519881/15074350810612.jpg) 278 | 279 | 280 | 其对应的物理地址均为 `0x100000F60`,说明其 `LC_SEGMENT` 对于 Segment 和 Section 的加载与我们的预期完全一致。 281 | 282 | ## 对于 `__TEXT.__stubs` 的一些探究 283 | 284 | 这是对于五子棋大神的 [*深入剖析Macho (1)*](http://satanwoo.github.io/2017/06/13/Macho-1/) 中的过程进行再次验证。在查阅过关于 `__stubs` 的相关资料后还是不太理解到底是个什么东西。在知乎中有这么一个[问题](https://www.zhihu.com/question/24844900),其中的观点是 Stub 会根据不同的代码上下文表示的含义不同。在 [wikipedia](https://en.wikipedia.org/wiki/Method_stub) 也有一个关于 [Method stub](https://en.wikipedia.org/wiki/Method_stub) 的词条,其中的解释是这样的: 285 | 286 | > A method stub or simply stub in software development is a piece of code used to stand in for some other programming functionality. A stub may simulate the behavior of existing code (such as a procedure on a remote machine) or be a temporary substitute for yet-to-be-developed code. Stubs are therefore most useful in porting, distributed computing as well as general software development and testing. 287 | 288 | 大意就是:*Stub 是指用来替换一部分功能的程序段。桩程序可以用来模拟已有程序的行为(比如一个远端机器的过程)或是对将要开发的代码的一种临时替代。* 289 | 290 | 我们将 Calculator 应用拖入到 *Synalyze It!* 和 *Hopper Disassembler* 中。首先使用 *Synalyze It!* 来查找一个 `__stubs` 地址: 291 | 292 | ![](../../image/15058343519881/15074371126384.jpg) 293 | 294 | 取出地址 `0x100016450` 并在 *Hopper* 中查找对应的代码,并以此双击进入: 295 | 296 | ![](../../image/15058343519881/15074373726740.jpg) 297 | 298 | ![](../../image/15058343519881/15074375282210.jpg) 299 | 300 | 到达第二幅图的位置的时候,我们发现无法继续进入,因为 `_CFRelease` 中的代码是没有意义的。我们拿出 `0x100031000` 这个首地址,在 *MachOView* 中查找: 301 | 302 | ![](../../image/15058343519881/15074383379688.jpg) 303 | 304 | 发现其低 32 位的值为 `0x00000010001663E`,将这个地址继续在 *hopper* 中搜索: 305 | 306 | ![](../../image/15058343519881/15074385597132.jpg) 307 | 308 | 发现这一系列操作都会跳到 `0x000000010001650c` 这个位置,而这里就是 `__TEXT.__stub_helper` 的表头。 309 | 310 | ![](../../image/15058343519881/15074386833986.jpg) 311 | 312 | 313 | 也就是说,`__la_symbol_ptr` 里面的所有表项的数据在开始时都会被 binding 成 `__stub_helper`。而在之后的调用中,虽然依旧会跳到 `__stub` 区域,但是 `__la_symbol_ptr` 中由于在之前的调用中获取到了对应方法的真实地址,所以无需在进入 *dyld_stub_binder* 阶段,并直接调用函数。这样就完成了一次近似于 **lazy** 思想的延时 binding 过程。(这个过程可以用 lldb 来加以验证,在之后会补充。) 314 | 315 | 总结一下 Stub 机制。其实和 `wikipedia` 上的说法一致,设置函数占位符并采用 **lazy** 思想做成延迟 binding 的流程。在 macOS 中也是如此,外部函数引用在 `__DATA` 段的 `__la_symbol_ptr` 区域先生产一个占位符,当第一个调用启动时,就会进入符号的动态链接过程,一旦找到地址后,就将 `__DATA` Segment 中的 `__la_symbol_ptr` Section 中的占位符修改为方法的真实地址,这样就完成了只需要一个符号绑定的执行过程。 316 | 317 | ## 参考文献 318 | 319 | * [深入剖析MachO - satanwoo 五子棋](http://satanwoo.github.io/2017/06/13/Macho-1/) 320 | * [Mach-O二进制文件解析 - 刘振天](http://blog.tingyun.com/web/article/detail/1341?spm=5176.100239.blogcont64288.7.IuEYnv) 321 | * [由App的启动说起 - Jamin's Blog](http://oncenote.com/2015/06/01/How-App-Launch/) 322 | * [dylib浅析 - leisuro 的村屋](https://makezl.github.io/2016/06/27/dylib/) 323 | * [Mach-O文件格式 - 非虫](https://zhuanlan.zhihu.com/p/24858664) 324 | 325 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 326 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Desgard_Duan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Objective-C/Foundation/CFArray 的历史渊源及实现原理.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](http://www.desgard.com/CFArray/) 4 | 5 | # CFArray 的历史渊源及实现原理 6 | 7 | 在 iOS 开发中,`NSArray` 是一个很重要的数据结构。尤其 TableView 中的数据缓存与更新, `NSArray` 来缓存数据以及对于显示数据的修改操作。而在 Core Foundation 中 `CFArray` 与 `NSArray` 相互对应,这引起了笔者对 Core Foundation 和 Foundation 库中的原生数据结构实现产生兴趣,所以来研究一下。 8 | 9 | ## CFArray 历史渊源 10 | 11 | `NSArray` 和 `CFArray` 是 **Toll-Free Bridged** 的,在 [opensource.apple.com](http://opensource.apple.com/) 中, `CFArray` 是开源的。这更有助于我们的学习与研究。在 [Garan no Dou](http://blog.ibireme.com/2014/02/17/cfarray/) 大神之前在做个人工具库的时候,曾经研究过 `CFArray` 的历史渊源和实现手段,在阅读此文之前可以参考一下前辈的优秀博文。 12 | 13 | *[Array](http://ridiculousfish.com/blog/posts/array.html)* 这篇 2005 年的早期文献中,最早介绍过 `CFArray` ,并且测试过其性能水平。它将 `CFArray` 和 STL 中的 `Vector` 容器进行了性能对比,由于后者的实现我们可以理解成是对 C 中的数组封装,所以在性能图上大多数操作都是线性的。而在 `CFArray` 的图中,会发现很多不一样的地方。 14 | 15 | ![vector_results](http://7xwh85.com1.z0.glb.clouddn.com/vector_results.jpg) 16 | 17 | ![cfarray_results](http://7xwh85.com1.z0.glb.clouddn.com/cfarray_results.jpg) 18 | 19 | 上图分析可以看出, `CFArray` 在头插、尾插插入时候的效率近乎常数,而对于中间元素的操作会从小数据的线性效率在一个阀值上突然转变成线性效率,而这个跃变灰不由得想起在 Java 8 当中的 `HashMap` 的数据结构转变方式。 20 | 21 | 在 ObjC 的初期,`CFArray` 是使用 *deque 双端队列* 实现,所以会呈现出头尾操作高效,而中间操作成线性的特点。在容量超过 300000 左右时(实际应该是 262140 = 2^18 ),时间复杂度发生陡变。在源代码中,阀值被宏定义为 `__CF_MAX_BUCKETS_PER_DEQUE` ,具体代码可以见 *[CF-550-CFArray.c](http://opensource.apple.com/source/CF/CF-550/CFArray.c)* (2011 年版本): 22 | 23 | ```c 24 | if (__CF_MAX_BUCKETS_PER_DEQUE <= futureCnt) { 25 | // 创建 CFStorage 引用 26 | CFStorageRef store 27 | // 转换 CFArray 为 Storage 28 | __CFArrayConvertDequeToStore(array); 29 | store = (CFStorageRef)array->_store; 30 | } 31 | ``` 32 | 33 | 可以看到,当数据超出阀值 `__CF_MAX_BUCKETS_PER_DEQUE` 的时候,会将数据结构从 `CFArray` 转换成 `CFStorage` 。 `CFStorage` 是一个平衡二叉树的结构,为了维护数组的顺序访问,将 Node 的权值使用下标完成插入和旋转操作。具体的体现可以看 `CFStorageInsertValues` 操作。具体代码可以查看 [CF-368.18-CFStorage.c](http://opensource.apple.com/source/CF/CF-368.18/Collections.subproj/CFStorage.c) 。 34 | 35 | 在 2011 年以后的 [CF-635.15-CFArray.c](http://opensource.apple.com/source/CF/CF-635.15/CFArray.c) 版本中, `CFArray` 取消了数据结构转换这一功能。或许是为了防止大数据时候二叉树建树的时间抖动问题从而取消了这一特性。直接来看下数据结构的描述: 36 | 37 | ```c 38 | struct __CFArrayDeque { 39 | uintptr_t _leftIdx; // 自左开始下标位置 40 | uintptr_t _capacity; // 当前容量 41 | }; 42 | 43 | struct __CFArray { 44 | CFRuntimeBase _base; 45 | CFIndex _count; // 元素个数 46 | CFIndex _mutations; // 元素抖动量 47 | int32_t _mutInProgress; 48 | __strong void *_store; 49 | }; 50 | ``` 51 | 52 | 从命名上可以看出 `CFArray` 由单一的双端队列进行实现,而且记录了一些容器信息。 53 | 54 | ## C 数组的一些问题 55 | 56 | C 语言中的数组,会开辟一段连续的内存空间来进行数据的读写、存储操作。另外说一句,**数组和指针并不相同**。有一种被很多教材书籍上滥用的说法:一块被 malloc 过的内存空间等于一个数组。这是错误的。最简单的解释,指针需要申请一个指针区域来存储(指向)一块空间的起始位置,而数组(的头部)是对一块空间起始位置的直接访问。另外想了解更多可以看 *[Are pointers and arrays equivalent in C?](http://eli.thegreenplace.net/2009/10/21/are-pointers-and-arrays-equivalent-in-c/)* 这篇博文。 57 | 58 | C 中的数组最显著的缺点就是,在下标 0 处插入时,需要移动所有的元素(即 `memmove()` 函数的原理)。类似的,当删除第一个元素、在第一个元素前插入一个元素也会造成 **O(n)复杂度的操作** 。然而数组是常读写的容器,所以 O(n) 的操作会造成很严重的时间开销。 59 | 60 | ## 当前版本中 CFArray 的部分实现细节 61 | 62 | 在 [CF-855.17](https://opensource.apple.com/source/CF/CF-855.17/CFArray.h.auto.html) 中,我们可以看到当前版本的 `CFArray` 的实现。文档中对 `CFArray` 有如下的描述: 63 | 64 | `CFArray` 实现了一个可被指针顺序访问的紧凑容器。其值可通过整数键(索引下标)进行访问,范围从 0 至 N-1,其中 N 是数组中值的数量。称其**紧凑 (compact)** 的原因是该容器进行删除或插入某个值的时候,不会再内存空间中留下间隙,访问顺序仍旧按照原有键值数值大小排列,使得有效检索集合范围总是在整数范围 [0, N-1] 之中。因此,特定值的下标可能会随着其他元素插入至数组或被删除时而改变。 65 | 66 | 数组有两种类型:**不可变(immutable)** 类型在创建数组之后,不能向其添加或删除元素,而 **可变(mutable)** 类型可以添加或从中删除元素。可变数组的元素数量无限制(或者称只受 `CFArray` 外部的约束限制,例如可用内存空间大小)。与所有的 CoreFoundation 集合类型同理,数组将保持与元素对象的强引用关系。 67 | 68 | 为了进一步弄清 `CFArray` 的细节,我们来分析一下 `CFArray` 的几个操作方法: 69 | 70 | ```c 71 | // 通过下标查询元素值 72 | const void *CFArrayGetValueAtIndex(CFArrayRef array, CFIndex idx) { 73 | // 这个函数尚未开源 74 | // 通过给定的 CFTypeID 来验证指定元素是否匹配 Core Foundation 桥接类 75 | CF_OBJC_FUNCDISPATCHV(__kCFArrayTypeID, const void *, (NSArray *)array, objectAtIndex:idx); 76 | // 尚未开源 77 | // 通过给定的 CFTypeID 来验证 Core Foundation 类型合法性 78 | __CFGenericValidateType(array, __kCFArrayTypeID); 79 | CFAssert2(0 <= idx && idx < __CFArrayGetCount(array), __kCFLogAssertion, "%s(): index (%d) out of bounds", __PRETTY_FUNCTION__, idx); 80 | CHECK_FOR_MUTATION(array); 81 | // 从内存位置取出元素 82 | return __CFArrayGetBucketAtIndex(array, idx)->_item; 83 | } 84 | 85 | // 返回查询元素的地址 86 | CF_INLINE struct __CFArrayBucket *__CFArrayGetBucketAtIndex(CFArrayRef array, CFIndex idx) { 87 | switch (__CFArrayGetType(array)) { 88 | // 只允许两种数组类型 89 | // 不可变对应普通线性结构,可变对应双端队列 90 | case __kCFArrayImmutable: 91 | case __kCFArrayDeque: 92 | // 取地址再加上索引偏移量,返回元素地址 93 | return __CFArrayGetBucketsPtr(array) + idx; 94 | } 95 | return NULL; 96 | } 97 | ``` 98 | 99 | 通过索引下标查询操作中,`CFArray` 仍然继承了传统数组的连续地址空间的性质,所以其时间仍然可保持在 O(1) 复杂度,十分高效。 100 | 101 | ```c 102 | void CFArrayInsertValueAtIndex(CFMutableArrayRef array, CFIndex idx, const void *value) { 103 | // 通过给定的 CFTypeID 来验证指定元素是否匹配 Core Foundation 桥接 104 | CF_OBJC_FUNCDISPATCHV(__kCFArrayTypeID, void, (NSMutableArray *)array, insertObject:(id)value atIndex:(NSUInteger)idx); 105 | // 通过给定的 CFTypeID 来验证 Core Foundation 类型合法性 106 | __CFGenericValidateType(array, __kCFArrayTypeID); 107 | CFAssert1(__CFArrayGetType(array) != __kCFArrayImmutable, __kCFLogAssertion, "%s(): array is immutable", __PRETTY_FUNCTION__); 108 | CFAssert2(0 <= idx && idx <= __CFArrayGetCount(array), __kCFLogAssertion, "%s(): index (%d) out of bounds", __PRETTY_FUNCTION__, idx); 109 | // 类型检查 110 | CHECK_FOR_MUTATION(array); 111 | // 调用该函数进行具体的数组变动过程 112 | _CFArrayReplaceValues(array, CFRangeMake(idx, 0), &value, 1); 113 | } 114 | 115 | // 这个函数没有经过 ObjC 的调度检查,即 CF_OBJC_FUNCDISPATCHV 方法 116 | // 所以为安全考虑,只能用在已经进行调度检查的函数入口之后 117 | void _CFArrayReplaceValues(CFMutableArrayRef array, CFRange range, const void **newValues, CFIndex newCount) { 118 | // 进一步类型检查 119 | CHECK_FOR_MUTATION(array); 120 | // 加锁操作,增加自旋锁防止竞争 121 | BEGIN_MUTATION(array); 122 | // 声明回调 123 | const CFArrayCallBacks *cb; 124 | // 偏移下标,元素总数,数组改变后元素总数 125 | CFIndex idx, cnt, futureCnt; 126 | const void **newv, *buffer[256]; 127 | // 获取数组中元素个数 128 | cnt = __CFArrayGetCount(array); 129 | // 新数组元素总数 = 原数组元素总数 - 删除的元素个数 + 增加的元素个数 130 | futureCnt = cnt - range.length + newCount; 131 | CFAssert1(newCount <= futureCnt, __kCFLogAssertion, "%s(): internal error 1", __PRETTY_FUNCTION__); 132 | // 获取数组中定义的回调方法 133 | cb = __CFArrayGetCallBacks(array); 134 | // 构造分配释放内存抽象 135 | CFAllocatorRef allocator = __CFGetAllocator(array); 136 | // 需要的情况下持有新元素,并为其分配一个临时缓冲区 137 | // 标准是新元素的个数是否超过256 138 | if (NULL != cb->retain && !hasBeenFinalized(array)) { 139 | newv = (newCount <= 256) ? (const void **)buffer : (const void **)CFAllocatorAllocate(kCFAllocatorSystemDefault, newCount * sizeof(void *), 0); 140 | if (newv != buffer && __CFOASafe) __CFSetLastAllocationEventName(newv, "CFArray (temp)"); 141 | // 为新元素增加数据缓冲区 142 | for (idx = 0; idx < newCount; idx++) { 143 | newv[idx] = (void *)INVOKE_CALLBACK2(cb->retain, allocator, (void *)newValues[idx]); 144 | } 145 | } else { 146 | newv = newValues; 147 | } 148 | // 数据抖动量自加 149 | array->_mutations++; 150 | // 现在将一个数组的存储区域分成了三个部分,每个部分都有可能为空 151 | // A: 从索引下标零的位置到小于 range.location 的区域 152 | // B: 传入的 range.location 区域 153 | // C: 从 range.location + range.length 到数组末尾 154 | // 需要注意的是,索引0的位置不一定位于可用存储的最低位,当变化位置新值数量与旧值数量不同时,B区域需要先释放再替换,然后A和C中的值根据情况进行位移 155 | if (0 < range.length) { 156 | // 正常释放变化区域操作 157 | __CFArrayReleaseValues(array, range, false); 158 | } 159 | // B 区现在为清空状态,需要重新填充数据 160 | if (0) { 161 | // 此处隐藏了判断条件和代码。 162 | // 大概操作是排除其他的干扰项,例如 B 区数据未完全释放等。 163 | } else if (NULL == array->_store) { 164 | // 通过数据的首地址引用指针来判断 B 区释放 165 | if (0) { 166 | // 此处隐藏了判断条件和代码 167 | // 排除干扰条件,例如 futureCnt 不合法等 168 | } else if (0 <= futureCnt) { 169 | // 声明一个双端队列对象 170 | struct __CFArrayDeque *deque; 171 | // 根据元素总数确定环状缓冲区域可载元素总个数 172 | CFIndex capacity = __CFArrayDequeRoundUpCapacity(futureCnt); 173 | // 根据元素个数确定空间分配大小 174 | CFIndex size = sizeof(struct __CFArrayDeque) + capacity * sizeof(struct __CFArrayBucket); 175 | // 通过缓冲区构造器来构造存储缓存 176 | deque = (struct __CFArrayDeque *)CFAllocatorAllocate((allocator), size, isStrongMemory(array) ? __kCFAllocatorGCScannedMemory : 0); 177 | if (__CFOASafe) __CFSetLastAllocationEventName(deque, "CFArray (store-deque)"); 178 | // 确定双端队列左值 179 | deque->_leftIdx = (capacity - newCount) / 2; 180 | deque->_capacity = capacity; 181 | __CFAssignWithWriteBarrier((void **)&array->_store, (void *)deque); 182 | // 完成 B 区构造,安全释放数组 183 | if (CF_IS_COLLECTABLE_ALLOCATOR(allocator)) auto_zone_release(objc_collectableZone(), deque); 184 | } 185 | } else { // Deque 186 | // 根据 B 区元素变化,重新定位 A 和 C 区元素存储状态 187 | if (0) { 188 | } else if (range.length != newCount) { 189 | // 传入 array 引用,最终根据变化使得数组更新A、B、C分区规则 190 | __CFArrayRepositionDequeRegions(array, range, newCount); 191 | } 192 | } 193 | // 将区域B的新变化拷贝到B区域 194 | if (0 < newCount) { 195 | if (0) { 196 | } else { // Deque 197 | // 访问线性存储区 198 | struct __CFArrayDeque *deque = (struct __CFArrayDeque *)array->_store; 199 | // 在原基础上,增加一段缓存区域 200 | struct __CFArrayBucket *raw_buckets = (struct __CFArrayBucket *)((uint8_t *)deque + sizeof(struct __CFArrayDeque)); 201 | // 更改B区域数据,类似与 memcpy,但是有写屏障(write barrier),线程安全 202 | objc_memmove_collectable(raw_buckets + deque->_leftIdx + range.location, newv, newCount * sizeof(struct __CFArrayBucket)); 203 | } 204 | } 205 | // 设置新的元素个数属性 206 | __CFArraySetCount(array, futureCnt); 207 | // 释放缓存区域 208 | if (newv != buffer && newv != newValues) CFAllocatorDeallocate(kCFAllocatorSystemDefault, newv); 209 | // 解除线程安全保护 210 | END_MUTATION(array); 211 | } 212 | ``` 213 | 214 | 在 `CFArray` 的插入元素操作中,可以很清楚的看出这是一个**双端队列**(dequeue)的插入元素操作,而且是一种仿照 C++ STL 标准库的存储方式,**缓冲区嵌套 map 表**的静态实现。用示意图来说明一下数据结构: 215 | 216 | ![cfarray-1](http://7xwh85.com1.z0.glb.clouddn.com/cfarray-1.png) 217 | 在 STL 中的 deque,是使用的 map 表来记录的映射关系,而在 Core Foundation 中,`CFArray` 在保证这样的二次映射关系的时候很直接地运用了二阶指针 `_store`。在修改元素的操作中,`CFArray` 也略显得暴力一些,**先对数组进行大块的分区操作,再按照顺序填充数据,组合成为一块新的双端队列**,例如在上图中的双端队列中,在下标为 7 的元素之前增加一个值为 `100` 的元素: 218 | 219 | ![cfarray-2.1](http://7xwh85.com1.z0.glb.clouddn.com/cfarray-2.1.png) 220 | 221 | 222 | 223 | 根据索引下标会找到指定部分的缓存区,将其拿出并进行重新构造。构造过程中或将其划分成 A、B、C 三个区域,B 区域是修改部分。当然如果不够的话,系统会自己进行缓存区的扩容,即 `CFAllocatorRef` 官方提供的内存分配/释放策略。 224 | 225 | `CFAllocatorRef` 是 Core Foundation 中的分配和释放内存的策略。多数情况下,只需要用默认分配器 `kCFAllocatorDefault` ,等价于传入 `NULL` 参数,这用会用 Core Foundation 所谓的“常规方法”来分配和释放内存。这种方法可能会有变化,我们不应该以来与任何特殊行为。用到特殊分配器的情况很少,下来是官方文档中给出的标准分配器及其功能。 226 | 227 | 228 | | kCFAllocatorDefault | 默认分配器,与传入`NULL`等价。 | 229 | | --- | --- | 230 | | kCFAllocatorSystemDefault | 原始的默认系统分配器。这个分配器用来应对万一用`CFAllocatorSetDefault`改变了默认分配器的情况,很少用到。 | 231 | | kCFAllocatorMalloc | 调用`malloc`、`realloc`和`free`。如果用`malloc`创建了内存,那这个分配器对于释放`CFData`和`CFString`就很有用。 | 232 | | kCFAllocatorMallocZone | 在默认的`malloc`区域中创建和释放内存。在 Mac 上开启了垃圾收集的话,这个分配器会很有用,但在 iOS 中基本上没什么用。 | 233 | | kCFAllocatorNull | 什么都不做。跟`kCFAllocatorMalloc`一样,如果不想释放内存,这个分配器对于释放`CFData`和`CFString`就很有用。 | 234 | | KCFAllocatorUseContext | 只有`CFAllocatorCreate`函数用到。创建`CFAllocator`时,系统需要分配内存。就像其他所有的`Create`方法,也需要一个分配器。这个特殊的分配器告诉`CFAllocatorCreate`用传入的函数来分配`CFAllocator`。 | 235 | 236 | 在 `_CFArrayReplaceValues` 方法中的最后一个判断: 237 | 238 | ```c 239 | if (newv != buffer && newv != newValues) 240 | CFAllocatorDeallocate(kCFAllocatorSystemDefault, newv); 241 | ``` 242 | 243 | 会检查一下缓存区的数量问题,如果数量过多会释放掉多余的缓存区。这是因为这个方法具有通用性,不仅仅可以使用在插入元素操作,在增加(`CFArrayAppendValue`)、替换(`CFArrayReplaceValues`)、删除(`CFArrayRemoveValueAtIndex`)操作均可使用。由于将数据结构采取分块管理,所以时间分摊,复杂度大幅度降低。所以,我们看到 `CFArray` 的时间复杂度在查询、增添元素操作中均有较高的水平。 244 | 245 | 而在 `NSMutableArray` 的实现中,苹果为了解决移动端的小内存特点,使用 `CFArray` 中在两端增加可扩充的缓存区则会造成大量的浪费。在 [NSMutableArray原理揭露](http://blog.joyingx.me/2015/05/03/NSMutableArray%20%E5%8E%9F%E7%90%86%E6%8F%AD%E9%9C%B2/) 一文中使用逆向的思路,挖掘 `NSMutableArray` 的实现原理,其做法是使用*环形缓冲区*对缓存部分做到最大化的压缩,这是苹果针对于移动设备的局限而提出的方案。 246 | 247 | 248 | 249 | 250 | 251 | 252 | ## 参考资料: 253 | 254 | [Let's Build NSMutableArray](https://www.mikeash.com/pyblog/friday-qa-2012-03-09-lets-build-nsmutablearray.html) 255 | 256 | [GNUStep · NSArray](https://github.com/opensource-apple/CF/blob/master/CFArray.h) 257 | 258 | [What is the data structure behind NSMutableArray?](http://stackoverflow.com/questions/22591296/what-is-the-data-structure-behind-nsmutablearray) 259 | 260 | [Apple Source Code - CF-855.17](https://opensource.apple.com/source/CF/CF-855.17/CFArray.c) 261 | 262 | 263 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 264 | -------------------------------------------------------------------------------- /Objective-C/Foundation/从经典问题来看 Copy 方法.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](https://desgard.com/2016/08/11/copy/) 4 | 5 | # 从经典问题来看 Copy 方法 6 | 7 | > 本文中所用的 Test 可以从[这里](https://github.com/Desgard/iOS-Source-Probe/tree/master/project/TestCopy)获取。 8 | 9 | 在初学 iOS 的时候,可能会被灌输这么一个常识,**切记 NSString 的 property 的修饰变量要写作 copy ,而不是 strong**,那么这是为什么? 10 | 11 | > 经典面试题:为什么 NSString 类型成员变量的修饰属性用 copy 而不是 strong (或 retain ) ? 12 | 13 | ## review Copy Operation 14 | 15 | ### Test 1 16 | 17 | 先来模拟一个程序设计错误的场景。有一个叫做 Person 的 Class,其中它拥有一个 NSString 类型的 s_name 属性(代表 name 是 strong),我们想给一个对象的 s_name 赋值,并且之前的赋值变量还想重复使用到其他场景。所以,我们在引入这个 Class 的 ViewController 进行如下操作: 18 | 19 | ```c 20 | - (void)test1 { 21 |     self.one = [[Person alloc] init]; 22 |     NSMutableString *name = [NSMutableString stringWithFormat:@"iOS"]; 23 |     self.one.s_name = name; 24 |      25 |     NSLog(@"%@", self.one.s_name); 26 |      27 |     [name appendString:@" Source Probe"]; 28 |      29 |     NSLog(@"%@", self.one.s_name); 30 | } 31 | ``` 32 | 33 | 如果在 Person 这个 Class 中,我们的 s_name 的修饰属性是 **strong** 的话,会看到如下输出结果。 34 | 35 | ```c 36 | 2016-08-12 05:51:21.262 TestCopy[64714:20449045] iOS 37 | 2016-08-12 05:51:21.262 TestCopy[64714:20449045] iOS Source Probe 38 | ``` 39 | 40 | 可是,我们操作的仅仅是对 s_name 那个变量,为什么连属性当中的 s_name 也会被改变?对这段代码稍做修改,重新测试。 41 | 42 | ### Test 2 43 | 44 | ```c 45 | - (void)test2 { 46 |     self.one = [[Person alloc] init]; 47 |     NSString *name = [NSMutableString stringWithFormat:@"iOS"]; 48 |     self.one.s_name = name; 49 |      50 |     NSLog(@"%@", self.one.s_name); 51 |      52 |     name = @"iOS Source Probe"; 53 |      54 |     NSLog(@"%@", self.one.s_name); 55 | } 56 | ``` 57 | 58 | 这一次我们看到了输出结果是正常的: 59 | 60 | ```c 61 | 2016-08-12 05:56:57.162 TestCopy[64842:20459179] iOS 62 | 2016-08-12 05:56:57.162 TestCopy[64842:20459179] iOS 63 | ``` 64 | 65 | ### Test 3 66 | 67 | 再来做第三个实验,我们换用 copy 类型的成员 c_name,来替换实验1中的 s_name ,查看一下输出结果。 68 | 69 | 最后发现输出结果依旧是我们所需要的。 70 | 71 | ```c 72 | - (void)test3 { 73 |     self.one = [[Person alloc] init]; 74 |     NSMutableString *name = [NSMutableString stringWithFormat:@"iOS"]; 75 |     self.one.c_name = name; 76 |      77 |     NSLog(@"%@", self.one.c_name); 78 |      79 |     [name appendString:@" Source Probe"]; 80 |      81 |     NSLog(@"%@", self.one.c_name); 82 | } 83 | ``` 84 | 85 | ```c 86 | 2016-08-12 06:03:40.226 TestCopy[64922:20479646] iOS 87 | 2016-08-12 06:03:40.227 TestCopy[64922:20479646] iOS 88 | ``` 89 | 90 | 做过如上三个实验,或许你会知道对 property 使用 copy 修饰属性的原因了。也就是在一个特定场景下:**当我们通过一个 NSMutableString 对 NSString 变量进行赋值,如果 NSString 的 property 是 strong 类型的时候,就会随着 NSMutableString 类型的变量一起变化**。 91 | 92 | 这个猜测是正确的。在 [stackoverflow](http://stackoverflow.com/questions/11249656/should-an-nsstring-property-under-arc-be-strong-or-copy) 上也对这个场景进行单独的描述。可是原因是什么?继续做下面的实验: 93 | 94 | ### Test 4 95 | 96 | ```c 97 | - (void)test4 { 98 | NSMutableString *str = [NSMutableString stringWithFormat:@"iOS"]; 99 | 100 | NSLog(@"%p", str); 101 | 102 | NSString *str_a = str; 103 | 104 | NSLog(@"%p", str_a); 105 | 106 | NSString *str_b = [str copy]; 107 | 108 | NSLog(@"%p", str_b); 109 | } 110 | ``` 111 | 112 | 输出地址后,我们发现以下结果: 113 | 114 | ```c 115 | 2016-08-12 06:15:45.169 TestCopy[65230:20515110] 0x7faf28429e70 116 | 2016-08-12 06:15:45.170 TestCopy[65230:20515110] 0x7faf28429e70 117 | 2016-08-12 06:15:45.170 TestCopy[65230:20515110] 0xa00000000534f693 118 | ``` 119 | 120 | 发现当令 NSString 对象指针指向一个 NSMutableString 类型变量通过 copy 方法返回的对象,则会对其进行**深复制**。这也就是我们一直所说的在一个 Class 的成员是 NSString 类型的时候,修饰属性应该使用 copy ,其实就是在使用 mutable 对象进行赋值的时候,防止 mutable 对象的改变从而影响成员变量。从 MRC 的角度来看待修饰属性,若一个属性的关键字为 retain (可等同于 strong ),则在进行指针的指向修改时,如上面的`self.one.name = str`,其实是执行了`self.one.name = [str retain]`,而 copy 类型的属性则会执行`self.one.name = [str copy]`。 121 | 122 | 而在 Test 2 中,我们的实验是将一个 NSString 对象指向另外一个 NSString 对象,那么如果前者是 copy 的成员,还会进行**深复制**吗?进行下面的 Test 5,我们令 c_name 的修饰变量为 copy。 123 | 124 | ### Test 5 125 | 126 | ```c 127 | - (void)test5 { 128 | self.one = [[Person alloc] init]; 129 | NSString *name = [NSMutableString stringWithFormat:@"iOS"]; 130 | 131 | self.one.c_name = name; 132 | NSLog(@"%@", self.one.c_name); 133 | 134 | name = @"iOS Source Probe"; 135 | NSLog(@"%@", self.one.c_name); 136 | 137 | NSString *str = [NSString stringWithFormat:@"iOS"]; 138 | NSLog(@"%p", str); 139 | 140 | NSString *str_a = str; 141 | NSLog(@"%p", str_a); 142 | 143 | NSString *str_b = [str copy]; 144 | NSLog(@"%p", str_b); 145 | } 146 | ``` 147 | 148 | 发现结果符合猜测: 149 | 150 | ```c 151 | 2016-08-12 08:09:28.125 TestCopy[66402:20671038] iOS 152 | 2016-08-12 08:09:28.126 TestCopy[66402:20671038] iOS 153 | 2016-08-12 08:09:28.126 TestCopy[66402:20671038] 0xa00000000534f693 154 | 2016-08-12 08:09:28.126 TestCopy[66402:20671038] 0xa00000000534f693 155 | 2016-08-12 08:09:28.126 TestCopy[66402:20671038] 0xa00000000534f693 156 | ``` 157 | 158 | 从一个 NSString 进行 copy 后赋值,copy 方法仍旧是**浅拷贝**。这个效果就等同于`str_b = [str retain]`,在 ARC 中即 `str_b = str`。 159 | 160 | 那么,如何在这种情况下,让`str_b`指向一个`str`的深拷贝呢,答案就是`str_b = [str mutableCopy]`。这也就是 copy 和 mutableCopy 的区别。 161 | 162 | ## copy & mutableCopy 163 | 164 | 下面我们开始对 copy 和 mutableCopy 原理进行分析。以下也是我的源码学习笔记。 165 | 166 | 在[opensource.apple.com的git仓库中](git@github.com:RetVal/objc-runtime.git)的Runtime源码中有`NSObject.mm`这个文件,其中有如下方法是关于 copy 的: 167 | 168 | ```c 169 | - (id)copy { 170 | return [(id)self copyWithZone:nil]; 171 | } 172 | 173 | - (id)mutableCopy { 174 | return [(id)self mutableCopyWithZone:nil]; 175 | } 176 | ``` 177 | 178 | 发现`copy`和`mutableCopy`两个方法只是简单调用了`copyWithZone:`和`mutableCopyWithZone:`两个方法。所以有了以下猜想:对于 NSString 和 NSMutableString,Foundation 框架已经为我们实现了 copyWithZone 和 mutableCopyWithZone 的源码。我在[searchcode.com](https://searchcode.com)找到了 Hack 版的 NSString 和 NSMutableString 的 Source Code。 179 | 180 | 在[NSString.m](https://searchcode.com/file/12532490/libFoundation/Foundation/NSString.m)中,看到了以下关于 copy 的方法。 181 | 182 | ```c 183 | - (id)copyWithZone:(NSZone *)zone { 184 | if (NSStringClass == Nil) 185 | NSStringClass = [NSString class]; 186 | return RETAIN(self); 187 | } 188 | 189 | - (id)mutableCopyWithZone:(NSZone*)zone { 190 | return [[NSMutableString allocWithZone:zone] initWithString:self]; 191 | } 192 | ``` 193 | 194 | 而在 [NSMutableString.m](https://searchcode.com/file/68838008/jni%20w:%20itoa%20runtime%20and%20allocator/Foundation/NSMutableString.m) 中只发现了`copyWithZone:`和`copy:`方法,并且它调用了父类的**全能初始化方法(designated initializer)**,所以构造出来的对象是由 NSString 持有的: 195 | 196 | ```c 197 | -(id)copy { 198 | return [[NSString alloc] initWithString:self]; 199 | } 200 | 201 | -(id)copyWithZone:(NSZone*)zone { 202 | return [[NSString allocWithZone:zone] initWithString:self]; 203 | } 204 | ``` 205 | 206 | 也就是说, NSMutableString 进行 copy 的对象从源码上看也会变成深复制方法。我们做下试验。 207 | 208 | ### Test 6 209 | 210 | ```c 211 | - (void)test6 { 212 | NSMutableString *str = [NSMutableString stringWithFormat:@"iOS"]; 213 | NSLog(@"%p", str); 214 | NSMutableString *str2 = [str copy]; 215 | NSLog(@"%p", str2); 216 | } 217 | ``` 218 | 219 | ```c 220 | 2016-08-12 15:12:12.845 TestCopy[73658:21549553] 0x7f96f8410e10 221 | 2016-08-12 15:12:12.846 TestCopy[73658:21549553] 0xa00000000534f693 222 | ``` 223 | 224 | 输出结果如我们所预料的,同样是 NSMutableString 之间的指针传递,即使类型相同,使用了该类型下的 copy 方法,也会变成深复制,因为返回的对象如源码所示,调用了 NSString 的全能初始化方法,并且由一个新的 NSString 持有。那么在 NSMutableString 中使用`mutableCopy`,可以做到单纯的 retain 操作吗。答案也是否定的,同样是源码中写道,在源码中并没有重写`mutableCopy`方法,也没有实现`mutableCopyWithZone:`方法,所以会调用父类的`mutableCopyWithZone`。而在父类中 `mutableCopyWithZone:`方法中调用了 NSMutableString 的全局初始化方法,所以依旧是深复制。 225 | 226 | 以上原则试用于大多数 Foundation 框架中的常用类,如 NSArray 、 NSDictionary 等等。 227 | 228 | ## 关于自定义 Class 的 Copy 方法 229 | 230 | 对于以上所有试验,我们可以总结一种关系: 231 | 232 | 233 | ![14709874979001.jpg](http://upload-images.jianshu.io/upload_images/208988-5769dfa1eed5e9fd.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 234 | 235 | 236 | 237 | 由此,我们可以总结一下。其实 Copy 对应的也就是类似于 **immutableCopy** 这种感觉,因为通过 copy 出的对象总是不可变的。所以对于一个 Class 中的 mutableCopy 和 copy 的方法命名而言,其实是否有 mutable ,是**针对于返回值而言的**。在 Foundation Framework 中,把拷贝方法称为 copy 而非 immutableCopy 的原因在于,NSCopying 这个基础协议不仅设计给那些具有可变和不可变版本的类来用,而且还要供其他一些没有“可变”和“不可变”之分的类来用。 238 | 239 | 所以在实现自定义 Class 的 Copy 方法适合,我们需要确定一个问题,应该执行深复制还是浅复制。然后在去实现对应的 `copyWithZone:` 和 `mutableCopyWithZone:` 两个方法。这里我不再多论,可以查看 *Effective Objective-C 2.0 52 Specific Ways to Improve Your iOS and OS X Programes* 的 *Tips 22* 。 240 | 241 | ## 对于很多博文的一些疑问 242 | 243 | 在很多关于讨论自定义 Class 中的 Copy 方法,都会强调一句:**我们一定要遵循 NSCopying 或 NSMutableCopying 这两个协议**,并且在实例代码中也显式写出了自定义的 Class 是遵循这两个协议的,如下: 244 | 245 | ```c 246 | @interface XXObject: NSObject 247 | 248 | @end 249 | ``` 250 | 251 | 但是如果我们不显式的写出,我们发现不但没有 crash ,而且结果也是完全一样的。而在 *[Objective-C copy那些事儿](http://zhangbuhuai.com/copy-in-objective-c/)* 此文中,作者写道: 252 | 253 | > 正确的做法是让自定义类遵循NSCopying协议(**NSObject并未遵循该协议**) 254 | 255 | 我的猜想是,某次苹果所用的 Foundation 框架升级,使得 NSObject 开始遵循 NSCopying 方法,但是没有去实现(这就好比 c++ 中的 virtual 虚函数)。这里有待考证,如果有朋友知道,欢迎补充这一部分知识,请大家多多指教。 256 | 257 | --- 258 | 多谢4楼 @[hpppp](http://www.jianshu.com/users/bfa2516c1fa2) 的解释: 259 | 260 | > [hpppp](http://www.jianshu.com/users/bfa2516c1fa2): 显式写明遵循该协议只是说运行时如果调用conformToProtocol的话,返回会是true,否则返回false,等可能还有一些其它的运行时信息,就像c#/java的反射一样。 而这里没有显式声明,但你依然实现了该协议中的方法,这时候运行时调用copy时,会转成调用copyWithZone,此时该方法存在,那么调用就不会抛出异常。 261 | 262 | --- 263 | 264 | > 以上是个人在学习 Foundation 框架的一些源码分析和猜想,如果想了解更多的 *iOS Source Probe* 系列文章,可以访问 github 仓库 [iOS-Source-Probe](https://github.com/Desgard/iOS-Source-Probe)。 265 | 266 | 267 | -------------------------------------------------------------------------------- /Objective-C/Runtime/load 方法全程跟踪.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](http://www.desgard.com/isa/) 4 | 5 | # load 方法全程跟踪 6 | 7 | 几天前 Github 的 [RetVal](https://github.com/RetVal) 大神更新了可 debug 版本的 706 `` 源码,于是让源码阅读学习得以继续。本文将介绍个人学习 `load` 方法的全部流程。 8 | 9 | ## load 方法的调用时机 10 | 11 | 从 *Effective Objective-C 2.0 - 52 Specific Ways to Improve Your iOS and OS X Programs* 一书中讲述到:Objective-C 中绝大多数类都继承自 `NSObject` 根类,每个类都有两个初始化方法,其中之一就是 `load` 方法。 12 | 13 | ```c 14 | + (void)load 15 | ``` 16 | 17 | 对于每一个 *Class* 和 *Category* 来说,必定会调用此方法,而且仅调用一次。当包含 *Class* 和 *Category* 的程序库载入系统时,就会执行此方法,并且此过程通常是在程序启动的时候执行。 18 | 19 | 不同的是,现在的 iOS 系统中已经加入了**动态加载特性(Dynamic Loading)**,这是从 macOS 应用程序中迁移而来的特性,等应用程序启动好之后再去加载程序库。如果 *Class* 和其 *Category* 中都重写了 `load` 方法,则先调用 *Class* 中的。 20 | 21 | 我们通过 [RetVal](https://github.com/RetVal) 封装好的 debug 版最新源码进行断点调试,来追踪一下 `load` 方法的全部处理过程,以便于了解这个函数以及 Objective-C 强大的动态性。 22 | 23 | 创建一个 *Class* 文件 `DGObject.m`,然后在其中增加 `load` 方法。在运行 proj 后,可以看见 `load` 方法的调用时机是在入口函数主程序之前。 24 | 25 | ![](http://7xwh85.com1.z0.glb.clouddn.com/14837770930294.jpg) 26 | 27 | 下面在 `load` 方法下增加断点,查看其调用栈并跟踪函数执行时候的上层代码: 28 | 29 | ![](http://7xwh85.com1.z0.glb.clouddn.com/14837787124422.jpg) 30 | 31 | 调用栈显示栈情况为如下方法对象: 32 | 33 | ```c 34 | 0 +[XXObject load] 35 | 1 call_class_loads() 36 | 2 call_load_methods 37 | 3 load_images 38 | 4 dyld::notifySingle(dyld_image_states, ImageLoader const*) 39 | 11 _dyld_start 40 | ``` 41 | 42 | 追其源头,从 `_dyld_start` 开始探究。**dyld(The Dynamic Link Editor)**是 Apple 的动态链接库,系统内核做好启动程序的初始准备后,将其他事务交给 dyld 负责。对于 dyld 这里不再细究,在以后对于动态库的学习时进行研究。 43 | 44 | 在研究 `load_images` 方法之前,先来研究一下什么是 **images**。**images**表示的是二进制文件(可执行文件或者动态链接库.so文件)编译后的符号、代码等。所以 `load_images` 的工作是**传入处理过后的二进制文件并让 `Runtime` 进行处理**,并且每一个文件对应一个抽象实例来负责加载,这里的实例是 `ImageLoader`,我们从调用栈的方法 4 可以清楚的看到参数类型: 45 | 46 | ```c 47 | dyld::notifySingle(dyld_image_states, ImageLoader const*) 48 | ``` 49 | 50 | `ImageLoader` 处理二进制文件的时机是在 `main` 入口函数以前,它在加载文件时主要做两个工作: 51 | 52 | * 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻) 53 | * 再从可执行文件 image 递归加载所有符号 54 | 55 | ## 简单了解 image 56 | 57 | 在 [你真的了解 load 方法么?](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/%E4%BD%A0%E7%9C%9F%E7%9A%84%E4%BA%86%E8%A7%A3%20load%20%E6%96%B9%E6%B3%95%E4%B9%88%EF%BC%9F.md) 这篇文章中,*Draveness* 提供了一种断点发来打印出所有加载的镜像。 58 | 59 | ![](http://7xwh85.com1.z0.glb.clouddn.com/14838483024359.jpg) 60 | 61 | 这样可以将当前载入的 image 全部显示,我们展示的是 image 的 *path* 和 *slice* 信息。 62 | 63 | ```c 64 | ... 65 | (const char *) $22 = 0x00007fff9c1f07a0 "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices" 66 | (const mach_header *) $23 = 0x00007fff9c1f0000 67 | (const char *) $24 = 0x00007fff9c51bb10 "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList" 68 | (const mach_header *) $25 = 0x00007fff9c51b000 69 | (const char *) $26 = 0x00007fff9ca70d90 "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation" 70 | (const mach_header *) $27 = 0x00007fff9ca70000 71 | (const char *) $28 = 0x00007fff5fbff870 "/Users/Desgard_Duan/Library/Developer/Xcode/DerivedData/objc-frsvxngqnjxvxwahvxtwjglbkjlt/Build/Products/Debug/debug-objc" 72 | (const mach_header *) $29 = 0x0000000100000000 73 | ``` 74 | 75 | 这里会传入很多的动态链接库 `.dylib` 以及官方静态框架 `.framework` 的 image,而 *path* 就是其对应的二进制文件的地址。在 `` 动态库头文件中,也为我们提供了查询所有动态库 image 的方法,在这里也简单介绍一下: 76 | 77 | ```c 78 | #include 79 | #include 80 | 81 | void listImages(){ 82 | uint32_t i; 83 | uint32_t ic = _dyld_image_count(); 84 | 85 | printf("Got %d images\n", ic); 86 | for (i = 0; i < ic; ++ i) { 87 | printf("%d: %p\t%s\t(slide: %p)\n", 88 | i, 89 | _dyld_get_image_header(i), 90 | _dyld_get_image_name(i), 91 | _dyld_get_image_vmaddr_slide(i)); 92 | } 93 | } 94 | 95 | int main() { 96 | listImages(); 97 | return 0; 98 | } 99 | ``` 100 | 101 | 我们可以通过系统库提供的接口方法,来深入学习官方的动态库情况。 102 | 103 | ![](http://7xwh85.com1.z0.glb.clouddn.com/14838490225420.jpg) 104 | 105 | ## 继续研究 load_images 106 | 107 | ```c 108 | // load_images 109 | // 执行 dyld 提供的并且已被 map_images 处理后的 image 中的 +load 110 | // 锁定状态:runtimeLock写操作和 loadMethodLock 方法,保证线程安全 111 | extern bool hasLoadMethods(const headerType *mhdr); 112 | extern void prepare_load_methods(const headerType *mhdr); 113 | void 114 | load_images(const char *path __unused, const struct mach_header *mh) { 115 | // 没有查询到传入 Class 中的 load 方法,视为锁定状态 116 | // 则无需给其加载权限,直接返回 117 | if (!hasLoadMethods((const headerType *)mh)) return; 118 | 119 | // 定义可递归锁对象 120 | // 由于 load_images 方法由 dyld 进行回调,所以数据需上锁才能保证线程安全 121 | // 为了防止多次加锁造成的死锁情况,使用可递归锁解决 122 | recursive_mutex_locker_t lock(loadMethodLock); 123 | 124 | // 收集所有的 +load 方法 125 | { 126 | // 对 Darwin 提供的线程写锁的封装类 127 | rwlock_writer_t lock2(runtimeLock); 128 | // 提前准备好满足 +load 方法调用条件的 Class 129 | prepare_load_methods((const headerType *)mh); 130 | 131 | } 132 | 133 | // 调用 +load 方法 (without runtimeLock - re-entrant) 134 | call_load_methods(); 135 | } 136 | ``` 137 | 138 | 重新回到 `load_images` 方法,`hasLoadMethods` 函数引起注意。其中为了查询 `load` 函数列表,会分别查询该函数在内存数据段上指定 section 区域是否有所记录。 139 | 140 | ```c 141 | // 快速查询是否存在 +load 函数列表 142 | bool hasLoadMethods(const headerType *mhdr) { 143 | size_t count; 144 | if (_getObjc2NonlazyClassList(mhdr, &count) && count > 0) return true; 145 | if (_getObjc2NonlazyCategoryList(mhdr, &count) && count > 0) return true; 146 | return false; 147 | } 148 | ``` 149 | 150 | 在 `objc-file.mm` 文件中存有以下定义: 151 | 152 | ```c 153 | // 类似于 C++ 的模板写法,通过宏来处理泛型操作 154 | // 函数内容是从内存数据段的某个区下查询该位置的情况,并回传指针 155 | #define GETSECT(name, type, sectname) \ 156 | type *name(const headerType *mhdr, size_t *outCount) { \ 157 | return getDataSection(mhdr, sectname, nil, outCount); \ 158 | } \ 159 | type *name(const header_info *hi, size_t *outCount) { \ 160 | return getDataSection(hi->mhdr(), sectname, nil, outCount); \ 161 | } 162 | 163 | // 根据 dyld 对 images 的解析来在特定区域查询内存 164 | GETSECT(_getObjc2ClassList, classref_t, "__objc_classlist"); 165 | GETSECT(_getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist"); 166 | ``` 167 | 168 | 在 Apple 的官方文档中,我们可以在 `__DATA` 段中查询到 `__objc_classlist` 的用途,主要是用在**访问 Objective-C 的类列表**,而 `__objc_nlcatlist` 用于**访问 Objective-C 的 `+load` 函数列表,比 `__mod_init_func` 更早被执行**。这一块对类信息的解析是由 dyld 处理时期完成的,也就是我们上文提到的 `map_images` 方法的解析工作。而且从侧面可以看出,Objective-C 的强大动态性,与 dyld 前期处理密不可分。 169 | 170 | ## 可递归锁 171 | 172 | 在 `load_images` 方法所在的 `objc-runtime-new.mm` 中,全局 `loadMethodLock` 是一个 `recursive_mutex_t` 类型的变量。这个是苹果公司通过 C 实现的一个互斥递归锁 Class,来解决多次上锁而不会发生死锁的问题。 173 | 174 | 其作用与 `NSRecursiveLock` 相同,但不是由 `NSLock` 再封装,而是通过 C 为 Runtime 的使用场景而写的一个 Class。更多关于线程锁的知识,强烈推荐 *bestswifter* 这篇博文 *[深入理解 iOS 开发中的锁](https://bestswifter.com/ios-lock/)*。 175 | 176 | ## 准备 +load 运行的从属 Class 177 | 178 | ```c 179 | void prepare_load_methods(const headerType *mhdr) { 180 | size_t count, i; 181 | 182 | runtimeLock.assertWriting(); 183 | 184 | // 收集 Class 中的 +load 方法 185 | // 获取所有的类的列表 186 | classref_t *classlist = 187 | _getObjc2NonlazyClassList(mhdr, &count); 188 | for (i = 0; i < count; i++) { 189 | // 通过 remapClass 获取类指针 190 | // schedul_class_load 递归到父类逐层载入 191 | schedule_class_load(remapClass(classlist[i])); 192 | } 193 | 194 | // 收集 Category 中的 +load 方法 195 | category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count); 196 | for (i = 0; i < count; i++) { 197 | category_t *cat = categorylist[i]; 198 | // 通过 remapClass 获取 Category 对象存有的 Class 对象 199 | Class cls = remapClass(cat->cls); 200 | if (!cls) continue; 201 | // 对类进行第一次初始化,主要用来分配可读写数据空间并返回真正的类结构 202 | realizeClass(cls); 203 | assert(cls->ISA()->isRealized()); 204 | // 将需要执行 load 的 Category 添加到一个全局列表中 205 | add_category_to_loadable_list(cat); 206 | } 207 | } 208 | ``` 209 | 210 | `prepare_load_methods` 作用是为 load 方法做准备,从代码中可以看出 Class 的 load 方法是优先于 Category。其中在收集 Class 的 load 方法中,因为需要对 Class 关系树的根节点逐层遍历运行,在 `schedule_class_load` 方法中使用深层递归的方式递归到根节点,优先进行收集。 211 | 212 | 213 | ```c 214 | // 用来规划执行 Class 的 load 方法,包括父类 215 | // 递归调用 +load 方法通过 cls 指针以及 216 | // 要求是 cls 指针的 Class 必须已经进行链接操作 217 | static void schedule_class_load(Class cls) { 218 | if (!cls) return; 219 | // 查看 RW_REALIZED 是否被标记 220 | assert(cls->isRealized()); 221 | 222 | // 查看 RW_LOADED 是否被标记 223 | if (cls->data()->flags & RW_LOADED) return; 224 | 225 | // 递归到深层(超类)运行 226 | schedule_class_load(cls->superclass); 227 | 228 | // 将需要执行 load 的 Class 添加到一个全局列表中 229 | add_class_to_loadable_list(cls); 230 | // 标记 RW_LOADED 符号 231 | cls->setInfo(RW_LOADED); 232 | } 233 | ``` 234 | 235 | 在 `schedule_class_load` 中,Class 的读取方式是用 cls 指针方式,其中有很多内存符号位用来记录状态。`isRealized()` 查看的就是 `RW_REALIZED` 位,该位记录的是**当前 Class 是否初始化一个类的指标**。而之后查看的 `RW_LOADED` 是记录当前类的 `+load` 方法是否被被调用。 236 | 237 | 在存储静态表的方法中,方法对象会以指针的方式作为参数传递,然后用名为 `loadable_classes` 的静态类数组对即将运行的 load 方法进行存储,其下标索引 `loadable_classes_used` 为(从零开始的)全局量,并在每次录入方法后做自加操作实现索引的偏移。 238 | 239 | 由此可以看到,在 `prepare_load_methods` 方法中,Runtime 方法进行了 Class 和 Category 的筛选过滤工作,并且将即将执行的 load 方法以指针的形式组织成了一个线性表结构,为之后执行操作中打下基础。 240 | 241 | ## 通过函数指针让 load 方法跑起来 242 | 243 | 经过加载镜像、缓存类列表后,开始执行 `call_load_methods` 方法。 244 | 245 | ```c 246 | void call_load_methods(void) { 247 | // 是否已经录入 248 | static bool loading = NO; 249 | // 是否有关联的 Category 250 | bool more_categories; 251 | loadMethodLock.assertLocked(); 252 | 253 | // 由于 loading 是全局静态布尔量,如果已经录入方法则直接退出 254 | if (loading) return; 255 | loading = YES; 256 | // 声明一个 autoreleasePool 对象 257 | // 使用 push 操作其目的是为了创建一个新的 autoreleasePool 对象 258 | void *pool = objc_autoreleasePoolPush(); 259 | 260 | do { 261 | // 重复调用 load 方法,直到没有 262 | while (loadable_classes_used > 0) { 263 | call_class_loads(); 264 | } 265 | 266 | // 调用 Category 中的 load 方法 267 | more_categories = call_category_loads(); 268 | 269 | // 继续调用,直到所有 Class 全部完成 270 | } while (loadable_classes_used > 0 || more_categories); 271 | // 将创建的 autoreleasePool 对象释放 272 | objc_autoreleasePoolPop(pool); 273 | // 更改全局标记,表示已经录入 274 | loading = NO; 275 | } 276 | ``` 277 | 278 | 其实 `call_load_methods` 由以上代码可知,仅是运行 load 方法的入口。其中最重要的方法 `call_class_loads` 会从一个待加载的类列表 `loadable_classes` 中寻找对应的类,并使用 `selector(load)` 的实现并执行。 279 | 280 | ```c 281 | static void call_class_loads(void) { 282 | // 声明下标偏移 283 | int i; 284 | // 分离加载的 Class 列表 285 | struct loadable_class *classes = loadable_classes; 286 | // 调用标记 287 | int used = loadable_classes_used; 288 | loadable_classes = nil; 289 | loadable_classes_allocated = 0; 290 | loadable_classes_used = 0; 291 | 292 | // 调用列表中的 Class 类的 load 方法 293 | for (i = 0; i < used; i++) { 294 | // 获取 Class 指针 295 | Class cls = classes[i].cls; 296 | // 获取方法对象 297 | load_method_t load_method = (load_method_t)classes[i].method; 298 | if (!cls) continue; 299 | if (PrintLoading) { 300 | _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging()); 301 | } 302 | // 方法调用 303 | (*load_method)(cls, SEL_load); 304 | } 305 | // 释放 Class 列表 306 | if (classes) free(classes); 307 | } 308 | ``` 309 | 310 | 读完源码,也许会好奇为什么 `(*load_method)(cls, SEL_load);` 这一句可以调用 load 方法? 311 | 312 | 其实这是 C 中的**函数指针**基本概念。在这里我用一个简单的例子做个简要说明(如果没有看懂,需要补补基础了0.0): 313 | 314 | ```c 315 | #include 316 | #import 317 | 318 | void run() { 319 | printf("Hello World\n"); 320 | } 321 | 322 | int main(int argc, const char * argv[]) { 323 | @autoreleasepool { 324 | void (*dy_run)() = run; 325 | (*dy_run)(); 326 | } 327 | return 0; 328 | } 329 | ``` 330 | 331 | 其结果会发现执行了 `run` 方法,并输出了 `Hello World`。这里,我们通过一个 `void (*fptr)()` 类型的函数指针,将 `run` 函数获取出,并运行函数。实际上其中的工作是抓取 `run` 函数的地址并存储在指针变量中。我们通过指针运行对应的地址部分,其效果为执行了 `run` 函数。 332 | 333 | 返回方法中的 `load_method_t`,我们在全局位置发现了该类型的定义: 334 | 335 | ```c 336 | typedef void(*load_method_t)(id, SEL); 337 | ``` 338 | 339 | `id` 参数可以传递一个类信息,这里是将 `cls` Class 的指针和 `SEL` 选择子作为参数传入。 340 | 341 | 至此完成了 load 方法的动态调用。 342 | 343 | ## 全局 Class 存储线性表数据结构 344 | 345 | 总结一下 Class 中 load 方法的全部流程,用流程图将其描述一下: 346 | 347 | ![](http://7xwh85.com1.z0.glb.clouddn.com/14844929413362.jpg) 348 | 349 | 下面来研究一下,存储 Class 的全局表数据结构是怎样的。 350 | 351 | 找到之前的 `add_class_to_loadable_list` 开始分析: 352 | 353 | ```c 354 | void add_class_to_loadable_list(Class cls) { 355 | // 定义方法指针 356 | // 目的是构造函数指针 357 | IMP method; 358 | 359 | loadMethodLock.assertLocked(); 360 | // 通过 cls 中的 getLoadMethod 方法,直接获得 load 方法体存储地址 361 | method = cls->getLoadMethod(); 362 | // 没有 load 方法直接返回 363 | if (!method) return; 364 | if (PrintLoading) { 365 | _objc_inform("LOAD: class '%s' scheduled for +load", 366 | cls->nameForLogging()); 367 | } 368 | // 判断数组是否已满 369 | if (loadable_classes_used == loadable_classes_allocated) { 370 | // 动态扩容,为线性表释放空间 371 | loadable_classes_allocated = loadable_classes_allocated*2 + 16; 372 | loadable_classes = (struct loadable_class *) 373 | realloc(loadable_classes, 374 | loadable_classes_allocated * 375 | sizeof(struct loadable_class)); 376 | } 377 | // 将 Class 指针和方法指针记录 378 | loadable_classes[loadable_classes_used].cls = cls; 379 | loadable_classes[loadable_classes_used].method = method; 380 | // 游标自加偏移 381 | loadable_classes_used++; 382 | } 383 | ``` 384 | 385 | 在记录过程中,可以看到其 Class 指针和方法指针的记录手段是通过构造 `loadable_classes` 这个类型的数组进行静态线性表记录。这个类型的数组其数据结构定义如下: 386 | 387 | ```c 388 | typedef struct objc_class *Class; 389 | struct loadable_class { 390 | Class cls; 391 | IMP method; 392 | }; 393 | ``` 394 | 395 | 其 `objc_class` 结构笔者在*[用 isa 承载对象的类信息](http://www.desgard.com/isa/)*一文中有较为详细的介绍,这是对于 Class 的抽象。从此看出,全局 Class 存储线性表结构,内部记录的信息只有 Class 指针和方法指针,这已经足够了。 396 | 397 | load 方法的调用情况至此已经全部清晰。思路梳理如下三大流程: 398 | 399 | * Load Images: 通过 `dyld` 载入 image 文件,引入 Class。 400 | * Prepare Load Methods: 准备 load 方法。过滤无效类、无效方法,将 load 方法指针和所属 Class 指针收集至全局 Class 存储线性表 `loadable_classes` 中,其中会涉及到自动扩展空间和父类优先的递归调用问题。 401 | * Call Load Methods: 根据收集到的函数指针,对 load 方法进行动态调用。进一步过滤无效方法,并记录 log 日志。 402 | 403 | 404 | ## Load 方法作用 405 | 406 | `load` 方法是我们在开发中最接近 app 启动的可控方法。即在 app 启动以后,入口函数 `main` 之前。 407 | 408 | 由于调用有着 *non-lazy* 属性,并且在运行期只调用一次,于是我们可以使用 `load` 独有的特性和调用时机来尝试 Method Swizzling。当然因为 load 调用时机过早,并且当多个 Class 没有关联(继承与派生),我们无法知道 Class 中 load 方法的优先调用关系,所以一般不会在 load 方法中引入其他的类,这是在开发当中需要注意的。 409 | 410 | 411 | ## 参考文献 412 | 413 | [你真的了解 Objective-C 中的load 方法么?](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/%E4%BD%A0%E7%9C%9F%E7%9A%84%E4%BA%86%E8%A7%A3%20load%20%E6%96%B9%E6%B3%95%E4%B9%88%EF%BC%9F.md) 414 | 415 | [NSObject +load and +initialize - What do they do?](http://stackoverflow.com/questions/13326435/nsobject-load-and-initialize-what-do-they-do) 416 | 417 | [Objective-C Class Loading and Initialization](https://www.mikeash.com/pyblog/friday-qa-2009-05-22-objective-c-class-loading-and-initialization.html) 418 | 419 | [+load VS +initialize](https://medium.com/@kostiakoval/load-vs-initialize-a1b3dc7ad6eb#.5fhb7mfip) 420 | 421 | [Objective-C +load vs +initialize](http://blog.leichunfeng.com/blog/2015/05/02/objective-c-plus-load-vs-plus-initialize/) 422 | 423 | [Objective-C: What is a lazy class?](http://stackoverflow.com/questions/15315668/objective-c-what-is-a-lazy-class) 424 | 425 | 426 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 427 | -------------------------------------------------------------------------------- /Objective-C/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](https://desgard.com/2016/08/07/objc_msgSend1/) 4 | 5 | # 对象方法消息传递流程 6 | 7 | 在*Effective Objective-C 2.0 - 52 Specific Ways to Improve Your iOS and OS X Programs*一书中,*tip 11*主要讲述了Objective-C中的消息传递机制。这也是Objective-C在C的基础上,做的最基础也是最重要的封装。 8 | 9 | ## Static Binding And Dynamic Binding 10 | 11 | C中的函数调用方式,是使用的静态绑定(static binding),即**在编译期就能决定运行时所应调用的函数**。而在Objective-C中,如果向某对象传递消息,就会使用动态绑定机制来决定需要调用的方法。而对于Objective-C的底层实现,都是C的函数。对象在收到消息之后,调用了哪些方法,完全取决于Runtime来决定,甚至可以在Runtime期间改变。 12 | 13 | 一般地,对对象发送消息,我们使用这种写法: 14 | 15 | ```c 16 | id returnValue = [DGObject test]; 17 | ``` 18 | 19 | 其中`someObject`为接收者(receiver),`messageName`为选择子(selector)。当Compiler看的这条语句时,会将其转换成为一条标准的消息传递的C函数,`objc_msgSend`,形如: 20 | 21 | ```c 22 | void objc_msgSend(id self, SEL cmd, ...) 23 | ``` 24 | 25 | 其中,`SEL`也就是之前对应的选择子,即为此文讨论的重点。我们对应的写出之前代码在Compiler处理后的C语句: 26 | 27 | ```c 28 | id returnValue = objc_msgSend(DGObject, @selector(test)); 29 | ``` 30 | 31 | ## @selector() 32 | 33 | 对于`SEL`类型,也就是我们经常使用的`@selector()`,在很多的书籍资料中的定义是这样: 34 | 35 | ```c 36 | typedef struct objc_selector *SEL; 37 | ``` 38 | 39 | 而至于这个`objc_selector`的结构体是如何定义的,这就要取决于我们Runtime框架的类型,在iOS开发中,我们使用的是Apple的``(GNU也有Runtime的framework)。在OS X中`SEL`被映射成为一个C字符串(char[]),这个字符串也就是方法名。 40 | 41 | 我们在lldb中,进行测试: 42 | 43 | ![img_1.jpg](http://upload-images.jianshu.io/upload_images/208988-ad7ee751bf89c344.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 44 | 45 | 46 | (图释:`test`是在`DGObject`Class中已经定义的方法名,而`not_define_test`和`not_define_test_2`没有定义) 47 | 48 | 第一行我们验证了`@selector`是一个char[]类型。其他的结果我们可以总结出:`@selector()`选择子**只与函数名有关**。而且还有一个规律,那就是倘若选择子方法已经在编译期由Compiler进行静态绑定,则其存储的地址就会更加的具体。 49 | 50 | 发送消息所依托的选择子只与函数名有关,我们便可以猜想到为什么Objective-C中没有像C++、C#那样的**[函数重载](https://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD)**特性,因为**选择子并不由参数和函数名共同决定**。 51 | 52 | 那么为什么要有这个选择子呢?在*[从源代码看 ObjC 中消息的发送](http://draveness.me/message/)*一文中,作者*Draveness*对其原因进行了推断: 53 | 54 | > 1. Objective-C 为我们维护了一个巨大的选择子表 55 | 2. 在使用 `@selector()` 时会从这个选择子表中根据选择子的名字查找对应的 `SEL`。如果没有找到,则会生成一个 `SEL` 并添加到表中 56 | 3. 在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 `@selector()` 生成的选择子加入到选择子表中 57 | 58 | ## objc_msgSend 59 | 60 | 在选择子拿到对应的地址后,`objc_msgSend`会依据接收者与选择子的类型来调用适当方法。为了学习此过程,我从[opensource.apple.com的git仓库中](git@github.com:RetVal/objc-runtime.git)clone了Runtime源码,并在`x86_64`架构下macOS环境进行运行。 61 | 62 | 另外,我在整个工程中增加了一个Class: 63 | 64 | ```c 65 | // DGObject.h 66 | @interface DGObject : NSObject 67 | - (void)test; 68 | @end 69 | 70 | // DGObject.m 71 | #import "DGObject.h" 72 | @implementation DGObject 73 | 74 | - (void)test { 75 | printf("Hello World. "); 76 | } 77 | @end 78 | ``` 79 | 80 | 并在main入口函数中进行改动: 81 | 82 | ```c 83 | int main(int argc, const char * argv[]) { 84 | @autoreleasepool { 85 | DGObject *obj = [[DGObject alloc]init]; 86 | NSLog(@"%p", @selector(test)); 87 | [obj test]; 88 | } 89 | return 0; 90 | } 91 | ``` 92 | 93 | 然后我们在`objc-runtime-new.mm`中,进行debug。为了研究清楚Runtime是如何查询到调用函数,我们在`lookUpImpOrForward`下断点。当程序执行`[obj test]`后,我们发现到达断点位置,并且观察此时的调用栈情况: 94 | 95 | 96 | 97 | ![img_2.jpg](http://upload-images.jianshu.io/upload_images/208988-16fcf54446702ed9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 98 | 99 | 100 | `objc_msgSend`并不是直接调用查询方法,而是先调用了`_class_lookupMethodAndLoadCache3`这个函数。看下它的源码: 101 | 102 | ```c 103 | IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls){ 104 | return lookUpImpOrForward(cls, sel, obj, 105 | YES/*initialize*/, NO/*cache*/, YES/*resolver*/); 106 | } 107 | ``` 108 | 109 | `_class_lookupMethodAndLoadCache3`就好像一个中转函数,并给出了在查询IMP指针前默认参量的几个布尔值。而由于我们的方法没有进行方法转发,则直接调用了`_class_lookupMethodAndLoadCache3`这个函数。而当对象在收到无法解读的消息之后,即启动消息转发机制,这时候应该会进入`lookUpImpOrNil`这个方法。这也是objc_msgSend的一种优化方式。 110 | 111 | 这里还要注意一点,就是关于Cache的默认参数是`NO`,因为在objc_msgSend中已经进行过缓存查询。以下是objc_msgSend的汇编实现: 112 | 113 | ```c 114 | ENTRY _objc_msgSend 115 | MESSENGER_START 116 | // NilTest:宏,判断被发送消息的对象是否为nil。 117 | // 如果为nil直接返回。 118 | NilTest NORMAL 119 | 120 | // GetIsaFast快速获取isa指针地址,并放入r11寄存器 121 | GetIsaFast NORMAL // r11 = self->isa 122 | 123 | // 查找类缓存中selector的IMP指针,并放到r10寄存器 124 | // 如果不存在,则在class的方法list中查询 125 | CacheLookup NORMAL // calls IMP on success 126 | // NilTest的许可量以及GetIsaFast的许可量 127 | NilTestSupport NORMAL 128 | GetIsaSupport NORMAL 129 | 130 | // cache miss: go search the method lists 131 | LCacheMiss: 132 | // isa still in r11 133 | // MethodTableLoopup这个宏是__class_lookupMethodAndLoadCache3函数的入口 134 | // 调用条件是在缓存中没有查询到方法对应IMP 135 | MethodTableLookup %a1, %a2 // r11 = IMP 136 | cmp %r11, %r11 // set eq (nonstret) for forwarding 137 | jmp *%r11 // goto *imp 138 | 139 | END_ENTRY _objc_msgSend 140 | ``` 141 | 142 | 趁热打铁,再来看一下*MethodTableLoopup*这个宏的实现: 143 | 144 | ```c 145 | .macro MethodTableLookup 146 | 147 | MESSENGER_END_SLOW 148 | 149 | SaveRegisters 150 | 151 | // _class_lookupMethodAndLoadCache3(receiver, selector, class) 152 | // 从a1, a2, a3中分别拿到对应参数 153 | movq $0, %a1 154 | movq $1, %a2 155 | movq %r11, %a3 156 | // 调用__class_lookupMethodAndLoadCache3 157 | call __class_lookupMethodAndLoadCache3 158 | 159 | // IMP is now in %rax 160 | // 将IMP从r11挪至rax 161 | movq %rax, %r11 162 | 163 | RestoreRegisters 164 | 165 | .endmacro 166 | ``` 167 | 168 | 而在`objc-msg-x86_64.s`中有多个以objc_msgSend为前缀的方法,这个是根据返回值类型和调用者类型分别处理的,我列举三个常用的 169 | 170 | | objc_msgSend_stret | 待发送的消息要返回结构体前提是只有当CPU的寄存器能够容纳的下消息返回类型。 | 171 | | --- | --- | 172 | | objc_msgSend_fpret | 消息返回的是浮点数。因为某些架构的CPU调用函数,需要对浮点数寄存器做特殊处理。 | 173 | | objc_msgSendSuper | 需要向superClass发送消息时调用。 | 174 | 175 | 176 | 177 | 178 | ## lookUpImpOrForward 179 | 180 | 之后我们随着调用栈往上看,在接受到消息入口的命令后,Runtime要开始进行查找方法的操作,源码如下: 181 | 182 | ```c 183 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 184 | bool initialize, bool cache, bool resolver) { 185 | Class curClass; 186 | IMP imp = nil; 187 | Method meth; 188 | bool triedResolver = NO; 189 | 190 | runtimeLock.assertUnlocked(); 191 | 192 | // 检查是否添加缓存锁,如果没有进行缓存查询。 193 | // 查到便返回IMP指针 194 | if (cache) { 195 | imp = cache_getImp(cls, sel); 196 | if (imp) return imp; 197 | } 198 | // 通过调用realizeClass方法,分配可读写`class_rw_t`的空间 199 | if (!cls->isRealized()) { 200 | rwlock_writer_t lock(runtimeLock); 201 | realizeClass(cls); 202 | } 203 | 204 | // 倘若未进行初始化,则初始化 205 | if (initialize && !cls->isInitialized()) { 206 | _class_initialize (_class_getNonMetaClass(cls, inst)); 207 | } 208 | // 保证方法查询,并进行缓存填充(cache-fill) 209 | retry: 210 | runtimeLock.read(); 211 | 212 | // 是否忽略GC垃圾回收机制(仅用在macOS中) 213 | if (ignoreSelector(sel)) { 214 | imp = _objc_ignored_method; 215 | cache_fill(cls, sel, imp, inst); 216 | goto done; 217 | } 218 | 219 | // 当前类的缓存列表中进行查找 220 | imp = cache_getImp(cls, sel); 221 | if (imp) goto done; 222 | 223 | // 从类的方法列表中进行查询 224 | meth = getMethodNoSuper_nolock(cls, sel); 225 | if (meth) { 226 | log_and_fill_cache(cls, meth->imp, sel, inst, cls); 227 | imp = meth->imp; 228 | goto done; 229 | } 230 | 231 | // 从父类中循环遍历 232 | curClass = cls; 233 | while ((curClass = curClass->superclass)) { 234 | // 父类的缓存列表中查询 235 | imp = cache_getImp(curClass, sel); 236 | if (imp) { 237 | if (imp != (IMP)_objc_msgForward_impcache) { 238 | // 如果在父类中发现方法,则填充到该类缓存列表 239 | log_and_fill_cache(cls, imp, sel, inst, curClass); 240 | goto done; 241 | } 242 | else { 243 | break; 244 | } 245 | } 246 | 247 | // 从父类的方法列表中查询 248 | meth = getMethodNoSuper_nolock(curClass, sel); 249 | if (meth) { 250 | log_and_fill_cache(cls, meth->imp, sel, inst, curClass); 251 | imp = meth->imp; 252 | goto done; 253 | } 254 | } 255 | 256 | // 进入method resolve过程 257 | if (resolver && !triedResolver) { 258 | runtimeLock.unlockRead(); 259 | // 调用_class_resolveMethod,解析没有实现的方法 260 | _class_resolveMethod(cls, sel, inst); 261 | // 进行二次尝试 262 | triedResolver = YES; 263 | goto retry; 264 | } 265 | 266 | // 没有找到方法,启动消息转发 267 | imp = (IMP)_objc_msgForward_impcache; 268 | cache_fill(cls, sel, imp, inst); 269 | done: 270 | runtimeLock.unlockRead(); 271 | return imp; 272 | } 273 | ``` 274 | 275 | 以上就是整个的查找方法流程,然后我们再对其中的一些方法逐一解读。 276 | 277 | ```c 278 | 279 | static method_t *getMethodNoSuper_nolock(Class cls, SEL sel) { 280 | runtimeLock.assertLocked(); 281 | // 遍历所在类的methods,这里的methods是List链式类型,里面存放的都是指针 282 | for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) { 283 | method_t *m = search_method_list(*mlists, sel); 284 | if (m) return m; 285 | } 286 | 287 | return nil; 288 | } 289 | ``` 290 | 291 | 这里的对于 class 存储方式,我在以后的博文中会分析其存储结构。 292 | 293 | 而对于没有实现方法的解析过程中,会有以下过程: 294 | 295 | ```c 296 | void _class_resolveMethod(Class cls, SEL sel, id inst) { 297 | if (! cls->isMetaClass()) { 298 | // try [cls resolveInstanceMethod:sel] 299 | // 针对于对象方法的操作 300 | // 这个方法是动态方法解析中,当收到无法解读的消息后调用。 301 | // 这个方法也会用在@dynamic,以后会在消息转发机制的源码分析中介绍 302 | _class_resolveInstanceMethod(cls, sel, inst); 303 | } 304 | else { 305 | // try [nonMetaClass resolveClassMethod:sel] 306 | // and [cls resolveInstanceMethod:sel] 307 | // 针对于类方法的操作,说明同上 308 | _class_resolveClassMethod(cls, sel, inst); 309 | // 再次启动查询,并且判断是否拥有缓存中消息标记_objc_msgForward_impcache 310 | if (!lookUpImpOrNil(cls, sel, inst, 311 | NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { 312 | // 说明可能不是 metaclass 的方法实现,当做对象方法尝试 313 | _class_resolveInstanceMethod(cls, sel, inst); 314 | } 315 | } 316 | } 317 | ``` 318 | 319 | 来单步调试一下程序,由于我们的test方法属于正常的类方法,所以会进入正常地查询类方法列表中查到,进入done函数块,返回到objc_msgSend方法,最终会到我们的函数调用位置: 320 | 321 | 322 | ![img_3.jpg](http://upload-images.jianshu.io/upload_images/208988-4b516206878c802f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 323 | 324 | 325 | 326 | ## IMP in Method List Flow 327 | 328 | 来简单总结一下在第一次调用某个对象方法的消息传递流程:当代码执行到某个对象(第一次)调用某个方法后,首先会确定这个方法的接收者和选择子,并组装成C的objc_msgSend函数形式,启动消息传递机制。 329 | 330 | objc_msgSend函数是使用汇编语言实现的,其中我们先尝试的从缓存表中(也就是常说的快速映射表)查询缓存,倘若查询失败,则会将具体的类对象、选择子、接收者在指定的内存单元中存储,并调用`__class_lookupMethodAndLoadCache3`函数。`__class_lookupMethodAndLoadCache3`我们俗称为**在方法列表中查询的入口函数**,他会直接调用`lookUpImpOrForward`进行查询方法对应的IMP指针。由于我们是方法函数,在获取方法列表后,即可查询到IMP指针。由于是第一次调用,则会把我们的方法加入缓存,并goto到done代码块,返回IMP指针。当objc_msgSend接收到IMP指针后存储至`rax`寄存器,返回调用函数位置,完成整个消息传递流程。 331 | 332 | 333 | ![img_4.jpg](http://upload-images.jianshu.io/upload_images/208988-2923522e2c65114f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 334 | 335 | 336 | ## 写在最后 337 | 338 | 其实消息传递及转发流程是一个相对来说比较复杂的机制。本文所讲述的流程是我们最常见的一种形式。在之后的消息传递与转发的博文中,还会更加深入的探讨这一机制相关流程并深入的阅读源码。 339 | 340 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 341 | 342 | -------------------------------------------------------------------------------- /Objective-C/Runtime/objc_msgSend消息传递学习笔记 - 消息转发.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](https://desgard.com/objc_msgSend2/) 4 | 5 | 6 | # 消息转发 7 | 8 | 该文是 *[objc_msgSend消息传递学习笔记 - 对象方法消息传递流程]()* 的基础上继续探究源码,请先阅读上文。 9 | 10 | ## 消息转发机制(message forwarding) 11 | 12 | Objective-C 在调用对象方法的时候,是通过消息传递机制来查询且执行方法。如果想令该类能够理解并执行方法,必须以程序代码实现出对应方法。但是,在编译期间向类发送了无法解读的消息并不会报错,因为在 runtime 时期可以继续向类添加方法,所以编译器在编译时还无法确认类中是否已经实现了消息方法。 13 | 14 | 当对象接受到无法解读的消息后,就会启动**消息转发**机制,并且我们可以由此过程告诉对象应该如何处理位置消息。 15 | 16 | 本文的研究目标:当 Class 对象的 `.h` 文件中声明了成员方法,但是没有对其进行实现,来跟踪一下 runtime 的消息转发过程。于是创造一下实验场景: 17 | 18 | > 同上一篇文章一样,定义一个自定义 Class `DGObject` ,并且声明改 Class 中拥有方法 `- (void)test_no_exist` ,而在 `.m` 文件中不给予实现。在 `main.m` 入口中直接调用该类对象的 `- (void)test_no_exist` 方法。 19 | 20 | ![](http://7xwh85.com1.z0.glb.clouddn.com/14722248187304.jpg) 21 | 22 | ## 动态方法解析 23 | 24 | 依旧在 lookUpImpOrForward 方法中下断点,并单步调试,观察代码走向。由于方法在方法列表中无法找到,所以立即进入 method resolve 过程。 25 | 26 | ```c 27 | // 进入method resolve过程 28 | if (resolver && !triedResolver) { 29 | // 释放读入锁 30 | runtimeLock.unlockRead(); 31 | // 调用_class_resolveMethod,解析没有实现的方法 32 | _class_resolveMethod(cls, sel, inst); 33 | // 进行二次尝试 34 | triedResolver = YES; 35 | goto retry; 36 | } 37 | ``` 38 | 39 | `runtimeLock.unlockRead()` 是释放读入锁操作,这里是指缓存读入,即缓存机制不工作从而不会有缓存结果。随后进入 `_class_resolveMethod(cls, sel, inst)` 方法。 40 | 41 | ```c 42 | void _class_resolveMethod(Class cls, SEL sel, id inst) { 43 | // 用 isa 查看是否指向元类 Meta Class 44 | if (! cls->isMetaClass()) { 45 | // try [cls resolveInstanceMethod:sel] 46 | _class_resolveInstanceMethod(cls, sel, inst); 47 | } 48 | else { 49 | // try [nonMetaClass resolveClassMethod:sel] 50 | // and [cls resolveInstanceMethod:sel] 51 | _class_resolveClassMethod(cls, sel, inst); 52 | if (!lookUpImpOrNil(cls, sel, inst, 53 | NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 54 | { 55 | _class_resolveInstanceMethod(cls, sel, inst); 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | 此方法是动态方法解析的入口,会间接地发送 `+resolveInstanceMethod` 或 `+resolveClassMethod` 消息。通过对 isa 指向的判断,从而分辨出如果是对象方法,则进入 `+resolveInstanceMethod` 方法,如果是类方法,则进入 `+resolveClassMethod` 方法。 62 | 63 | 而上述代码中的 `_class_resolveInstanceMethod` 方法,我们从源码中看到是如此定义的: 64 | 65 | ```c 66 | static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) { 67 | // 首先查找是否有 resolveInstanceMethod 方法 68 | if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 69 | NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 70 | { 71 | // Resolver not implemented. 72 | return; 73 | } 74 | // 构造布尔类型变量表达式,动态绑定函数 75 | BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend; 76 | // 获得是否重新传递消息标记 77 | bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); 78 | 79 | // Cache the result (good or bad) so the resolver doesn't fire next time. 80 | // +resolveInstanceMethod adds to self a.k.a. cls 81 | // 调用 lookUpImpOrNil 并重新启动缓存,查看是否已经添加上了选择子对应的 IMP 82 | 指针 83 | IMP imp = lookUpImpOrNil(cls, sel, inst, 84 | NO/*initialize*/, YES/*cache*/, NO/*resolver*/); 85 | // 对查询到的 IMP 进行 log 输出 86 | if (resolved && PrintResolving) { 87 | if (imp) { 88 | _objc_inform("RESOLVE: method %c[%s %s] " 89 | "dynamically resolved to %p", 90 | cls->isMetaClass() ? '+' : '-', 91 | cls->nameForLogging(), sel_getName(sel), imp); 92 | } 93 | else { 94 | // Method resolver didn't add anything? 95 | _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES" 96 | ", but no new implementation of %c[%s %s] was found", 97 | cls->nameForLogging(), sel_getName(sel), 98 | cls->isMetaClass() ? '+' : '-', 99 | cls->nameForLogging(), sel_getName(sel)); 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | 通过 `_class_resolveInstanceMethod` 可以了解到,这只是通过 `+resolveInstanceMethod` 来查询是否开发者已经在运行时将其动态插入类中的实现函数。并且重新触发 `objc_msgSend` 方法。这里有一个 C 的语法值得我们去延伸学习一下,就是关于关键字 `__typeof__` 的。`__typeof__(var)` 是 GCC 对 C 的一个扩展保留字([官方文档](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html)),这里是用来描述一个指针的类型。 106 | 107 | 我们发现,最终都会返回到 `objc_msgSend` 中。反观一下上一篇文章写的 `objc_msgSend` 函数,是通过汇编语言实现的。在 *[Let's build objc_msgsend](https://www.mikeash.com/pyblog/friday-qa-2012-11-16-lets-build-objc_msgsend.html)* 这篇资料中,记录了一个关于 `objc_msgSend` 的伪代码。 108 | 109 | ```c 110 | id objc_msgSend(id self, SEL _cmd, ...) { 111 | Class c = object_getClass(self); 112 | IMP imp = cache_lookup(c, _cmd); 113 | if(!imp) 114 | imp = class_getMethodImplementation(c, _cmd); 115 | return imp(self, _cmd, ...); 116 | } 117 | ``` 118 | 119 | 在缓存中无法直接击中 IMP 时,会调用 `class_getMethodImplementation` 方法。在 runtime 中,查看一下 `class_getMethodImplementation` 方法。 120 | 121 | ```c 122 | IMP class_getMethodImplementation(Class cls, SEL sel) 123 | { 124 | IMP imp; 125 | 126 | if (!cls || !sel) return nil; 127 | // 上一篇文章的搜索入口 128 | imp = lookUpImpOrNil(cls, sel, nil, 129 | YES/*initialize*/, YES/*cache*/, YES/*resolver*/); 130 | 131 | // Translate forwarding function to C-callable external version 132 | if (!imp) { 133 | return _objc_msgForward; 134 | } 135 | 136 | return imp; 137 | } 138 | ``` 139 | 140 | 在上一篇文中,详细介绍过了 `lookUpImpOrNil` 函数成功搜索的流程。而本例中与前相反,我们我发现该函数返回了一个 `_objc_msgForward` 的 IMP。此时,我们击中的函数是 `_objc_msgForward` 这个 IMP ,于是消息转发机制进入了**备援接收**流程。 141 | 142 | ## Forwarding 备援接收 143 | 144 | `_objc_msgForward` 居然可以返回,说同 IMP 一样是一个指针。在 `objc-msg-x86_64.s` 中发现了其汇编实现。 145 | 146 | ```c 147 | ENTRY __objc_msgForward 148 | // Non-stret version 149 | // 调用 __objc_forward_handler 150 | movq __objc_forward_handler(%rip), %r11 151 | jmp *%r11 152 | 153 | END_ENTRY __objc_msgForward 154 | ``` 155 | 156 | 发现在接收到 `_objc_msgForward` 指针后,会立即进入 `__objc_forward_handler` 方法。其源码在 `objc-runtime.mm` 中。 157 | 158 | ```c 159 | #if !__OBJC2__ 160 | 161 | // Default forward handler (nil) goes to forward:: dispatch. 162 | void *_objc_forward_handler = nil; 163 | void *_objc_forward_stret_handler = nil; 164 | 165 | #else 166 | 167 | // Default forward handler halts the process. 168 | __attribute__((noreturn)) void 169 | objc_defaultForwardHandler(id self, SEL sel) { 170 | _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p " 171 | "(no message forward handler is installed)", 172 | class_isMetaClass(object_getClass(self)) ? '+' : '-', 173 | object_getClassName(self), sel_getName(sel), self); 174 | } 175 | void *_objc_forward_handler = (void*)objc_defaultForwardHandler; 176 | ``` 177 | 178 | 在 ObjC 2.0 以前,`_objc_forward_handler` 是 nil ,而在最新的 runtime 中,其实现由 `objc_defaultForwardHandler` 完成。其源码仅仅是在 log 中记录一些相关信息,这也是 handler 的主要功能。 179 | 180 | 而抛开 runtime ,看见了关键字 `__attribute__((noreturn))` 。这里简单介绍一下 GCC 中的又一扩展 **__attribute__机制** 。它用于与编译器直接交互,这是一个编译器指令(Compiler Directive),用来在函数或数据声明中设置属性,从而进一步进行优化(继续了解可以阅读 *[NShipster \__attribute__](http://nshipster.com/__attribute__/)*)。而这里的 `__attribute__((noreturn))` 是告诉编译器此函数不会返回给调用者,以便编译器在优化时去掉不必要的函数返回代码。 181 | 182 | Handler 的全部工作是记录日志、触发 crash 机制。如果开发者想实现细节转发,则需要重写 `_objc_forward_handler` 中的实现。这时引入 `objc_setForwardHandler` 方法: 183 | 184 | ```c 185 | void objc_setForwardHandler(void *fwd, void *fwd_stret) { 186 | _objc_forward_handler = fwd; 187 | #if SUPPORT_STRET 188 | _objc_forward_stret_handler = fwd_stret; 189 | #endif 190 | } 191 | ``` 192 | 193 | 这是一个十分简单的动态绑定过程,让方法指针指向传入参数指针得以实现。 194 | 195 | ## Core Foundation 衔接 196 | 197 | 引入 `objc_setForwardHandler` 方法后,会有一个疑问:如何调用它?先来看一段异常信息: 198 | 199 | ```bash 200 | 2016-08-27 08:26:08.264 debug-objc[7013:29381250] -[DGObject test_no_exist]: unrecognized selector sent to instance 0x101200310 201 | 2016-08-27 10:09:16.495 debug-objc[7013:29381250] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DGObject test_no_exist]: unrecognized selector sent to instance 0x101200310' 202 | *** First throw call stack: 203 | ( 204 | 0 CoreFoundation 0x00007fff842c64f2 __exceptionPreprocess + 178 205 | 1 libobjc.A.dylib 0x000000010002989f objc_exception_throw + 47 206 | 2 CoreFoundation 0x00007fff843301ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205 207 | 3 CoreFoundation 0x00007fff84236571 ___forwarding___ + 1009 208 | 4 CoreFoundation 0x00007fff842360f8 _CF_forwarding_prep_0 + 120 209 | 5 debug-objc 0x0000000100000e9e main + 94 210 | 6 libdyld.dylib 0x00007fff852a95ad start + 1 211 | 7 ??? 0x0000000000000001 0x0 + 1 212 | ) 213 | libc++abi.dylib: terminating with uncaught exception of type NSException 214 | ``` 215 | 216 | 这个日志场景都接触过。从调用栈上,发现了最终是通过 Core Foundation 抛出异常。在 Core Foundation 的 [CFRuntime.c](https://github.com/opensource-apple/CF/blob/master/CFRuntime.c) 无法找到 `objc_setForwardHandler` 方法的调用入口。综合参看 *[Objective-C 消息发送与转发机制原理](http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/)* 和 *[Hmmm, What's that Selector?](http://arigrant.com/blog/2013/12/13/a-selector-left-unhandled)* 两篇文章,我们发现了在 [CFRuntime.c](https://github.com/opensource-apple/CF/blob/master/CFRuntime.c) 的 `__CFInitialize()` 方法中,实际上是调用了 `objc_setForwardHandler` ,这段代码被苹果公司隐藏。 217 | 218 | 在上述调用栈中,发现了在 Core Foundation 中会调用 `___forwarding___` 。根据资料也可以了解到,在 `objc_setForwardHandler` 时会传入 `__CF_forwarding_prep_0` 和 `___forwarding_prep_1___` 两个参数,而这两个指针都会调用 `____forwarding___` 。这个函数中,也交代了消息转发的逻辑。在 *[Hmmm, What's that Selector?](http://arigrant.com/blog/2013/12/13/a-selector-left-unhandled)* 文章中,复原了 `____forwarding___` 的实现。 219 | 220 | ```c 221 | // 两个参数:前者为被转发消息的栈指针 IMP ,后者为是否返回结构体 222 | int __forwarding__(void *frameStackPointer, int isStret) { 223 | id receiver = *(id *)frameStackPointer; 224 | SEL sel = *(SEL *)(frameStackPointer + 8); 225 | const char *selName = sel_getName(sel); 226 | Class receiverClass = object_getClass(receiver); 227 | 228 | // 调用 forwardingTargetForSelector: 229 | // 进入 备援接收 主要步骤 230 | if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) { 231 | // 获得方法签名 232 | id forwardingTarget = [receiver forwardingTargetForSelector:sel]; 233 | // 判断返回类型是否正确 234 | if (forwardingTarget && forwarding != receiver) { 235 | // 判断类型,是否返回值为结构体,选用不同的转发方法 236 | if (isStret == 1) { 237 | int ret; 238 | objc_msgSend_stret(&ret,forwardingTarget, sel, ...); 239 | return ret; 240 | } 241 | return objc_msgSend(forwardingTarget, sel, ...); 242 | } 243 | } 244 | 245 | // 僵尸对象 246 | const char *className = class_getName(receiverClass); 247 | const char *zombiePrefix = "_NSZombie_"; 248 | size_t prefixLen = strlen(zombiePrefix); // 0xa 249 | if (strncmp(className, zombiePrefix, prefixLen) == 0) { 250 | CFLog(kCFLogLevelError, 251 | @"*** -[%s %s]: message sent to deallocated instance %p", 252 | className + prefixLen, 253 | selName, 254 | receiver); 255 | 256 | } 257 | 258 | // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation 259 | // 进入消息转发系统 260 | if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) { 261 | NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel]; 262 | // 判断返回类型是否正确 263 | if (methodSignature) { 264 | BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct; 265 | if (signatureIsStret != isStret) { 266 | CFLog(kCFLogLevelWarning , 267 | @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", 268 | selName, 269 | signatureIsStret ? "" : not, 270 | isStret ? "" : not); 271 | } 272 | if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) { 273 | // 传入消息的全部细节信息 274 | NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; 275 | 276 | [receiver forwardInvocation:invocation]; 277 | 278 | void *returnValue = NULL; 279 | [invocation getReturnValue:&value]; 280 | return returnValue; 281 | } else { 282 | CFLog(kCFLogLevelWarning , 283 | @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message", 284 | receiver, 285 | className); 286 | return 0; 287 | } 288 | } 289 | } 290 | 291 | SEL *registeredSel = sel_getUid(selName); 292 | 293 | // selector 是否已经在 Runtime 注册过 294 | if (sel != registeredSel) { 295 | CFLog(kCFLogLevelWarning , 296 | @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", 297 | sel, 298 | selName, 299 | registeredSel); 300 | } 301 | // doesNotRecognizeSelector,主动抛出异常 302 | // 也就是前文我们看到的 303 | // 表明选择子未能得到处理 304 | else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) { 305 | [receiver doesNotRecognizeSelector:sel]; 306 | } 307 | else { 308 | CFLog(kCFLogLevelWarning , 309 | @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort", 310 | receiver, 311 | className); 312 | } 313 | 314 | // The point of no return. 315 | kill(getpid(), 9); 316 | } 317 | ``` 318 | 319 | ## Message-Dispatch System 消息派发系统 320 | 321 | 在大概了解过 Message-Dispatch System 的源码后,来简单的说明一下。由于在前两步中,我们无法找到那条消息的实现。则创建一个 NSInvocation 对象,并将消息全部属性记录下来。 NSInvocation 对象包括了选择子、target 以及其他参数。 322 | 323 | 随后,调用 `forwardInvocation:(NSInvocation *)invocation` 方法,其中的实现仅仅是改变了 target 指向,使消息保证能够调用。倘若发现本类无法处理,则继续想父类进行查找。直至 NSObject ,如果找到根类仍旧无法找到,则会调用 `doesNotRecognizeSelector:` ,以抛出异常。此异常表明选择子最终未能得到处理。 324 | 325 | 而对于 `doesNotRecognizeSelector:` 内部是如何实现,如何捕获异常。或者说 override 改方法后做自定义处理,等笔者实践后继续记录学习笔记。 326 | 327 | ## 对于消息转发的总结梳理 328 | 329 | 在 Core Foundation 的消息派发流程中,由于源码被隐藏,所以笔者无法亲自测试代码。倘若以后学习了逆向,可以再去探讨一下这里面发生的过程。 330 | 331 | 对于这篇文章记录的消息转发流程,大致如下图所示: 332 | 333 | 334 | ![Desktop](http://7xwh85.com1.z0.glb.clouddn.com/Desktop.png) 335 | 336 | 337 | 以上是对于 objc_msgSend 消息转发的源码学习笔记,请多指正。 338 | 339 | --- 340 | 341 | ## 参考资料 342 | 343 | [Let's Build objc_msgSend](https://www.mikeash.com/pyblog/friday-qa-2012-11-16-lets-build-objc_msgsend.html) 344 | 345 | [Hmmm, What's that Selector?](http://arigrant.com/blog/2013/12/13/a-selector-left-unhandled) 346 | 347 | [NShipster \__attribute__](http://nshipster.com/__attribute__/) 348 | 349 | [Objective-C 消息发送与转发机制原理](http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/) 350 | 351 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 352 | 353 | 354 | 355 | -------------------------------------------------------------------------------- /Objective-C/Runtime/weak 弱引用的实现方式.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](https://desgard.com/weak) 4 | 5 | 6 | # weak 弱引用的实现方式 7 | 8 | 对于 runtime 的分析还有很长的路,最近在写 block 系列的同时,也回顾一下之前疏漏的细节知识。这篇文章是关于 weak 的具体实现以及其生命周期的学习笔记。 9 | 10 | ## runtime 对 __weak 弱引用处理方式 11 | 12 | 切入主题,这里笔者使用的 runtime 版本为 [objc4-680.tar.gz](http://opensource.apple.com/tarballs/objc4/)。 13 | 我在入口文件 main.m 中加入如下代码: 14 | 15 | ```c 16 | int main(int argc, const char * argv[]) { 17 | @autoreleasepool { 18 | // insert code here... 19 | NSObject *p = [[NSObject alloc] init]; 20 | __weak NSObject *p1 = p; 21 | } 22 | return 0; 23 | } 24 | ``` 25 | 26 | 单步运行,发现会跳入 `NSObject.mm` 中的 `objc_initWeak()` 这个方法。在进行编译过程前,clang 其实对 __weak 做了转换,将声明方式做出了如下调整。 27 | 28 | ```c 29 | NSObject objc_initWeak(&p, 对象指针); 30 | ``` 31 | 32 | 其中的对象指针,就是代码中的 `[[NSObject alloc] init]` ,而 p 是我们传入的一个弱引用指针。而对于 `objc_initWeak()` 方法的实现,在 runtime 中的源码如下: 33 | 34 | ```c 35 | id objc_initWeak(id *location, id newObj) { 36 | // 查看对象实例是否有效 37 | // 无效对象直接导致指针释放 38 | if (!newObj) { 39 | *location = nil; 40 | return nil; 41 | } 42 | 43 | // 这里传递了三个 bool 数值 44 | // 使用 template 进行常量参数传递是为了优化性能 45 | return storeWeak 46 | (location, (objc_object*)newObj); 47 | } 48 | ``` 49 | 50 | 可以看出,这个函数仅仅是一个深层函数的调用入口,而一般的入口函数中,都会做一些简单的判断(例如 objc_msgSend 中的缓存判断),这里判断了其指针指向的类对象是否有效,无效直接释放,不再往深层调用函数。 51 | 52 | 需要注意的是,当修改弱引用的变量时,这个方法非线程安全。所以切记选择竞争带来的一些问题。 53 | 54 | 继续阅读 `objc_storeWeak()` 的实现: 55 | 56 | ```c 57 | // HaveOld: true - 变量有值 58 | // false - 需要被及时清理,当前值可能为 nil 59 | // HaveNew: true - 需要被分配的新值,当前值可能为 nil 60 | // false - 不需要分配新值 61 | // CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停 62 | // false - 用 nil 替代存储 63 | template 64 | static id storeWeak(id *location, objc_object *newObj) { 65 | // 该过程用来更新弱引用指针的指向 66 | 67 | // 初始化 previouslyInitializedClass 指针 68 | Class previouslyInitializedClass = nil; 69 | id oldObj; 70 | 71 | // 声明两个 SideTable 72 | // ① 新旧散列创建 73 | SideTable *oldTable; 74 | SideTable *newTable; 75 | 76 | // 获得新值和旧值的锁存位置(用地址作为唯一标示) 77 | // 通过地址来建立索引标志,防止桶重复 78 | // 下面指向的操作会改变旧值 79 | retry: 80 | if (HaveOld) { 81 | // 更改指针,获得以 oldObj 为索引所存储的值地址 82 | oldObj = *location; 83 | oldTable = &SideTables()[oldObj]; 84 | } else { 85 | oldTable = nil; 86 | } 87 | if (HaveNew) { 88 | // 更改新值指针,获得以 newObj 为索引所存储的值地址 89 | newTable = &SideTables()[newObj]; 90 | } else { 91 | newTable = nil; 92 | } 93 | 94 | // 加锁操作,防止多线程中竞争冲突 95 | SideTable::lockTwo(oldTable, newTable); 96 | 97 | // 避免线程冲突重处理 98 | // location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改 99 | if (HaveOld && *location != oldObj) { 100 | SideTable::unlockTwo(oldTable, newTable); 101 | goto retry; 102 | } 103 | 104 | // 防止弱引用间死锁 105 | // 并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向 106 | if (HaveNew && newObj) { 107 | // 获得新对象的 isa 指针 108 | Class cls = newObj->getIsa(); 109 | 110 | // 判断 isa 非空且已经初始化 111 | if (cls != previouslyInitializedClass && 112 | !((objc_class *)cls)->isInitialized()) { 113 | // 解锁 114 | SideTable::unlockTwo(oldTable, newTable); 115 | // 对其 isa 指针进行初始化 116 | _class_initialize(_class_getNonMetaClass(cls, (id)newObj)); 117 | 118 | // 如果该类已经完成执行 +initialize 方法是最理想情况 119 | // 如果该类 +initialize 在线程中 120 | // 例如 +initialize 正在调用 storeWeak 方法 121 | // 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记 122 | previouslyInitializedClass = cls; 123 | 124 | // 重新尝试 125 | goto retry; 126 | } 127 | } 128 | 129 | // ② 清除旧值 130 | if (HaveOld) { 131 | weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); 132 | } 133 | 134 | // ③ 分配新值 135 | if (HaveNew) { 136 | newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, 137 | (id)newObj, location, 138 | CrashIfDeallocating); 139 | // 如果弱引用被释放 weak_register_no_lock 方法返回 nil 140 | 141 | // 在引用计数表中设置若引用标记位 142 | if (newObj && !newObj->isTaggedPointer()) { 143 | // 弱引用位初始化操作 144 | // 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用 145 | newObj->setWeaklyReferenced_nolock(); 146 | } 147 | 148 | // 之前不要设置 location 对象,这里需要更改指针指向 149 | *location = (id)newObj; 150 | } 151 | else { 152 | // 没有新值,则无需更改 153 | } 154 | 155 | SideTable::unlockTwo(oldTable, newTable); 156 | 157 | return (id)newObj; 158 | } 159 | ``` 160 | 161 | 其中标注的一些要点,开始逐一介绍: 162 | 163 | 164 | ## 引用计数和弱引用依赖表 SideTable 165 | 166 | `SideTable` 这个结构体,我给他起名**引用计数和弱引用依赖表**,因为它主要用于管理对象的引用计数和 `weak` 表。在 `NSObject.mm` 中声明其数据结构: 167 | 168 | ```c 169 | struct SideTable { 170 | // 保证原子操作的自旋锁 171 | spinlock_t slock; 172 | // 引用计数的 hash 表 173 | RefcountMap refcnts; 174 | // weak 引用全局 hash 表 175 | weak_table_t weak_table; 176 | } 177 | ``` 178 | 179 | 在之前的 runtime 版本中,有一个较为重要的成员方法,用来根据对象的地址在缓存中取出对应的 `SideTable` 实例: 180 | 181 | ```c 182 | static SideTable *tableForPointer(const void *p); 183 | ``` 184 | 185 | 而在上面 `objc_storeWeak` 方法中,取出实例的方法变成了 `&SideTables()[xxxObj];` 这种方式。查看方法的实现,发现了如下函数: 186 | 187 | ```c 188 | static StripedMap& SideTables() { 189 | return *reinterpret_cast*>(SideTableBuf); 190 | } 191 | ``` 192 | 193 | 在取出实例方法的实现中,使用了 C++ 标准转换运算符 **reinterpret_cast** ,其表达方式为: 194 | 195 | ```c 196 | reinterpret_cast (expression) 197 | ``` 198 | 199 | 用来处理无关类型之间的转换。该关键字会产生一个新值,并**保证与原参数(expression)拥有完全相同的比特位**。 200 | 201 | 而 `StripedMap` 是一个模板类(Template Class),通过传入类(结构体)参数,会动态修改在该类中的一个 `array` 成员存储的元素类型,并且其中提供了一个针对于地址的 hash 算法,用作存储 key。可以说, `StripedMap` 提供了一套拥有将地址作为 key 的 hash table 解决方案,而该方案采用了模板类,是拥有泛型性的。 202 | 203 | 介绍了与对象相关联的 SideTable 检索方式,再来看 SideTable 的成员和作用。 204 | 205 | 对于 slock 和 refcnts 两个成员不用多说,第一个是为了防止竞争选择的自旋锁,第二个是协助对象的 isa 指针的 `extra_rc` 共同引用计数的变量(对于对象结果,在今后的文中提到)。这里主要看 `weak` 全局 hash 表的结构与作用。 206 | 207 | ```c 208 | struct weak_table_t { 209 | // 保存了所有指向指定对象的 weak 指针 210 | weak_entry_t *weak_entries; 211 | // 存储空间 212 | size_t num_entries; 213 | // 参与判断引用计数辅助量 214 | uintptr_t mask; 215 | // hash key 最大偏移值 216 | uintptr_t max_hash_displacement; 217 | }; 218 | ``` 219 | 220 | 这是一个全局弱引用表。使用不定类型对象的地址作为 key ,用 weak_entry_t 类型结构体对象作为 value 。其中的 weak_entries 成员,从字面意思上看,即为弱引用表入口。其实现也是这样的。 221 | 222 | ```c 223 | typedef objc_object ** weak_referrer_t; 224 | 225 | struct weak_entry_t { 226 | DisguisedPtr referent; 227 | union { 228 | struct { 229 | weak_referrer_t *referrers; 230 | uintptr_t out_of_line : 1; 231 | uintptr_t num_refs : PTR_MINUS_1; 232 | uintptr_t mask; 233 | uintptr_t max_hash_displacement; 234 | }; 235 | struct { 236 | // out_of_line=0 is LSB of one of these (don't care which) 237 | weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; 238 | }; 239 | } 240 | ``` 241 | 242 | 在 weak_entry_t 的结构中,`DisguisedPtr referent` 是对泛型对象的指针做了一个封装,通过这个泛型类来解决内存泄漏的问题。从注释中写 `out_of_line` 成员为最低有效位,当其为0的时候, `weak_referrer_t` 成员将扩展为多行静态 hash table。其实其中的 `weak_referrer_t` 是二维 `objc_object` 的别名,通过一个二维指针地址偏移,用下标作为 hash 的 key,做成了一个弱引用散列。 243 | 244 | 那么在有效位未生效的时候,`out_of_line` 、 `num_refs`、 `mask` 、 `max_hash_displacement` 有什么作用?以下是笔者自身的猜测: 245 | 246 | * **out_of_line**:最低有效位,也是标志位。当标志位 0 时,增加引用表指针纬度。 247 | * **num_refs**:引用数值。这里记录弱引用表中引用有效数字,因为弱引用表使用的是静态 hash 结构,所以需要使用变量来记录数目。 248 | * **mask**:计数辅助量。 249 | * **max_hash_displacement**:hash 元素上限阀值。 250 | 251 | 其实 out_of_line 的值通常情况下是等于零的,所以弱引用表总是一个 objc_objective 指针二维数组。一维 objc_objective 指针可构成一张弱引用散列表,通过第三纬度实现了多张散列表,并且表数量为 WEAK_INLINE_COUNT 。 252 | 253 | 254 | 总结一下 `StripedMap[]` : `StripedMap` 是一个模板类,在这个类中有一个 array 成员,用来存储 PaddedT 对象,并且其中对于 `[]` 符的重载定义中,会返回这个 PaddedT 的 value 成员,这个 value 就是我们传入的 T 泛型成员,也就是 SideTable 对象。在 array 的下标中,这里使用了 indexForPointer 方法通过位运算计算下标,实现了静态的 Hash Table。而在 weak_table 中,其成员 weak_entry 会将传入对象的地址加以封装起来,并且其中也有访问全局弱引用表的入口。 255 | 256 | ![](http://7xwh85.com1.z0.glb.clouddn.com/sidetable.png) 257 | 258 | 259 | 260 | ## 旧对象解除注册操作 weak_unregister_no_lock 261 | 262 | ```c 263 | #define WEAK_INLINE_COUNT 4 264 | 265 | void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 266 | id *referrer_id) { 267 | // 在入口方法中,传入了 weak_table 弱引用表,referent_id 旧对象以及 referent_id 旧对象对应的地址 268 | // 用指针去访问 oldObj 和 *location 269 | objc_object *referent = (objc_object *)referent_id; 270 | objc_object **referrer = (objc_object **)referrer_id; 271 | 272 | weak_entry_t *entry; 273 | // 如果其对象为 nil,无需取消注册 274 | if (!referent) return; 275 | // weak_entry_for_referent 根据首对象查找 weak_entry 276 | if ((entry = weak_entry_for_referent(weak_table, referent))) { 277 | // 通过地址来解除引用关联 278 | remove_referrer(entry, referrer); 279 | bool empty = true; 280 | // 检测 out_of_line 位的情况 281 | // 检测 num_refs 位的情况 282 | if (entry->out_of_line && entry->num_refs != 0) { 283 | empty = false; 284 | } 285 | else { 286 | // 将引用表中记录为空 287 | for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { 288 | if (entry->inline_referrers[i]) { 289 | empty = false; 290 | break; 291 | } 292 | } 293 | } 294 | // 从弱引用的 zone 表中删除 295 | if (empty) { 296 | weak_entry_remove(weak_table, entry); 297 | } 298 | } 299 | 300 | // 这里不会设置 *referrer = nil,因为 objc_storeWeak() 函数会需要该指针 301 | } 302 | ``` 303 | 304 | 该方法主要作用是将旧对象在 weak_table 中接触 weak 指针的对应绑定。根据函数名,称之为解除注册操作。从源码中,可以知道其功能就是从 weak_table 中接触 weak 指针的绑定。而其中的遍历查询,就是针对于 weak_entry 中的多张弱引用散列表。 305 | 306 | ## 新对象添加注册操作 weak_register_no_lock 307 | 308 | ```c 309 | id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 310 | id *referrer_id, bool crashIfDeallocating) { 311 | // 在入口方法中,传入了 weak_table 弱引用表,referent_id 旧对象以及 referent_id 旧对象对应的地址 312 | // 用指针去访问 oldObj 和 *location 313 | objc_object *referent = (objc_object *)referent_id; 314 | objc_object **referrer = (objc_object **)referrer_id; 315 | 316 | // 检测对象是否生效、以及是否使用了 tagged pointer 技术 317 | if (!referent || referent->isTaggedPointer()) return referent_id; 318 | 319 | // 保证引用对象是否有效 320 | // hasCustomRR 方法检查类(包括其父类)中是否含有默认的方法 321 | bool deallocating; 322 | if (!referent->ISA()->hasCustomRR()) { 323 | // 检查 dealloc 状态 324 | deallocating = referent->rootIsDeallocating(); 325 | } 326 | else { 327 | // 会返回 referent 的 SEL_allowsWeakReference 方法的地址 328 | BOOL (*allowsWeakReference)(objc_object *, SEL) = 329 | (BOOL(*)(objc_object *, SEL)) 330 | object_getMethodImplementation((id)referent, 331 | SEL_allowsWeakReference); 332 | if ((IMP)allowsWeakReference == _objc_msgForward) { 333 | return nil; 334 | } 335 | deallocating = 336 | ! (*allowsWeakReference)(referent, SEL_allowsWeakReference); 337 | } 338 | // 由于 dealloc 导致 crash ,并输出日志 339 | if (deallocating) { 340 | if (crashIfDeallocating) { 341 | _objc_fatal("Cannot form weak reference to instance (%p) of " 342 | "class %s. It is possible that this object was " 343 | "over-released, or is in the process of deallocation.", 344 | (void*)referent, object_getClassName((id)referent)); 345 | } else { 346 | return nil; 347 | } 348 | } 349 | 350 | // 记录并存储对应引用表 weak_entry 351 | weak_entry_t *entry; 352 | // 对于给定的弱引用查询 weak_table 353 | if ((entry = weak_entry_for_referent(weak_table, referent))) { 354 | // 增加弱引用表于附加对象上 355 | append_referrer(entry, referrer); 356 | } 357 | else { 358 | // 自行创建弱引用表 359 | weak_entry_t new_entry; 360 | new_entry.referent = referent; 361 | new_entry.out_of_line = 0; 362 | new_entry.inline_referrers[0] = referrer; 363 | for (size_t i = 1; i < WEAK_INLINE_COUNT; i++) { 364 | new_entry.inline_referrers[i] = nil; 365 | } 366 | // 如果给定的弱引用表满容,进行自增长 367 | weak_grow_maybe(weak_table); 368 | // 向对象添加弱引用表关联,不进行检查直接修改指针指向 369 | weak_entry_insert(weak_table, &new_entry); 370 | } 371 | 372 | // 这里不会设置 *referrer = nil,因为 objc_storeWeak() 函数会需要该指针 373 | return referent_id; 374 | } 375 | ``` 376 | 377 | 这一步与上一步相反,通过 weak_register_no_lock 函数把心的对象进行注册操作,完成与对应的弱引用表进行绑定操作。 378 | 379 | ## 初始化弱引用对象流程一览 380 | 381 | 弱引用的初始化,从上文的分析中可以看出,主要的操作部分就在弱引用表的取键、查询散列、创建弱引用表等操作,可以总结出如下的流程图: 382 | 383 | ![weaktable-2](http://7xwh85.com1.z0.glb.clouddn.com/weaktable-2.png) 384 | 385 | 386 | 387 | 这个图中省略了很多情况的判断,但是当声明一个 `__weak` 会调用上图中的这些方法。当然, `storeWeak` 方法不仅仅用在 `__weak` 的声明中,在 class 内部的操作中也会常常通过该方法来对 weak 对象进行操作。 388 | 389 | 以上就是对于 weak 弱引用对象的初始化时 runtime 内部的执行过程,想必阅读后会对其结构有更深的理解。 390 | 391 | 392 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 393 | 394 | -------------------------------------------------------------------------------- /Objective-C/Runtime/浅谈 block(1) - clang 改写后的 block 结构.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](https://desgard.com/2016/08/11/copy/) 4 | 5 | 6 | # 浅谈 block - clang 改写后的 block 结构 7 | 8 | 这几天为了巩固知识,从 iOS 的各个知识点开始学习,希望自己对每一个知识理解的更加深入的了解。这次来分享一下 block 的学习笔记。 9 | 10 | ## block 简介 11 | 12 | block 被当做扩展特性而被加入 GCC 编译器中的。自从 OS X 10.4 和 iOS 4.0 之后,这个特性被加入了 Clang 中。因此我们今天使用的 block 在 C、C++、Objective-C 和 Objective-C++ 中均可使用。 13 | 14 | 对于 block 的语法,只放一张图即可。在之后的 block 系列文章中会详细说明其用法。 15 | 16 | 17 | 18 | ![img_1.jpg](http://upload-images.jianshu.io/upload_images/208988-0e4c759180531b28.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 19 | 20 | 21 | 22 | ## C 中的 block 23 | 24 | 说起 Xcode 的默认编译器 clang ,不得不提及 clang 在整个 编译 - 链接 过程中所起到的作用。在编译期, clang 首先对 Objective-C 代码做分析检查,确保代码中没有任何明显的错误,然后将其转换成为低级的类汇编代码,即我们经常说的**中间码**。 25 | 26 | 在学习 Objective-C 中的 block ,会经常使用的 clang 的 `-rewrite-objc` 命令来将 block 的语法转换成C语言的 struct 结构,从而供我们学习参考。 27 | 28 | 先从最简单的C语言中的 block 看起: 29 | 30 | ```c 31 | #include 32 | 33 | void (^outside)(void) = ^{ 34 | printf("Hello block!\n"); 35 | }; 36 | 37 | int main () { 38 | outside(); 39 | return 0; 40 | } 41 | ``` 42 | 43 | 然后使用 `clang -rewrite-objc` 命令对 `blockTest.c` 进行 block 语法转换,得到 blockTest.cpp 这个文件。 44 | 45 | 46 | ![img_2.jpg](http://upload-images.jianshu.io/upload_images/208988-b590f135364802bb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 47 | 48 | 49 | 在精简代码后,选取出主要关注的代码片段。 50 | 51 | ```c 52 | struct __block_impl { 53 | void *isa; 54 | int Flags; 55 | int Reserved; 56 | void *FuncPtr; 57 | }; 58 | 59 | struct __outside_block_impl_0 { 60 | struct __block_impl impl; 61 | struct __outside_block_desc_0* Desc; 62 | __outside_block_impl_0(void *fp, struct __outside_block_desc_0 *desc, int flags=0) { 63 | impl.isa = &_NSConcreteGlobalBlock; 64 | impl.Flags = flags; 65 | impl.FuncPtr = fp; 66 | Desc = desc; 67 | } 68 | }; 69 | 70 | static void __outside_block_func_0(struct __outside_block_impl_0 *__cself) { 71 | printf("Hello block!\n"); 72 | } 73 | 74 | static struct __outside_block_desc_0 { 75 | size_t reserved; 76 | size_t Block_size; 77 | } __outside_block_desc_0_DATA = { 78 | 0, 79 | sizeof(struct __outside_block_impl_0) 80 | }; 81 | 82 | int main () { 83 | ((void (*)(__block_impl *))((__block_impl *)outside)->FuncPtr)((__block_impl *)outside); 84 | return 0; 85 | } 86 | 87 | ``` 88 | 89 | 代码可能有些难懂,逐一来分析。 90 | 91 | ```c 92 | static void __outside_block_func_0(struct __outside_block_impl_0 *__cself) { 93 | printf("Hello block!\n"); 94 | } 95 | ``` 96 | 97 | 这个函数应该是和源代码最相近的部分。并且,源代码中的 block 名被重新组合成一种新的字符串形式,而生成了这个函数的函数名。在参数上发现其实这个参数名又是一种新的字符串组合形式(`__xxx_block_impl_y`:这里的 xxx 是 **block 名称**,y 是**该函数出现的顺序值**)。 98 | 99 | 继续来看看参数 `__cself` 的声明: 100 | 101 | ```c 102 | struct __outside_block_impl_0 { 103 | struct __block_impl impl; 104 | struct __outside_block_desc_0* Desc; 105 | 106 | // 构造函数 107 | __outside_block_impl_0(void *fp, struct __outside_block_desc_0 *desc, int flags=0) { 108 | impl.isa = &_NSConcreteGlobalBlock; 109 | impl.Flags = flags; 110 | impl.FuncPtr = fp; 111 | Desc = desc; 112 | } 113 | }; 114 | ``` 115 | 116 | 第一个成员`impl`,是 __block_impl 类型,结构体在生成文件中也是出现的: 117 | 118 | ```c 119 | struct __block_impl { 120 | void *isa; 121 | int Flags; 122 | int Reserved; 123 | void *FuncPtr; 124 | }; 125 | ``` 126 | 127 | * isa指针:指向一个类对象。在非 GC 模式下有三种类型:`_NSConcreteStackBlock`、`_NSConcreteGlobalBlock`、`_NSConcreteMallocBlock`。 128 | * Flags:block 的负载信息(引用计数和类型信息),按位存储。在下面有详细说明。 129 | * Reserved:保留变量。 130 | * FuncPtr:指向 block 函数地址的指针。 131 | 132 | 在 runtime 的源码中,对于 Flags 的枚举要比文档中描述的更加详细,其定义如下。 133 | 134 | ```c 135 | enum { 136 | BLOCK_DEALLOCATING = (0x0001), // runtime 137 | BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 138 | BLOCK_NEEDS_FREE = (1 << 24), // runtime 139 | BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 140 | BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code 141 | BLOCK_IS_GC = (1 << 27), // runtime 142 | BLOCK_IS_GLOBAL = (1 << 28), // compiler 143 | BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE 144 | BLOCK_HAS_SIGNATURE = (1 << 30) // compiler 145 | }; 146 | ``` 147 | 148 | 在 clang 的官方文档中,有这么一句话: 149 | 150 | > The flags field is set to zero unless there are variables imported into the Block that need helper functions for program level `Block_copy()` and `Block_release()` operations, in which case the (1<<25) flags bit is set. 151 | 152 | 也就是说,一般情况下,一个 block 的 flags 成员默认设置为 0。如果当 block 需要 `Block_copy()` 和 `Block_release` 这类拷贝辅助函数,则会设置成 `1 << 25` ,也就是 **BLOCK_HAS_COPY_DISPOSE** 类型。可以搜索到大量讲述 `Block_copy` 方法的博文,其中涉及到了 **BLOCK_HAS_COPY_DISPOSE** 。 153 | 154 | 总结一下枚举类的用法,前 16 位即起到标记作用,又可记录引用计数: 155 | 156 | * **BLOCK_DEALLOCATING**:释放标记。一般常用 **BLOCK_NEEDS_FREE** 做 位与 操作,一同传入 Flags ,告知该 block 可释放。 157 | * **BLOCK_REFCOUNT_MASK**:一般参与判断引用计数,是一个可选用参数。 158 | * **BLOCK_NEEDS_FREE**:通过设置该枚举位,来告知该 block 可释放。意在说明 block 是 heap block ,即我们常说的 **_NSConcreteMallocBlock** 。 159 | * **BLOCK_HAS_COPY_DISPOSE**:是否拥有拷贝辅助函数(a copy helper function)。 160 | * **BLOCK_HAS_CTOR**:是否拥有 block 析构函数(dispose function)。 161 | * **BLOCK_IS_GC**:是否启用 GC 机制(Garbage Collection)。 162 | * **BLOCK_HAS_SIGNATURE**:与 **BLOCK_USE_STRET** 相对,判断是否当前 block 拥有一个签名。用于 runtime 时动态调用。 163 | 164 | 我们返回结构体 `__outside_block_impl_0` 继续看第二个成员 Desc 指针。以下是 `__outside_block_desc_0` 结构体声明。 165 | 166 | ```c 167 | static struct __outside_block_desc_0 { 168 | size_t reserved; 169 | size_t Block_size; 170 | } __outside_block_desc_0_DATA = { 171 | 0, 172 | sizeof(struct __outside_block_impl_0) 173 | }; 174 | ``` 175 | 176 | 其中两个成员也可以从名字看出,描述的是 block 的预留区空间和 block 的大小。其中`size_t`类型在64位环境下应为`long unsigned int`,该宏定义在 C标准库 的 **stddef.h** 中。`__outside_block_desc_0_DATA` 是该结构体类型的环境量,使用成员对齐方式进行快捷构造。 177 | 178 | 再来看最重要的部分,即 `__outside_block_impl_0` 的构造函数。 179 | 180 | ```c 181 | // 构造函数 182 | __outside_block_impl_0(void *fp, struct __outside_block_desc_0 *desc, int flags=0) { 183 | impl.isa = &_NSConcreteGlobalBlock; 184 | impl.Flags = flags; 185 | impl.FuncPtr = fp; 186 | Desc = desc; 187 | } 188 | ``` 189 | 190 | 这里的所有过程除了 &_NSConcreteGlobalBlock 以外都比较好理解。先跳过这部分,放在文章最后进行分析。继续看一下入口函数 main()。 191 | 192 | ```c 193 | int main () { 194 | ((void (*)(__block_impl *))((__block_impl *)outside)->FuncPtr)((__block_impl *)outside); 195 | return 0; 196 | } 197 | ``` 198 | 199 | 去掉强制转换部分,增强可读性: 200 | 201 | ```c 202 | outside -> FuncPtr(outside); 203 | ``` 204 | 205 | 也就是说,在执行我们定义的 block 的时候,会访问 impl 的 FuncPrt 这个函数指针。而在初始化(析构)时,这个函数会被指向 block 的执行函数体,也就是一开始分析的 `__outside_block_func_0` 方法。并且传入自身为参数。所以上文所提及的 `__cself` 参数,其实可以理解为面向对象中的**所属对象**,在 C++ 中我们常用 this 指针描述;而在 Objective-C 中,常常使用 self 。 206 | 207 | 写到这里,笔者有一些很有意思的联想。在 Objective-C 的设计中,为了突出对象与方法间的所属关系,经常会传递一个指针作为参数。例如在许多 Foundation 框架中的 Delegate 方法,第一个参数往往是委托方法的发起者本身。 208 | 209 | 最后再来看一下之前略过的 _NSConcreteGlobalBlock 。 210 | 211 | 对于任意一个对象的 isa 指针,其指向的对象是自身的类对象;而类对象的 isa 指针,指向的是元类(meta class)。而 block 虽然也是对象,但其结构是异于 NSObject 的。最新版本的 object 结构如下: 212 | 213 | ```c 214 | struct objc_class : objc_object { 215 | // Class ISA; 216 | Class superclass; 217 | cache_t cache; // formerly cache pointer and vtable 218 | class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 219 | } 220 | ``` 221 | 222 | ![img_3.png](http://upload-images.jianshu.io/upload_images/208988-26fce38e44961829.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 223 | 224 | 225 | 226 | 其中 object 的 isa 指针是从 objc_objcet 中继承而来的。而 block 为了模拟 object 结构,也用到了 isa 对其进行了分类。其中 _NSConcreteGlobalBlock 就是其中之一。 227 | 228 | 关于 block 类型将会在 block 系列其他文中介绍,这里由于我们的 block 是处在全局位置,所以其类型为 _NSConcreteGlobalBlock。 229 | 230 | 在学习 C 中的 block ,通过 clang 的语义转换将 block 语法使用 C 语言描述,使得我们更进一步的深入学习 block 的内部实现。 231 | 232 | ## 对于 clang -rewrite-objc 一种误区 233 | 234 | 很多时候,会想当然的认为,在编译期,clang 对代码进行语义判断之后,会像 `-rewrite-objc` 一样对代码进行转译成 C 语言,进而转换成中间码。但是,该命令并**不能代表编译后所执行的代码**。 235 | 236 | 在巧哥很久之前[谈Objective-C Block的实现](http://blog.devtang.com/2013/07/28/a-look-inside-blocks/)的文中,有这么一个代码片段: 237 | 238 | ```c 239 | #include 240 | 241 | int main() { 242 | ^{ printf("Hello, World!\n"); } (); 243 | return 0; 244 | } 245 | ``` 246 | 247 | 在使用 `-rewrite-objc` 进行语法转换后,所显示的 block 类型为 _NSConcreteStackBlock 。而根据我们对于 block 的认知,当 block 没有引用外部的变量对象时,其类型应为 _NSConcreteGlobalBlock。难道,clang 对于 Objective-C 中的 block 和 C 中的 block 处理,会有差异吗?其实不是的,我们来做这个实验: 248 | 249 | ```c 250 | #include 251 | 252 | void (^outside)(void) = ^(void) { 253 | printf("Hello, block!\n"); 254 | }; 255 | 256 | int main() { 257 | void (^inside)(void) = ^(void) { 258 | printf("Hello, block!\n"); 259 | }; 260 | printf("outside: %p\n", outside); 261 | printf("inside: %p\n", inside); 262 | 263 | return 0; 264 | } 265 | ``` 266 | 267 | ```c 268 | outside: 0x10d48e040 269 | inside: 0x10d48e080 270 | ``` 271 | 272 | 从输出结果上看,两个 block 被存储在同一区域,也就是 .data 常量区。 273 | 274 | 275 | 276 | ![img_4.jpg](http://upload-images.jianshu.io/upload_images/208988-8deb7e7cc8475e58.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 277 | 278 | 279 | 280 | 可是在 main 函数内声明的 block 类型,通过 `clang -rewrite-objc` 工具转换后仍为 _NSConcreteStackBlock 栈存储 block 类型。从这个侧面,可以明白其实 clang 对语法的解释转换,**不一定出现在编译过程中**。而在编译期间转换成中间码的过程中,在新版本的 clang 编译器已经不需要解释成c的语法进行过度,从而翻译成中间码。而是,在语法检测后,直接转至中间码,提交至 llvm 进行链接处理。 281 | 282 | 所以,通过 `clang -rewrite-objc` 命令,仅将扩展语法通过可读性更高的 C 语法进行改写,而不是编译期中的子编译过程。我们仅仅通过他来了解 block 真正的结构就已经足够了。 283 | 284 | ## 尾声 285 | 286 | 这篇文章讲述了 block 的结构以及指向 block 函数体的具体方式。在以后的 block 系列学习笔记中,还会继续记录 block 类型、 block 使用等相关知识。 287 | 288 | --- 289 | 290 | 291 | 292 | ## 参考资料 293 | 294 | [A look inside blocks (Block_copy)](http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/) 295 | 296 | [clang官方文档:block 扩展语法](http://clang.llvm.org/docs/BlockLanguageSpec.html) 297 | 298 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 299 | 300 | -------------------------------------------------------------------------------- /Objective-C/Runtime/浅谈 block(2) - 截获变量方式.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](http://www.desgard.com/block2/) 4 | 5 | 6 | # 浅谈 block - 截获变量方式 7 | 8 | 本文会通过 clang 的 `-rewrite-objc` 选项来分析 block 的 C 转换源代码。其分析方式在该系列上一篇有详细介绍。请先阅读 *[浅谈 block(1) - clang 改写后的 block 结构](https://desgard.com/block1/)* 。 9 | 10 | ## 截获自动变量 11 | 12 | 首先需要做代码准备工作,我们编写一段 block 引用外部变量的 c 代码。 13 | 14 | ![7E32C4CC-DE35-469E-8EC1-C20BCAE4CD0](http://7xwh85.com1.z0.glb.clouddn.com/7E32C4CC-DE35-469E-8EC1-C20BCAE4CD0C.png) 15 | 16 | 编译运行成功后,使用 `-rewrite-objc` 进行改写。 17 | 18 | ```shell 19 | clang -rewrite-objc block.c 20 | ``` 21 | 22 | 简化代码后,得到以下主要代码: 23 | 24 | ```objc 25 | struct __main_block_impl_0 { 26 | struct __block_impl impl; 27 | struct __main_block_desc_0* Desc; 28 | char *str; 29 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int flags=0) : str(_str) { 30 | impl.isa = &_NSConcreteStackBlock; 31 | impl.Flags = flags; 32 | impl.FuncPtr = fp; 33 | Desc = desc; 34 | } 35 | }; 36 | 37 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 38 | char *str = __cself->str; // bound by copy 39 | printf("%s\n", str); 40 | } 41 | 42 | static struct __main_block_desc_0 { 43 | size_t reserved; 44 | size_t Block_size; 45 | } __main_block_desc_0_DATA = { 46 | 0, 47 | sizeof(struct __main_block_impl_0) 48 | }; 49 | 50 | int main() { 51 | char *str = "Desgard_Duan"; 52 | void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str)); 53 | ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); 54 | return 0; 55 | } 56 | 57 | ``` 58 | 59 | 与上一篇转换的源码不同的是,block 语法表达中的变量作为成员添加到了 `__main_block_func_0` 结构体中。 60 | 61 | ```c 62 | struct __main_block_impl_0 { 63 | struct __block_impl impl; 64 | struct __main_block_desc_0* Desc; 65 | char *str; // 外部引用变量 66 | } 67 | ``` 68 | 69 | 并且,在该结构体中的应用变量类型与外部的类型完全相同。在初始化该结构体实例的构造函数也自然会有所差异: 70 | 71 | ```c 72 | void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str)); 73 | ``` 74 | 75 | 去掉强转语法简化代码: 76 | 77 | ```c 78 | void (*block)() = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, str); 79 | ``` 80 | 81 | 在构造时,除了要传递自身(self) `__main_block_func_0` 结构体,而且还要传递 block 的基本信息,即 reserved 和 size 。这里传递了一个全局结构体对象 `__main_block_desc_0_DATA` ,因为他是为 block 量身设计的。最后在将引用值参数传入构造函数中,以便于构造带外部引用参数的 block。 82 | 83 | 进入构造函数后,发现了含有冒号表达的构造语法: 84 | 85 | ```c 86 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int flags=0) : str(_str) { 87 | impl.isa = &_NSConcreteStackBlock; 88 | impl.Flags = flags; 89 | impl.FuncPtr = fp; 90 | Desc = desc; 91 | } 92 | ``` 93 | 94 | 其实,冒号表达式是 C++ 中的一个固有语法。这是显示构造的方法之一。另外还有一种构造显示构造方式,其语法较为繁琐,即使用 this 指针构造。(关于 C++ 构造函数,可以学习 msdn 文档 *[构造函数 (C++)](https://msdn.microsoft.com/zh-cn/library/s16xw1a8.aspx)* ) 95 | 96 | 之后的代码与前一篇分析相同,不再讨论。 97 | 98 | 通过整个构造 block 流程分析,我们发现当 block 引用外部对象时,会在结构体内部新建立一个成员进行存储。此处我们使用的是 char* 类型,而在结构体中所使用的 char* 是结构体的成员,所以可以得知:**block 引用外部对象时候,不是简单的指针引用(浅复制),而是一种重建(深复制)方式**(括号内外分别对于基本数据类型和对象分别描述)**)**。所以如果在 block 中对外部对象进行修改,无论是值修改还是指针修改,自然是没有任何效果。 99 | 100 | ## 引入 __block 关键字对截取变量一探究竟 101 | 102 | 上文中的 block 所引用的外部成员是一个字符型指针,当我们在 block 内部对其修改后,很容易的想到,会改变该指针的指向。而当 block 中引用外部变量为常用数据类型会有些许的不同: 103 | 104 | 我们来看这个例子 (这是来自 *Pro multithreading and memory management for iOS and OS X* 2.3.3 一节的例子): 105 | 106 | ```c 107 | int val = 0; 108 | void (^blk)(void) = ^{val = 1}; 109 | ``` 110 | 111 | 执行代码后会报 error : 112 | 113 | ```bash 114 | error: variable is not assignable (missing __block type specifier) 115 | void (^blk)(void) = ^{val = 1}; 116 | ``` 117 | 118 | 上述书中对此情况是这样解释的: 119 | 120 | > block 中所使用的被截获自动变量如同“带有自动变量值的匿名函数”,仅截获自动变量的值。 block 中使用自动变量后,在 block 的结构体实力中重写该自动变量也不会改变原先截获的自动变量。 121 | 122 | 这应该是 clang 对 block 的引用外界局部值做的保护措施,也是为了维护 C 语言中的作用域特性。既然谈到了作用域,那么是否可以使用显示声明存储域类型从而在 block 中修改该变量呢?答案是可以的。当 block 中截取的变量为静态变量(static),使用下例进行试验: 123 | 124 | ```c 125 | int main() { 126 | static int static_val = 2; 127 | void (^blk)(void) = ^{ 128 | static_val = 3; 129 | }; 130 | } 131 | ``` 132 | 133 | 装换后的代码: 134 | 135 | ```c 136 | struct __main_block_impl_0 { 137 | struct __block_impl impl; 138 | struct __main_block_desc_0* Desc; 139 | int *static_val; 140 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) { 141 | impl.isa = &_NSConcreteStackBlock; 142 | impl.Flags = flags; 143 | impl.FuncPtr = fp; 144 | Desc = desc; 145 | } 146 | }; 147 | 148 | int main() { 149 | static int static_val = 2; 150 | void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_ 151 | return 0; 152 | } 153 | ``` 154 | 155 | 会发现在构造函数中使用的静态指针 `int *_static_val` 对其进行访问。将静态变量 `static_val` 的指针传递给 `__main_block_impl_0` 结构体的构造函数并加以保存。通过指针进行作用域拓展,是 C 中很常见的思想及做法,也是超出作用域使用变量的最简单方法。 156 | 157 | 那么我们为什么在引用自动变量的时候,不使用该自动变量的指针呢?是应为在 block 截获变量后,原来的自动变量已经废弃,因此block 中超过变量作用域从而无法通过指针访问原来的自动变量。 158 | 159 | 为了解决这个问题,其实在 block 扩展中已经提供了方法([官方文档](http://clang.llvm.org/docs/BlockLanguageSpec.html#the-block-storage-qualifier))。即使用 `__block` 关键字。 160 | 161 | `__block` 关键字更准确的表达应为 *__block说明符(__block storage-class-specifier)* ,用来描述存储域。在 C 语言中已经存有如下存储域声明关键字: 162 | 163 | * typedef:常用在为数据类型起别名,而不是一般认识的存储域声明关键字作用。但在归类上属于存储域声明关键字。 164 | * extern:限制标示,限制定义变量在所有模块中作为全局变量,并只能被定义一次。 165 | * static:静态变量存储在 .data 区。 166 | * auto:自动变量存储在栈中。 167 | * register:约束变量为单值,存储在CPU寄存器内。 168 | 169 | `__block` 关键字类似于 `static`、`auto`、`register`,用于将变量存于指定存储域。来分析一下在变量声明前增加 `__block` 关键字后 clang 对于 block 的转换动作。 170 | 171 | ```c 172 | __block int val = 1; 173 | void (^blk)(void) = ^ { 174 | val = 2; 175 | }; 176 | ``` 177 | 178 | ```c 179 | // 要点 1:__block 变量转换结构 180 | struct __Block_byref_val_0 { 181 | void *__isa; 182 | __Block_byref_val_0 *__forwarding; 183 | int __flags; 184 | int __size; 185 | int val; 186 | }; 187 | 188 | struct __main_block_impl_0 { 189 | struct __block_impl impl; 190 | struct __main_block_desc_0* Desc; 191 | __Block_byref_val_0 *val; // by ref 192 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) { 193 | impl.isa = &_NSConcreteStackBlock; 194 | impl.Flags = flags; 195 | impl.FuncPtr = fp; 196 | Desc = desc; 197 | } 198 | }; 199 | 200 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 201 | __Block_byref_val_0 *val = __cself->val; // bound by ref 202 | // 要点 2:__forwarding 自环指针存在意义 203 | (val->__forwarding->val) = 2; 204 | } 205 | 206 | static struct __main_block_desc_0 { 207 | size_t reserved; 208 | size_t Block_size; 209 | // 要点 3:copy/dispose 方法内部实现 210 | void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); 211 | void (*dispose)(struct __main_block_impl_0*); 212 | } __main_block_desc_0_DATA = { 213 | 0, 214 | sizeof(struct __main_block_impl_0), 215 | __main_block_copy_0, 216 | __main_block_dispose_0 217 | }; 218 | 219 | int main() { 220 | __attribute__((__blocks__(byref))) __Block_byref_val_0 val = { 221 | (void*)0, 222 | (__Block_byref_val_0 *)&val, 223 | 0, 224 | sizeof(__Block_byref_val_0), 1 225 | }; 226 | void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344)); 227 | return 0; 228 | } 229 | ``` 230 | 231 | 发现核心代码部分有所增加,我们先从入口函数看起。 232 | 233 | ```c 234 | __Block_byref_val_0 val = { 235 | (void*)0, 236 | (__Block_byref_val_0 *)&val, 237 | 0, 238 | sizeof(__Block_byref_val_0), 239 | 1 240 | }; 241 | ``` 242 | 243 | 原先的 val 变成了 `__Block_byre_val_0` 结构体类型变量。并且这个结构体的定义是之前未曾见过的。并且我们将 val 初始化的数值 1,也出现在这个构造中,说明该结构体持有原成员变量。 244 | 245 | ```c 246 | struct __Block_byref_val_0 { 247 | void *__isa; 248 | __Block_byref_val_0 *__forwarding; 249 | int __flags; 250 | int __size; 251 | int val; 252 | }; 253 | ``` 254 | 255 | 在 `__block` 变量的结构体中,除了有指向类对象的 `isa` 指针,对象负载信息 `flags`,大小 `size`,以及持有的原变量 `val`,还有一个自身类型的 `__forwarding` 指针。从构造函数中,会发现一个有趣的现象,**`__forwarding` 指针会指向自身,形成自环**。后面会详细介绍它。 256 | 257 | 而在 block 体执行段,是这样定义的。 258 | 259 | ```c 260 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 261 | __Block_byref_val_0 *val = __cself->val; // bound by ref 262 | (val->__forwarding->val) = 2; 263 | } 264 | ``` 265 | 266 | 第一步中获得 val 的方法和 block 中引用外部变量的方式是一致的,通过 self 来获取变量。而对于外部 __block 变量赋值的时候,这种写法引起了我们的注意:`(val->__forwarding->val) = 2;` ,这样做的目的何在,在后文会做出分析。 267 | 268 | ## __block 变量结构 269 | 270 | ![__block结构](http://7xwh85.com1.z0.glb.clouddn.com/__block%E7%BB%93%E6%9E%84.png) 271 | 272 | 当 block 内部引用外部的 __block 变量,会使用以上结构对 __block 做出转换。另外,该结构体并不声明在 `__main_block_impl_0` block 结构体中,是因为这样可以对多个 block 引用 __block 情况下,达到复用效果,从而节省不必要的空间开销。 273 | 274 | ```c 275 | __block int val = 0; 276 | void (^blk1)(void) = ^{val = 1;}; 277 | void (^blk2)(void) = ^{val = 2;}; 278 | ``` 279 | 280 | 只观察入口方法: 281 | 282 | ```c 283 | __Block_byref_val_0 = {0, &val, 0, sizeof(__Block_byref_val_0), 10}; 284 | blk1 = &__main_block_impl_0(__main_block_func_0 285 | , &__main_block_desc_0_DATA 286 | , &val 287 | , 0x22000000); 288 | 289 | blk2 = &__main_block_impl_0(__main_block_func_1 290 | , &__main_block_desc_1_DATA 291 | , &val 292 | , 0x22000000); 293 | ``` 294 | 295 | 发现 val 指针被复用,使得两个 block 同时使用一个 __block 只需要对其结构声明一次即可。 296 | 297 | ## 接触 Objective-C 语言环境下的 block 298 | 299 | 通过两篇文的 block 的结构转换,我们发现其实 block 的实质是一个*对象 (Object)*,从封装成结构体对象,再到 isa 指针结构,都是明显的体现。对于 __block 也是如此,在转换后将其封装成了 __block 结构体类型,以对象方式处理。 300 | 301 | 带着 C 代码中的 block 扩展转换规则开始进入 Objective-C block 的学习。首先需要知道 block 的三个类型。 302 | 303 | | 类型 | 对象存储域 | 地址单元 | 304 | | :-------- | --------:| ---- | 305 | | _NSConcreteStackBlock | 栈 | 高地址 | 306 | | _NSConcreteMallocBlock | 堆 | | 307 | | _NSConcreteGloalBlock | 静态区(.data) | 低地址 | 308 | 309 | 在上一篇文中的末尾部分,简单的说了一下全局静态的存储问题。这里再一次强调, `_NSConcreteGloalBlock` 的 block 会在一下两种情况下出现(与 clang 转换结果不大相同): 310 | 311 | * 全局变量位置 312 | * block 中不引用外部变量 313 | 314 | 而在其他情况下,基本上 block 的类型都为 _NSConcreteStackBlock 。但是在栈上的 block 会受到作用域的限制,一旦所属的变量作用域结束,该 block 就会被释放。由此,引出了 _NSConcreteMallocBlock 堆 block 类型。 315 | 316 | block 提供了将 block 和 __block 变量从栈上复制到堆上的方法来解决这个问题。将配置在站上的 block 复制到堆上,这样可以保证在 block 变量作用域结束后,堆上仍旧可访问。 317 | 318 | __block 变量通过 __forwarding 可以无论在堆上还是栈上都能正常访问。当 block 存储在堆上的时候,对应的栈上 block 的 __forwarding 成员会断开自环,而指向堆上的 block 对象。这也就是 __forwarding 指针存在的真实用意。 319 | 320 | ![block_forwarding](http://7xwh85.com1.z0.glb.clouddn.com/block_forwarding.png) 321 | 322 | 323 | 在复制到堆的过程中,__forwarding 指针是如何更改指向的?这个问题在下一篇中进行介绍。这篇文主要讲述了 __block 变量在 block 中的结构,以及如何获取外部变量,并可以对其修改的详细过程,希望有所收获。 324 | 325 | 326 | 327 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 328 | 329 | -------------------------------------------------------------------------------- /Objective-C/Runtime/浅谈Associated Objects.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](http://www.desgard.com/2016/07/30/AssociatedObjectsIntroduction/) 4 | 5 | # 浅谈Associated Objects 6 | 7 | 俗话说:“金无足赤,人无完人。”对于每一个Class也是这样,尽管我们说这个Class的代码规范、逻辑清晰合理等等,但是总会有它的短板,或者随着需求演进而无法订制实现功能。于是在Objective-C 2.0中引入了**category**这个特性,用以动态地为已有类添加新行为。面向对象的设计用来描述事物的组成往往是使用Class中的属性成员,这也就**局限了方法的广度**(在官方文档称之为**[An otherwise notable shortcoming for Objective-C](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html)**,译为:*Objc的一个显著缺陷*)。所以在Runtime中引入了**Associated Objects**来弥补这一缺陷。 8 | 9 | 另外,请带着以下疑问来阅读此文: 10 | 11 | * Associated Objects 使用场景。 12 | * Associated Objects 五种`objc_AssociationPolicy`有什么区别。 13 | * Associated Objects 的存储结构。 14 | 15 | ## Associated Objects Introduction 16 | 17 | Associated Objects是Objective-C 2.0中Runtime的特性之一。最早开始使用是在*OS X Snow Leopard*和*iOS 4*中。在``中定义的三个方法,也是我们深入探究Associated Objects的突破口: 18 | 19 | * objc_setAssociatedObject 20 | * objc_getAssociatedObject 21 | * objc_removeAssociatedObjects 22 | 23 | > void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) 24 | 25 | 26 | * `object`:传入关联对象的所属对象,也就是增加成员的实例对象,一般来说传入self。 27 | * `key`:一个唯一标记。在官方文档中推荐使用`static char`,当然更推荐是指针。为了便捷,一般使用`selector`,这样在后面getter中,我们就可以利用`_cmd`来方便的取出`selector`。 28 | * `value`:传入关联对象。 29 | * `policy`:`objc_AssociationPolicy`是一个ObjC枚举类型,也代表关联策略。 30 | 31 | > void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) 32 | 33 | > void objc_removeAssociatedObjects(id object) 34 | 35 | 从参数类型参数类型上,我们可以轻易的得出getter和remove方法传入参数的含义。要注意的是,**objc_removeAssociatedObjects这个方法会移除一个对象的所有关联对象。**其实,该方法我们一般是用不到的,移除所有关联意味着将类恢复成**无任何关联的原始状态**,这不是我们希望的。所以一般的做法是通过`objc_setAssociatedObject`来传入`nil`,从而移除某个已有的关联对象。 36 | 37 | 我们用[Associated Objects](http://nshipster.com/associated-objects/)这篇文中的例子来举例: 38 | 39 | ```c 40 | // NSObject+AssociatedObject.h 41 | 42 | @interface NSObject (AssociatedObject) 43 | @property (nonatomic, strong) id associatedObject; 44 | @end 45 | ``` 46 | 47 | ```c 48 | // NSObject+AssociatedObject.m 49 | 50 | @implementation NSObject (AssociatedObject) 51 | @dynamic associatedObject; 52 | 53 | - (void)setAssociatedObject:(id)object { 54 | objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 55 | } 56 | 57 | - (id)associatedObject { 58 | return objc_getAssociatedObject(self, @selector(associatedObject)); 59 | } 60 | ``` 61 | 62 | 这时我们已经发现`associatedObject`这个属性已经添加至`NSObject`的实例中了。并且我们可以通过category指定的getter和setter方法对这个属性进行存取操作。(注:这里使用`@dynamic`关键字是为了告知编译器:**在编译期不要自动创建实现属性所用的存取方法**。因为对于Associated Objects我们**必须手动添加**。当然,不写这个关键字,使用同名方法进行override也是可以达到相同效果的。但从编码规范和优化效率来讲,显式声明是最好的。) 63 | 64 | 65 | ![1.jpg](http://upload-images.jianshu.io/upload_images/208988-10a9d08b532258d3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 66 | 67 | 68 | ## AssociationPolicy 69 | 70 | 通过上面的例子,我们注意到了`OBJC_ASSOCIATION_RETAIN_NONATOMIC`这个参数,它的枚举类型各个元素的含义如下: 71 | 72 | Behavior | @property Equivalent | Description 73 | --------- | ------------- | -------- 74 | OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) | 指定一个关联对象的弱引用。 75 | OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | 指定一个关联对象的强引用,不能被原子化使用。 76 | OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一个关联对象的copy引用,不能被原子化使用。 77 | OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一个关联对象的强引用,能被原子化使用。 78 | OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一个关联对象的copy引用,能被原子化使用。 79 | OBJC_ASSOCIATION_GETTER_AUTORELEASE | | 自动释放类型 80 | 81 | OBJC_ASSOCIATION_ASSIGN类型的关联对象和`weak`有一定差别,而更加接近于`unsafe_unretained`,即当目标对象遭到摧毁时,属性值不会自动清空。(翻译自[Associated Objects](http://nshipster.com/associated-objects/)) 82 | 83 | ## Usage Sample 84 | 85 | 同样是[Associated Objects](http://nshipster.com/associated-objects/)文中,总结了三个关于Associated Objects用法: 86 | 87 | * **为Class添加私有成员**:例如在AFNetworking中,[在UIImageView里添加了**imageRequestOperation**对象](https://github.com/AFNetworking/AFNetworking/blob/2.1.0/UIKit%2BAFNetworking/UIImageView%2BAFNetworking.m#L57-L63),从而保证了异步加载图片。 88 | * **为Class添加共有成员**:例如在FDTemplateLayoutCell中,使用Associated Objects来缓存每个cell的高度([代码片段1](https://github.com/mconintet/UITableView-FDTemplateLayoutCell/blob/master/Classes/UITableView+FDIndexPathHeightCache.m#L124)、[代码片段2](https://github.com/mconintet/UITableView-FDTemplateLayoutCell/blob/master/Classes/UITableView+FDKeyedHeightCache.m#L81))。通过分配不同的key,在复用cell的时候即时取出,增加效率。 89 | * **创建KVO对象**:建议使用category来创建关联对象作为观察者。可以参考[*Objective-C Associated Objects*](http://kingscocoa.com/tutorials/associated-objects/)这篇文的例子。 90 | 91 | 92 | ## Analysis Source Code 93 | 94 | 在[*Objective-C Associated Objects 的实现原理*](http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/)这篇文中,作者有一个[例子](https://github.com/leichunfeng/AssociatedObjects),作者分析了在Associated Objects中弱引用的区别。其代码片段如下: 95 | 96 | ```c 97 | #import "ViewController.h" 98 | #import "ViewController+AssociatedObjects.h" 99 | 100 | __weak NSString *string_weak_assign = nil; 101 | __weak NSString *string_weak_retain = nil; 102 | __weak NSString *string_weak_copy = nil; 103 | 104 | @implementation ViewController 105 | 106 | - (void)viewDidLoad { 107 | [super viewDidLoad]; 108 | // 通过[NSString stringWithFormat:]来持有一个字符串对象 109 | self.associatedObject_assign = [NSString stringWithFormat:@"associatedObject_assign"]; 110 | self.associatedObject_retain = [NSString stringWithFormat:@"associatedObject_retain"]; 111 | self.associatedObject_copy = [NSString stringWithFormat:@"associatedObject_copy"]; 112 | 113 | // 强调指向各个属性的指针均为弱类型指针 114 | // 以保证weak、assign类型属性会被释放 115 | string_weak_assign = self.associatedObject_assign; 116 | string_weak_retain = self.associatedObject_retain; 117 | string_weak_copy = self.associatedObject_copy; 118 | } 119 | 120 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 121 | NSLog(@"self.associatedObject_assign: %@", self.associatedObject_assign); // Will Crash 122 | NSLog(@"self.associatedObject_retain: %@", self.associatedObject_retain); 123 | NSLog(@"self.associatedObject_copy: %@", self.associatedObject_copy); 124 | } 125 | 126 | @end 127 | ``` 128 | 129 | 在测试时候,我们发现有些情况下不至于导致crash。我猜想可能是因为`[NSString stringWithFormat:]`方法的持有字符串可能会被编译器优化成*compile-time constant*。你可以尝试着做如下修改: 130 | 131 | ```c 132 | self.associatedObject_assign = @"associatedObject_assign"; 133 | self.associatedObject_retain = @"associatedObject_retain"; 134 | self.associatedObject_copy = @"associatedObject_copy"; 135 | ``` 136 | 137 | 你会发现全部正常输出。因为所有字符串都变成了编译期常量而存储起来。所以探究方法,应该是讲类型更改成NSObject进行试验。 138 | 139 | ### Setter Source Code 140 | 141 | 我们一直有个疑问,就是关联对象是如何存储的。下面我们看下*Runtime*的源码。 142 | 143 | 以下源码来自于[opensource.apple.com](http://opensource.apple.com/tarballs/objc4/)的*objc4-680.tar.gz*。 144 | 145 | ```c 146 | class ObjcAssociation { 147 | uintptr_t _policy; 148 | id _value; 149 | public: 150 | ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {} 151 | ObjcAssociation() : _policy(0), _value(nil) {} 152 | 153 | uintptr_t policy() const { return _policy; } 154 | id value() const { return _value; } 155 | 156 | bool hasValue() { return _value != nil; } 157 | }; 158 | 159 | class AssociationsHashMap : public unordered_map { 160 | public: 161 | void *operator new(size_t n) { return ::malloc(n); } 162 | void operator delete(void *ptr) { ::free(ptr); } 163 | }; 164 | 165 | class AssociationsManager { 166 | static spinlock_t _lock; 167 | static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap. 168 | public: 169 | AssociationsManager() { _lock.lock(); } 170 | ~AssociationsManager() { _lock.unlock(); } 171 | 172 | AssociationsHashMap &associations() { 173 | if (_map == NULL) 174 | _map = new AssociationsHashMap(); 175 | return *_map; 176 | } 177 | }; 178 | 179 | static id acquireValue(id value, uintptr_t policy) { 180 | // 遇见不合法policy或者assign直接返回,也就是说将其他无效policy当做assign处理 181 | switch (policy & 0xFF) { 182 | case OBJC_ASSOCIATION_SETTER_RETAIN: 183 | return ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain); 184 | case OBJC_ASSOCIATION_SETTER_COPY: 185 | return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); 186 | } 187 | return value; 188 | } 189 | 190 | inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); } 191 | 192 | void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) { 193 | // retain the new value (if any) outside the lock. 194 | // 创建一个ObjcAssociation对象 195 | ObjcAssociation old_association(0, nil); 196 | 197 | // 通过policy为value创建对应属性,如果policy不存在,则默认为assign 198 | id new_value = value ? acquireValue(value, policy) : nil; 199 | { 200 | // 创建AssociationsManager对象 201 | AssociationsManager manager; 202 | 203 | // 在manager取_map成员,其实是一个map类型的映射 204 | AssociationsHashMap &associations(manager.associations()); 205 | 206 | // 创建指针指向即将拥有成员的Class 207 | // 至此该类已经包含这个关联对象 208 | disguised_ptr_t disguised_object = DISGUISE(object); 209 | 210 | // 以下是记录强引用类型成员的过程 211 | if (new_value) { 212 | // break any existing association. 213 | // 在即将拥有成员的Class中查找是否已经存在改关联属性 214 | AssociationsHashMap::iterator i = associations.find(disguised_object); 215 | if (i != associations.end()) { 216 | // secondary table exists 217 | // 当存在时候,访问这个空间的map 218 | ObjectAssociationMap *refs = i->second; 219 | // 遍历其成员对应的key 220 | ObjectAssociationMap::iterator j = refs->find(key); 221 | if (j != refs->end()) { 222 | // 如果存在key,重新更改Key的指向到新关联属性 223 | old_association = j->second; 224 | j->second = ObjcAssociation(policy, new_value); 225 | } else { 226 | // 否则以新的key创建一个关联 227 | (*refs)[key] = ObjcAssociation(policy, new_value); 228 | } 229 | } else { 230 | // create the new association (first time). 231 | // key不存在的时候,直接创建关联 232 | ObjectAssociationMap *refs = new ObjectAssociationMap; 233 | associations[disguised_object] = refs; 234 | (*refs)[key] = ObjcAssociation(policy, new_value); 235 | object->setHasAssociatedObjects(); 236 | } 237 | } else { 238 | // setting the association to nil breaks the association. 239 | // 这种情况是policy不存在或者为assign的时候 240 | // 在即将拥有的Class中查找是否已经存在Class 241 | // 其实这里的意思就是如果之前有这个关联对象,并且是非assign形的,直接erase 242 | AssociationsHashMap::iterator i = associations.find(disguised_object); 243 | if (i != associations.end()) { 244 | // 如果有该类型成员检查是否有key 245 | ObjectAssociationMap *refs = i->second; 246 | ObjectAssociationMap::iterator j = refs->find(key); 247 | if (j != refs->end()) { 248 | // 如果有key,记录旧对象,释放 249 | old_association = j->second; 250 | refs->erase(j); 251 | } 252 | } 253 | } 254 | } 255 | // release the old value (outside of the lock). 256 | // 如果存在旧对象,则将其释放 257 | if (old_association.hasValue()) ReleaseValue()(old_association); 258 | } 259 | ``` 260 | 261 | 我们读过代码后发现是其储存结构是这样的一个逻辑: 262 | 263 | 264 | ![2.png](http://upload-images.jianshu.io/upload_images/208988-67f51f426f98ce53.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 265 | 266 | 267 | * 橙色的是`AssociationsManager`是顶级结构体,维护了一个`spinlock_t`锁和一个`_map`的哈希表。这个哈希表中的键为`disguised_ptr_t`,在得到这个指针的时候,源码中执行了`DISGUISE`方法,这个方法的功能是获得指向**self**地址的指针,即为指向**对象地址**的指针。通过地址这个唯一标识,可以找到对应的value,即一个子哈希表。(@饶志臻 勘误) 268 | * 子哈希表是`ObjectAssociationMap`,键就是我们传入的`Key`,而值是`ObjcAssociation`,即这个成员对象。从而维护一个成员的所有属性。 269 | 270 | 在每次执行setter方法的时候,我们会逐层遍历Key,逐层判断。并且当持有Class有了关联属性的时候,在执行成员的Getter方法时,会优先查找Category中的关联成员。 271 | 272 | 这样会带来一个问题:**如果category中的一个关联对象与Class中的某个成员同名,虽然key值不一定相同,自身的Class不一定相同,policy也不一定相同,但是我这样做会直接覆盖之前的成员,造成无法访问,但是其内部所有信息及数据全部存在。**例如我们对`ViewController`做一个Category,来创建一个叫做view的成员,我们会发现在运行工程的时候,模拟器直接黑屏。 273 | 274 | 275 | ![3.jpg](http://upload-images.jianshu.io/upload_images/208988-97d8f5bde8f5de41.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 276 | 277 | 278 | 我们在viewDidLoad中下断点,甚至无法进入debug模式。因为view属性已经被覆盖,所以不会继续进行viewController的生命周期。 279 | 280 | 281 | ![4.jpg](http://upload-images.jianshu.io/upload_images/208988-12aa766163679316.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 282 | 283 | 284 | 285 | 这一点很危险,所以我们要杜绝覆盖Class原来的属性,这会破坏Class原有的功能。(当然,我是十分不推荐在业务项目中使用Runtime的,因为这样的代码可读性和维护性太低。) 286 | 287 | 288 | ### Getter Source Code & Remove 289 | 290 | 这两种方法我们直接看源码,在看过Setter中的遍历嵌套map结构的代码片段后,你会很容易理解这两个方法。 291 | 292 | ```c 293 | id _object_get_associative_reference(id object, void *key) { 294 | id value = nil; 295 | uintptr_t policy = OBJC_ASSOCIATION_ASSIGN; 296 | { 297 | AssociationsManager manager; 298 | AssociationsHashMap &associations(manager.associations()); 299 | disguised_ptr_t disguised_object = DISGUISE(object); 300 | AssociationsHashMap::iterator i = associations.find(disguised_object); 301 | if (i != associations.end()) { 302 | ObjectAssociationMap *refs = i->second; 303 | ObjectAssociationMap::iterator j = refs->find(key); 304 | if (j != refs->end()) { 305 | ObjcAssociation &entry = j->second; 306 | value = entry.value(); 307 | policy = entry.policy(); 308 | if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain); 309 | } 310 | } 311 | } 312 | if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) { 313 | ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease); 314 | } 315 | return value; 316 | } 317 | 318 | void _object_remove_assocations(id object) { 319 | vector< ObjcAssociation,ObjcAllocator > elements; 320 | { 321 | AssociationsManager manager; 322 | AssociationsHashMap &associations(manager.associations()); 323 | if (associations.size() == 0) return; 324 | disguised_ptr_t disguised_object = DISGUISE(object); 325 | AssociationsHashMap::iterator i = associations.find(disguised_object); 326 | if (i != associations.end()) { 327 | // copy all of the associations that need to be removed. 328 | ObjectAssociationMap *refs = i->second; 329 | 330 | // 将所有的关联成员放到一个vector,然后统一清理 331 | for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) { 332 | elements.push_back(j->second); 333 | } 334 | // remove the secondary table. 335 | delete refs; 336 | associations.erase(i); 337 | } 338 | } 339 | // the calls to releaseValue() happen outside of the lock. 340 | for_each(elements.begin(), elements.end(), ReleaseValue()); 341 | } 342 | ``` 343 | 344 | 另外,对于remove有一点补充。在Runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用`_object_remove_assocations`做关联对象的清理工作。 345 | 346 | ## Thinking About Hash Table 347 | 348 | 不光是本文讲述的关于Class关联对象的存储方式,还是Apple中其他的Souce Code(例如引用计数管理),我们能感受到Apple对Hash Table(本文中的map数据结构)这种数据结构情有独钟。在大量的实践中可以说明,Hash Table对于优化效率的提升,这是毋庸置疑的。 349 | 350 | 细究使用这种数据结构的原因,唯一的Key可对应指定的Value。我们从计算机存储的角度考虑,因为每个内存地址是唯一的,也就可以假象成Key,通过唯一的Key来读写数据,这是效率最高的方式。 351 | 352 | ## The End 353 | 354 | 通过阅读此文,想必你已经知道那三个问题的答案。笔者原本想对**UITableView-FDTemplateLayoutCell**进行源码分析来撰写一篇文,但是发现里面存储cell的Key值使用到了Associated Objects该技术,所以对此进行了学习探究。后面,我会分析一下**UITableView-FDTemplateLayoutCell**的源码,这些将收录在我的这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 355 | 356 | 357 | -------------------------------------------------------------------------------- /Objective-C/SDWebImage/SDWebImage Source Probe - Downloader.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](https://desgard.com/SDWebImage3/) 4 | 5 | # SDWebImage Source Probe: Downloader 6 | 7 | 为了进行图片下载操作,通过 `SDWebImageManager` 这座桥梁,有效控制了图片下载的时机和同缓存的协同操作。这篇来关注一下在 SD 中,Downloader Class 的具体实现。 8 | 9 | ## Downloader 中的一些枚举 10 | 11 | 在 `SDWebImageDownloader.m` 中,可以发现这么一个属性: 12 | 13 | ```objc 14 | @property (strong, nonatomic) NSOperationQueue *downloadQueue; 15 | ``` 16 | 17 | `NSOperation` 表示一个独立的控制单元,也就是我们所说的线程。而 `NSOperationQueue` 控制着这些并行操作的执行,以队列的数据结构特点,从而实现线程优先级的控制。而在 `SDWebImage` 中,很显然是用来管理 `SDWebImageDownloaderOperation` 。对于 `SDWebImageDownloaderOperation` 后面将会单独放在一篇博文中介绍。 18 | 19 | 同 Manager 一样,我们先来看看在 `.h` 文件中所有的下载模式枚举。 20 | 21 | ```objc 22 | typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { 23 | // 低优先级(常用) 24 | SDWebImageDownloaderLowPriority = 1 << 0, 25 | // 显示下载进程 26 | SDWebImageDownloaderProgressiveDownload = 1 << 1, 27 | 28 | // 默认情况下是不需要 NSURLCache 的。 29 | // 如果启用这个模式,缓存策略将会更改成 NSURLCache 30 | SDWebImageDownloaderUseNSURLCache = 1 << 2, 31 | 32 | // 如果图片是从 NSURLCache 中读取到的,则使用 nil 来作为回调 block 的传入参数 33 | // 常常会与 SDWebImageDownloaderUseNSURLCache 组合使用 34 | SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, 35 | 36 | // 当设备为 iOS 4 以上的情况,则在后台可以继续下载图片 37 | // 通过向系统额外申请时间来完成数据请求操作 38 | // 如果后台任务终止,则操作会取消 39 | SDWebImageDownloaderContinueInBackground = 1 << 4, 40 | 41 | // 设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES 42 | // 从而处理存储在 NSHTTPCookieStore 的 cookie 43 | SDWebImageDownloaderHandleCookies = 1 << 5, 44 | 45 | // 允许使用不受信的 SSL 证书 46 | // 主要用于测试 47 | // 常用在开发环境下 48 | SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, 49 | 50 | // 图片放在优先级更高的队列中 51 | SDWebImageDownloaderHighPriority = 1 << 7, 52 | }; 53 | ``` 54 | 55 | 另外,对于下载顺序,SD 也为我们提供了两种不同的下载顺序枚举: 56 | 57 | ```objc 58 | typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { 59 | // 先进先出 默认操作顺序 60 | SDWebImageDownloaderFIFOExecutionOrder, 61 | 62 | // 后进先出 63 | SDWebImageDownloaderLIFOExecutionOrder 64 | }; 65 | ``` 66 | 67 | options 枚举已经几乎将所有的开发场景所需要的模式考虑进来。下面我们来看一看具体的实现代码。 68 | 69 | ## Downloader 的私有成员对象 70 | 71 | 先来看下 Class 的 property 对象的作用: 72 | 73 | ```objc 74 | @interface SDWebImageDownloader () 75 | 76 | // NSOperation 操作队列 77 | @property (strong, nonatomic) NSOperationQueue *downloadQueue; 78 | 79 | // 最后添加的 Operation ,顺序为后进先出顺序 80 | @property (weak, nonatomic) NSOperation *lastAddedOperation; 81 | 82 | // 图片下载类 83 | @property (assign, nonatomic) Class operationClass; 84 | 85 | // URL 回调字典 86 | // key 是图片的 URL 87 | // value 是一个数组,包含每个图片的回调信息 88 | @property (strong, nonatomic) NSMutableDictionary *URLCallbacks; 89 | 90 | // HTTP 头信息 91 | @property (strong, nonatomic) NSMutableDictionary *HTTPHeaders; 92 | 93 | // 并行的处理所有下载操作的网络响应 94 | // 实现网络序列化的实例 95 | // 对于 URLCallbacks 的所有修改,都需要放在 barrierQueue 中,并通过 dispatch_barrier_sync 形式 96 | // 用于保证线程安全性 97 | @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue; 98 | 99 | @end 100 | ``` 101 | 102 | 由于需要保证多个图片可以同时下载,为了保证 `URLCallbacks` 的线程安全,我们使用 GCD 中的 `dispatch_barrier_sync` 为进程设置**栅栏(barrier)**,它会等待所有位于栅栏函数之前的操作执行完成后再执行,并且在栅栏函数执行完成后,其后续操作才会开始执行,这个函数需要同 `dispatch_queue_create` 生成的 Dispatch 的**同步队列**(Concurrent Dispatch Queue)共同使用。 103 | 104 | 有了这些对于类成员的认识,开始阅读 Downloader 的源码: 105 | 106 | ```objc 107 | /** 108 | * 下载操作 109 | * 110 | * @param url 下载 URL 111 | * @param options 下载操作选项 112 | * @param progressBlock 过程 block 113 | * @param completedBlock 完成 block 114 | * 115 | * @return 遵循 SDWebImageOperation 协议的对象 116 | */ 117 | - (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { 118 | 119 | // 定义下载 operation 120 | __block SDWebImageDownloaderOperation *operation; 121 | // weakly self 接触引用环 122 | __weak __typeof(self)wself = self; 123 | 124 | // 添加回调闭包,传入URL、过程 block、完成 block 125 | [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ 126 | // 设置下载时限,默认为 15 秒 127 | NSTimeInterval timeoutInterval = wself.downloadTimeout; 128 | if (timeoutInterval == 0.0) { 129 | timeoutInterval = 15.0; 130 | } 131 | // 创建 HTTP 请求,并根据下载模式枚举设置相关属性 132 | // 为了防止有可能出现的重复缓存问题,如果没有显式声明需要缓存管理,则不启用图片请求的缓存操作 133 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; 134 | // 是否处理 cookie 135 | request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); 136 | // 是否需要传输数据 137 | // 返回在接到上一个请求的响应之前,是否需要传输数据 138 | request.HTTPShouldUsePipelining = YES; 139 | // 设置请求头,需要根据需要过滤指定 URL 140 | if (wself.headersFilter) { 141 | request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); 142 | } 143 | else { 144 | request.allHTTPHeaderFields = wself.HTTPHeaders; 145 | } 146 | // 创建下载 operation 147 | operation = [[wself.operationClass alloc] initWithRequest:request 148 | inSession:self.session 149 | options:options 150 | progress:^(NSInteger receivedSize, NSInteger expectedSize) { 151 | // strongly self,保证生命周期 152 | SDWebImageDownloader *sself = wself; 153 | if (!sself) return; 154 | // URL 回调数组,以 URL 为 key 存储回调 callback 155 | __block NSArray *callbacksForURL; 156 | dispatch_sync(sself.barrierQueue, ^{ 157 | // 从全局字典中获取指定的 callback 158 | callbacksForURL = [sself.URLCallbacks[url] copy]; 159 | }); 160 | for (NSDictionary *callbacks in callbacksForURL) { 161 | // 执行运行时指定图片的回调 block 162 | dispatch_async(dispatch_get_main_queue(), ^{ 163 | SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; 164 | if (callback) callback(receivedSize, expectedSize); 165 | }); 166 | } 167 | } 168 | completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { 169 | // strongly self, 保证生命周期 170 | SDWebImageDownloader *sself = wself; 171 | if (!sself) return; 172 | // 完成时 callback 取方法与上方相同 173 | // 因为是使用字典进行管理 174 | __block NSArray *callbacksForURL; 175 | 176 | // 需要注意的是,这里使用了栅栏函数解决了选择竞争问题 177 | dispatch_barrier_sync(sself.barrierQueue, ^{ 178 | callbacksForURL = [sself.URLCallbacks[url] copy]; 179 | if (finished) { 180 | [sself.URLCallbacks removeObjectForKey:url]; 181 | } 182 | }); 183 | for (NSDictionary *callbacks in callbacksForURL) { 184 | SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; 185 | if (callback) callback(image, data, error, finished); 186 | } 187 | } 188 | cancelled:^{ 189 | // strongly self,保证生命周期 190 | SDWebImageDownloader *sself = wself; 191 | if (!sself) return; 192 | // 与前方的执行操作进行栅栏隔离操作 193 | // 保证在删除的时候没有执行自定对于 callback 的读写操作 194 | dispatch_barrier_async(sself.barrierQueue, ^{ 195 | [sself.URLCallbacks removeObjectForKey:url]; 196 | }); 197 | }]; 198 | // 是否需要对图片进行压缩处理 199 | operation.shouldDecompressImages = wself.shouldDecompressImages; 200 | 201 | // 认证请求操作,后面详细分析 202 | if (wself.urlCredential) { 203 | operation.credential = wself.urlCredential; 204 | } else if (wself.username && wself.password) { 205 | operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; 206 | } 207 | 208 | // 设置下载操作的优先级操作,需要根据下载模式枚举来判断 209 | if (options & SDWebImageDownloaderHighPriority) { 210 | operation.queuePriority = NSOperationQueuePriorityHigh; 211 | } else if (options & SDWebImageDownloaderLowPriority) { 212 | operation.queuePriority = NSOperationQueuePriorityLow; 213 | } 214 | 215 | // 向下载操作的队列中增加当前操作 216 | [wself.downloadQueue addOperation:operation]; 217 | if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { 218 | // 如果执行顺序为后进先出的栈结构 219 | // 则将新添加的 operation 作为当前最后一个 operation 的依赖,按照顺序逐个执行 220 | [wself.lastAddedOperation addDependency:operation]; 221 | wself.lastAddedOperation = operation; 222 | } 223 | }]; 224 | ``` 225 | 226 | 整个流程已经了解,下面分析一些细小的细节问题: 227 | 228 | ## 全局字典,将 URL 与回调 block 的映射容器 229 | 230 | ```objc 231 | __block NSArray *callbacksForURL; 232 | 233 | dispatch_sync(sself.barrierQueue, ^{ 234 | // dispatch_barrier_sync (sself.barrierQueue, ^{ 235 | callbacksForURL = [sself.URLCallbacks[url] copy]; 236 | }); 237 | for (NSDictionary *callbacks in callbacksForURL) { 238 | dispatch_async(dispatch_get_main_queue(), ^{ 239 | SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; 240 | if (callback) callback(receivedSize, expectedSize); 241 | }); 242 | } 243 | ``` 244 | 245 | 在执行进行中 block、完成 block 的时候,都会使用以上这几行代码。其作用是为了维护一个字典,key 为图片的唯一标识 url ,值为一个 block 的数组,来统一管理这些回调方法。其大致的结构图如下表示: 246 | 247 | ![URLCallBacks](http://7xwh85.com1.z0.glb.clouddn.com/URLCallBacks.png) 248 | (图片来源:[polobymulberry](http://www.cnblogs.com/polobymulberry/p/5017995.html#_label4)) 249 | 250 | 执行过程中的 block 的时候,在初始化字典管理的时候使用了 `dispatch_sync` 同步执行操作,而没有增加栅栏函数(注释中为增加栅栏函数)。但在对于完成 block 的管理时,为了保证线程安全的竞争选择问题,SD 作者选用了栅栏函数对线程进行了先后执行的规定。为什么这里不用栅栏呢?笔者的理解如下:**由于这两个位置,都是对于 `URLCallbacks` 的读写操作,而在这之前是没有任何更新 `URLCallbacks` 的操作,所以不需要设置栅栏,只需要同步继续即可。而对于栅栏函数,是用在异步操作中对于操作顺序进行控制,由于 SD 需要支持多图片同时下载,所以需要在每次的 `URLCallbacks` 写数据结束后,再进行读操作。** 251 | 252 | ## NSMutableURLRequest 网络请求 253 | 254 | ```objc 255 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url 256 | cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) 257 | timeoutInterval:timeoutInterval]; 258 | ``` 259 | 260 | `initWithURL` 的作用是根据 url 、缓存策略(Cache Policy)、下载最大时限(Time Out Interval)来产生一个 `NSURLRequest`。先来看下缓存策略的选择: 261 | 262 | * **SDWebImageDownloaderUseNSURLCache**:在 SDWebImage 中,默认条件下,请求是不使用 `NSURLCache` 的。如果使用该选项,`NSURLCache` 就应该使用默认的缓存策略 `NSURLRequestUseProtocolCachePolicy`。 263 | * **NSURLRequestUseProtocolCachePolicy**:对特定 url 请求使用网络协议(例如 HTTP)中实现的缓存逻辑。这是一个默认的策略。该策略表示如果缓存不存在,直接从服务端获取。如果缓存存在,会根据 Response 中的 Cache-Control 字段判断下一步操作。例如:当 Cache-Control 字段为 must-revalidata ,则会询问服务端该数据是否有更新,无更新则返回给用户缓存数据,若已经更新,则请求服务器以获取最新数据。 264 | * **NSURLRequestReloadIgnoringLocalCacheData**:数据需要从原始地址(一般就是重新从服务器获取)。不使用现有缓存。 265 | 266 | ```objc 267 | // 要点一 268 | request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); 269 | 270 | // 要点二 271 | request.HTTPShouldUsePipelining = YES; 272 | 273 | // 要点三 274 | if (wself.headersFilter) { 275 | request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); 276 | } 277 | else { 278 | request.allHTTPHeaderFields = wself.HTTPHeaders; 279 | } 280 | ``` 281 | 282 | 后面就是对于 request 的一些属性的设置,从属性名上可以看出使用的是 HTTP 协议: 283 | 284 | * 要点一:`HTTPShouldHandleCookies` 如果设置为 YES,在处理时我们直接查询 `NSHTTPCookieStore` 中的 cookies 即可。`HTTPShouldHandleCookies` 这个策略表示是否应该给 Request 设置 cookie 并伴随着 Request 一起发送出去。然后 Response 返回的 cookie 会继续根据访问策略(Cookie Acceptance Policy)接收到系统中。 285 | * 要点二:`HTTPShouldUsePipelining` 表示 receiver (常常理解为 client 客户端)的下一个信息是否必须等到上一个请求回复才能发送。如果为 YES 表示可以, NO 反之。这个就是我们常常提到的 **HTTP 管线化**(HTTP Pipelining),如此可以显著降低请求的加载时间。 286 | * 要点三:`headersFilter` 是使用自定义方法来设置 HTTP 的 Head Filed。这里可以看下 HTTPHeader 的初始化(下载 webp 图片与通常情况下的 header 不同): 287 | 288 | ```objc 289 | #ifdef SD_WEBP 290 | _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; 291 | #else 292 | _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy]; 293 | #endif 294 | ``` 295 | 296 | ## NSURLCredential 身份认证 297 | 298 | 299 | > web 服务可以在返回 HTTP 响应时附带认证要求的 Challenge,作用是询问 HTTP 请求的发起方是谁,这时候发起方应提供正确的用户名和密码(认证信息),然后 web 服务才会返回真正的 HTTP 响应。 300 | 301 | > 收到认证要求时,`NSURLConnection` 的委托对象会收到相应的消息并得到一个 `NSURLAuthenticationChallenge` 实例。该实例的发送方遵守 `NSURLAuthenticationChallengeSender` 协议。为了继续收到真实的数据,需要向该发送方向发回一个 `NSURLCredential` 实例。 302 | 303 | ```objc 304 | if (wself.urlCredential) { 305 | operation.credential = wself.urlCredential; 306 | } else if (wself.username && wself.password) { 307 | operation.credential = [NSURLCredential credentialWithUser:wself.username 308 | password:wself.password 309 | persistence:NSURLCredentialPersistenceForSession]; 310 | } 311 | ``` 312 | 313 | 当已经有用 `NSURLCredential` ,则直接使用,没有的话则重新构建一个实例并存储下来。`NSURLCredential` 在其中的作用就是缓存对于证书的授权处理。这是对于 https 协议而言,如果想了解更多建议阅读 [Foundation的官方文档](https://developer.apple.com/reference/foundation/nsurlcredential)。 314 | 315 | ## 总结 316 | 317 | 在 Downloader 中,主要的操作就是用于组织一个 `URLCallbacks` 字典,用于管理图片指定的进行 block 、完成 block。并且,在 `downloadImageWithURL:` 方法中,Downloader 其实一直在更新一个 operation 并作为返回值。所以,Downloader 的主要作用是实现多图片异步下载请求,并将其封装为一个 operation 提交给上层统一管理。 318 | 319 | 下一篇主要讲解一下 DownloaderOperation 下载操作任务管理。 320 | 321 | 322 | 323 | 324 | 325 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 326 | 327 | 328 | -------------------------------------------------------------------------------- /Objective-C/SDWebImage/SDWebImage Source Probe - WebCache.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](https://desgard.com/SDWebImage1/) 4 | 5 | # SDWebImage Source Probe: WebCache 6 | 7 | 最近两天,在完成工作业务之余,除了看书,自己也要开始深入的阅读经典的源码。来完善我的 [iOS 源码探求](https://desgard.com/iOS-Source-Probe/) 系列文章。 8 | 9 | 对源码的阅读是一个长久的学习过程,我会将业务中最常用的一些经典三方库拿出来进行学习。这一点我很敬佩 [@Draveness](http://draveness.me/) 的精神,并向他看齐。 10 | 11 | ## SDWebImage 简单介绍 12 | 13 | [SDWebImage](https://github.com/rs/SDWebImage) 根据官方文档,其实就是提供了以下功能: 14 | 15 | > Asynchronous image downloader with cache support with an UIImageView category. 16 | 17 | 一个异步下载图片并且带有 `UIImageView` Category 的缓存库。其好用的原因还在于其简介的接口。话不多说,开始主要内容。本系列文章使用的 SDWebImage 版本为 `v3.8.1`。 18 | 19 | ## 多重入口委托构造器 20 | 21 | 在使用 SD 库的时候,最常调用的方法如下: 22 | 23 | ```c 24 | [self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] 25 | placeholderImage:[UIImage imageNamed:@"placeholder.png"]]; 26 | ``` 27 | 28 | 由此,对 `UIImageView` 的图片一部加载完成了。进入到该方法内部,在其 `.h` 的文件中看到以下接口: 29 | 30 | ```c 31 | - (void)sd_setImageWithURL:(NSURL *)url; 32 | - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; 33 | - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options; 34 | ... 35 | - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock; 36 | ``` 37 | 38 | 作为 SD 的入口函数,在 sd_setImageWithURL 方法中采用了多种参数灵活搭配的同名方法。而内部实质,都在向最后一个 sd_setImageWithURL 传入参数最多的方法进行调用处理。 39 | 40 | 在 c++ 0x 中,这种方式被广泛的使用在系统库的 class 中作为类的委托构造器(Delegate Constructor)。这样做的好处是,**可以清晰的梳理函数构造逻辑,减轻代码编写量**。 41 | 42 | ## setImageWithURL 处理流程 43 | 44 | ```c 45 | // 委托构造器最高级入口 46 | - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { 47 | // 【要点 1】取消该 UIImageView 的下载队列 48 | [self sd_cancelCurrentImageLoad]; 49 | 50 | // 【要点 2】对应的 UIImageView 增加一个对应的 url 属性 51 | objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 52 | 53 | // 根据参数条件,是一个 optional 执行块 54 | // 在未发送请求过去网络图片之前,增加 placeHolder 55 | // 如果有 delay 延迟标记 56 | if (!(options & SDWebImageDelayPlaceholder)) { 57 | // 创建主线程异步队列来更新 UI 58 | dispatch_main_async_safe(^{ 59 | self.image = placeholder; 60 | }); 61 | } 62 | 63 | // 判断 url 是否为空 64 | if (url) { 65 | // 是否展示 ActivityIndicator 66 | if ([self showActivityIndicatorView]) { 67 | [self addActivityIndicator]; 68 | } 69 | // 防止 block 的 retain cycle,进行弱引用转换 70 | __weak __typeof(self)wself = self; 71 | // 使用图片 download 方法,并完成 callback 回调 72 | id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { 73 | // 停止 ActivityIndicator 转动 74 | [wself removeActivityIndicator]; 75 | if (!wself) return; 76 | dispatch_main_sync_safe(^{ 77 | if (!wself) return; 78 | // 图片是否使用了默认参数 79 | if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) 80 | { 81 | // 【要点 3】对 download 中的 completedBlock 成员传参 82 | completedBlock(image, error, cacheType, url); 83 | return; 84 | } 85 | else if (image) { 86 | // 更新图片,需要重新进行 layout 布局,主动调用 setNeedsLayout 方法 87 | wself.image = image; 88 | [wself setNeedsLayout]; 89 | } else { 90 | if ((options & SDWebImageDelayPlaceholder)) { 91 | wself.image = placeholder; 92 | [wself setNeedsLayout]; 93 | } 94 | } 95 | // 判断 finished 标记,传入 block 方法参数 96 | if (completedBlock && finished) { 97 | completedBlock(image, error, cacheType, url); 98 | } 99 | }); 100 | }]; 101 | // 将上面的 operation 添加到字典中,key 为 UIImageViewImageLoad 102 | [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; 103 | } else { 104 | // 处理 url 为 nil 的状态 105 | // 保证在主线程中处理,因为涉及到 UI 106 | dispatch_main_async_safe(^{ 107 | [self removeActivityIndicator]; 108 | if (completedBlock) { 109 | NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; 110 | completedBlock(nil, error, SDImageCacheTypeNone, url); 111 | } 112 | }); 113 | } 114 | } 115 | ``` 116 | 117 | 下面来看深入看一下上述代码注释中的 3 个要点。 118 | 119 | * [self sd_cancelCurrentImageLoad] 120 | 121 | 进入函数内层是以下代码: 122 | 123 | ```c 124 | - (void)sd_cancelCurrentImageLoad { 125 | // 通过 key 来取消操作 126 | [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]; 127 | } 128 | ``` 129 | 130 | ```c 131 | // UIView+WebCacheOperation.m 132 | 133 | - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key { 134 | // Cancel in progress downloader from queue 135 | // 取消正在下载的队列 136 | NSMutableDictionary *operationDictionary = [self operationDictionary]; 137 | id operations = [operationDictionary objectForKey:key]; 138 | // 如果 operationDictionary 可以取到 key,则可以得到与该视图相关的操作 139 | // 并根据 key 从字典中取消这些操作 140 | if (operations) { 141 | // 检查 operations 是否为 array ,防止重名 142 | if ([operations isKindOfClass:[NSArray class]]) { 143 | // 比那里当中所有遵循给定代理的对象,对其下载任务进行取消 144 | for (id operation in operations) { 145 | if (operation) { 146 | [operation cancel]; 147 | } 148 | } 149 | // 如果不是集合,那么可能是一个下载对象 150 | } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){ 151 | [(id) operations cancel]; 152 | } 153 | // 所有元素已过滤,删除 key 154 | [operationDictionary removeObjectForKey:key]; 155 | } 156 | } 157 | ``` 158 | 159 | 从代码中,可以看出:SD 使用 `NSDictionary` 来管理满足 `SDWebImageOperation` 代理的实例。通过对代理实例的判断,以及使用键值查询 operation 的方式,SD 可以有效、迅速的管理所有下载任务。 160 | 161 | * objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 162 | 163 | 这里是使用关联对象(Associated Object)(如果这里陌生,可以查阅 *[浅谈Associated Objects](https://desgard.com/Associated-Objects/)* 这篇博文),来对 UIImageView 做了一个关联值。在第二个参数上,一般是关联对象的唯一标记,在 *UIImageView_WebCache.m* 中使用了一个静态量地址来作为这个 key。 164 | 165 | ```c 166 | static char imageURLKey; 167 | ``` 168 | 169 | 增加这个 url 属性便于在其他地方迅速访问。在获取时候,只需要调用 `- (NSURL *)sd_imageURL` 便可以直接通过关联对象迅速查询。这个 url 的访问在其他地方会有多次调用。 170 | 171 | * completedBlock(image, error, cacheType, url); 172 | 173 | 默认情况下,SD 会等 image 完全从网络下载完成之后,直接替换 UIImageView 中的 image 。如果想在获取图片之后,手动处理之后的所有事情,则需要设置此方法。 174 | 175 | 这个方法是将会在 `SDWebImageManager.h` 出现。 176 | 177 | ## setAnimationImagesWithURLs 图片组 178 | 179 | 在这个 Category 中,还有对于动画组图的设置。观看源码可知,这个方法在实现上是将多个图片的 URL 打包成 array,传入 `sd_setImageLoadOperation` 方法来增加图片加载的 Operation 。而在打包中,相当于多次执行了 `sd_setImageWithURL` (其中的处理细节是一样的)。唯一不同的是,`setAnimationImagesWithURLs` 没有去设置关联对象。因为在展示中,我不需要对其做任何的控制,所以也就没有提供访问的快捷方法。 180 | 181 | ## UIImageView+WebCache.m 源码解读总结 182 | 183 | 对于常用的 `setImageWithURL` 方法,可以总结成以下流程: 184 | 185 | ![UIImageView+WebCache](http://7xwh85.com1.z0.glb.clouddn.com/UIImageView+WebCache.png) 186 | 187 | 从最常用的 `setImageWithURL` 可以看出,其实 SDWebImage 的逻辑很清晰,其源码阅读起来可读性也很高。 188 | 189 | 在阅读三方库源码的同时,也可以感受到作者的代码经验所在。如同委托构造的方式,也是经验的积累总结。所以在学习代码的同时,也可以学习编码思想。 190 | 191 | ## 延伸阅读 192 | 193 | [cocoadocs SDWebImage](http://cocoadocs.org/docsets/SDWebImage/3.7.0/Categories/UIImageView+WebCache.html#//api/name/sd_setAnimationImagesWithURLs:) 194 | 195 | [iOS 源代码分析----SDWebImage](http://draveness.me/ios-yuan-dai-ma-jie-xi-sdwebimage/) 196 | 197 | > 若想查看更多的iOS Source Probe文章,收录在这个[Github仓库中](https://github.com/Desgard/iOS-Source-Probe)。 198 | 199 | 200 | -------------------------------------------------------------------------------- /Python/Shadowsocks/Shadowsocks Probe I - Socks5 与 EventLoop 事件分发.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](http://www.desgard.com/Shadowsocks-1/) 4 | 5 | # Shadowsocks Probe I - Socks5 与 EventLoop 事件分发 6 | 7 | ## #define 爱国 科学 8 | 9 | 最近 Apple Store 在大陆下架了所有 VPN 应用。然而日常的爱国上网已经成为了刚需。这也就是促使我阅读 Shadowsocks 源码的原因。希望后期可以自行编写移动设备的 Client 端而努力。 10 | 11 | ## Shadowsocks 的原理初探 12 | 13 | 关于 Shadowsocks 的原理,有一张经典的图示解释的十分清晰: 14 | 15 | ![whats-shadowsocks-041](media/15024138759233/whats-shadowsocks-041.png) 16 | 17 | (该图引用自 [vc2tea · 写给非专业人士看的 Shadowsocks 简介](http://vc2tea.com/whats-shadowsocks/)) 18 | 19 | Shadowsocks 是将原先的 ssh 创建的 Sock5 协议分成了 Server 端和 Client 端,这是一种类 **ssh tunnel** 的解决方案。 20 | 21 | 客户端发出的 Socks5 协议与 SS Local 进行通信以后,由于 SS Local 是当前使用端或是一个路由器越过 GFW,与 SS Server 进行通信,避免了 GFW 的分析干扰问题。并且在 SS Local 和 SS Server 两端可通过各种各样的加密方式进行通信,并且经过 GFW 的网络包就是很普通的 TCP 包,没有特征码,GFW 也无法对其数据进行解密。SS Server 对数据进行解密,还原请求并触发,在以相同的通信方式回传 SS Local。 22 | 23 | 一句话总结就是 Shadowsocks 可以加密数据包并伪装成常规的 TCP 包,从而达到数据交互。 24 | 25 | ## Socks5 协议 26 | 27 | [Shadowsocks 源码分析——协议与结构](https://loggerhead.me/posts/shadowsocks-yuan-ma-fen-xi-xie-yi-yu-jie-gou.html) 这篇文中讲述了 Socks5 协议的三个过程:**握手阶段**、**建立连接**、**传输阶段**。再具体一些可将其扩展成这么一个工作流: 28 | 29 | 1. Client 向 Proxy 发出请求信息,用以写上传输方式; 30 | 2. Proxy 做出应答; 31 | 3. Client 接到应答后向 Proxy 发送 *Destination Server* (很多书中称之为**目的主机**)的 IP 和 Port; 32 | 4. Proxy 来评估 *Destination Server* 的主机地址,并返回自身的 IP 和 Port,此时 C/P 的链接建立; 33 | 5. Proxy 和 Dst Server 链接; 34 | 6. Proxy 将 Client 发出的信息传至 Server,将 Server 响应的信息转发给 Client,完成整个代理过程。 35 | 36 | 在 Client 连接 Proxy 的时候,通过第一个报文信息来**协商认证**,比如其中的信息包括:是否使用用户名/密码方式进行认证等等。以下是格式信息,数字表示对应字段占用的 Byte 值: 37 | 38 | ```bash 39 | +----+----------+----------+ 40 | |VER | NMETHODS | METHODS | 41 | +----+----------+----------+ 42 | | 1 | 1 | 1~255 | 43 | +----+----------+----------+ 44 | ``` 45 | 46 | * `VER`:是当前协议的版本号,这里是 `5`; 47 | * `NMETHODS`:是 `METHODS` 字段占用的 Byte 数; 48 | * `METHOD`:每一个字节表示一种认证方式,表示客户端支持的全部认证方式。 49 | 50 | Proxy 在收到客户端请求后,检查是否有认证方式,并返回一下格式的消息: 51 | 52 | ```bash 53 | +----+--------+ 54 | |VER | METHOD | 55 | +----+--------+ 56 | | 1 | 1 | 57 | +----+--------+ 58 | ``` 59 | 60 | 对于 Shadowsocks 而言,只有两种可能: 61 | 62 | * `0x05 0x00`:告诉 Client 采用无认证方式来建立连接; 63 | * `0x05 0xff`:客户端的任意一种认证方式 Proxy 都不支持。 64 | 65 | ![handshake-time](media/15024138759233/handshake-time.png) 66 | 67 | 在握手之后,Client 会向 Proxy 发送请求,格式如下: 68 | 69 | ```bash 70 | +----+-----+-------+------+----------+----------+ 71 | |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 72 | +----+-----+-------+------+----------+----------+ 73 | | 1 | 1 | 1 | 1 | Variable | 2 | 74 | +----+-----+-------+------+----------+----------+ 75 | ``` 76 | 77 | * `CMD`:一些配置标识,Shadowsocks 只用到了以下两种: 78 | * `0x01`:建立 TCP 连接; 79 | * `0x03`:关联 UDP 请求; 80 | * `RSV`:保留字段,值为 `0x00`; 81 | * `ATYP`:`address type` 的缩写,取值为: 82 | * `0x01`:IPv4; 83 | * `0x03`:域名; 84 | * `0x04`:IPv6 85 | * `DST.ADDR`:`destination address` 的缩写,取值会随着 `ATYP` 变化: 86 | * `ATYP == 0x01`:4 个字节的 IPv4 地址; 87 | * `ATYP == 0x03`:1 个字节表示域名长度,紧随其后的是对应的域名; 88 | * `ATYP == 0x04`:16 个字节的 IPv6 地址; 89 | * `DST.PORT` 字段:目的服务器端口号。 90 | 91 | 在收到请求后,Proxy 也会对应的返回如下格式的消息: 92 | 93 | ```bash 94 | +----+-----+-------+------+----------+----------+ 95 | |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 96 | +----+-----+-------+------+----------+----------+ 97 | | 1 | 1 | 1 | 1 | Variable | 2 | 98 | +----+-----+-------+------+----------+----------+ 99 | ``` 100 | 101 | `REP` 字段是用来告知 Client 请求的处理情况,正常情况下 Shadowsocks 会将其填充为 `0x00`,否则直接断开连接。其他的字段含义均同发送包的字段含义相同。 102 | 103 | 在万事具备之后,Socks5 协议就完成了自身的主要实名,在握手和建立连接之后,Socks5 的 Proxy 服务器就只做简单的消息转发。我们以通过 Shadowsocks 代理来访问 `apple.com:80` 为例,整个过程如下图所示: 104 | 105 | ![visit-apple.com.re](media/15024138759233/visit-apple.com.re.png) 106 | 107 | 而信息的传输过程可能是这样的: 108 | 109 | ```bash 110 | # 握手阶段 111 | # 无验证最简单的握手 112 | client -> ss: 0x05 0x01 0x00 113 | ss -> client: 0x05 0x00 114 | # 建立连接 115 | # b'apple.com' 表示 'apple.com' 的 ASCII 码 116 | client -> ss: 0x05 0x01 0x00 0x03 0x0a b'apple.com' 0x00 0x50 117 | ss -> client: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10 118 | # 传输阶段 119 | client -> ss -> remote 120 | remote -> ss -> client 121 | ... 122 | ``` 123 | 124 | ## Shadowsocks 模块划分 125 | 126 | 在真正深入到源码之前,先看看各个模块的主要功能划分: 127 | 128 | ```bash 129 | .(shadowsocks) 130 | ├── __init__.py 131 | ├── asyncdns.py # 实现了简单的异步 DNS 查询 132 | ├── common.py # 提供一些工具函数,重要的是解析 Socks5 请求 133 | ├── crypto # 封装加密库的调用 134 | │   ├── __init__.py 135 | │   ├── openssl.py 136 | │   ├── rc4_md5.py 137 | │   ├── sodium.py 138 | │   ├── table.py 139 | │   └── util.py 140 | ├── daemon.py # 用于实现守护进程(daemon) 141 | ├── encrypt.py # 提供统一的加密和解密接口 142 | ├── eventloop.py # 封装了 IO 常用方法 epoll, kqueue 和 select ,提供统一接口 143 | ├── local.py # shadowsocks 客户端入口 - sslocal 命令 144 | ├── lru_cache.py # LRU Cache,说白了就是限时缓存,过量删除的一种机制 145 | ├── manager.py # 总控入口,用于组织组件逻辑 146 | ├── server.py # shadowsocks 服务端 - ssserver 命令 147 | ├── shell.py # shell 命令封装包 148 | ├── tcprelay.py # 核心部分,实现整个 Socks5 协议,负责 TCP 代理部分 149 | └── udprelay.py # 负责 UDP 代理实现 150 | ``` 151 | 152 | Shadowsocks 利用 Socks5 协议来进行数据传输,而增加的一个过程就是对 TCP 包的数据加密。这里我们称之为能爱国上网的 Socks5 代理: 153 | 154 | ![patriotic-networ-2](media/15024138759233/patriotic-networ-2.png) 155 | 156 | 157 | 在加密解密过程中,数据经过 *sslocal* 加密后转发给 *ssserver*,这是过程中最重要的环节。然后我们开始对细节进行剖析。 158 | 159 | ## server.py 一个通往爱国的大门 160 | 161 | 我们从 `server.py` 这个入口函数开始看起,这样也便于把握整体代码的流程。 162 | 163 | ```python 164 | def main(): 165 | # 配置代码 166 | # 检测 python 版本 167 | shell.check_python() 168 | 169 | # 从命令行中获得配置参数 170 | config = shell.get_config(False) 171 | 172 | # 根据配置决定要不要以 daemon 的方式运行 173 | daemon.daemon_exec(config) 174 | 175 | # 端口加密模式输出 log 176 | ... 177 | tcp_servers = [] 178 | udp_servers = [] 179 | 180 | # dns 服务器配置 181 | if 'dns_server' in config: # allow override settings in resolv.conf 182 | dns_resolver = asyncdns.DNSResolver(config['dns_server'], 183 | config['prefer_ipv6']) 184 | else: 185 | dns_resolver = asyncdns.DNSResolver(prefer_ipv6=config['prefer_ipv6']) 186 | 187 | # 将 port password 存入缓存,从配置字典中删除 188 | port_password = config['port_password'] 189 | del config['port_password'] 190 | # 从配置中读入每一组配置信息 191 | for port, password in port_password.items(): 192 | 193 | a_config = config.copy() 194 | a_config['server_port'] = int(port) 195 | a_config['password'] = password 196 | logging.info("starting server at %s:%d" % 197 | (a_config['server'], int(port))) 198 | # 在做完备份至内存后实例化一个 tcprelay.TCPRelay 并放入指定容器中 199 | tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False)) 200 | udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False)) 201 | run_server() 202 | 203 | def run_server(): 204 | try: 205 | # 创建 EventLoop 206 | loop = eventloop.EventLoop() 207 | dns_resolver.add_to_loop(loop) 208 | 209 | # 观察者模式 210 | # epoll/kqueue/select 观察者 socket 的状态 211 | # 当 socket 状态法伤变化时,调用消息处理函数 212 | # 将已经打开的 socket 注册到 Eventloop 用来监听响应的时间 213 | list(map(lambda s: s.add_to_loop(loop), tcp_servers + udp_servers)) 214 | 215 | daemon.set_user(config.get('user', None)) 216 | # 启动 循环,等待 shadowsocks 客户端的连接 217 | loop.run() 218 | except Exception as e: 219 | shell.print_exception(e) 220 | sys.exit(1) 221 | ``` 222 | 223 | 在 `TCPRelay` 初始化的时候会根据配置项新建一个 Socket 并绑定至指定端口进行监听。下面列举核心代码: 224 | 225 | ```python 226 | # tcprelay.py 227 | 228 | def __init__(self, config, dns_resolver, is_local, stat_callback=None): 229 | # 判断是 SS Local 还是 SS Server,此构造方法两端复用,仅仅是配置文件中的 key 不同 230 | if is_local: 231 | listen_addr = config['local_address'] 232 | listen_port = config['local_port'] 233 | else: 234 | listen_addr = config['server'] 235 | listen_port = config['server_port'] 236 | self._listen_port = listen_port 237 | 238 | addrs = socket.getaddrinfo(listen_addr, listen_port, 0, 239 | socket.SOCK_STREAM, socket.SOL_TCP) 240 | if len(addrs) == 0: 241 | raise Exception("can't get addrinfo for %s:%d" % 242 | (listen_addr, listen_port)) 243 | af, socktype, proto, canonname, sa = addrs[0] 244 | # 创建 Socket 245 | server_socket = socket.socket(af, socktype, proto) 246 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 247 | # 绑定监听端口 248 | server_socket.bind(sa) 249 | server_socket.setblocking(False) 250 | if config['fast_open']: 251 | try: 252 | # 设置 Socket 相关选项,这里只有 TCP,后两个参数是选项值的缓冲以及其最大长度 253 | server_socket.setsockopt(socket.SOL_TCP, 23, 5) 254 | except socket.error: 255 | logging.error('warning: fast open is not available') 256 | self._config['fast_open'] = False 257 |   # 允许连接数 @vegaoqiang 勘误 258 |   server_socket.listen(1024) 259 | self._server_socket = server_socket 260 | self._stat_callback = stat_callback 261 | ``` 262 | 263 | `TCPRelay` 中的 `_server_socket` 表示的是所监听端口的 Socket。再然后当 `TCPRelay` 的 `handle_event` 逻辑便分成了两个部分,如果是 `_server_socket`,那么客户端请求建立连接,`_server_socket` 负责接受之后创建的新的 `TCPRelayHandler`;如果不是,则说明客户端连接的是读写事件,直接分发到对应的 `TCPRelayHandler` 调用 `handle_event` 来处理。这一部分的详细代码在后面可以看到。 264 | 265 | ## 事件处理阶段 266 | 267 | Shadowsocks 封装了三种常见的 IO 复用方法:`epoll`、`kqueue` 和 `select`,并通过 `eventloop.py` 提供统一的接口。之所以要用 OI 复用,因为能提供更好的性能和更少的内存开销。多线程方式在 PC 上表现良好,但是在路由器上就表现不佳了。 268 | 269 | ### eventloop.py 270 | 271 | `eventloop.py` 主要逻辑都在 `run` 方法中。EventLoop 是 Shadowsocks 的网络通信核心模块,它封装了 `epoll`、`kqueue`、`select` 三者,而且为后两者实现了类似 `epoll` 接口,且执行优先级为 `epoll` > `kqueue` > `select`。 272 | 273 | ```python 274 | def run(self): 275 | events = [] 276 | while not self._stopping: 277 | asap = False 278 | # 获取事件 279 | try: 280 | # 等待事件触发,返回触发事件 281 | events = self.poll(TIMEOUT_PRECISION) 282 | except (OSError, IOError) as e: 283 | if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): 284 | # EPIPE: 当客户关闭连接时触发 285 | # EINTR: 收到信号时触发 286 | # 尽可能触发 287 | asap = True 288 | logging.debug('poll:%s', e) 289 | else: 290 | logging.error('poll:%s', e) 291 | traceback.print_exc() 292 | continue 293 | # 找到事件对应的 handler,将事件交由它处理 294 | for sock, fd, event in events: 295 | # 通过 fd 找到对应的 handler 296 | # 一个 handler 可能对应多个 fd (reactor 模式) 297 | # 同步的将请求输入多路复用到 Request Handler 298 | handler = self._fdmap.get(fd, None) 299 | if handler is not None: 300 | handler = handler[1] 301 | try: 302 | # handler 有三种可能 303 | # TCPRelay, UDPRelay, DNSResolver 304 | handler.handle_event(sock, fd, event) 305 | except (OSError, IOError) as e: 306 | shell.print_exception(e) 307 | # 计时器,10s 间隔调用注册的 handler_periodic 函数 308 | now = time.time() 309 | if asap or now - self._last_time >= TIMEOUT_PRECISION: 310 | for callback in self._periodic_callbacks: 311 | callback() 312 | self._last_time = now 313 | ``` 314 | 315 | `run` 方法是一个典型的 Event Loop 方法,通过 `poll` 来阻塞,等待事件繁盛,然后用事件对应的文件描述 `fd` 命中 `handler`,调用 `handler.handle_event(sock, fd, event)` 来讲事件交由 `handler` 处理,同时间隔 `TIMEOUT_PRECISION` 秒调用 `TCPRelay`、`UDPRelay`、`DNSResolver` 的 `handler_periodic` 函数处理超时情况或是清理缓存。 316 | 317 | 看到这里,你肯定对 `TCPRelay` 对于事件封装产生了兴趣,我们重新回到 `tcprelay.py` 文件,来查看一下 `handle_event` 方法。 318 | 319 | ### tcprelay.py 320 | 321 | 之前提到过,一个 `TCPRelay` 会对应监听指定的 Socket,并要分发到指定的 `TCPRelayHandler`,`eventloop.py` 的 `run` 方法中也能看到 `handler.handle_event(sock, fd, event)` 的调用。这个方法是如何实现的呢? 322 | 323 | ```python 324 | def handle_event(self, sock, fd, event): 325 | # 处理 Event 并发送到对应的 handler 326 | if sock: 327 | logging.log(shell.VERBOSE_LEVEL, 'fd %d %s', fd, 328 | eventloop.EVENT_NAMES.get(event, event)) 329 | # 如果是 TCPRelay 的 socket 330 | # 这时候说明有 TCP 连接,创建 TCPRelayHandler 并封装 331 | if sock == self._server_socket: 332 | if event & eventloop.POLL_ERR: 333 | raise Exception('server_socket error') 334 | try: 335 | logging.debug('accept') 336 | # 接受连接 337 | conn = self._server_socket.accept() 338 | # 完成 handler 封装 339 | TCPRelayHandler(self, self._fd_to_handlers, 340 | self._eventloop, conn[0], self._config, 341 | self._dns_resolver, self._is_local) 342 | except (OSError, IOError) as e: 343 | error_no = eventloop.errno_from_exception(e) 344 | if error_no in (errno.EAGAIN, errno.EINPROGRESS, 345 | errno.EWOULDBLOCK): 346 | return 347 | else: 348 | shell.print_exception(e) 349 | if self._config['verbose']: 350 | traceback.print_exc() 351 | else: 352 | if sock: 353 | # 找到 fd 对应的 TCPRelayHandler 354 | handler = self._fd_to_handlers.get(fd, None) 355 | if handler: 356 | # 启用 handler 来处理读写事件 357 | handler.handle_event(sock, event) 358 | else: 359 | logging.warn('poll removed fd') 360 | ``` 361 | 362 | 读写事件由 `EventLoop` 协调后分发给 `TCPRelay`,再经 `TCPRelay` 将事件下发给对应的 `TCPRelayHandler` 处理。那么这个“对应”该如何区别?这是都是 *fd(File Descriptor)* 的功劳。文件描述符 - fd 是内核为了高效管理已被打开的文件所创建的索引,通常用一个非负正数数来区分,用于智代被打开的文件,所有执行的 I/O 操作的系统都会通过文件描述符。 363 | 364 | 扩展来说,每个 Event 可以当做一个服务处理程序的时间。使用 epoll、select 等等方法以及向指定的 `handler` 投递意在实现**解多路分配策略,并同步派发请求及相关请求来处理**,这是 *Reactor Pattern* (反应器模式)的完美实现。说到这里不得说下 Reactor Pattern。 365 | 366 | Reactor 的结构主要由**资源**,**同步事件解多路器**,**分发器**和**请求处理器**组成,并且该系统在原则上是存于单线程系统中。注明项目 **Netty** 就是基于该设计模式。通过 Reactor 的方式,将用户线程轮训 I/O 操作状态的工作统一交给了 `handler_event` 事件循环来进行处理。用户线程注册事件后处理器可以继续执行其他的工作,这就是单线程异步的体现,而 Reactor 线程负责调用内核的 select 函数检测 socket 状态,当 socket 被激活时,通知响应的 Client 线程执行对应的 `handler`。这里给出一张 I/O 多路复用的模型示意图: 367 | 368 | ![IO-muti-road](media/15024138759233/IO-muti-road.png) 369 | 370 | 是不是中间的 Event Loop 感觉似曾相识呢?是的,我们的 `EventLoop` 与其的工作十分类似。在这里,我们完全不需要关注 I/O 的问题,因为这些都已经被封装好了。我们只需要知道,其 Event 传递以及击中对应的 Handler 就已经足够了。下面是 Shadowsocks 中 `EventLoop` 将 Event 发送至指定 Handler 的大体流程: 371 | 372 | ![eventloop-flo](media/15024138759233/eventloop-flow.png) 373 | 374 | ## 待续 375 | 376 | 在笔者阅读 Shadowsocks 源码的时候,也巩固了之前计算机网络和 Linux kernel I/O Operation 有了更进一步的理解和认识。 377 | 378 | 之后我们将对 Shadowsocks 中对于 Event 处理进一步了解,并阅读 TCP Proxy 的相关源码。 379 | 380 | ## 延伸及参考 381 | 382 | * [Shadowsocks 源码分析——协议与结构](https://loggerhead.me/posts/shadowsocks-yuan-ma-fen-xi-xie-yi-yu-jie-gou.html) 383 | * [shadowsocks源码分析:ssserver](http://huiliu.github.io/2016/03/19/shadowsocks.html) 384 | 385 | -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15024138759233/IO-muti-road.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15024138759233/IO-muti-road.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15024138759233/eventloop-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15024138759233/eventloop-flow.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15024138759233/handshake-time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15024138759233/handshake-time.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15024138759233/patriotic-networ-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15024138759233/patriotic-networ-2.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15024138759233/visit-apple.com.re.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15024138759233/visit-apple.com.re.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15024138759233/whats-shadowsocks-041.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15024138759233/whats-shadowsocks-041.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/15033138825661.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/15033138825661.jpg -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/15033138971502.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/15033138971502.jpg -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/15033139083269.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/15033139083269.jpg -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/Agreement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/Agreement.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/STAGE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/STAGE.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/TCPRelay.description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/TCPRelay.description.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/agreement_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/agreement_new.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/handle_addr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/handle_addr.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/handle_addr_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/handle_addr_new.png -------------------------------------------------------------------------------- /Python/Shadowsocks/media/15027549268791/handle_event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/Python/Shadowsocks/media/15027549268791/handle_event.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](banner-logo.jpg) 2 | 3 | (图片设计:**[冬瓜](https://www.behance.net/gallery/41562273/Gua-iOS-Source-Prode)**) 4 | ## Link 5 | 6 | Gitbook:[iOS 源码探求](https://www.gitbook.com/book/desgard/source-probe/details) 7 | 8 | 个人博客:[Guardia · 瓜地](http://desgard.com) 9 | 10 | 小专栏:[iOS 源码探求](https://xiaozhuanlan.com/ios-source-probe) 11 | 12 | > 其中**小专栏**需要付费,其他均为免费。当然如果觉得其中收录文章有价值,欢迎到小专栏付费阅读。**如果需要下载成 pdf、mobi 或 epub 可以到 Gitbook 上免费下载。** 13 | 14 | ## Description 15 | 16 | 为什么要做这个仓库,是为了促进自己阅读代码,从而巩固自身基础。从非科班出身、acm的错误洗礼下,本人需要认识更多的底层知识,从而纠正自己对*Computer Science*的观念。我将自己理解和阅读的一些源码、文档的笔记与博文与大家分享,接受希望大家的指正。倘若你有兴趣和我一起来阅读源码并分享阅读笔记,可以发起`pull request`。 17 | 18 | 如有疑问,可以在issue中发起。讨论得出结论,才能获得进步。 19 | 20 | 本仓库的源码分享暂时以*Objective-C*、*Swift*、*C++*、*C*、*Python*为主,这些是笔者日常接触的语言。如果想与笔者交流,可以关注新浪微博 [@冬瓜争做全栈瓜](http://weibo.com/3633493894/profile?topnav=1&wvr=6) 21 | 22 | 另外,也可以访问作者个人blog来阅读:[http://www.desgard.com](http://www.desgard.com/) 23 | 24 | ## Content 25 | 26 | | Language | Framework | Version | Article | 27 | | ----- | --------- | ------ | -------- | 28 | | Objective-C | `` | `708` | [浅谈Associated Objects](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/%E6%B5%85%E8%B0%88Associated%20Objects.md)
[对象方法消息传递流程](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/objc_msgSend%E6%B6%88%E6%81%AF%E4%BC%A0%E9%80%92%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%20-%20%E5%AF%B9%E8%B1%A1%E6%96%B9%E6%B3%95%E6%B6%88%E6%81%AF%E4%BC%A0%E9%80%92%E6%B5%81%E7%A8%8B.md)
[消息转发过程分析](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/objc_msgSend%E6%B6%88%E6%81%AF%E4%BC%A0%E9%80%92%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%20-%20%E6%B6%88%E6%81%AF%E8%BD%AC%E5%8F%91.md)
[用 isa 承载对象的类信息](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/%E7%94%A8%20isa%20%E6%89%BF%E8%BD%BD%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%B1%BB%E4%BF%A1%E6%81%AF.md)
[weak 弱引用的实现方式](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/weak%20%E5%BC%B1%E5%BC%95%E7%94%A8%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F.md)
[load 方法全程跟踪](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/load%20%E6%96%B9%E6%B3%95%E5%85%A8%E7%A8%8B%E8%B7%9F%E8%B8%AA.md)
[浅谈 block(1) - clang 改写后的 block 结构](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/%E6%B5%85%E8%B0%88%20block%EF%BC%881%EF%BC%89%20-%20clang%20%E6%94%B9%E5%86%99%E5%90%8E%E7%9A%84%20block%20%E7%BB%93%E6%9E%84.md)
[浅谈 block(2) - 截获变量方式](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/%E6%B5%85%E8%B0%88%20block%EF%BC%882%EF%BC%89%20-%20%E6%88%AA%E8%8E%B7%E5%8F%98%E9%87%8F%E6%96%B9%E5%BC%8F.md) 29 | | C | `cctools/include/mach-o` | `895` | [Mach-O 文件格式探索](https://github.com/Desgard/iOS-Source-Probe/blob/master/C/mach-o/Mach-O%20%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E6%8E%A2%E7%B4%A2.md) | 30 | | C | `Fishhook` | | [巧用符号表 - 探求 fishhook 原理(一)](https://github.com/Desgard/iOS-Source-Probe/blob/master/C/fishhook/%E5%B7%A7%E7%94%A8%E7%AC%A6%E5%8F%B7%E8%A1%A8%20-%20%E6%8E%A2%E6%B1%82%20fishhook%20%E5%8E%9F%E7%90%86%EF%BC%88%E4%B8%80%EF%BC%89.md)
[验证试验 - 探求 fishhook 原理(二)](https://github.com/Desgard/iOS-Source-Probe/blob/master/C/fishhook/%E9%AA%8C%E8%AF%81%E8%AF%95%E9%AA%8C%20-%20%E6%8E%A2%E6%B1%82%20fishhook%20%E5%8E%9F%E7%90%86%EF%BC%88%E4%BA%8C%EF%BC%89.md) | 31 | | Objective-C | Foundation | | [从经典问题来看 Copy 方法](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Foundation/%E4%BB%8E%E7%BB%8F%E5%85%B8%E9%97%AE%E9%A2%98%E6%9D%A5%E7%9C%8B%20Copy%20%E6%96%B9%E6%B3%95.md)
[CFArray 的历史渊源及实现原理](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Foundation/CFArray%20%E7%9A%84%E5%8E%86%E5%8F%B2%E6%B8%8A%E6%BA%90%E5%8F%8A%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.md)
[Runloop 记录与源码注释(作者:@kylinroc)](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Foundation/Run%20Loop%20%E8%AE%B0%E5%BD%95%E4%B8%8E%E6%BA%90%E7%A0%81%E6%B3%A8%E9%87%8A.md) 32 | | Objective-C | UIKit | | [复用的精妙 - UITableView 复用技术原理分析](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Foundation/%E5%A4%8D%E7%94%A8%E7%9A%84%E7%B2%BE%E5%A6%99%20-%20UITableView%20%E5%A4%8D%E7%94%A8%E6%8A%80%E6%9C%AF%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md)
[AutoLayout 中的线性规划 - Simplex 算法](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/UIKit/AutoLayout%20%E4%B8%AD%E7%9A%84%E7%BA%BF%E6%80%A7%E8%A7%84%E5%88%92%20-%20Simplex%20%E7%AE%97%E6%B3%95.md) | 33 | | Objective-C | SDWebImage |`v3.8.1` | [SDWebImage Source Probe: WebCache](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/SDWebImage/SDWebImage%20Source%20Probe%20-%20WebCache.md)
[SDWebImage Source Probe: Manager](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/SDWebImage/SDWebImage%20Source%20Probe%20-%20Manager.md)
[SDWebImage Source Probe: Downloader](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/SDWebImage/SDWebImage%20Source%20Probe%20-%20Downloader.md)
[SDWebImage Source Probe: Operation](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/SDWebImage/SDWebImage%20Source%20Probe%20-%20Operation.md) 34 | | Swift | Source Code | `v4.0` | [Swift Probe - Optional](https://github.com/Desgard/iOS-Source-Probe/blob/master/Swift/Swift%20Probe%20-%20Optional.md) | 35 | | Python | Shadowsocks | `v2.9.1` | [Shadowsocks Probe I - Socks5 与 EventLoop 事件分发](https://github.com/Desgard/iOS-Source-Probe/blob/master/Python/Shadowsocks/Shadowsocks%20Probe%20I%20-%20Socks5%20%E4%B8%8E%20EventLoop%20%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91.md)
[Shadowsocks Probe II - TCP 代理过程](https://github.com/Desgard/iOS-Source-Probe/blob/master/Python/Shadowsocks/Shadowsocks%20Probe%20II%20-%20TCP%20%E4%BB%A3%E7%90%86%E8%BF%87%E7%A8%8B.md) | 36 | 37 | 38 | ## Errata 39 | 40 | 可以发起issue或者pull request进行勘误。 41 | 42 | ## Copyright 43 | 44 | © 以下文章版权属于 **《iOS 成长之路》** 所有。 45 | 46 | * [load 方法全程跟踪](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Runtime/load%20%E6%96%B9%E6%B3%95%E5%85%A8%E7%A8%8B%E8%B7%9F%E8%B8%AA.md) 47 | * [CFArray 的历史渊源及实现原理](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Foundation/CFArray%20%E7%9A%84%E5%8E%86%E5%8F%B2%E6%B8%8A%E6%BA%90%E5%8F%8A%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.md) 48 | * [复用的精妙 - UITableView 复用技术原理分析](https://github.com/Desgard/iOS-Source-Probe/blob/master/Objective-C/Foundation/%E5%A4%8D%E7%94%A8%E7%9A%84%E7%B2%BE%E5%A6%99%20-%20UITableView%20%E5%A4%8D%E7%94%A8%E6%8A%80%E6%9C%AF%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md) 49 | 50 | © 以下文章版权属于 **[Devqa 专栏](https://xiaozhuanlan.com/DevQA)** 所有。 51 | 52 | * [巧用符号表 - 探求 fishhook 原理(一)](https://xiaozhuanlan.com/topic/9605283741) 53 | * [验证试验 - 探求 fishhook 原理(二)](https://github.com/Desgard/iOS-Source-Probe/blob/master/C/fishhook/%E9%AA%8C%E8%AF%81%E8%AF%95%E9%AA%8C%20-%20%E6%8E%A2%E6%B1%82%20fishhook%20%E5%8E%9F%E7%90%86%EF%BC%88%E4%BA%8C%EF%BC%89.md) 54 | 55 | ## The MIT License (MIT) 56 | 57 | iOS-Source-Probe 以 MIT 开源协议发布,转载引用请注明出处。 58 | 59 | ![MIT License](https://upload.wikimedia.org/wikipedia/commons/f/f8/License_icon-mit-88x31-2.svg) 60 | 61 | 62 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [简介](README.md) 4 | 5 | ## Part 1 - iOS Runtime 源码解析 6 | * objc/runtime.h 708 7 | * [浅谈Associated Objects](Objective-C/Runtime/浅谈Associated Objects.md) 8 | * [对象方法消息传递流程](Objective-C/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程.md) 9 | * [消息转发过程分析](Objective-C/Runtime/objc_msgSend消息传递学习笔记 - 消息转发.md) 10 | * [用 isa 承载对象的类信息](Objective-C/Runtime/用 isa 承载对象的类信息.md) 11 | * [weak 弱引用的实现方式](Objective-C/Runtime/weak 弱引用的实现方式.md) 12 | * [load 方法全程跟踪](Objective-C/Runtime/load 方法全程跟踪.md) 13 | * [浅谈 block - clang 改写后的 block 结构](Objective-C/Runtime/浅谈 block(1) - clang 改写后的 block 结构.md) 14 | * [浅谈 block - 截获变量方式](Objective-C/Runtime/浅谈 block(2) - 截获变量方式.md) 15 | 16 | ## Part 2 - macOS & iOS 系统源码及相关开源库 17 | * cctools/include/mach-o 895 18 | * [Mach-O 文件格式探索](C/mach-o/Mach-O 文件格式探索.md) 19 | * Fishhook 20 | * [巧用符号表 - 探求 fishhook 原理(一)](C/fishhook/巧用符号表 - 探求 fishhook 原理(一).md) 21 |  * [验证试验 - 探求 fishhook 原理(二)](C/fishhook/验证试验 - 探求 fishhook 原理(二).md) 22 | 23 | ## Part 3 - Foundation 框架源码分析 24 | * Foundation 25 | * [从经典问题来看 Copy 方法](Objective-C/Foundation/从经典问题来看 Copy 方法.md) 26 | * [CFArray 的历史渊源及实现原理](Objective-C/Foundation/CFArray 的历史渊源及实现原理.md) 27 | * [Run Loop 记录与源码注释(作者Kylin)](Objective-C/Foundation/Run Loop 记录与源码注释.md) 28 | 29 | ## Part 4 - UIKit 源码分析 30 | * UIKit 31 | * [复用的精妙 - UITableView 复用技术原理分析](Objective-C/Foundation/复用的精妙 - UITableView 复用技术原理分析.md) 32 | * [AutoLayout 中的线性规划 - Simplex 算法](Objective-C/UIKit/AutoLayout 中的线性规划 - Simplex 算法.md) 33 | 34 | ## Part 5 - SDWebImage 源码分析 35 | * SDWebImage v3.8.1 36 | * [SDWebImage Source Probe: WebCache](Objective-C/SDWebImage/SDWebImage Source Probe - WebCache.md) 37 | * [SDWebImage Source Probe: Manager](Objective-C/SDWebImage/SDWebImage Source Probe - Manager.md) 38 | * [SDWebImage Source Probe: Downloader](Objective-C/SDWebImage/SDWebImage Source Probe - Downloader.md) 39 | * [SDWebImage Source Probe: Operation](Objective-C/SDWebImage/SDWebImage Source Probe - Operation.md) 40 | 41 | ## Part 6 - Swift 源码分析 42 | * Swift v4.0 43 | * [Swift Probe - Optional](Swift/Swift Probe - Optional.md) 44 | 45 | ## Part 7 - Shadowsocks 源码分析 46 | * Shadowsocks v2.9.1 47 | * [Shadowsocks Probe I - Socks5 与 EventLoop 事件分发](Python/Shadowsocks/Shadowsocks Probe I - Socks5 与 EventLoop 事件分发.md) 48 | * [Shadowsocks Probe II - TCP 代理过程](Python/Shadowsocks/Shadowsocks Probe II - TCP 代理过程.md) 49 | 50 | 51 | -------------------------------------------------------------------------------- /Swift/Swift Probe - Optional.md: -------------------------------------------------------------------------------- 1 | > 作者:冬瓜 2 | 3 | > 原文链接:[Guardia · 瓜地](http://www.desgard.com/swift-optional/) 4 | 5 | # Swift Probe - Optional 6 | 7 | 最近在研究 Swift 中好玩的东西,打算将一些学习笔记,整理成一个系列便于自己温习且与大家交流。这次来玩弄一下 Optional。 8 | 9 | ## Optional 引入由来 10 | 11 | Optional 特性是 Swift 中的一大特色,用来解决变量是否存有 `nil` 值的情况。这样既可减少在数据传递过程中,由于 `nil` 带来的不确定性,防止未处理 `nil` 而带来的程序崩溃。 12 | 13 | Optional 在高级语言中其实并不是 Swift 的首创,而是效仿其他语言学习来的特性。2015 年的时候,为了迎合 Swift 的 Optional 特性,在 Objective-C 中也引入了 Nullability 特性。Swift 作为一个强类型语言,需要在编译期进行安全检查,所以引入了类型推断的特性。为了保证推断的安全,于是又引入了 Optional 特性。 14 | 15 | 如果没有 Optional 到底有如何的危险呢?我们用 C++ 的一个例子来看一下: 16 | 17 | ```cpp 18 | #include 19 | using namespace std; 20 | int main() { 21 | auto numbers = { 1, 2, 3 }; 22 | auto iterator_of_4 = std::find(numbers.begin(), numbers.end(), 4); 23 | 24 | if (iterator_of_4 == numbers.end()) { 25 | // 未查找到 4 的操作 26 | cout << "Not found 4" << endl; 27 | } else { 28 | // 代码执行 29 | cout << "Got it" << endl; 30 | } 31 | return 0; 32 | } 33 | ``` 34 | 35 | 在使用迭代器的时候,我们往往要判断迭代器是否已经遍历到末尾,才可以去继续操作。因为有**值不存在的情况**,所以在以往的操作中都会使用**一个特殊值来表示某种特殊的含义**,通常情况下对于这种特殊值称作 *Sentinal Value*,在很多算法书中称其为**哨兵值**。使用哨兵值会有这么两个弊端:其一是**形如 `std::find` 或者是 `std::binary_search` 这种方法都从它们各自的签名以及调用上,都无法得知它的错误情况,以及对应的错误情况处理方式**。另外,以哨兵值的方式,使我们无法通过编译器来强制错误处理的行为。因为编译器对此是毫无感知的,其哨兵值都是由语言作者或是后期开发人员的约定俗成,例如 C 中文件读取的 `open` 函数,在读取失败下为 `-1`,或是上例中 `numbers.end()` 这个迭代位,只有在程序崩溃之后,才能显出原形。 36 | 37 | 为了突出 Optional 的必要性,[泊学网](https://boxueio.com/series/optional-is-not-an-option/ebook/138)(笔者也是最近才看过的,这里推荐一下😎)中给出了一个哨兵值方案也无法解决的问题,这是一个 Objective-C 的例子: 38 | 39 | ```ObjC 40 | NSString *tmp = nil; 41 | 42 | if ([tmp rangeOfString: @"Swift"].location != NSNotFound) { 43 | // Will print out for nil string 44 | NSLog(@"Something about swift"); 45 | } 46 | ``` 47 | 48 | 虽然 `tmp` 的值为 `nil`,但调用 `tmp` 的 `rangeOfString` 方法却是合法的,它会返回一个值为 0 的 `NSRange` ,所以 `location` 的值也是 0。但是 `NSNotFound` 的值却是 `NSIntegerMax`。所以尽管 `tmp` 的值为 `nil`, 我们还能够在 Terminal 中看到 `Something about swift` 的输出。所以,当为 `nil` 的时候,我们仍旧需要特殊考虑。 49 | 50 | 51 | 于是,这就是 Optional 的由来,为了解决使用 Sentinal Value 约定而无法解决的问题。 52 | 53 | ## 使用 Optional 实现方法 54 | 55 | 这里是 Swift Probe 系列,所以我们不说其用法。在 Swift 的源码中,Optional 以枚举类型来定义的: 56 | 57 | ```swift 58 | @_fixed_layout 59 | public enum Optional : ExpressibleByNilLiteral { 60 | case none 61 | case some(Wrapped) 62 | 63 | public init(_ some: Wrapped) 64 | public func map(_ transform: (Wrapped) throws -> U) rethrows -> U? 65 | public func flatMap(_ transform: (Wrapped) throws -> U?) rethrows -> U? 66 | public init(nilLiteral: ()) 67 | public var unsafelyUnwrapped: Wrapped { get } 68 | } 69 | ``` 70 | 71 | 当然在枚举中还有很多方法并没有列出,之后我们详细来谈。在枚举定义之前,有一个属性标识(attribute) - `@_fixed_layout`,由此标识修饰的类型在 SIL (Swift intermediate 72 | Language)生成阶段进行处理。它的主要作用是将这个类型确定为固定布局,也就是在内存中这个类型的空间占用确定且无法改变。 73 | 74 | 由于 Optional 是多类型的,所以我们通过 `` 来声明泛型。`ExpressibleByNilLiteral` 协议仅仅定义了一个方法: 75 | 76 | ```swift 77 | init(nilLiteral: ()) // 使用 nil 初始化一个实例 78 | ``` 79 | 80 | 不看方法,仅仅看这个枚举定义,其实我们就可以模拟一些很简单的方法。例如我们来解决上文中 C++ `std::find` 那个问题,对 `Array` 数据结构来写一个 `extension`: 81 | 82 | ```swift 83 | import Foundation 84 | 85 | enum Optional { 86 | case none 87 | case some(Wrapped) 88 | } 89 | 90 | extension Array where Element: Equatable { 91 | func find(_ element: Element) -> Optional { 92 | var index = startIndex 93 | while index != endIndex { 94 | if self[index] == element { 95 | return .some(index) 96 | } 97 | formIndex(after: &index) 98 | } 99 | return .none 100 | } 101 | } 102 | ``` 103 | 104 | 代码很简单,就是将当前数组做一次遍历来查找这个元素,如果找到则返回一个 `some` 类别代表这个 Optional 结果是存在的。如果没有则返回 `none`。我们来测试一下: 105 | 106 | ![](http://i2.kiimg.com/600799/e68a22fe9728f410.jpg) 107 | 108 | 发现如果 `find` 方法在 `Array` 中无法找到对应元素,则会返回一个 `none` 的 Optional 对象。 109 | 110 | 由于在 Swift 的源码中已经定义了 Optional,并且使用特定的重载标记符号进行简化,所以我们也可以简写上述的 `find` : 111 | 112 | ```swift 113 | extension Array where Element: Equatable { 114 | func find(_ element: Element) -> Index? { 115 | var index = startIndex 116 | while index != endIndex { 117 | if self[index] == element { 118 | return index 119 | } 120 | formIndex(after: &index) 121 | } 122 | return nil 123 | } 124 | } 125 | ``` 126 | 127 | 由于 Swift 通过 `?` 来对 Optional 类型做了简化,所以我们将返回值修改成 `Index?` 即可。其他地方也类似,如果有值直接返回,没有则返回 `nil`。我们使用 `if let` 使用范式来验证一下 Optioinal 的作用: 128 | 129 | ![](http://i2.kiimg.com/600799/25548e1aecb79f8a.jpg) 130 | 131 | ## Optional 中 map 和 flatMap 实现 132 | 133 | 在引入之前,我们来看以下代码: 134 | 135 | ```swift 136 | import Foundation 137 | 138 | let author: String? = "gua" 139 | var AUTHOR: String? = nil 140 | 141 | if let author = author { 142 | let AUTHOR = author.uppercased() 143 | } 144 | ``` 145 | 146 | 我们通过一段小写的 Optional 字符串常量做出修改后来为其他进行赋值。那么如果我们 `AUTHOR` 是个常量应该怎么做呢?其实字符串就是一个包含字符量和 `nil` 量的集合,处理这种集合的时候使用 `map` 就可以解决了: 147 | 148 | ```swift 149 | var AUTHOR: String? = author.map { $0.uppercased() } // Optional("GUA") 150 | ``` 151 | 152 | 这样我们就得到了一个新的 Optional 常量。那么 `map` 方法对于 Optional 量是怎么处理的呢?来阅读以下源码: 153 | 154 | ```swift 155 | @_inlineable 156 | public func map( 157 | _ transform: (Wrapped) throws -> U 158 | ) rethrows -> U? { 159 | switch self { 160 | case .some(let y): 161 | return .some(try transform(y)) 162 | case .none: 163 | return .none 164 | } 165 | } 166 | ``` 167 | 168 | 首先要说明的是 `Wrapped` ,这是 `Optional` 类型的泛型参数,表示 Optional 实际包装的的值类型。 169 | 170 | 另外来解释一下 `rethrows` 关键字:有这么一个场景,在很多方法中要传入一个闭包来执行,当传入的闭包中没有异常我们就不需要处理,有异常的时候,我们需要使用 `throws` 关键字来声明以下,代表我们需要进行异常处理。但是某些情况下,一个闭包函数本身不会产生异常,但是作为其他函数的参数就会出现异常情况。这时候我们使用 `rethrows` 对函数进行声明从而向上层传递异常情况。 171 | 172 | 暂且我们先不去考虑异常情况,根据源码的思路自行实现一个 `map` 方法来处理 Optional 问题: 173 | 174 | ```swift 175 | extension Optional { 176 | func myMap(_ transform: (Wrapped) -> T) -> T? { 177 | if let value = self { 178 | return transform(value) 179 | } 180 | return nil 181 | } 182 | } 183 | ``` 184 | 185 | 很简单的就实现了等同之前 `map` 效果的功能。 186 | 187 | 根据此处的 `map` 实现,继续引入下一个示例: 188 | 189 | ```swift 190 | let stringOne: String? = "1" 191 | let ooo = stringOne.map { Int($0) } // Optional> 192 | ``` 193 | 194 | 由于 `Int($0)` 会返回一个 `Int?` 的 Optional 量,而 `map` 由之前的源码可知,又会返回一个 Optional 类型,因此 `ooo` 变量就是一个双层嵌套 Optional 对象。而我们希望的仅仅是返回一个 `Int` 型整数就好了,此时引入 `flatMap` 来解决这个问题: 195 | 196 | ```swift 197 | let stringOne: String? = "1" 198 | let ooo = stringOne.flatMap { Int($0) } // Optional 199 | ``` 200 | 201 | `flatMap` 与 `map` 的区别是对 closure 参数的返回值进行处理,之后对其值直接返回,而不会像 `map` 一样对其进行一次 `.some()` 的 Optional 封装: 202 | 203 | ```swift 204 | @_inlineable 205 | public func flatMap( 206 | _ transform: (Wrapped) throws -> U? 207 | ) rethrows -> U? { 208 | switch self { 209 | case .some(let y): 210 | return try transform(y) 211 | case .none: 212 | return .none 213 | } 214 | } 215 | ``` 216 | 217 | 以上就是对于 Optional 的 `map` 和 `flatMap` 分析。 218 | 219 | ## Nil Coalescing 实现 220 | 221 | 有时候我们需要在 Optional 值为 `nil` 的时候,设定一个默认值。用以往的方法,肯定会使用三元操作符: 222 | 223 | ```swift 224 | var userInput: String? = nil 225 | let username = userInput != nil ? userInput! : "Gua" 226 | ``` 227 | 228 | 如此写法过于冗长,对开发者十分不友好。为了表意清晰,代码方便,Swift 引入了 Nil Coalescing 来简化书写。于是之前的 `username` 的定义可以简写成这样: 229 | 230 | ```swift 231 | let username = userInput ?? "Gua" 232 | ``` 233 | 234 | `??` 操作符强制要求可能为 `nil` 的变量要写在左边,默认值写在右边,这样也统一了代码风格。我们深入到源码来看 Nil Coalescing 操作符的实现: 235 | 236 | ```swift 237 | @_transparent 238 | public func ?? (optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T { 239 | switch optional { 240 | case .some(let value): 241 | return value 242 | case .none: 243 | return try defaultValue() 244 | } 245 | } 246 | ``` 247 | 248 | 解释两个标记: 249 | 250 | 1. `@_transparent`:标明该函数应该在 pipeline 中更早的进行函数内联操作。用于非常原始、简单的函数操作。他与 `@_inline` 的区别就是在没有优化设置的 debug 模式下也会使得函数内连接,与 `@_inline (__always)` 标记十分相似。 251 | 2. `@autoclosure`:这个标记在 @Onevcat 的 [Swifter Tips](http://swifter.tips/autoclosure/) 用已经有很好的介绍和实用场景说明。其作用是**将一句表达式自动地封装成一个闭包**。这样封装的目的是当默认值是经过一系列计算得到结构环境下,实用 `@autoclosure` 封装会简化传统闭包的开销,因为如果是传统闭包需要先执行再判断,而 `@autoclosure` 巧妙的避免了这一点。 252 | 253 | 254 | ## 结语 255 | 256 | Swift 源码分析是笔者一直想开的新坑。本文仅仅介绍了 Optional 的实现中最核心的部分,然而只是 Swift 的冰山一角。希望与读者多多交流,共同进步。 257 | 258 | 259 | ## 参考文献 260 | 261 | [Apple Swift Source Code](https://github.com/apple/swift/blob/master/stdlib/public/core/Optional.swift) 262 | 263 | [Swift 烧脑体操(一) - Optional 的嵌套](http://blog.devtang.com/2016/02/27/swift-gym-1-nested-optional/) 264 | 265 | 266 | -------------------------------------------------------------------------------- /banner-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/banner-logo.jpg -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "cn", 3 | "plugins": [ 4 | "splitter", 5 | "github", 6 | "multipart", 7 | "mathjax" 8 | ], 9 | "pluginsConfig": { 10 | "github": { 11 | "url": "https://github.com/Desgard" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /image/15058343519881/15073563641200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15073563641200.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074291855917.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074291855917.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074302081133.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074302081133.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074350810612.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074350810612.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074371126384.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074371126384.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074373726740.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074373726740.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074375282210.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074375282210.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074383379688.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074383379688.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074385597132.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074385597132.jpg -------------------------------------------------------------------------------- /image/15058343519881/15074386833986.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/15074386833986.jpg -------------------------------------------------------------------------------- /image/15058343519881/mach-o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15058343519881/mach-o.png -------------------------------------------------------------------------------- /image/15101394649922/15119364721450.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15119364721450.jpg -------------------------------------------------------------------------------- /image/15101394649922/15119414520860.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15119414520860.jpg -------------------------------------------------------------------------------- /image/15101394649922/15119418259666.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15119418259666.jpg -------------------------------------------------------------------------------- /image/15101394649922/15119440533652.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15119440533652.jpg -------------------------------------------------------------------------------- /image/15101394649922/15119461061753.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15119461061753.jpg -------------------------------------------------------------------------------- /image/15101394649922/15119572694685.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15119572694685.jpg -------------------------------------------------------------------------------- /image/15101394649922/15133014200273.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15133014200273.jpg -------------------------------------------------------------------------------- /image/15101394649922/15133015712894.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15133015712894.jpg -------------------------------------------------------------------------------- /image/15101394649922/15133019900468.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15133019900468.jpg -------------------------------------------------------------------------------- /image/15101394649922/15133214554985.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15133214554985.jpg -------------------------------------------------------------------------------- /image/15101394649922/15134932813922.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15134932813922.jpg -------------------------------------------------------------------------------- /image/15101394649922/15134938187077.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15134938187077.jpg -------------------------------------------------------------------------------- /image/15101394649922/15134988564426.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15101394649922/15134988564426.jpg -------------------------------------------------------------------------------- /image/15272074383889/15297228541122.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15272074383889/15297228541122.jpg -------------------------------------------------------------------------------- /image/15272074383889/15305805997704.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15272074383889/15305805997704.jpg -------------------------------------------------------------------------------- /image/15272074383889/15306636723931.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/15272074383889/15306636723931.jpg -------------------------------------------------------------------------------- /image/Objective/Foundation/从经典问题来看 Copy 方法/img_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Foundation/从经典问题来看 Copy 方法/img_1.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程/img_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程/img_1.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程/img_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程/img_2.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程/img_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程/img_3.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程/img_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 对象方法消息传递流程/img_4.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 消息转发/img.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/objc_msgSend消息传递学习笔记 - 消息转发/img.sketch -------------------------------------------------------------------------------- /image/Objective/Runtime/weak 弱引用的实现方式/sidetable.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/weak 弱引用的实现方式/sidetable.sketch -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈 block(1) - clang 改写后的 block 结构/img.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈 block(1) - clang 改写后的 block 结构/img.sketch -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈 block(1) - clang 改写后的 block 结构/img_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈 block(1) - clang 改写后的 block 结构/img_1.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈 block(1) - clang 改写后的 block 结构/img_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈 block(1) - clang 改写后的 block 结构/img_2.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈 block(2) - 截获变量方式/block.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈 block(2) - 截获变量方式/block.sketch -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈 block(2) - 截获变量方式/block_forwarding.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈 block(2) - 截获变量方式/block_forwarding.sketch -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈Associated Objects/img_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈Associated Objects/img_1.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈Associated Objects/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈Associated Objects/img_2.png -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈Associated Objects/img_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈Associated Objects/img_3.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/浅谈Associated Objects/img_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/浅谈Associated Objects/img_4.jpg -------------------------------------------------------------------------------- /image/Objective/Runtime/用 isa 承载对象的类信息/isa.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/Runtime/用 isa 承载对象的类信息/isa.sketch -------------------------------------------------------------------------------- /image/Objective/SDWebImage/SDWebImage Source Probe Manager/Manager.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/SDWebImage/SDWebImage Source Probe Manager/Manager.sketch -------------------------------------------------------------------------------- /image/Objective/SDWebImage/SDWebImage Source Probe WebCache/UIImageVIew-WebCache.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/image/Objective/SDWebImage/SDWebImage Source Probe WebCache/UIImageVIew-WebCache.sketch -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desgard/iOS-Source-Probe/90dc5ec6d683da8b228ea537f2eb4c788fc502a2/logo.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iOS-Source-Probe", 3 | "version": "1.0.0", 4 | "description": "![](banner-logo.jpg)", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Desgard/iOS-Source-Probe.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/Desgard/iOS-Source-Probe/issues" 18 | }, 19 | "homepage": "https://github.com/Desgard/iOS-Source-Probe#readme", 20 | "dependencies": { 21 | "gitbook-cli": "^2.3.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gitbook install >/dev/null 4 | echo 'Gitbook 插件更新成功' 5 | 6 | gitbook build >/dev/null 7 | echo '页面生成成功' 8 | 9 | mv _book ../ 10 | cd ../_book 11 | 12 | # git push --force 13 | git init 14 | git checkout --orphan gh-pages >/dev/null 15 | git add . >/dev/null 16 | git commit -am 'release new version' -s >/dev/null 17 | git remote add origin git@github.com:Desgard/iOS-Source-Probe.git 18 | git push origin gh-pages --force >/dev/null 19 | echo 'Gitpages 发布成功' 20 | 21 | # delete 22 | cd .. 23 | rm -rf _book/ 24 | echo '删除临时目录' 25 | 26 | # return 27 | cd iOS-Source-Probe/ 28 | 29 | --------------------------------------------------------------------------------