0%

CodeRunner本身是一个收费软件,功能强大,好处就不说了。在一些破解网站上可以找到破解版的,断网输入对应的激活码就能破解成功。

问题

每次重新打开,都需要重新破解。

解决方案

猜想的是每次打开CodeRunner时都会向其主站发送消息,查看是否激活,这里就是通过直接断开链接的方式来处理。

step1

Mac系统下打开/private/etc/文件,hosts是文本。

step2

在hosts文件中,增加一行:

1
2
# CodeRunner App
127.0.0.1 coderunnerapp.com

这样就可以验证成功了,不需要每次都重新激活。

出现的问题

在使用github的时候,执行git pull或者git push时,经常会出现以下错误:

【Failed to connect to github.com port 443: Operation timed out】

这个时候就一通百度、google发现有解决方案:

1
2
3
// 注意啊、这个是不行的
git config --global https.proxy http://127.0.0.1:1080
git config --global http.proxy http://127.0.0.1:1080

这个时候你可能觉得:OK终于解决了。

但是,可能再你下次使用的时候又会出现类似的问题,或者又有新的问题出现。

解决方案

step 1

打开网站:https://github.com.ipaddress.com/

web页面不要关,一会要用

step 2

打开网站:https://fastly.net.ipaddress.com/github.global.ssl.fastly.net

web页面不要关,一会要用

step 3

打开网站https://github.com.ipaddress.com/assets-cdn.github.com

web页面不要关,一会要用

step 4

打开系统host,进行编辑,我这里使用的是Mac,命令如下:

1
sudo vim /etc/hosts

sudo命令需要输入密码,之后,把我们上面打开的3个web对应的ip和host绑定,如下图:

1
2
3
4
5
6
7
8
# ip            对应的host    
# Github
140.82.114.4 github.com
199.232.69.194 github.global.ssl.fastly.net
185.199.108.153 assets-cdn.github.com
185.199.109.153 assets-cdn.github.com
185.199.110.153 assets-cdn.github.com
185.199.111.153 assets-cdn.github.com

ip以自己打开的那3个web显示的为准。Windows请自行百度如何操作host。

step 5

如果设置了http.proxyhttps.proxy http/https代理,需要取消代理。

1
2
git config --global --unset http.proxy
git config --global --unset https.proxy

step 6

刷新DNS,如果机型不同,不起作用,请自行查看还原OS X 中的DNS缓存

1
https://support.apple.com/zh-cn/HT202516

到这里就可以正常使用了。

Mach-O简介

Mach-O其实是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式, 类似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)。

Mach-O是一种用于可执行文件、目标代码、动态库的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性。

MachO格式的常见文件

  • 目标文件.o
  • 库文件
    • .a
    • .dylib
    • .Framework
      *可执行文件
    • dyld
    • .dsym

关于静态库、动态库,这里有一篇文章,写的很好。
iOS静态库 【.a 和framework】【超详细】

clang编译

可以通过clang命令把对应的文件编译成mach-o文件:

1
$ clang -c test.c

就会出现一个test.o的文件,这个就是mach-o类型的文件。

可以通过file命令查看文件类型。

1
2
3
4
5
$ file [文件路径]

// 比如:
$ file test.o
test.o: Mach-O 64-bit object x86_64

说明test.o文件是Mach-O,64位的object,适用于x86架构,64位

1
2
// 编译为可执行文件
$ clang text.o

这个命令会吧test.o文件转换为可执行文件,类型为.out

1
2
$ file test.out
test.out: Mach-O 64-bit executable x86_64

text.out是一个可执行文件。

clang编译多个文件

1
$ clang -o demo test.c test1.c

就是把test.ctest1.c两个文件合并为一个可执行文件demo

如果更改链接到顺序,则生成的可执行文件是不同的,md5值不同。

1
$ clang -o demo1 test1.c test.c

可以查看一下两个文件的md5值,是不同的,命令为md5 [filename]

也可以通过objdump命令查看内容是否一样,这个命令类似于MachOView工具。

1
2
3
$ objdump --macho -d [可执行文件file name]
// 查看demo可执行文件
$ objdump --macho -d demo

dyld

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。

dsym文件

当我们软件 release 模式打包或上线后,不会像我们在 Xcode 中那样直观的看到用崩溃的错误,这个时候我们就需要分析 crash report 文件了,iOS设备中会有日志文件保存我们每个应用出错的函数内存地址,通过 Xcode 的 Organizer 可以将 iOS 设备中的 DeviceLog 导出成 crash 文件,这个时候我们就可以通过出错的函数地址去查询 dSYM 文件中程序对应的函数名和文件名。大前提是我们需要有软件版本对应的 dSYM 文件,这也是为什么我们很有必要保存每个发布版本的 Archives 文件了。

在release下,编译之后查看dsym文件

1
2
3
4
$ file file HookDemo.app.dSYM/Contents/Resources/DWARF/HookDemo
HookDemo.app.dSYM/Contents/Resources/DWARF/HookDemo: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O dSYM companion file arm_v7] [arm64:Mach-O 64-bit dSYM companion file arm64]
HookDemo.app.dSYM/Contents/Resources/DWARF/HookDemo (for architecture armv7): Mach-O dSYM companion file arm_v7
HookDemo.app.dSYM/Contents/Resources/DWARF/HookDemo (for architecture arm64): Mach-O 64-bit dSYM companion file arm64

这里出现了universal binary。这是个啥?就是通用二进制文件

通用二进制文件

mac系统所支持的cpu及硬件平台发生了很大的变化,为了解决软件在多个硬件平台上的兼容性问题,苹果开发了一个通用的二进制文件格式(Universal Binary),又称胖二进制(Fat Binary)。

  • 苹果公司提出的一种程序代码。能同时适用多种架构的二进制文件
  • 同一个程序包中同时为多种架构提供最理想的性能。
  • 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大。但是由于两种架构有共通的非执行资源(代码以外的,图片等),所以并不会达到单一版本的两倍之多。
  • 而且由于执行中只调用一部分代码,运行起来也不需要额外的内存。

在xcode中可以更改Mach-O Type:

对于现在的Xcode来说,iOS11以上通过真机生成的可执行文件都是arm64架构,是单一架构。如果把最低版本修改为iOS10,重新真机编译,发现生成的可执行文件就是两种架构armv7 + arm64

接下来,随便找个工程,release下编译,然后查看HookDemo.app -> HookDemo可执行文件

1
2
3
4
$ file HookDemo
HookDemo: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64:Mach-O 64-bit executable arm64]
HookDemo (for architecture armv7): Mach-O executable arm_v7
HookDemo (for architecture arm64): Mach-O 64-bit executable arm64

也可以通过Targets -> Build Setting - Architectures修改架构,只不过目前来说都是arm64。可以添加armv7、armv7s。

armv7s是一种临时的支持iPhone5c上可用的架构。

iOS 指令集架构 armv6、armv7、armv7s、arm64、arm64e、x86_64、i386

原则上来说,架构都是向下兼容的。

lipo命令拆分、合并通用二进制文件

  • 查看Mach-O文件包含的架构信息

    1
    $ lipo -info [MachO文件]
  • 拆分某种架构

    1
    2
    $ lipo [MachO文件] –thin [架构] –output [输出文件路径]
    $ lipo HookDemo -thin armv7 -output HookDemo_armv7
  • 合并多种架构

    1
    2
    lipo -create [MachO文件1] [MachO文件2] -output [生成的MachO文件]
    lipo -create HookDemo_armv7 HookDemo_arm64 -output HookDemo_v7_64

可以在通过file命令查看拆分、合并的文件。

Macho文件结构

  • Header :包含该二进制文件的一般信息
    • 字节顺序、架构类型、加载指令的数量等。
    • 使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么
  • Load commands:包含加载所需要的指令(动态库、静态库等)
    • 内容包括区域的位置、符号表、动态符号表等
  • Data :内容包括区域的位置、符号表、动态符号表等
    • 包含Segement的具体数据

接下来使用MachOView工具来分析可执行文件,有两种类型:

  1. 通用二进制文件,则显示的是Fat Binary
  2. 单一架构的文件,直接显示对应的Executable

Fat Binary

在上图中可以看到,首先是一个Fat Header的结构。在header中,可以猜到两个架构之间必定存在某些关联。

armv7:offset=16384,size=79872
arm64:offset=98304,size=80672

arm64是从98304开始的,比armv7多了 98304-(16384+79872)=2048,这个值只是一个差值,而从偏移的差值来看98304-16384=81920 = 5 * 16 * 1024

正好是5页数据,iOS中1页是16k,所以armv7是5页数据,最后的2038只是第5页数据没有排满而已。

接下来看看arm64架构下的内容:

在Xcode中,我们可以在loader.h文件中找到Header的相关信息:

1
2
3
4
5
6
7
8
9
10
struct mach_header_64 {
uint32_t magic; /* 魔数,快速定位属于64还是32位 */
cpu_type_t cputype; /* CPU类型 */
cpu_subtype_t cpusubtype; /* CPU的具体类型 */
uint32_t filetype; /* 文件类型,比如可执行文件 */
uint32_t ncmds; /* Load Commands的条数 */
uint32_t sizeofcmds; /* Load Commands的大小 */
uint32_t flags; /* 标志位标识二进制文件支持的功能,主要是和系统加载、链接有关 */
uint32_t reserved; /* reserved */
};

这里只放了arm64下的内容,当然也有32位的,内容基本一致。
这里需要注意的是filettype类型,是一组宏定义,也能找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define	MH_OBJECT	0x1		/* relocatable object file */ object文件
#define MH_EXECUTE 0x2 /* demand paged executable file */ 可执行文件
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */ dylib文件
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static
linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug
sections */ dsym文件
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
#define MH_FILESET 0xc /* a file composed of other Mach-Os to
be run in the same userspace sharing
a single linkedit. */

所以当看到0x2时,标识的就是可执行文件。在MachOView中,在Header中,可以看到对应的数据信息。

Load Commands

LoadCommands 说明
LC_SEGMENT_64 将文件中(32位或64位)的段映射到进程地址空间中,
主要分为__TEXT、__DATA、LINKEDIT几大块
LC_DYLD_INFO_ONLY 动态链接相关信息
LC_SYMTAB 符号地址
LC_DYSYMTAB 动态符号表地址
LC_LOAD_DYLINKER 使用谁加载,我们使用dyld
LC_UUID Mach-O文件的唯一识别标识 UUID
LC_VERSION_MIN_MACOSX 支持最低的操作系统版本
LC_SOURCE_VERSION 源代码版本
LC_MAIN 设置程序主线程的入口地址和栈大小
当别人的app做了防护时,运行就是闪退,这个时候就需要从这里找切入点
LC_ENCRYPTION_INFO_64 加密信息
LC_LOAD_DYLIB 依赖库的路径,包含三方库
LC_FUNCTION_STARTS 函数起始地址表
LC_CODE_SIGNATURE 代码签名

LC_SEGMENT_64

这里面包含了一些基本信息:

  • VM Address:虚拟内存地址
  • VM Size:(虚拟内存)大小为4G
  • File Offset:数据在文件中的偏移地址
  • File Size:数据在文件中的大小

LC_DYLD_INFO_ONLY

这里主要说一下Rebase Info Offset,这个是重定向的偏移地址。

系统为了安全,在运行时,把Mach-O放在虚拟内存中,会随机生成一个ASLR,在运行时会进行重定向,比如查找字符串、方法等,都需要重定向,而重定向的方式是ASLR+Rebase Info Offset的值。

Section64

主要氛围两大部分:

###__TEXT

主要存放:代码、字符常量、类、方法等。

Section 解释
__TEXT, __text 主程序代码段
__TEXT, __stubs Stub可以理解为一段占位空间,placeholder,用于符号的lazy binding。
__TEXT, __stubs_helper 辅助绑定
__TEXT, __cstring C语言字符串
__TEXT, __entitlements __entitlements
__TEXT, __unwind_info C语言字符串
__TEXT, __const 常量段(const修饰)
__TEXT, __objc_classname OC的类名
__TEXT, __objc_methname OC方法名称
__TEXT, __objc_methtype OC方法类型,即方法签名

__DATA:

Section 解释
__DATA_CONST, __got __got
__DATA, __got __got
__DATA__data 已初始化的全局变量。static int a = 1;
__DATA, __bss 未初始化的静态变量。static int a;
__DATA, __const 常量。 char * const p = “foo”;
__DATA, __cfstring 字符串(CFStringRefs)
__DATA, __common 未初始化的外部全局变量。 int a;
__DATA, __la_symbol_ptr 懒绑定的符号指针表。
__DATA, __nl_symbol_ptr 非懒绑定的符号指针表。
__DATA, __objc_classlist OC的类列表,存储一个个指向objc_class结构体的指针
__DATA, __objc_nlclslist OC的类列表,+load相关?
__DATA, __objc_catlist OC的category列表,存储一个个指向__objc_category结构体的指针
__DATA, __objc_protolist OC的协议列表,存储一个个指向protocol_t结构体的指针
__DATA, __objc_imginfo OC的image信息
__DATA, __objc_selrefs 哪些SEL对应的字符串被引用了
__DATA, __objc_classrefs 类的引用,即msg_objSend相关
__DATA, __objc_superrefs super引用,记录了super方法调用的类。
如ViewController中的viewDidLoad中调用了[super viewDidLoad]
则ViewController class即被记录。
也可以理解为objc_msgSendSuper相关。
__DATA, __objc_protorefs 协议引用
__DATA, __objc_ivar 成员变量
__DATA, __objc_const 这里的const与__TEXT->const完全不同。
__objc_const指的是OC内存布局中的不可变部分,即class_ro_t类型。
__DATA, __objc_data 保存类所需的数据?

篇幅原因,动态加载以及符号表下一篇再介绍。

总结

  • Mach-O简介
  • clang命令
    • 编译成点O文件:$ clang -c test.c
    • 把点O文件编译为可执行文件:$ clang text.o
    • 查看文件类型:$ file text.out
  • lipo命令
    • 查看二进制文件 $ lipo -info [MachO文件]
    • 拆分为某一种架构:$ lipo [MachO文件] –thin [架构] –output [输出文件路径]
    • 合并多种架构:$ lipo -create [MachO文件1] [MachO文件2] -output [生成的MachO文件]
  • MachO就结构
    • Header
      • 用于快速群定该文件的CPU类型、文件类型
    • Load Commands
      • 指示加载器如何设置并且加载二进制数据
    • Section64
      • 存放数据:代码、数据、字符串常量、类、方法等

引用

iOS静态库 【.a 和framework】【超详细】
iOS 指令集架构 armv6、armv7、armv7s、arm64、arm64e、x86_64、i386

Runloop

简介

RunLoop是事件接收和分发机制的一个实现,是线程相关的基础框架的一部分,一个RunLoop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。

RunLoop本质是一个 do-while循环,没事做就休息,来活了就干活。与普通的while循环是有区别的,普通的while循环会导致CPU进入忙等待状态,即一直消耗cpu,而RunLoop则不会,RunLoop是一种闲等待,即RunLoop具备休眠功能。

RunLoop的作用

  • 保持程序的持续运行
  • 处理App中的各种事件(触摸、定时器、performSelector)
  • 节省cpu资源,提供程序的性能,该做事就做事,该休息就休息

源码分析

源码下载

runloop与线程

通常情况下获取runloop的两种方式:

1
2
3
4
// 主运行循环
CFRunLoopRef mainRunloop = CFRunLoopGetMain();
// 当前运行循环
CFRunLoopRef currentRunloop = CFRunLoopGetCurrent();

接下来看一下源码:

1
2
3
4
5
6
7
8
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
// 这是一个静态变量
static CFRunLoopRef __main = NULL; // no retain needed
// 没有获取到,则通过_CFRunLoopGet0函数去获取,参数是主线程
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}

查看一下_CFRunLoopGet0函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 如参数t不存在,则默认为主线程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);

// 创建一个字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 创建mainLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

// dict : key value
// 把main_thread和mainloop通过key-value的形式绑定
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}

CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}

// 从字典中通过线程获取run loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
// 没有则创建
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 没有loop也要存,存的是新创建的。
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}

上面的代码可以看出,runloo只有两种类型,一种主线程的mainloop,还有就是其它runloop。

runloop的创建

接下来看runloop是怎么创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
CFRunLoopRef loop = NULL;
CFRunLoopModeRef rlm;
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopTypeID, size, NULL);
// 如果loop为空,则直接返回NULL
if (NULL == loop) {
return NULL;
}
// runloop属性赋值
(void)__CFRunLoopPushPerRunData(loop);
__CFRunLoopLockInit(&loop->_lock);
loop->_wakeUpPort = __CFPortAllocate();
if (CFPORT_NULL == loop->_wakeUpPort) HALT;
__CFRunLoopSetIgnoreWakeUps(loop);
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
loop->_commonModeItems = NULL;
loop->_currentMode = NULL;
loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
loop->_blocks_head = NULL;
loop->_blocks_tail = NULL;
loop->_counterpart = NULL;
loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
loop->_winthread = GetCurrentThreadId();
#else
loop->_winthread = 0;
#endif
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
return loop;
}

里面又有了一个CFRunLoopRef,盲猜应该是结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};

从定义中可以得出,一个RunLoop有多个Mode,意味着一个RunLoop需要处理多个事务,即一个Mode对应多个Item,而一个item中,包含了timer、source、observer,如图:

mode类型

其中mode在苹果文档中提及的有五个,而在iOS中公开暴露出来的只有 NSDefaultRunLoopModeNSRunLoopCommonModes

NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode

  • NSDefaultRunLoopMode:默认的mode,正常情况下都是在这个mode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode:使用这个Mode去跟踪来自用户交互的事件(比如UITableView上下滑动)
  • NSRunLoopCommonModes:伪模式,灵活性更好

source

  • Source0 表示 非系统事件,即用户自定义的事件
  • Source1 表示系统事件,主要负责底层的通讯,具备唤醒能力

Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
//进入RunLoop
kCFRunLoopEntry = (1UL << 0),
//即将处理Timers
kCFRunLoopBeforeTimers = (1UL << 1),
//即将处理Source
kCFRunLoopBeforeSources = (1UL << 2),
//即将进入休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
//被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
//退出RunLoop
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

mode对应的items

  • block:CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
  • timer:CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
  • source0: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
  • source1: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
  • 主队列:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
    *observer: CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION

以Timer为例

在子线程创建的timer是没有办法一直执行的,而想让它继续执行,则需要添加到runloop中,并且run才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
self.isStopping = NO;
NSThread *thread = [[NSThread alloc] initWithBlock:^{

// thread.name = nil 因为这个变量只是捕捉
// LGThread *thread = nil
// thread = 初始化 捕捉一个nil进来
NSLog(@"%@---%@",[NSThread currentThread],[[NSThread currentThread] name]);
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"~~hello word"); // 退出线程--结果runloop也停止了
if (self.isStopping) {
[NSThread exit];
}
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
}];

thread.name = @"lgcode.com";
[thread start];

我们看一下addTimer是怎么操作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
oid CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
__CFRunLoopLock(rl);

// 重点 : kCFRunLoopCommonModes
if (modeName == kCFRunLoopCommonModes) {
//如果是kCFRunLoopCommonModes 类型
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;

if (NULL == rl->_commonModeItems) {
// modeItems是空,则创建一个defalut
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
//runloop与mode 是一对多的, mode与item也是一对多的
CFSetAddValue(rl->_commonModeItems, rlt);
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt};
/* add new item to all common-modes */
//执行
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
//如果是非commonMode类型
//查找runloop的模型
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm) {
if (NULL == rlm->_timers) {
CFArrayCallBacks cb = kCFTypeArrayCallBacks;
cb.equal = NULL;
rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
}
}
//判断mode是否匹配
if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
__CFRunLoopTimerLock(rlt);
if (NULL == rlt->_runLoop) {
rlt->_runLoop = rl;
} else if (rl != rlt->_runLoop) {
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
return;
}
// 如果匹配,则将runloop加进去,而runloop的执行依赖于 [runloop run]
CFSetAddValue(rlt->_rlModes, rlm->_name);
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopTimerFireTSRLock();
__CFRepositionTimerInMode(rlm, rlt, false);
__CFRunLoopTimerFireTSRUnlock();
if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {
// Normally we don't do this on behalf of clients, but for
// backwards compatibility due to the change in timer handling...
if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}

__CFRunLoopUnlock(rl);
}

主要目的就是把timer添加到对应的mode中。mode 和 item是一对多的关系,timer是item的一种。

__CFRunLoopRun

接下来上源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

...
do {
...

__CFRunLoopUnsetIgnoreWakeUps(rl);

if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

__CFRunLoopDoBlocks(rl, rlm);

Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
...

//如果是timer
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}

...

//如果是source1
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
#elif DEPLOYMENT_TARGET_WINDOWS
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
}
...

}while (0 == retVal);

...
}

__CFRunLoopDoTimers源码,主要是通过for循环,对单个timer进行处理。

1
2
3
4
5
6
7
8
9
10
11
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) {    /* DOES CALLOUT */
...
//循环遍历,做下层单个timer的执行
for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
// 执行timer
Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
timerHandled = timerHandled || did;
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// mode and rl are locked on entry and exit
static Boolean __CFRunLoopDoTimer(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt) { /* DOES CALLOUT */
Boolean timerHandled = false;
uint64_t oldFireTSR = 0;

/* Fire a timer */
CFRetain(rlt);
__CFRunLoopTimerLock(rlt);
if (__CFIsValid(rlt) && rlt->_fireTSR <= mach_absolute_time() && !__CFRunLoopTimerIsFiring(rlt) && rlt->_runLoop == rl) {
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopTimerFireTSRLock();
oldFireTSR = rlt->_fireTSR;
__CFRunLoopTimerFireTSRUnlock();

__CFArmNextTimerInMode(rlm, rl);

__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
// 执行timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);
CHECK_FOR_FORK();
if (doInvalidate) {
CFRunLoopTimerInvalidate(rlt); /* DOES CALLOUT */
}
if (context_release) {
context_release(context_info);
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
__CFRunLoopTimerLock(rlt);
timerHandled = true;
__CFRunLoopTimerUnsetFiring(rlt);
}
}

在timer执行的位置打上断点,使用lldb -> bt命令查看调用栈:

timer的调用顺序

  1. 自定义的timer,设置Mode,并将其加入RunLoop中
  2. 在RunLoop的run方法执行时,会调用__CFRunLoopDoTimers执行所有timer
  3. 在__CFRunLoopDoTimers方法中,会通过for循环执行单个timer的操作
  4. 在__CFRunLoopDoTimer方法中,timer执行完毕后,会执行对应的timer回调函数

是针对timer的执行分析,对于observer、block、source0、source1,其执行原理与timer是类似的

runloop底层原理

1
2
3
4
5
6
7
8
void CFRunLoopRun(void) {    /* DOES CALLOUT */
int32_t result;
do {
// 1.0e10 : 科学计数 1*10^10,很大的值
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

runloop就是一个do-while循环。当stop或者执行完成之后,则退出循环。

看一下CFRunLoopRunSpecific的内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);

//首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);

// 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

// 内部函数,进入loop,seconds是一个很大的值
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

// 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

return result;
}

接下来又回到__CFRunLoopRun的代码,上面提到的逻辑只是针对timer的,这里详细的说明一下,使用伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//核心函数
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){

//通过GCD开启一个定时器,然后开始跑圈
dispatch_source_t timeout_timer = NULL;
...
dispatch_resume(timeout_timer);

int32_t retVal = 0;

//处理事务,即处理items
do {

// 通知 Observers: 即将处理timer事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

// 通知 Observers: 即将处理Source事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)

// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);

// 处理sources0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

// 处理sources0返回为YES
if (sourceHandledThisLoop) {
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}

// 判断有无端口消息(Source1)
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
// 处理消息
goto handle_msg;
}


// 通知 Observers: 即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);

// 等待被唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);

// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);

// 通知 Observers: 被唤醒,结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);


handle_msg:
if (被timer唤醒) {
// 处理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}else if (被GCD唤醒){
// 处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}else if (被source1唤醒){
// 被Source1唤醒,处理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}

// 处理block
__CFRunLoopDoBlocks(rl, rlm);

if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;//处理源
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;//超时
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;//停止
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;//停止
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;//结束
}
}while (0 == retVal);

return retVal;
}

整理一下runloop的整体流程如下:

总结

引用

源码下载

autoreleasepool

新建一个工程,在main.m中就有一个autoreleasepool

1
2
3
4
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
}

通过clang编译一下

1
2
3
$ clang -rewrite-objc main.m -o main.cpp
// 或者
$ xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m -o main-arm64.cpp

我们打开对应的cpp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct __AtAutoreleasePool {
//构造函数
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
// 析构函数
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
{
// 是一个结构体
__AtAutoreleasePool __autoreleasepool;
}
return 0;
}

也就是说:

1
2
3
@autoreleasepool {}
//等价于
{__AtAutoreleasePool __autoreleasepool; }

__AtAutoreleasePool是一个结构体,有构造函数析构函数,在结构体定义的对象在作用域结束后,会自动调用析构函数。

在源码中有这么一段话:

Autorelease pool implementation

A thread’s autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased objects are stored.

通过上述描述,可以大概的知道以下几点:

通过描述,有以下几点说明

  1. 自动释放池 是一个 关于指针的栈结构
  2. 其中的指针是指要释放的对象或者 pool_boundary 哨兵(现在经常被称为 边界)
  3. 自动释放池是一个页的结构(虚拟内存中提及过) ,而且这个页是一个双向链表(表示有父节点 和 子节点,在类中有提及,即类的继承链)
  4. 自动释放池和线程有关系

接下来看一下源码中的实现:

AutoreleasePoolPage

1
2
3
4
5
6
7
8
9
10
11
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}

都有一个AutoreleasePoolPage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
`class AutoreleasePoolPage : private AutoreleasePoolPageData`

struct AutoreleasePoolPageData
{
//用来校验AutoreleasePoolPage的结构是否完整
magic_t const magic; //16个字节,以结构体中的变量对齐后的值为准,m[4] = 4*4 =16字节
//指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
__unsafe_unretained id *next;//8字节
//指向当前线程
pthread_t const thread;//8字节
//指向父节点,第一个结点的parent值为nil
AutoreleasePoolPage * const parent;//8字节
//指向子节点,最后一个结点的child值为nil
AutoreleasePoolPage *child;//8字节
//表示深度,从0开始,往后递增1
uint32_t const depth;//4字节
//表示high water mark 最大入栈数量标记
uint32_t hiwat;//4字节

//初始化
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};

接下来我们在回过头看看objc_autoreleasePoolPush

objc_autoreleasePoolPush

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//入栈
static inline void *push()
{
id *dest;
//判断是否有pool
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page
// 自动释放池从新池页面开始
//如果没有,则创建
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//压栈一个POOL_BOUNDARY,即压栈哨兵
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}

autoreleaseNewPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
//获取当前操作页
AutoreleasePoolPage *page = hotPage();
//如果存在,则压栈对象
if (page) return autoreleaseFullPage(obj, page);
//如果不存在,则创建页
else return autoreleaseNoPage(obj);
}

//******** hotPage方法 ********
//获取当前操作页
static inline AutoreleasePoolPage *hotPage()
{
//获取当前页
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
//如果是一个空池,则返回nil,否则,返回当前线程的自动释放池
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}

//******** autoreleaseNoPage方法 ********
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());

bool pushExtraBoundary = false;
//判断是否是空占位符,如果是,则压栈哨兵标识符置为YES
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
//如果对象不是哨兵对象,且没有Pool,则报错
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
objc_thread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
//如果对象是哨兵对象,且没有申请自动释放池内存,则设置一个空占位符存储在tls中,其目的是为了节省内存
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {//如果传入参数为哨兵
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();//设置空的占位符
}

// We are pushing an object or a non-placeholder'd pool.

// Install the first page.
//初始化第一页
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//设置page为当前聚焦页
setHotPage(page);

// Push a boundary on behalf of the previously-placeholder'd pool.
//压栈哨兵的标识符为YES,则压栈哨兵对象
if (pushExtraBoundary) {
//压栈哨兵
page->add(POOL_BOUNDARY);
}

// Push the requested object or pool.
//压栈对象
return page->add(obj);
}

其中autoreleaseNoPage方法中发现当前线程的自动释放池是通过AutoreleasePoolPage创建的,其定义中有构造方法,而构造方法的实现是通过父类AutoreleasePoolPageData的初始化方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//**********AutoreleasePoolPage构造方法**********
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),//开始存储的位置
objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
newParent,
newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
newParent ? newParent->hiwat : 0)
{
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
//this 表示 新建页面,将当前页面的子节点 赋值为新建页面
parent->child = this;
parent->protect();
}
protect();
}

//**********AutoreleasePoolPageData初始化方法**********
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}

看一下几个参数:

1
2
3
4
5
AutoreleasePoolPageData(begin(),//开始存储的位置
objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
newParent,
newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
newParent ? newParent->hiwat : 0)
  • begin():

    1
    2
    3
    4
    5
    //页的开始位置
    id * begin() {
    //等于 首地址+56(AutoreleasePoolPage类所占内存大小)
    return (id *) ((uint8_t *)this+sizeof(*this));
    }
  • objc_thread_self(): 表示的是当前线程,而当前线程时通过tls获取

  • newParent: 父节点

  • 后面两个参数是通过父节点的深度、最大入栈个数计算depth以及hiwat

自动释放池内存结构

由于在ARC模式下,是无法手动调用autorelease,所以将Demo切换至MRC模式。
Build Settings -> Objectice-C Automatic Reference Counting设置为NO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自动释放池打印
extern void _objc_autoreleasePoolPrint(void);

//************运行代码************
int main(int argc, const char * argv[]) {
@autoreleasepool {
//循环创建对象,并加入自动释放池
for (int i = 0; i < 5; i++) {
NSObject *objc = [[NSObject alloc] sutorelease];
}
//调用
_objc_autoreleasePoolPrint();
}
}

查看自动释放池的内存结构,发现page的开始与第一个对象的地址差是0x38,转换成十进制刚好是56,也就是 AutoreleasePoolPage自己本身的内存大小。

但是打印的是5个对象,但是这里有6个。第一个pool是啥?

哨兵对象。只在第一页有,防止释放时越界。

接着我们修改i的最大值为505。发现第二页开始有1个数据了。
接着在继续修改最大值为504+506,发现第三页也开始有1个数据了。

所以第一页有哨兵对象,可以存504个对象。第二页之后可以存505个对象。

在源码中AutoreleasePoolPage->size可以看到一页的大小。

1
2
3
4
5
public:
static size_t const SIZE = PAGE_MIN_SIZE;

#define PAGE_MIN_SHIFT 12
#define PAGE_MIN_SIZE (1 << PAGE_MIN_SHIFT)

size = 1<<12 = 4096。

首地址是从0x38开始的,也就是56个字节,4096-46 = 4040 = 505 * 8
所以一页有506个对象。

autoreleaseFast压栈

push方法的第二步,第一步是创建,第二步是直接存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline id *autoreleaseFast(id obj)
{
//获取当前操作页
AutoreleasePoolPage *page = hotPage();
//判断页是否满了
if (page && !page->full()) {
//如果未满,则压栈
return page->add(obj);
} else if (page) {
// 如果满了,则安排新的页面
return autoreleaseFullPage(obj, page);
} else {
//页不存在,则新建页
return autoreleaseNoPage(obj);
}
}

当前页没有满,则添加add

1
2
3
4
5
6
7
8
9
10
11
12
// 添加释放对象
id *add(id obj)
{
ASSERT(!full());
unprotect();
//传入对象存储的位置
id *ret = next; // faster than `return next-1` because of aliasing
//将obj压栈到next指针位置,然后next进行++,即下一个对象存储的位置
*next++ = obj;
protect();
return ret;
}

如果页满了则处理autoreleaseFullPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//添加自动释放对象,当页满的时候调用这个方法
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);

//do-while遍历循环查找界面是否满了
do {
//如果子页面存在,则将页面替换为子页面
if (page->child) page = page->child;
//如果子页面不存在,则新建页面
else page = new AutoreleasePoolPage(page);
} while (page->full());

//设置为当前操作页面
setHotPage(page);
//对象压栈
return page->add(obj);
}

压栈流程

  1. 如果页存在,且未满,则通过add方法压栈对象
  2. 如果页存在,且满了,则通过autoreleaseFullPage方法安排新的页面
  3. 如果页不存在,则通过autoreleaseNoPage方法创建新页

objc_autoreleasePoolPop

1
2
3
4
// 析构函数
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}

在析构函数中传了一个参数,就是atautoreleasepoolobj对象,这样就可以和创建的autoreleasepool关联上了。接下来看一下pop的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//出栈
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
//判断对象是否是空占位符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
//如果当是空占位符
// Popping the top-level placeholder pool.
//获取当前页
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
//如果当前页不存在,则清除空占位符
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
//如果当前页存在,则将当前页设置为coldPage,token设置为coldPage的开始位置
page = coldPage();
token = page->begin();
} else {
//获取token所在的页
page = pageForPointer(token);
}

stop = (id *)token;
//判断最后一个位置,是否是哨兵
if (*stop != POOL_BOUNDARY) {
//最后一个位置不是哨兵,即最后一个位置是一个对象
if (stop == page->begin() && !page->parent) {
//如果是第一个位置,且没有父节点,什么也不做
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
//如果是第一个位置,且有父节点,则出现了混乱
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}

if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
//出栈页
return popPage<false>(token, page, stop);
}

看一下popPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//出栈页面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
// 出栈当前操作页面对象
page->releaseUntil(stop);

// memory: delete empty children 删除空子项
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
//调试期间删除每个特殊情况下的所有池
//获取当前页面的父节点
AutoreleasePoolPage *parent = page->parent;
//将当前页面杀掉
page->kill();
//设置操作页面为父节点页面
setHotPage(parent);
}
else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
//特殊情况:调试丢失的自动释放池时删除pop(top)的所有内容
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full 如果页面已满一半以上,则保留一个空子级
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}

stop就是自动释放池创建的位置。拿到stop的位置后,释放到stop位置之后的所有对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//释放到stop位置之前的所有对象
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// 不是递归的:我们不想破坏堆栈
// if a thread accumulates a stupendous amount of garbage
//判断下一个对象是否等于stop,如果不等于,则进入while循环
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects 每次从hotPage()重新启动,以防-release自动释放更多对象
//获取当前操作页面,即hot页面
AutoreleasePoolPage *page = hotPage();

// fixme I think this `while` can be `if`, but I can't prove it
//如果当前页是空的
while (page->empty()) {
//将page赋值为父节点页
page = page->parent;
//并设置当前页为父节点页
setHotPage(page);
}

page->unprotect();
//next进行--操作,即出栈
id obj = *--page->next;
//将页索引位置置为SCRIBBLE,表示已经被释放
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();

if (obj != POOL_BOUNDARY) {
//释放
objc_release(obj);
}
}
//设置当前页
setHotPage(this);

#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}

再看一下kill

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//销毁
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
AutoreleasePoolPage *page = this;
//获取最后一个页
while (page->child) page = page->child;

AutoreleasePoolPage *deathptr;
do {
deathptr = page;
//子节点 变成 父节点
page = page->parent;
if (page) {
page->unprotect();
//子节点为nil
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}

autorelease pop流程

  1. 是否是空页,做容错处理
  2. releaseUntil(stop),按页倒序进行,循环释放所有对象,直到位置执行stop。先从child开始
  3. 页中的对象释放之后,page执行kill,循环删除child节点

autorelease底层

看一下mrc下autorelease的源码:

1
2
3
4
5
6
7
8
9
id
objc_autorelease(id obj)
{
//如果不是对象,则直接返回
if (!obj) return obj;
//如果是小对象,也直接返回
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
  • 如果是小对象,则直接return,不处理。
  • 如果是对象,执行obj->autorelease()
1
2
3
4
5
6
7
8
9
10
11
inline id 
objc_object::autorelease()
{
ASSERT(!isTaggedPointer());
// 自定义对象
if (fastpath(!ISA()->hasCustomRR())) {
return rootAutorelease();
}
// 系统
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}

自定义对象,执行rootAutorelease

1
2
3
4
5
6
7
8
9
inline id 
objc_object::rootAutorelease()
{
// 是否为小对象
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

return rootAutorelease2();
}

对象执行rootAutorelease2

1
2
3
4
5
6
7
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}

最后执行的还是AutoreleasePoolPage这个对象,调用autorelease

1
2
3
4
5
6
7
8
9
public:
static inline id autorelease(id obj)
{
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}

这里调用的就是autoreleaseFast。就又回到了上面的压栈流程。

总结

  • @autoreleasepool {} 等价于 {__AtAutoreleasePool __autoreleasepool; },这是构造和析构函数
  • autorelease push压栈流程
    1. 如果页存在,且未满,则通过add方法压栈对象
    2. 如果页存在,且满了,则通过autoreleaseFullPage方法安排新的页面
    3. 如果页不存在,则通过autoreleaseNoPage方法创建新页
  • autorelease pop流程
    1. 是否是空页,做容错处理
    2. releaseUntil(stop),按页倒序进行,循环释放所有对象,直到位置执行stop。先从child开始
    3. 页中的对象释放之后,page执行kill,循环删除child节点
  • mrc下 autorelease原理
    1. 判断是否为小对象,是直接return
    2. 执行autoreleaseFast,执行autorelease push压栈流程

内存管理方案

  1. ARC/MRC
  2. TarggedPointer: 专门用来处理小对象,比如NSNumber、NSDate、(NSString中有一种是targeed pointer)
  3. Nonpointer_isa:非指针类型的isa。主要用来优化64位地址。
  4. SideTables:散列表。主要有两种类型的表,引用计数表,弱引用表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// viewDidLoad添加
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cjl.cn", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
// alloc堆上,iOS优化之后变成 taggedpointer
// nameStr是NSTaggedPointerString
self.nameStr = [NSString stringWithFormat:@"aaa"];
NSLog(@"%@",self.nameStr);
});
}
}

// 点击
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
// nameStr是NSCFString
self.nameStr = [NSString stringWithFormat:@"aa-加油"];
NSLog(@"%@",self.nameStr);
});
}
}

运行上面的代码啊,发现在taggedPointerDemo方法中没有问题,但是点击屏幕,执行了touchesBegan就发生了崩溃。虽然在多线程时有过类似的例子,是由于多次释放造成的。但是这里的根本原因是nameStr在底层的类型不一致导致的,分别在两个赋值的方法处打上断点,看看是什么类型。

  • taggedPointerDemo方法中的nameStr类型是 NSTaggedPointerString,存储在常量区。因为nameStr在alloc分配时在堆区,由于较小,所以经过xcode中iOS的优化,成了NSTaggedPointerString类型,存储在常量区。
  • touchesBegan方法中的nameStr类型是 NSCFString类型,存储在堆上

NSString的类型

  1. NSCFConstantString:字符串常量,是一种编译时常量,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区。
  2. NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上。
  3. NSTaggedPointerString:标签指针,是苹果在64位环境下对NSString、NSNumber等对象做的优化。对于NSString对象来说,当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区。
  4. 当有中文或者其他特殊符号时,会直接成为__NSCFString类型,存储在堆区。

Tagged Pointer小对象

接下来看一下tagged pointer对象的引用计数相关逻辑。直接上源码:

void objc_setProperty -> reallySetProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}

id oldValue;
id *slot = (id*) ((char*)self + offset);

// copy和mutableCopy处理
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
// retain操作
newValue = objc_retain(newValue);
}

if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// release操作
objc_release(oldValue);
}

接下来我们看看retain和release内部做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
//判断是否是小对象,如果是,则直接返回对象
if (obj->isTaggedPointer()) return obj;
//如果不是小对象,则retain
return obj->retain();
}

//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
//如果是小对象,则直接返回
if (obj->isTaggedPointer()) return;
//如果不是小对象,则release
return obj->release();
}

如果是Tagged Pointer小对象,不会对引用计数做处理。

小对象地址分析

1
2
3
4
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"啊"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"%p-%@",str2,str2);

看一下输出结果:

1
2
0xd3c9816ac08c01c6-a
0x6000033631e0-啊

在类的加载时,其中的_read_images源码有一个方法对小对象进行了处理,即initializeTaggedPointerObfuscator方法。

查看一下源码:_read_images -> initializeTaggedPointerObfuscator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
// _OBJC_TAG_MASK 进行混淆
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}

全局搜索objc_debug_taggedpointer_obfuscator,找到了encode和decode方法。分别是对tagged pointer的编码和解码。

1
2
3
4
5
6
7
8
9
10
11
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

encode时进行了一次^操作,decode时也进行了一次^操作。可以对值进行还原。

那我们分别打印一下decode后的值:

1
2
3
4
5
6
7
NSString *str1 = [NSString stringWithFormat:@"a"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str1));

NSNumber *number1 = @1;
NSLog(@"%@-%p-%@",object_getClass(number1),number1,number1);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));

输出结果:

1
2
3
4
5
0xe4742f5bd16235e6-a
0xa000000000000611

__NSCFNumber-0xf4742f5bd16233e5-1
0xb000000000000012

在源码中有一个判断条件,是否为TaggedPointer:

1
2
3
4
5
6
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
//等价于 ptr & 1左移63,即2^63,相当于除了64位,其他位都为0,即只是保留了最高位的值
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

所以0xa、0xb主要是用于判断是否是小对象taggedpointer,即判断条件,判断第64位上是否为1(taggedpointer指针地址即表示指针地址,也表示值)

0xa 转换成二进制为 1 010(64为为1,63~61后三位表示 tagType类型 - 2),表示NSString类型

0xb 转换为二进制为 1 011(64为为1,63~61后三位表示 tagType类型 - 3),表示NSNumber类型,这里需要注意一点,如果NSNumber的值是-1,其地址中的值是用补码表示的

这里可以通过_objc_makeTaggedPointer方法的参数tag类型objc_tag_index_t进入其枚举,其中 2表示NSString,3表示NSNumber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2, // NSString
OBJC_TAG_NSNumber = 3, // NSNumber
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,

// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,

// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,

OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,

OBJC_TAG_RESERVED_264 = 264
};

跟我们上面得到的结果是一样的。

总结

  • Tagged Pointer小对象类型(用于存储NSNumber、NSDate、小NSString),小对象指针不再是简单的地址,而是地址 + 值,即真正的值,它只是一个披着对象皮的普通变量而以。所以可以直接进行读取。优点是占用空间小,节省内存
  • Tagged Pointer小对象 不会进入retain 和 release,意味着不需要ARC进行管理,所以可以直接被系统自主的释放和回收。
  • Tagged Pointer的内存并不存储在堆中,而是在常量区中,也不需要malloc和free。
  • 对于NSString类型,建议直接使用@""初始化赋值。

SideTables 散列表

SideTables是一个hash表。在weak修饰时会存放在SideTables这个表中。

对于OC正常的对象来说当执行retain操作时,当引用计数达到一定的值(256)时,则会存放在SideTables中。

我们接下来看一下retain 的流程

retain流程

看一些源码:进入objc_retain -> retain -> rootRetain查看源码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
LWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;

bool sideTableLocked = false;
bool transcribeToSideTable = false;
//为什么有isa?因为需要对引用计数+1,即retain+1,而引用计数存储在isa的bits中,需要进行新旧isa的替换
isa_t oldisa;
isa_t newisa;
//重点
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否为nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是 nonpointer isa,直接操作散列表sidetable
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
//dealloc源码
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}


uintptr_t carry;
//执行引用计数+1操作,即对bits中的 1ULL<<45(arm64) 即extra_rc,用于该对象存储引用计数值
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//判断extra_rc是否满了,carry是标识符
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
//如果extra_rc满了,则直接将满状态的一半拿出来存到extra_rc
newisa.extra_rc = RC_HALF;
//给一个标识符为YES,表示需要存储到散列表
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//将另一半存在散列表的rc_half中,即满状态下是8位,一半就是1左移7位,即除以2
//这么操作的目的在于提高性能,因为如果都存在散列表中,当需要release-1时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以直接操作extra_rc即可,不需要操作散列表。性能会提高很多
sidetable_addExtraRC_nolock(RC_HALF);
}

if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}

流程分析:

  1. isTaggedPointer直接返回。
  2. 不是nonpointer,直接存sideTable
  3. 是否正在释放,deallocating,返回nil
  4. 引用计数+1。
  5. 判断引用计数是否存满了,满了则变成一半,另一半存放在散列表中。

之所以不直接把引用计数存放在散列表中,是因为对表的操作,需要用到锁,这是耗时操作。
如果每一个对象都需要一个散列表,也会造成性能问题。如果所有对象公用一个散列表,则其他数据可能不安全,所以也不会公用一个表。真机上最多有8个表。

release流程

release流程与retain相反。

setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;

bool sideTableLocked = false;

isa_t oldisa;
isa_t newisa;

retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否是Nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是,则直接操作散列表-1
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
//进行引用计数-1操作,即extra_rc-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
//如果此时extra_rc的值为0了,则走到underflow
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));

if (slowpath(sideTableLocked)) sidetable_unlock();
return false;

underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate

// abandon newisa to undo the decrement
newisa = oldisa;
//判断散列表中是否存储了一半的引用计数
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}

// Transfer retain count from side table to inline storage.

if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}

// Try to remove some retain counts from the side table.
//从散列表中取出存储的一半引用计数
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.

if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
//进行-1操作,然后存储到extra_rc中
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}

if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}

// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
//此时extra_rc中值为0,散列表中也是空的,则直接进行析构,即自动触发dealloc流程
// Really deallocate.
//触发dealloc的时机
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

if (slowpath(sideTableLocked)) sidetable_unlock();

__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

if (performDealloc) {
//发送一个dealloc消息
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}

流程分析如下:

  1. 判断是否是Nonpointer isa,如果不是,则直接对散列表进行-1操作

  2. 如果是Nonpointer isa,则对extra_rc中的引用计数值进行-1操作,并存储此时的extra_rc状态到carry中

  3. 如果此时的状态carray为0,则走到underflow流程

    underflow流程有以下几步:

    1. 判断散列表中是否存储了一半的引用计数,如果是,则从散列表中取出存储的一半引用计数,进行-1操作,然后存储到extra_rc中
    2. 如果此时extra_rc没有值,散列表中也是空的,则直接进行析构,即dealloc操作,属于自动触发

dealloc分析

dealloc是在retainCount为0时系统自动触发的。

dealloc -> _objc_rootDealloc -> rootDealloc查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline void
objc_object::rootDealloc()
{
//对象要释放,需要做哪些事情?
//1、isa - cxx - 关联对象 - 弱引用表 - 引用计数表
//2、free
if (isTaggedPointer()) return; // fixme necessary?

//如果没有这些,则直接free
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
//如果有
object_dispose((id)this);
}
}
  1. 根据条件判断是否有isa、cxx、关联对象、弱引用表、引用计数表,如果没有,则直接free释放内存
  2. 如果有,则进入object_dispose方法
1
2
3
4
5
6
7
8
9
10
11
id 
object_dispose(id obj)
{
if (!obj) return nil;
// 销毁实例而不会释放内存
objc_destructInstance(obj);
//释放内存
free(obj);

return nil;
}

objc_destructInstance为了消耗实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
//调用C ++析构函数
if (cxx) object_cxxDestruct(obj);
//删除关联引用
if (assoc) _object_remove_assocations(obj);
//释放
obj->clearDeallocating();
}
return obj;
}
  1. 在内部判断是否有析构函数,如果有则调用。
  2. 是否有关联对象,有的花移除关联对象。
  3. 执行clearDeallocating
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline void 
objc_object::clearDeallocating()
{
//判断是否为nonpointer isa
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果不是,则直接释放散列表
sidetable_clearDeallocating();
}
//如果是,清空弱引用表 + 散列表
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}

assert(!sidetable_present());
}

clearDeallocating的目的主要是为了清空散列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));

SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//清空弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
//清空引用计数
table.refcnts.erase(this);
}
table.unlock();
}

清空引用计数,情况弱引用表。

以上就是dealloc 的流程。

retainCount

1
2
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

上面输出的引用计数是多少?这是一个经典的面试题。

这里输出的结果是1。但是1是不对的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
👇
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);

return obj->rootRetainCount();
}
👇
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//如果是nonpointer isa,才有引用计数的下层处理
if (bits.nonpointer) {
//alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
//如果不是,则正常返回
sidetable_unlock();
return sidetable_retainCount();
}

当对象创建时,并没有存引用计数,bits.extra_rc还是0,当调用了retainCount时执行了1 + bits.extra_rc,所以就变成了1 。如果reatinCount=0,相当于创建成功之后就会被释放掉。

所以这里的答案应该是0。不管我有没有执行init操作,都是0 。在读的时候才会是1。