文章目录
一、NSProxy
1、作用: 用于消息转发
2、定时器的细节
二、内存布局
三、标记指针(Tagged pointer)
四、OC 对象的内存管理
1、内存管理
2、copy、mutableCopy 关键字
3、引用计数
4、weak 指针
5、autorelease
一、 NSProxy
1、作用 : 用于消息转发
NSProxy
没有父类,和 NSObject 算是同一级别的存在,都遵循 NSObject
协议,分别定义如下:
@interface NSProxy <NSObject> {
Class isa;
}
@interface NSObject <NSObject> {
Class isa ;
}
那么两者有什么区别?
首先,NSProxy
是没有 init
方法的,直接 alloc
就产生实例
其次,NSProxy
是专门用来处理消息转发的,也就是说,对 NSProxy 实例对象发送任何消息,直接进入消息转发环节。
举一个实际例子,以前用定时器,用方法scheduledTimerWithTimeInterval:target: selector: userInfo: repeats:
创建一个定时器,该方法会对 target
产生强引用。无论 target
传的是弱引用进去,NSTimter
里面都有一个成员对 target
进行强引用。这样就会造成 viewcontroller 强引用 timer,timer 强引用 self,也就是 target 。造成循环引用问题,导致内存泄露。
一般来说,我们解决方法有三种
1、第一种, 使用 scheduledTimerWithTimeInterval: repeats: block:
API 去创建定时器,这样就没有 NSTimer
对 target
产生强引用问题,NSTimer
只对 block
产生强引用,只要在block
内部使用弱引用的话,就不会造成内存泄露。
2、第二种,使用中间对象和消息转发
截取部分代码如下
中间对象的代码
#import "TargetObject.h"
@interface TargetObject : NSObject
+ (id)generalWithTarget:(id)target;
@property (nonatomic,weak) id target;
@end
@implementation TargetObject
+ (id)generalWithTarget:(id)target
{
TargetObject *anyObject = [[TargetObject alloc]init];
anyObject.target = target;
return anyObject;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
第三种 : 使用 NSProxy
+ 消息转发
@interface ProxyObject : NSProxy
@property (nonatomic,weak) id target;
+ (id)generalWithTarget:(id)target;
@end
@implementation ProxyObject
+ (id)generalWithTarget:(id)target
{
ProxyObject *anyObject = [ProxyObject alloc];
anyObject.target = target;
return anyObject;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
关于定时器
- (void)viewDidLoad {
[super viewDidLoad];
//self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TargetObject generalWithTarget:self] selector:@selector(run) userInfo:nil repeats:YES];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[ProxyObject generalWithTarget:self] selector:@selector(run) userInfo:nil repeats:YES];
}
那么 方案二 和 方案三 有什么区别呢?
区别主要是在性能上,具体指方法查找上的性能。
方案二,TargetObject : NSObject
需要从TargetObject
类对象的 cache、methods
查找方法到NSObject
,最后触发消息转发机制。
方案三: ProxyObject : NSProxy
,并不会去父类查找方法, 直接进入消息转发
.
要深刻理解 专门用于消息转发 的功用
ProxyObject *proxy = [ProxyObject generalWithTarget:self];
NSLog(@"%d",[proxy isKindOfClass:[self class]]); //结果为 1
2、定时器的细节
NSTimer
依赖于RunLoop
,如果RunLoop
的任务过于繁重,可能导致NSTimer
不准确。
比较通俗易懂的话就是说: RunLoop
每跑一圈计算下时间,如果遇到 NSTimer
,发现时间还没到,继续跑圈。但是 RunLoop
每跑一圈的时间是不固定的。如果中间插入一些比较重的任务,就会导致要执行定时器的时候被延迟了。
那如何保证定时器准确呢? 使用 GCD
的定时器,因为 GCD 定时器直接跟系统内核挂钩,不依赖于 RunLoop
//创建下定时器(注意,这里需要强引用)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//设置时间(什么时候开始,间隔几秒,误差多少) 立即执行,1s 执行一次
dispatch_source_set_timer(self.timer ,
dispatch_time(DISPATCH_TIME_NOW, 0*NSEC_PER_SEC),
1 * NSEC_PER_SEC,
0);
//设置回调
dispatch_source_set_event_handler(self.timer , ^{
NSLog(@"TO DO");
});
//启动定时器
dispatch_resume(self.timer);
二、内存布局
任何一个程序在运行的时候时机上是运行在内存中的,这个内存也就是我们通常说的主存,也叫运行内存,是可以直接与 CPU
进行交换数据的内部存储器。内存读取速度很快,所以作为操作系统运行程序的区域。通常我们将内存划分为以下几大块:栈区、堆区、全局区、常量区、代码区
如图所示(哈哈,这张图是扒别人博客的)
下面来详细说明下每个分区的功能:
栈区(stack
): 栈区是有系统来自动分配释放,是一个栈的数据结构,存储函数的参数、局部变量、引用。高地址向低地址扩展。
堆区(heap
) : 堆区是由开发者"手动管理"或程序结束时由系统全部回收。是一种树状的数据结构,一般用于存储 alloc、new
等方式创建的对象。在 iOS 开发中,大多数内存管理方面的问题也多出于此,开发者没有及时回收内存、内存溢出、内存泄露等。低地址向高地址扩展。
全局区 : 用于存放全局变量和静态变量。存储方式是 未经初始化的全局变量和静态变量放在一块区域,初始化后的全局变量和静态变量在另外一个区域。回收也是等进程结束后由系统回收。
常量区 : 字符串常量和基本数据类型值。回收也是进程结束后系统自动回收。
代码区 : 存储编译后的代码
三、标记指针(Tagged pointer
)
从 64bit
开始,iOS 引入了 Tagged Pointer
技术,用于优化NSString、NSNumber、NSDate
等小对象存储。在没有使用 Tagged Pointer
之前,NSString、NSNumber、NSDate
等对象需要动态分配内存,维护引用计数,对应的指针存储堆上对象的地址值。使用Tagged Pointer
之后,指针存储的数据变成:Tag+Data
将数据直接存储在指针中,当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。对于 objc_msgSend
或者objc_msgSendSuper
能够识别 Tagged Pointer
,直接从指针中提取数据,节省了调用的开销。
如何判断一个指针是否是标记指针?在 runtime
源码objc-internal.h
中有给出一个方法用于判断一个指针是否为标记指针。
#if TARGET_OS_OSX && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
从上面可以看出,在 iOS 中,指针的最高有效位为 1 (1<<63
)是标记指针。
标记指针有什么用?
节省内存 (只有在 8 字节无法存储的条件下才需要在堆上动态分配内存),
提高访问速度 (objc_msgSend
能够识别 标记指针,无法去进行方法查找,直接从指针取值)。
看下具体的应用
dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
for (int i = 0 ; i < 10000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"%@",@"fnawggjoowrgnwg4y34y"];
});
}
上面,运行 crash,访问坏内存。分析: 多个线程同时访问 self.name 的 setter
方法。可能对同一块内存释放两次。
dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
for (int i = 0 ; i < 10000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"%@",@"abc"];
});
}
上面,运行正常。因为,"abc"
这个字符串的值被存储在标记指针中,取值的时候被objc_msgSend
识别,直接从指针地址直接读取。不存在访问堆上对象。
为验证想法,进一步写了个 demo,验证
bool isToggedPointer(id obj)
{
return (long)(__bridge void *)obj & 1;
}
- (void)viewDidLoad {
[super viewDidLoad];
NSString *name = [NSString stringWithFormat:@"fnawggjoowrgnwg4y34y"];
NSString *toggedStr = [NSString stringWithFormat:@"abc"];
NSLog(@"%p----%p",name,toggedStr);
NSLog(@"%@----%@",[name class],[toggedStr class]);
}
运行结果:
0x60400024e9d0----0xa000000006362613
__NSCFString----NSTaggedPointerString
iOS 内存是 小端模式
,即 数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。第一位 : 0x61
,第二位: ox62
,第三位 ,ox63
。6*16+1 = 97,依次类推。另一方面,0x a000 0000 0636 2613
指针最高有效位为 a, 转成二进制 : 0b1010,最高位为 1,满足条件。
四、OC 对象的内存管理
1、内存管理
在 iOS 中,使用引用计数来管理 OC 对象的内存。
一个新创建的对象的引用计数(retainCount
)默认为 1,当引用计数为 0, 对象就会销毁,释放其占用的空间。
调用 retain
会让对象的引用计数 +1,调用release
回让引用计算 -1
当调用 alloc、new、copy、mutableCopy
方法返回一个对象,在不需要这个对象时,要调用release、autorelease
来释放他
想拥有某个对象,就让它引用对象 + 1;不想再拥有某个对象,就让它引用计算 -1
内存管理解决的问题:
内存泄露(该释放的对象没有释放) 和 野指针( 坏内存访问 )
2、copy
关键字
拷贝的目的: 产生一个副本对象,跟源对象互不影响
iOS 提供了两个拷贝方法
copy
: 不可变拷贝,产生不可变副本
mutableCopy
: 可变拷贝,产生可变副本,
深拷贝 :内容拷贝,产生新的对象
浅拷贝: 指针拷贝,没有产生新的对象
— | 不可变 | 可变 |
---|---|---|
copy | 不可变副本(不产生新对象,被拷贝对象引用计数+1) | 不可变副本(产生新对象) |
mutableCopy | 可变副本 (产生新对象) | 可变副本(产生新对象) |
总结: 不可变对象调用copy
是浅拷贝,其他都是深拷贝。
3、引用计数
在 64 bit
中,引用计数存储在 isa
指针中, 有可能存在 sideTable
类中
union isa_t
{
Class cls;
uintptr_t bits;
//此结构体仅为了增加可读性,纯属摆设
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1; //当 19 位不够存储的时候,为1 ,存在sideTable_t 结构中
uintptr_t extra_rc : 19; //存储引用计数 19 位
};
其中 SideTable
定义如下
struct SideTable {
spinlock_t slock; //自旋锁
RefcountMap refcnts; // 引用计数 散列表
weak_table_t weak_table; //弱引用
};
weak_table_t
定义如下
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
看下 获取引用计数的底层实现
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
继续看 RootRetainCount
函数实现
inline uintptr_t objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
//获取 bits
ClearExclusive(&isa.bits);
//判断是不是优化过的指针
if (bits.nonpointer) {
// extra_rc 本来存储就是引用计算 -1 ,直接加上 1 上去
uintptr_t rc = 1 + bits.extra_rc;
// 如果存在 sideTable_t
if (bits.has_sidetable_rc) {
//就继续去 sideTable_t结构中去找
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
4、 weak
指针
__weak 和 _unsafe_unretain 共同点都不会产生强引用,指向对象如销毁了,__weak 会将指向对象的指针置为 nil,而 _unsafe_unretain 不会。
看下 weak
修饰的指针如果在对象销毁的时候(即调用 dealloc
)如何将指针设置成 nil
的
还是查找 runtime
源码
dealloc
源码
- (void)dealloc {
_objc_rootDealloc(self);
}
_objc_rootDealloc
源码
void _objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
rootDealloc
源码
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
/**
如果是优化过的 isa,
且没有弱引用
且没有关联对象
且没有 c++ 析构函数
且没有 sizdeTable_t
直接 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 函数
*/
object_dispose((id)this);
}
}
object_dispose
函数
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
// 在对象销毁之前,调用了 objc_destructInstance 函数
free(obj);
return nil;
}
看下,在释放对象之前,调用了 objc_destructInstance
函数
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++ 析构函数、存在关联对象
先进行释放
后,继续调用 clearDeallocating 函数
*/
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
clearDeallocating
函数的调用
inline void objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
/**
如果存在弱引用 和 has_sidetable_rc不为空
*/
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
函数
void objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
//将散列表取出来,并 weak_clear 清除 weak 修饰的指针
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_clear_no_lock
的实现,结合 weak_table_t
的结构
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask; //这个 mask 应该是容量 -1
uintptr_t max_hash_displacement;
};
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
//取出来 entry
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
/**
这里可以证明,entry 里面有个类似数组的数据结构,存放这被 weak 修饰的指针
遍历,然后设置成 nil
*/
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
// 从 弱引用表中删除
weak_entry_remove(weak_table, entry);
}
看下如何取 entry
static weak_entry_t *weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
assert(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
//看到这里,也是进行 & 操作 得到 index
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
总上所述,可以得出如下结果: 当出现不存在 关联对象、弱引用、析构函数,对象是直接调用 free()
函数直接释放。如果存在,需要对这些进行额外的释放工作。其中,对于弱引用,需要调用 clearDeallocating
函数对齐进行释放,需要根据当前对象找到 sideTable
,进入通过 sideTabl->weakTable
,最后,根据当前对象的地址去 weak_table_t
这张散列表表中根据 &obj & mask
取出weak_entry_t
,先把 weak_entry_t
中的指针数组取出来,遍历设置为 nil
,然后从 weakTable
中remove
这个weak_entry_t
。其中这个 weak_entry_t
里面有个 弱引用指针数组。这个过程跟runtime
消息发送过程中,查找类对象方法缓存cache
非常类似。
5、 autorelease
对一个对象发送 autorelease
消息后,这个对象会被放进 autoreleasepool
(自动释放池)。
为了,看清楚 autoreleasepool
的结构,创建了命令行工程,并对其进行重写 生成 c++
代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
经过命令行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
得到 main.cpp
struct __AtAutoreleasePool {
void * atautoreleasepoolobj;
//构造函数(默认初始化方法)
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析构函数(相当于 dealloc )
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hm_w26qvm0x6ld5k2pfy0007h3r0000gn_T_main_8920aa_mi_0);
}
return 0;
}
可以知道,生成是个对象,相当于 大括号开始调用objc_autoreleasePoolPush
,大括号结束之前调用 objc_autoreleasePoolPop()
这两个函数可在 runtime
源码中,NSObject.mm
找打对应是实现
void *objc_autoreleasePoolPush(void)
{ //调用 AutoreleasePoolPage 对象的 push()方法
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
//调用 AutoreleasePoolPage 对象的 pop()方法
AutoreleasePoolPage::pop(ctxt);
}
由此可见,自动释放池的主要底层数据结构为 : __AtAutoreleasePool
和 AutoreleasePoolPage
。调用autorelease
的对象最终都是通过 AutoreleasePoolPage
对象来管理的。
那 AutoreleasePoolPage
定义如何,也可在 runtime
中找到对应定义如下(仅摘取部分代码)
class AutoreleasePoolPage {
magic_t const magic;
id *next; //返回下一个能存放 autorelase 对象的内存地址
pthread_t const thread;
AutoreleasePoolPage * const parent; // piror 指针,指向上一个 AutoreleasePoolPage 对象
AutoreleasePoolPage *child; // next 指针,指向下一个 AutoreleasePoolPage 对象
uint32_t const depth;
uint32_t hiwat;
//返回开始存放 autorelease 对象的地址
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
//返回整个对象的内存的结束地址
id * end() {
return (id *) ((uint8_t *)this+SIZE); // 这个size 为 4096
}
//添加 autorelease 对象
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;
}
}
每个 AutoreleasePoolPage
对象占用了 4096
字节内存,除了用它来存放内部的成员变量(7*8 = 56 字节),剩下的空间(4096 - 56 = 4040 字节)用来存放 autorelease
对象的地址。
所有的AutoreleasePoolPage
对象通过双向链表的形式连接在一起。
什么是双向链表 :双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
接下来就需要搞懂,autorelease
对象如何存放?
存放过程:
当调用 objc_autoreleasePoolPush()
的时候,
会将 POOL_BOUNDARY
存入 第一个 AutoreleasePoolPage
对象中从 第 56 字节内存并返回对应内存地址,赋值给 next
指针
当有 autorelease
对象,继续往下面存(每次存放都会更新 next
指针的值,指向下一个能存放autorelease
对象地址的区域)
如果 第一个 AutoreleasePoolPage
不够用了,会创建第二个 AutoreleasePoolPage
对象,并将该对象的 parent
指针指向第一个AutoreleasePoolPage
对象,第一个AutoreleasePoolPage
对象的child
指针指向第二个 AutoreleasePoolPage
对象。然后继续存放 autorelease
对象
…
当调用 objc_autoreleasePoolPop(POOL_BOUNDARY)
依次从栈顶取出 autorelease
对象 直到遇到 POOL_BOUNDARY
,对其 发送 release
消息进行释放 。
注意 :POOL_BOUNDARY
从 调用 PUSH
返回,然后 调用 Pop
中传入。对于 autoreleasepool
嵌套那么就有 三个 POOL_BOUNDARY
。嵌套的时候,每个POOL_BOUNDARY
返回的内存地址都不同。当销毁的时候,暂停遇到的POOL_BOUNDARY
也不同。
如何嵌套是这么存储的?关键看个方法 _objc_autoreleasePoolPrint()
。这个方法可以用来查看自动释放池的情况。
#import <Foundation/Foundation.h>
//在 buildsetting 搜索 automatic reference 将其改成 MRC
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object1 = [[[NSObject alloc]init] autorelease];
@autoreleasepool{
NSObject *object2 = [[[NSObject alloc]init] autorelease];
NSObject *object3 = [[[NSObject alloc]init] autorelease];
@autoreleasepool{
NSObject *object4 = [[[NSObject alloc]init] autorelease];
_objc_autoreleasePoolPrint();
}
}
}
return 0;
}
控制台输出
objc[27124]: ##############
objc[27124]: AUTORELEASE POOLS for thread 0x100392380
objc[27124]: 7 releases pending. // 7 个对象准备 release
objc[27124]: [0x103001000] ................ PAGE (hot) (cold) // page对象(hot 表示当前正在使用的 哪一页 )
objc[27124]: [0x103001038] ################ POOL 0x103001038 // 这个是POOL_BOUNDRY
objc[27124]: [0x103001040] 0x100727ff0 NSObject
objc[27124]: [0x103001048] ################ POOL 0x103001048 // 这个是POOL_BOUNDRY
objc[27124]: [0x103001050] 0x1007230f0 NSObject
objc[27124]: [0x103001058] 0x100721310 NSObject
objc[27124]: [0x103001060] ################ POOL 0x103001060 // 这个是POOL_BOUNDRY
objc[27124]: [0x103001068] 0x10071dd60 NSObject
objc[27124]: ##############
Program ended with exit code: 0
调用 autorelease
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
函数 rootAutorelease
inline id
objc_object::rootAutorelease()
{
/**
如果是标记指针,直接返回当期那对象
*/
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
调用rootAutorelease2()
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
可以看到,调用 autorelease
最后底层调用的是 AutoreleasePoolPage
的公开方法,里面继续走了存放过程的流程。
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);
}
}
下面看下 pop
流程, 也是调用AutoreleasePoolPage
的公开方法, token
参数为POOL_BOUNDARY
,里面的核心方法为 page->releaseUntil(stop)
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
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 (PrintPoolHiwat) printHiwat();
/**
核心代码: 从栈顶开始将 autorelease 对象释放
*/
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
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();
}
}
}
下面看下 核心方法 void releaseUntil(id *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
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;
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
}
那么,剩下最后一个问题,autorelease
对象释放时机的问题。
我觉得回答这个问题,得分两种情况,
第一种,就是对象被 autoreleasepool
包裹着,且有机会调用 AutoReleasePoolPage.pop(POOL_BOUNDARY)
,那么,对象就会立即被释放。
第二种,就是 下面这种情况
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
没机会调用 AutoReleasePoolPage.pop(POOL_BOUNDARY)
,因为这里会有 runloop
,那是不是意味着在其他页面创建的 autorelease
对象就没办法释放了?事实上,并不是。那就说明 main
函数里的autoreleasepool
并不管理其他地方创建的autorelease
对象。事实上,这个跟 runloop
有关系,iOS
在主线程的RunLoop
中注册了两个Observer
第 一 个 Observer
监听了 RunLoop
的 kCFRunLoopEntry
事件,会调用 objc_autoreleasePoolPush()
第 二 个 Observer
监听了 RunLoop
的 kCFRunLoopBeforeWaiting
事件,会调用 objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
第 二 个 Observer
还监听了 RunLoop
的 kCFRunLoopExit
事件,调用objc_autoreleasePoolPop()
这个如何验证,简单,打印下[NSRunLoop mainRunLoop]
即可。
activities = 0x1 = kCFRunLoopEntry
// 第一个观察者
<CFRunLoopObserver 0x604000135720 [0x10bd9ec80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10bf4cd92), context = <CFArray 0x60400005a9a0 [0x10bd9ec80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f8861003048>\n)}}",
activities = 0xa0 = 160 ( = 128 + 32 ) = kCFRunLoopBeforeWaiting | kCFRunLoopExit
// 第二个观察者
<CFRunLoopObserver 0x604000135540 [0x10bd9ec80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10bf4cd92), context = <CFArray 0x60400005a9a0 [0x10bd9ec80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f8861003048>\n)}}"
),
再贴下 RunLoop
的 activity
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
讲了这么多,其实内管管理主要关注两个问题,也是主要解决的两大问题 : 内存泄露和坏内存的访问(僵尸对象)。其中,内存泄露检查,可以使用 instrument
中的 leak
工具检测,僵尸对象可以勾选 Zoombie object
进行捕获。了解底层实现原理的只是帮助你更好的理解整个流程,我觉得还是取决于平常的编码习惯和经验。