内存管理对iOS开发者来说,是很重要的一环。
内存结构
在聊内存管理之前,先看看iOS中内存是怎么分布的吧
内存结构如下图所示:
- 代码段:编译之后的代码
- 数据段
- 字符串常量:比如NSString *str = @"123"
- 已初始化数据:已初始化的全局变量、静态变量等
- 未初始化数据:未初始化的全局变量、静态变量等
- 堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
- 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
举个例子:
int a = 10;
int b;
int main(int argc, char * argv[]) {
@autoreleasepool {
static int c = 20;
static int d;
int e;
int f = 20;
NSString *str = @"123";
NSObject *obj = [[NSObject alloc] init];
NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr=%p\nobj=%p\n",
&a, &b, &c, &d, &e, &f, str, obj);
return UIApplicationMain(argc, argv, nil,NSStringFromClass([AppDelegate class]));
}
}
&a = 0x105604d98 //已初始化的全局变量、静态变量
&c = 0x105604d9c //已初始化的全局变量、静态变量
&b = 0x105604e64 //未初始化的全局变量、静态变量
&d = 0x105604e60 //未初始化的全局变量、静态变量
obj = 0x608000012210 // 堆
&e = 0x7ffeea5fcff4 // 栈
&f = 0x7ffeea5fcff0 // 栈
str = 0x105604068 //字符串常量
内存管理
- 1、分配需要的内存(初始化值时)
- 2、使用分配的内存
- 3、不需要时将其内存释放(垃圾回收)
内存管理主要关注的就是第三个阶段:垃圾回收
说道内存管理(垃圾回收),先来说一下有几种内存管理的方式;
引用计数
堆区需要程序员进行管理,如何管理、记录、回收就是一个很值得思考的问题。
iOS 采用的是引用计数(Reference Counting)的方式,将资源被引用的次数保存起来,当被引用次数变为零时就将其空间释放回收。
对于早期 iOS 来说,使用的是 MRC(Mannul Reference Counting)手动管理引用计数,通过插入 retain、release 等方法来管理对象的生命周期。但由于 MRC 维护起来实在是太麻烦了,2011 年的 WWDC 大会上提出了 ARC(Automatic Reference Counting)自动管理引用计数,通过编译器的静态分析,自动插入引入计数的管理逻辑,从而避免繁杂的手动管理。
ARC是一种编译器特性,只是编译器在对应的时间给我们插入了内存管理的代码,其本质还是按照MRC的规则
标记-清除
在其他编程语言中,除了有引用计数之外,还有一种标记-清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。 标记阶段是把所有活动对象都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。通过这两个阶段,就可以令不能利用的内存空间重新得到利用
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
可达性算法(Tracing GC)
此处不做过多说明。
相比之下,引用计数由于只记录了对象的被引用次数,实际上只是一个局部的信息,而缺乏全局信息,因此可能产生循环引用的问题,于是在代码层面就需要格外注意。
那么为什么 iOS 还要采用引用计数呢?
首先使用引用计数,对象生命周期结束时,可以立刻被回收,而不需要等到全局遍历之后再回首。其次,在内存不充裕的情况下,可达性算法 算法的延迟更大,效率反而更低,由于 iPhone 整体内存偏小,所以引用计数算是一种更为合理的选择。
Tagged Pointer
在说引用计数之前,先说一个"法外之徒":Tagged Pointer
-
从64bit开始,iOS引入了Tagged Pointer技术,用于优化
NSNumber、NSDate、NSString等小对象的存储 -
在没有使用
Tagged Pointer之前,NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值 -
使用
Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中 -
当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
-
objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销 -
如何判断一个指针是否为
Tagged Pointer?- iOS平台,最高有效位是1(第64bit)
- Mac平台,最低有效位是1
判断是否是Tagged Pointer代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSNumber *number1 = @4;
NSNumber *number2 = @5;
NSNumber *number3 = @(0xFFFFFFFFFFFFFFF);
NSLog(@"\nnumber1=%p\nnumber2=%p\nnumber3=%p", number1, number2, number3);
}
return 0;
}
打印:
number1=0xb327e58317e80524
number2=0xb327e58317e80534
number3=0x600000f2a220
NSString *str1 = [NSString stringWithFormat:@"abc"];
NSString *str2 = [NSString stringWithFormat:@"abcdefghijk"];
NSLog(@"\n[str1 class]=%@\n[str2 class]=%@",[str1 class],[str2 class]);
打印:
[str1 class]=NSTaggedPointerString
[str2 class]=__NSCFString
根据打印发现str1是NSTaggedPointerString类型,是不通过set方法找对象的。
我们也可以在源码中找到相关实现,
1、在NSObject.mm中查找retain方法的实现
- (id)retain {
return ((id)self)->rootRetain();
}
2、点击进入rootRetain方法,我们可以在里面找到if (isTaggedPointer()) return (id)this;也就是说如果是TaggedPointer类型,直接返回,不需要根据指针查找。
问:这两段代码打印结果有什么区别?
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 100; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghijk"];
});
}
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 100; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
OC对象的内存管理
在iOS中,使用引用计数来管理OC对象的内存
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结:
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
copy
为什么要使用copy?
- 1、拷贝的目的:产生一个副本对象,跟源对象互不影响
- 修改了源对象,不会影响副本对象
- 修改了副本对象,不会影响源对象
- 2、iOS提供了2个拷贝方法
- 1、copy,不可变拷贝,产生不可变副本
- 2、mutableCopy,可变拷贝,产生可变副本
- 3、深拷贝和浅拷贝
- 1、深拷贝:内容拷贝,产生新的对象
- 2、浅拷贝:指针拷贝,没有产生新的对象
NSString & NSMutableString
- 对
不可变字符串进行copy&mutableCopy操作
void test1()
{
NSString *str1 = [NSString stringWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 返回的是NSString
NSMutableString *str3 = [str1 mutableCopy]; // 返回的是NSMutableString
NSLog(@"%p %p %p", str1, str2, str3);
}
打印:
0xaa073e462fbfcdc7
0xaa073e462fbfcdc7
0x282143150
根据打印的地址可以看出不可变字符串在copy时是浅拷贝,只拷贝了指针没有拷贝对象;mutableCopy则是深拷贝,产生了新的对象
补充: 如果str1的字符串比较短,有可能会采用TaggedPointer,不会是对象类型
- 对
可变字符串进行copy&mutableCopy操作
void test2()
{
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test"]; // 1
NSString *str2 = [str1 copy]; // 深拷贝
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝
NSLog(@"%p %p %p", str1, str2, str3);
}
打印:
0x281dd76c0
0x83e9bdf3daead453
0x281dd7a50
根据打印的地址可以看出对于可变字符串不论是copy还是mutableCopy都是深拷贝
NSArray & NSMutableArray
- 对不可变数组进行copy&mutableCopy操作
void test3()
{
NSArray *array1 = [[NSArray alloc] initWithObjects:@"a", @"b", nil];
NSArray *array2 = [array1 copy]; // 浅拷贝
NSMutableArray *array3 = [array1 mutableCopy]; // 深拷贝
NSLog(@"%p %p %p", array1, array2, array3);
}
打印:
0x2823ac740
0x2823ac740
0x282dd6ca0
根据打印的地址可以看出不可变数组在copy时是浅拷贝,只拷贝了指针没有拷贝对象;mutableCopy则是深拷贝,产生了新的对象
- 对可变数组进行copy&mutableCopy操作
void test4()
{
NSMutableArray *array1 = [[NSMutableArray alloc] initWithObjects:@"a", @"b", nil];
NSArray *array2 = [array1 copy]; // 深拷贝
NSMutableArray *array3 = [array1 mutableCopy]; // 深拷贝
NSLog(@"%p %p %p", array1, array2, array3);
}
打印:
0x282993600
0x2827fc760
0x282993270
根据打印的地址可以看出对于可变数组不论是copy还是mutableCopy都是深拷贝
NSDictionary & NSMutableDictionary
- 对不可变字典进行copy&mutableCopy操作
void test5()
{
NSDictionary *dict1 = [[NSDictionary alloc] initWithObjectsAndKeys:@"jack", @"name", nil];
NSDictionary *dict2 = [dict1 copy]; // 浅拷贝
NSMutableDictionary *dict3 = [dict1 mutableCopy]; // 深拷贝
NSLog(@"%p %p %p", dict1, dict2, dict3);
}
打印:
0x2824dfbc0
0x2824dfbc0
0x2824dfb20
根据打印的地址可以看出不可变字典在copy时是浅拷贝,只拷贝了指针没有拷贝对象;mutableCopy则是深拷贝,产生了新的对象
- 对可变字典进行copy&mutableCopy
void test6()
{
NSMutableDictionary *dict1 = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"jack", @"name", nil];
NSDictionary *dict2 = [dict1 copy]; // 深拷贝
NSMutableDictionary *dict3 = [dict1 mutableCopy]; // 深拷贝
NSLog(@"%p %p %p", dict1, dict2, dict3);
}
打印:
0x283e50c60
0x283e50bc0
0x283e50ba0
根据打印的地址可以看出对于可变数组不论是copy还是mutableCopy都是深拷贝
对普通的类对象进行copy实现?
若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
- 1、需声明该类遵从 NSCopying 协议
- 2、实现 NSCopying 协议
- (id)copyWithZone:(NSZone *)zone; - 3、在
- (id)copyWithZone:(NSZone *)zone;方法中对类对象进行重新赋值
demo:
@interface Dog : NSObject<NSCopying>
@property (nonatomic,assign) int age;
@property (nonatomic,copy) NSString *name;
@end
- (id)copyWithZone:(NSZone *)zone{
Dog *d = [[self class]allocWithZone:zone];
d.age = _age;
d.name = _name;
return d;
}
- (void)setName:(NSString *)name {
if (_name != name) {
//[_name release];//MRC
_name = [name copy];
}
}
关于copy与mutablCopy总结:
assign weak
assign一般用来修饰基本的数据类型,包括基础数据类型 (NSInteger,CGFloat)和C数据类型(int, float, double, char, 等等),为什么呢?
assign声明的属性是不会增加引用计数的,也就是说声明的属性释放后,就没有了,即使其他对象用到了它,也无法留住它,只会crash。
但是,即使被释放,指针却还在,成为了野指针,如果新的对象被分配到了这个内存地址上,又会crash,所以一般只用来声明基本的数据类型,因为它们会被分配到栈上,而栈会由系统自动处理,不会造成野指针
weak和assign的区别
-
1.修饰变量类型的区别
weak只可以修饰对象。如果修饰基本数据类型,编译器会报错-Property with ‘weak’ attribute must be of object type。assign可修饰对象,和基本数据类型。当需要修饰对象类型时,MRC时代使用unsafe_unretained。当然,unsafe_unretained也可能产生野指针,所以它名字是unsafe_。
-
2.是否产生野指针的区别
weak不会产生野指针问题。因为weak修饰的对象释放后(引用计数器值为0),指针会自动被置nil,之后再向该对象发消息也不会崩溃。 weak是安全的。assign如果修饰对象,会产生野指针问题;如果修饰基本数据类型则是安全的。修饰的对象释放后,指针不会自动被置空,此时向对象发消息会崩溃。
下面详细说一下weak
weak底层实现其实就是一个hash表,key是所指对象的地址,value是weak指针的地址数组
weak 实现原理的概括
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组
weak 的实现原理可以概括一下三步:
-
1、初始化时:runtime会调用
objc_initWeak函数,初始化一个新的weak指针指向对象的地址。 -
2、添加引用时:
objc_initWeak函数会调用objc_storeWeak()函数,objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。 -
3、释放时,调用
clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
NSObject.mm文件中,有一个objc_initWeak方法,这个就是weak初始化函数
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {//无效对象直接导致指针释放
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
下面就是更新指针指向,创建对应的弱引用表
static id
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
//声明新旧两个SideTable散列表
SideTable *oldTable;
SideTable *newTable;
retry:
if (haveOld) {// 更改指针,获得以 oldObj 为索引所存储的值地址
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {// 更改新值指针,获得以 newObj 为索引所存储的值地址
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 加锁操作,防止多线程中竞争冲突
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);// 解锁
class_initialize(cls, (id)newObj); // 对其 isa 指针进行初始化
// 如果该类已经完成执行 +initialize 方法是最理想情况
// 如果该类 +initialize 在线程中
// 例如 +initialize 正在调用 storeWeak 方法
// 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记
previouslyInitializedClass = cls;
goto retry;
}
}
// 清除旧值
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// 分配新值
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
if (!newObj->isTaggedPointerOrNil()) {
newObj->setWeaklyReferenced_nolock();
}
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
查看SideTable的结构
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
};
可以发现SideTable是一个结构体
- 1、
spinlock_t slock;保证原子操作的自旋锁 - 2、
RefcountMap refcnts;引用计数的 hash 表 - 3、
weak_table_t weak_table;weak 引用全局 hash 表
研究一下weak_table_t这个hash表
struct weak_table_t {
weak_entry_t *weak_entries; // 保存了所有指向指定对象的 weak 指针
size_t num_entries; // 存储空间
uintptr_t mask; //参与判断引用计数辅助量
uintptr_t max_hash_displacement;//hash key 最大偏移值
};
这是一个全局弱引用hash表。使用对象的地址作为key ,用 weak_entry_t类型结构体对象作为value
再来看看weak_entry_t
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表
再来看看如何清除的clearDeallocating
objc_clear_deallocating该函数的动作如下:
- 1、从weak表中获取废弃对象的地址为键值的记录
- 2、将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
- 3、将weak表中该记录删除
- 4、从引用计数表中删除废弃对象的地址为键值的记录
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this]; //从weak表中获取废弃对象的地址为键值的记录
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();
}
clearDeallocating_slow中首先是找到weak表中获取废弃对象的地址为键值的记录,然后调用weak_clear_no_lock函数进行清除操作
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id; //找到对象
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
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;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil; //清除对象,赋值为nil
}else if (*referrer) {
objc_weak_error();
}
}
}
//从引用计数表中删除废弃对象的地址为键值的记录
weak_entry_remove(weak_table, entry);
}
dealloc
在NSObject.mm可以查找到dealloc函数
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
可以看出:
- 1、首先判断对象是不是
isTaggedPointer,如果是TaggedPointer那么没有采用引用计数技术,所以直接return - 2、不是
TaggedPointer,判断是否有弱引用、是否有关联对象、是否有析构函数、是否引用计数器是否过大无法存储在isa中等,再就去销毁这个对象object_dispose
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
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, /*deallocating*/true); //清除成员变量
obj->clearDeallocating(); //将指向当前对象的弱引用指针置为nil
}
return obj;
}
可以看出:
- 1、判断是否有析构函数,是否有关联对象
- 2、如果有析构函数,就销毁;如果有关联对象,清除关联对象;
- 3、将指向当前对象的弱引用指针置为nil
atomic
前面说到锁的时候,就提到了atomic
atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
可以参考源码objc4的objc-accessors.mm 它并不能保证使用属性的过程是线程安全的
相关源码:
//objc-accessors.mm
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
//如果不是atomic,就直接返回值
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
//如果是atomic,就进行加锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
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);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
//如果不是atomic,就直接设置newValue
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
//如果是atomic,就使用spinlock_t加锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
atomic 在对象getter/setter的时候,会有一个spinlock_t控制。
上面的两个函数,可以说就是setter``getter方法底层的实现了。
autoreleasepool
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[[Person alloc]init] autorelease];
}
return 0;
}
通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令将 main.m 转成 C++ 代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
return 0;
}
可以发现: @autoreleasepool 被转成__AtAutoreleasePool __autoreleasepool;
而__AtAutoreleasePool我们全局查找发现他是一个结构体
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
更换一下就是:
@autoreleasepool {
Person *p = [[[Person alloc]init] autorelease];
}
上面这段代码其实就是这个样子
##
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
对于objc_autoreleasePoolPush和objc_autoreleasePoolPop 的实现我们可以在runtime源码中查找相关实现
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
研究可以发现,push()函数和pop(ctxt)函数都是有AutoreleasePoolPage类来调用的。
AutoreleasePoolPage
对于AutoreleasePoolPage类,查看成员变量,对于一些静态常亮就不过多的探究,就来查看一下成员变量。
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
//有删减
magic_t const magic;// 用来校验AutoreleasePoolPage结构是否完整;
id *next;// 指向最新添加的autoreleased对象的下一个位置,初始化时指向begin;
pthread_t const thread;//指向当前线程;
AutoreleasePoolPage * const parent;//指向父结点,第一个AutoreleasePoolPage结点的父结点为nil;
AutoreleasePoolPage *child;//指向子结点,最后一个AutoreleasePoolPage结点的子结点为nil;
uint32_t const depth;//当前结点的深度,从0开始,往后递增;
uint32_t hiwat; //代表hige water mark最大入栈数量标记;
// ...
}
- 1、每个
AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址 - 2、所有的
AutoreleasePoolPage对象通过双向链表的形式连接在一起 - 3、调用push方法会将一个
POOL_BOUNDARY入栈,并且返回其存放的内存地址 - 4、调用pop方法时传入一个
POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY - 5、
id *next指向了下一个能存放autorelease对象地址的区域 - 6、
AutoreleasePoolPage空间被占满时,会以链表的形式新建链接一个AutoreleasePoolPage对象,然后将新的autorelease对象的地址存在child指针
push()函数实现
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
- 1、在
DebugPoolAllocation线程池满了以后,会调用autoreleaseNewPage(POOL_BOUNDARY)来创建一个新的线程池。 - 2、线程池没有满的时候调用
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);
}
}
- 有
hotPage并且当前page 不满,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中 - 有
hotPage并且当前page 已满,调用autoreleaseFullPage初始化一个新的页,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中 - 无
hotPage,调用autoreleaseNoPage 创建一个hotPage,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中
pop()函数
// 简化后
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
// 1.根据 token,也就是上文的占位 POOL_BOUNDARY 释放 `autoreleased` 对象
page->releaseUntil(stop);
// hysteresis: keep one empty child if page is more than half full
// 2.释放 `Autoreleased` 对象后,销毁多余的 page。
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
来到 releaseUntil(...) 内部:
// 简化后
void releaseUntil(id *stop)
{
// 1.
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
// 2.
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
// 3.
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
// 4.
setHotPage(this);
}
- 1、外部循环挨个遍历
autoreleased对象,直到遍历到stop这个POOL_BOUNDARY。 - 2、如果当前
hatPage没有POOL_BOUNDARY,将hotPage设置为父节点。 - 3、给当前
autoreleased对象发送release消息。 - 4、再次配置
hotPage。
int main(int argc, const char * argv[]) {
@autoreleasepool {//r1 = push()
Person *p1 = [[[Person alloc]init] autorelease];
Person *p2 = [[[Person alloc]init] autorelease];
@autoreleasepool {//r2 = push()
Person *p3 = [[[Person alloc]init] autorelease];
@autoreleasepool {//r3 = push()
Person *p4 = [[[Person alloc]init] autorelease];
_objc_autoreleasePoolPrint();
}//pop(r3)
}//pop(r2)
}//pop(r1)
return 0;
}
每次 Push 后,都会先添加一个 POOL_BOUNDARY 来占位,是为了对应一次 Pop 的释放 例如图中的 page 就需要两次 Pop 然后完全的释放
有一个私有变量,我们可以打印线程池内容extern void _objc_autoreleasePoolPrint(void);
(命令行项目中运行)
如下图:
_objc_autoreleasePoolPrint源码:
void
_objc_autoreleasePoolPrint(void)
{
AutoreleasePoolPage::printAll();
}
static void printAll()
{
_objc_inform("##############");
_objc_inform("AUTORELEASE POOLS for thread %p", pthread_self());
AutoreleasePoolPage *page;
ptrdiff_t objects = 0;
for (page = coldPage(); page; page = page->child) {
objects += page->next - page->begin();
}
_objc_inform("%llu releases pending.", (unsigned long long)objects);
if (haveEmptyPoolPlaceholder()) {
_objc_inform("[%p] ................ PAGE (placeholder)",
EMPTY_POOL_PLACEHOLDER);
_objc_inform("[%p] ################ POOL (placeholder)",
EMPTY_POOL_PLACEHOLDER);
}
else {
for (page = coldPage(); page; page = page->child) {
page->print();
}
}
_objc_inform("##############");
}
AutoreleasePool 和 Runloop
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[[Person alloc]init] autorelease];
NSLog(@"%s",__func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
MRC环境下打印:
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[Person dealloc]
-[ViewController viewDidAppear:]
ARC环境下打印:
-[ViewController viewDidLoad]
-[Person dealloc]
-[ViewController viewWillAppear:]
-[ViewController viewDidAppear:]
Person明显在ARC下比MRC下提早释放了,
可以猜测在ARC环境下,编译器会帮我们做一些操作,就是在viewDidLoad结束之前帮我们release
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc]init];
NSLog(@"%s",__func__);
[p release];
}
我们打印一下当前RunLoop[NSRunLoop currentRunLoop]
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[[Person alloc]init] autorelease];
NSLog(@"%@", [NSRunLoop mainRunLoop]);
}
可以看出callout是_wrapRunLoopWithAutoreleasePoolHandler
而activities = 0x1, activities = 0xa0, (转成10进制0x1 = 1,0xa0=160 = 32+128 )
根据RunLoop的状态枚举
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
};
具体步骤
- 1、iOS在主线程的Runloop中注册了2个Observer
- 2、第1个Observer监听了
kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush() - 3、第2个Observer监听了
kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush(); 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
autoreleased 对象是在 runloop 的即将进入休眠时进行释放的
再来看看子线程:
默认主线的运行循环(runloop)是开启的,子线程的运行循环(runloop)默认是不开启的,也就意味着子线程中不会创建autoreleasepool,所以需要我们自己在子线程中创建一个自动释放池。(子线程里面使用的类方法都是autorelease,就会没有池子可释放,也就意味着后面没有办法进行释放,造成内存泄漏。)
在主线程中如果产生事件那么runloop才回去创建autoreleasepool,通过这个道理我们就知道为什么子线程中不会创建自动释放池了,因为子线程的runloop默认是关闭的,所以他不会自动创建autoreleasepool,需要我们手动添加;
NSThread和NSOperationQueue开辟子线程需要手动创建autoreleasepool,GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool
以上若有错误,欢迎指正。转载请注明出处。