底层探索-内存管理
当提到App的优化时,优化内存必然是会被提到的,本篇章就来探索下内存管理。
一:内存布局
关于内存的分区,在 底层探索-多线程基础已经做了相关介绍,在此进行一些补充。除了常说的五大区,还有内核区和保留区域。
内核区:交给系统进行处理的内核区域。
保留区:预留给系统进行一些处理的区域
Tips:栈区就会向下增长,堆区就会向上增长
二:内存管理方案
Tagged Pointer
概述和示例
在 2013 年 9 月,苹果推出了 iPhone5s,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer
的概念。
使用Tagged Pointer
之前,如果声明一个NSNumber *number = @10;
变量,需要一个占8字节的指针变量number,和一个占16字节的NSNumber对象,指针变量number指向NSNumber对象的地址。这样需要耗费24个字节内存空间。
而使用Tagged Pointer
之后,NSNumber指针里面存储的数据变成了:Tag + Data
,也就是将数据直接存储在了指针中。直接将数据10保存在指针变量number中,这样仅占用8个字节。
但是当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。
下面来看代码
- (void)taggedPointerDemo1 { self.queue = dispatch_queue_create("com.wj.cn", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i<10000; i++) { dispatch_async(self.queue, ^{ self.nameStr = [NSString stringWithFormat:@"wj"]; NSLog(@"%@",self.nameStr); }); } } - (void)taggedPointerDemo2 { self.queue = dispatch_queue_create("com.wj.cn", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i<10000; i++) { dispatch_async(self.queue, ^{ self.nameStr = [NSString stringWithFormat:@"wj_十五的月亮十六圆哈哈"]; NSLog(@"%@",self.nameStr); }); } }复制代码
taggedPointerDemo1
和taggedPointerDemo2
中的代码几乎一样,但是实际运行结果却不一样,taggedPointerDemo1
能够正常运行,而taggedPointerDemo2
却崩溃了。
其实崩溃的原因很简单,当调用
self.nameStr
赋值的时候,实际上会频繁的调用setter
,因而retain
和release
也会被频繁调用,因为多线程的影响,在某个瞬间会对同一个对象做多次释放,导致过度释放而造成崩溃。
那么taggedPointerDemo1
凭什么能正常运行呢?我们通过断点的方式来看看!
taggedPointerDemo1
中对象类型是NSTaggedPointerString
taggedPointerDemo2
中对象类型是NSCFString
在objc
源码中对于retain
和release
方法定义如下:
__attribute__((aligned(16), flatten, noinline)) id objc_retain(id obj) { if (obj->isTaggedPointerOrNil()) return obj; return obj->retain(); } __attribute__((aligned(16), flatten, noinline)) void objc_release(id obj) { if (obj->isTaggedPointerOrNil()) return; return obj->release(); }复制代码
系统判断如果对象类型是
isTaggedPointer
类型就执行返回了,不做其他操作,因此taggedPointerDemo1
中的对象已经被优化了,压根不会去调用setter
源码解读
在前文的他不就中read_image
,其实就包含对Tagged Pointer
的处理initializeTaggedPointerObfuscator()
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_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK; } }复制代码
底层对objc_debug_taggedpointer_obfuscator
进行了处理,通过两次异或
来进行编解码。
extern uintptr_t objc_debug_taggedpointer_obfuscator; 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; }复制代码
举个例子来看打印结果
- (void)viewDidLoad { [super viewDidLoad]; NSString *str1 = [NSString stringWithFormat:@"a"]; NSString *str2 = [NSString stringWithFormat:@"b"]; NSLog(@"%p-%@",str1,str1); NSLog(@"%p-%@",str2,str2); // NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2)); } uintptr_t _objc_decodeTaggedPointer_(id ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; }复制代码
打印结果其实并非真正的指针地址,这个地址是经过混淆的,我们需要_objc_decodeTaggedPointer
解码来获取真正的地址,将上述代码中注释部分打开就能得到真正的指针地址。
_objc_makeTaggedPointer
在_objc_makeTaggedPointer
中会传入一个标志位tag
进行了不同的位运算最后将标志位+值+值类型
格式的taggedPointer
呈现出来。
下面是系统定义的各种Tagged Pointer
标志位。
enum objc_tag_index_t : uint16_t { // 60-bit payloads OBJC_TAG_NSAtom = 0, OBJC_TAG_1 = 1, OBJC_TAG_NSString = 2, OBJC_TAG_NSNumber = 3, 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_First60BitPayload = 0, OBJC_TAG_Last60BitPayload = 6, OBJC_TAG_First52BitPayload = 8, OBJC_TAG_Last52BitPayload = 263, OBJC_TAG_RESERVED_264 = 264 };复制代码
总结
从64bit开始,iOS引入了
Tagged Pointer
技术,用于优化NSNumber
、NSDate
、NSString
等小对象存储在没有使用
Tagged Pointer
之前,NSNumber
等对象需要动态分配内存、维护引用计数等,NSNumber
指针存储的是堆中NSNumber
对象的地址值使用
Tagged Pointer
之后,NSNumber
指针里面存储的数据变成了:Tag + Data
,也就是将数据直接存储在了指针中,Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc
和free
。在内存读取上有着3倍的效率,创建时比以前快106倍。不但减少了64位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。
这是一个特别的指针,不指向任何一个地址
当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
NONPOINTER_ISA
nonpointer_isa
,在之前 对象原理之isa的本质探究 已经做了介绍,它也是内存管理方案之一,isa
是个8字节(64位)的指针,仅用来isa
指向比较浪费,所以isa
中就掺杂了一些其他数据来节省内存。
SideTable
先看下SideTable
结构
struct SideTable { spinlock_t slock; RefcountMap refcnts; weak_table_t weak_table; ...... 省略 };复制代码
slock
: 开解锁refcnts
: 引用计数表weak_table
: 弱引用表
在全局其实散列表是SideTables
static StripedMap<SideTable>& SideTables() { return SideTablesMap.get(); }复制代码
为什么不放在一张表呢?
安全性能低——若所有对象全存在一张散列表中,某个对象需要处理时就对散列表进行unlock,表中其他对象的安全性无法得到保障。
优化加锁、解锁速度——对于散列表的操作频率较高,分为多表可以提高性能。
三:MRC & ARC
alloc
对象原理之alloc & init 探究
retain
objc_retain
先判断是否为isTaggedPointer
,是就直接返回不需要处理,不是在调用obj->retain()
objc_object::retain
通过fastpath
大概率调用rootRetain()
,小概率通过消息发送调用对外提供的SEL_retain
rootRetain
调用rootRetain(false, false)
objc_object::rootRetain(bool tryRetain, bool handleOverflow) { if (isTaggedPointer()) return (id)this; bool sideTableLocked = false; bool transcribeToSideTable = false; isa_t oldisa; isa_t newisa; do { transcribeToSideTable = false; oldisa = LoadExclusive(&isa.bits); newisa = oldisa; ???? 判断是否是nonpointer if (slowpath(!newisa.nonpointer)) { 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 ???? 判断是否正在释放 if (slowpath(tryRetain && newisa.deallocating)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); return nil; } uintptr_t carry; ???? 对extra_rc进行操作,处理引用计数 newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ 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; newisa.extra_rc = RC_HALF; 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. ???? 存入散列表 sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); return (id)this; }复制代码
rootRetain
内部实现其实是个do-while
循环:
先判断是否为
nonpointer_isa
(小概率事件)不是的话则对散列表sideTable
中的引用技术表
进行处理。找到对应的散列表进行
+=SIDE_TABLE_RC_ONE
,其中SIDE_TABLE_RC_ONE
是左移两位找到引用计数表正常情况下为
nonpointer_isa
会调用addc
函数进行引用计数的处理,并用carry
记录引用计数是否超负荷对
isa
中的第45位(RC_ONE
在arm64
中为45)extra_rc
进行操作处理超负荷情况下,将
extra_rc
的一半引用计数存入引用计数表
中,并标记isa->has_sidetable_rc
为true这里为什么优先考虑使用isa进行引用计数存储是因为对isa操作的性能强,操作引用计数表需要进行加锁解锁操作。
简单来说就是非nonpointer_isa
直接操作散列表,是nonpointer_isa
判断是否在释放,未释放的话操作extra_rc
,如果extra_rc
超负荷,那么满状态的一半会存入extra_rc
,另一半存入散列表。
release
release
和retain
非常类似。
objc_release
先判断是否为isTaggedPointer
,是就直接返回不需要处理,不是在调用obj->release()
objc_object::release
通过fastpath
大概率调用rootRelease()
,小概率通过消息发送调用对外提供的SEL_release
rootRelease
调用rootRelease(true, false)
rootRelease
内部实现也有个do-while
循环如果
isa
中的has_sidetable_rc
为true时就开始着手处理引用计数,否则就将isa
中的deallocating
标记为true准备释放如果
isa
中的extra_rc
减到只剩一半时会清空存放在引用计数表中的值,重新放回到extra_rc
中,返回retry
的do-while
循环。先判断是否为
nonpointer_isa
(小概率事件)不是的话则对散列表中的引用技术表进行处理正常情况下为
nonpointer_isa
会调用subc函数进行引用计数的处理,并用carry
记录引用计数是否超负荷超负荷情况下会来到
underflow
分支
retainCount
看下面这段代码
NSObject *objc = [[NSObject alloc]init]; NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));复制代码
打印结果为1
,init
只是构造方法,并没有对引用计数进行操作,alloc
我们也研究过,也未看到相关代码,那这个1
是怎么出现的呢?
retainCount
最终会调用rootRetainCount
inline uintptr_t objc_object::rootRetainCount() { if (isTaggedPointer()) return (uintptr_t)this; sidetable_lock(); isa_t bits = LoadExclusive(&isa.bits); ClearExclusive(&isa.bits); if (bits.nonpointer) { ???? alloc创建的对象其实引用计数为0,在这里进行了默认的+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(); }复制代码
先判断是否为
isTaggedPointer
再判断是否为
nonpointer
,如果是的话,当前引用计数=1+extrac_rc
这行代码就能说明
alloc
出来的对象引用计数为0
,是苹果人员为了不给开发人员造成引用计数为0
时就销毁造成错觉才默认加一接着判断
has_sidetable_rc
是否有额外的散列表有的话引用计数再加上引用计数表中的数量
所以
引用计数
=1 + extrac_rc + sidetable_getExtraRC_nolock
autorelease
autorelease
可以将对象加入自动释放池中。
autorelease
最终会调用rootAutorelease2 -> autorelease() -> autoreleaseFast()
autoreleaseFast
下文会做介绍
dealloc
先调用
_objc_rootDealloc
。_objc_rootDealloc
调用rootDealloc
rootDealloc
判断是否为
isTaggedPointer
,是的话直接返回,不是的话继续往下走判断isa标识位中是否有弱引用、关联对象、c++析构函数、额外的散列表,有的话调用
object_dispose
,否则直接free
inline void objc_object::rootDealloc() { if (isTaggedPointer()) return; // fixme necessary? 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); } }复制代码
object_dispose
中
先判空处理
接着调用
objc_destructInstance
(核心部分)最后再
free
释放对象
id object_dispose(id obj) { if (!obj) return nil; objc_destructInstance(obj); free(obj); return nil; }复制代码
objc_destructInstance
中
判断是否有
c++析构函数
和关联对象
,有的话分别调用object_cxxDestruct
、_object_remove_assocations
进行处理然后再调用
clearDeallocating
void *objc_destructInstance(id obj) { if (obj) { // Read all of the flags at once for performance. bool cxx = obj->hasCxxDtor(); bool assoc = obj->hasAssociatedObjects(); // This order is important. if (cxx) object_cxxDestruct(obj); if (assoc) _object_remove_assocations(obj); obj->clearDeallocating(); } return obj; }复制代码
clearDeallocating
中
判断是否是
nonpointer
,是的话调用sidetable_clearDeallocating
清空散列表判断是否有
弱引用
和额外的引用计数表has_sidetable_rc
,是的话调用clearDeallocating_slow
进行弱引用表和引用计数表的处理
inline void objc_object::clearDeallocating() { if (slowpath(!isa.nonpointer)) { // Slow path for raw pointer isa. sidetable_clearDeallocating(); } else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) { // Slow path for non-pointer isa with weak refs and/or side table data. clearDeallocating_slow(); } assert(!sidetable_present()); }复制代码
clearDeallocating_slow
会对弱引用表进行处理
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(); }复制代码
完整流程图:
四:强弱引用
weak原理
之前已经探究过 底层探索 - weak的实现原理
NSTimer中的循环引用
static int num = 0; @interface ViewController () @property (nonatomic, strong) NSTimer *timer; @end @implementation ViewController - (void)didMoveToParentViewController:(UIViewController *)parent{ // 无论push 进来 还是 pop 出去 正常跑 // 就算继续push 到下一层 pop 回去还是继续 if (parent == nil) { [self.timer invalidate]; self.timer = nil; NSLog(@"timer 走了"); } } - (void)viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES]; } - (void)fire { num++; NSLog(@"current - %d",num); } - (void)dealloc { [self.timer invalidate]; self.timer = nil; NSLog(@"%s", __FUNCTION__); }复制代码
以上代码肯定是会造成循环引用的,我们看下官方文档,scheduledTimerWithTimeInterval
中的target:self
,这个API会对传入的target
对象进行强引用。
那我们使用__weak typeof(self) weakSelf = self
能否像Block那样解决循环引用呢?
答案是 不能
控制台中,self
和weakSelf
都是0x12af05570
,也就是两者指向了同一个对象,但是我们打印self
和weakSelf
指针的地址,两者却不相同,也就是说self
和weakSelf
是两个完全不同指针,但是指向了相同的对象,而官方文档也说了NSTimer
会对传入的对象强引用,因而传入weakSelf
也无法解决循环引用。
而在Block
中,持有的是指针地址,为了解决循环引用,我们通常传入的是weakSelf
,而Block中拿到的就是weakSelf
,而非真正的对象。
循环引用解决
方案一:使用Block形式创建Timer
最简单的方案
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer fire - %@",timer); }];复制代码
方案二:提前invalidate
既然dealloc不能来,就在dealloc函数调用前解决掉这层强引用
可以在
viewWillDisappear
、viewDidDisappear
中处理NSTimer
,但这样处理效果并不好,因为跳转到下一页定时器也会停止工作,与业务不符使用
didMoveToParentViewController
可以很好地解决这层强引用。
完整代码
static int num = 0; @interface ViewController () @property (nonatomic, strong) NSTimer *timer; @end @implementation ViewController - (void)didMoveToParentViewController:(UIViewController *)parent{ // 无论push 进来 还是 pop 出去 正常跑 // 就算继续push 到下一层 pop 回去还是继续 if (parent == nil) { [self.timer invalidate]; self.timer = nil; NSLog(@"timer 走了"); } } - (void)viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES]; } - (void)fire { num++; NSLog(@"current - %d",num); }复制代码
方案三:中介者模式
使用中介者来实例化Timer
,通过判断target
是否存在来决定是否需要执行selector
#import "LGTimerWapper.h" #import <objc/message.h> @interface LGTimerWapper() @property (nonatomic, weak) id target; @property (nonatomic, assign) SEL aSelector; @property (nonatomic, strong) NSTimer *timer; @end @implementation LGTimerWapper - (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{ if (self == [super init]) { self.target = aTarget; // vc self.aSelector = aSelector; // 方法 -- vc 释放 if ([self.target respondsToSelector:self.aSelector]) { Method method = class_getInstanceMethod([self.target class], aSelector); const char *type = method_getTypeEncoding(method); class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type); self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo]; } } return self; } // 一直跑 runloop void fireHomeWapper(LGTimerWapper *warpper){ if (warpper.target) { // vc - dealloc void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend; lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer); }else{ // warpper.target [warpper.timer invalidate]; warpper.timer = nil; } } - (void)lg_invalidate{ [self.timer invalidate]; self.timer = nil; } - (void)dealloc{ NSLog(@"%s",__func__); } @end复制代码
方案四:使用NSProxy
NSProxy
是专门用来做消息转发,和NSObject
同级。
@interface MJProxy : NSProxy + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end #import "MJProxy.h" @implementation MJProxy + (instancetype)proxyWithTarget:(id)target { // NSProxy对象不需要调用init,因为它本来就没有init方法 MJProxy *proxy = [MJProxy alloc]; proxy.target = target; return proxy; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocatio{ [invocation invokeWithTarget:self.target]; } @end复制代码
VC使用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];复制代码
五:自动释放池
通过clang
命令对空白的main.m
输出一份main.cpp
。
int main(int argc, const char * argv[]) { @autoreleasepool { } return 0; } struct __AtAutoreleasePool { ???? 构造函数 __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ???? 析构函数 ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; }; int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; } return 0; }复制代码
自动释放池底层就是一个__AtAutoreleasePool
结构体,里面有构造函数和析构函数。
看下objc
源码中对其的解释:
自动释放池
是一个以栈为节点的结构,拥有栈的特性——先进后出自动释放池
的节点可以是对象(可以被释放)也可以是POOL_BOUNDARY
(边界/哨兵对象)自动释放池
的数据结构是双向链表
自动释放池
跟tls/线程
是有关系的
整体结构如下所示:
objc_autoreleasePoolPush
和objc_autoreleasePoolPop
都涉及到一个类AutoreleasePoolPage
AutoreleasePoolPage
结构
class AutoreleasePoolPage; struct AutoreleasePoolPageData { magic_t const magic;// ????16字节 __unsafe_unretained id *next; // ????8字节 pthread_t const thread;//8字节 AutoreleasePoolPage * const parent;// ????8字节 AutoreleasePoolPage *child;// ????8字节 uint32_t const depth;// ????4字节 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) { } };复制代码
magic
用来检验AutoreleasePoolPage
的结构是否完整next
指向最新添加的autoreleased
对象的下一个位置,初始化时指向begin()
thread
指向的的当前线程parent
指向父节点,第一个节点的parent值为nilchild
指向子节点,最后一个节点的child值为nildepth
代表深度,从0开始,往后递增1hiwat
代表high water mark
——最大入栈数量标记
官方注释中还提到了哨兵
对象,那么一个自动释放池包含多少哨兵
对象呢?其实只有一个。
# define POOL_BOUNDARY nil复制代码
哨兵对象本质上是个nil
,它的作用主要在调用objc_autoreleasePoolPop
时体现:
根据传入的
哨兵对象
地址找到哨兵对象
所在的page。在当前page中,将晚于
哨兵对象
插入的所有autorelese对象
都发送一次release
消息,并移动next指针
到正确位置。从最新加入的对象一直向前清理,可以向前跨越若干个page,直到
哨兵对象
所在的page。
那么一个AutoreleasePoolPage
能存储多少对象呢?
#define PAGE_MIN_SIZE PAGE_SIZE #define PAGE_SIZE I386_PGBYTES #define I386_PGBYTES 4096复制代码
根据源码我们知道一个AutoreleasePoolPage
共4096
个字节,除去本身属性占用的56
字节,剩余4040
的空间可以使用,由于在初始化自动释放池的时候,POOL_BOUNDARY哨兵对象
会被push到栈顶,因此第一页实际只能存放(4040 -8)/8 = 504对象
,从第二页开始可以存储505个对象
。
进栈
objc_autoreleasePoolPush
void * objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); }复制代码
push()
static inline void *push() { id *dest; if (slowpath(DebugPoolAllocation)) { // Each autorelease pool starts on a new pool page. // ???? 创建新的page dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }复制代码
autoreleaseNewPage()
static __attribute__((noinline)) id *autoreleaseNewPage(id obj) { AutoreleasePoolPage *page = hotPage(); if (page) return autoreleaseFullPage(obj, page); else return autoreleaseNoPage(obj); }复制代码
autoreleaseNewPage()
会进行相关的初始化,最终会调用以下这个函数
找到真正调用的地方,但值得注意的是,本来应该传入的是*next
,但是真正调用的地方传入的却是begin()
begin()
id * begin() { return (id *) ((uint8_t *)this+sizeof(*this)); }复制代码
我们用代码跑起来看看
这个56
是什么呢?原因在于AutoreleasePoolPage
本身对象里面就包含属性,总共56
字节,因此对象压栈的时候要从56
字节之后开始。
autoreleaseFast()
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); } }复制代码
autoreleaseFast
中分为三条分支:(hotPage
可以获取当前的AutoreleasePoolPage
)
无当前页(刚创建,意味着池子尚未被push)
调用
autoreleaseNoPage
创建一个hotPage
调用
page->add(obj)
将对象添加至AutoreleasePoolPage
的栈中,将next
指针平移并指向下一个位置有
hotPage
但page没有满(当前页尚未存满)调用
page->add(obj)
将对象添加至AutoreleasePoolPage
的栈中有
hotPage
且page已满(当前页已存满)调用
autoreleaseFullPage
初始化一个新page调用
page->add(obj)
将对象添加至AutoreleasePoolPage
的栈中
autoreleaseFullPage
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 { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); }复制代码
autoreleaseFullPage
通过递归遍历当前页的子页,如果存在继续遍历,如果不存在就开辟新的AutoreleasePoolPage
并设为HotPage
。
出栈
void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }复制代码
这里需要传入一个ctxt
,其实是哨兵对象atautoreleasepoolobj
,也就是下面构造函数中objc_autoreleasePoolPush()
的返回值,进出栈一一对应。
struct __AtAutoreleasePool { ???? 构造函数 __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ???? 析构函数 ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; };复制代码
AutoreleasePoolPage::pop(ctxt)->pop->popPage
最终会调用到popPage
popPage
其中
通过
page->releaseUntil(stop)
通过一个while循环
和next指针
来不断遍历调用objc_release(obj)
释放对象,直到next指针
指向栈顶才停止循环然后开始
page->kill()
,用于销毁当前的page
、page->child->kill()
等,最后setHotPage(nil)
与RunLoop的关系
kCFRunLoopEntry
:在即将进入RunLoop时,会自动创建一个__AtAutoreleasePool
结构体对象,并调用objc_autoreleasePoolPush()
函数。kCFRunLoopBeforeWaiting
:在RunLoop即将休眠时,会自动销毁一个__AtAutoreleasePool
对象,调用objc_autoreleasePoolPop()
。然后创建一个新的__AtAutoreleasePool对象
,并调用objc_autoreleasePoolPush()
。kCFRunLoopBeforeExi
t,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool
对象,并调用objc_autoreleasePoolPop()
。
作者:云先生
链接:https://juejin.cn/post/7016937951670042638