当提到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
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循环-
先判断是否为
nonpointer_isa(小概率事件)不是的话则对散列表中的引用技术表进行处理 -
正常情况下为
nonpointer_isa会调用subc函数进行引用计数的处理,并用carry记录引用计数是否超负荷 -
超负荷情况下会来到
underflow分支- 如果
isa中的has_sidetable_rc为true时就开始着手处理引用计数,否则就将isa中的deallocating标记为true准备释放 - 如果
isa中的extra_rc减到只剩一半时会清空存放在引用计数表中的值,重新放回到extra_rc中,返回retry的do-while循环。
- 如果
-
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值为nil -
child指向子节点,最后一个节点的child值为nil -
depth代表深度,从0开始,往后递增1 -
hiwat代表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()。 -
kCFRunLoopBeforeExit,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPop()。