iOS中的内存管理

818 阅读18分钟

内存管理对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)手动管理引用计数,通过插入 retainrelease 等方法来管理对象的生命周期。但由于 MRC 维护起来实在是太麻烦了,2011 年的 WWDC 大会上提出了 ARC(Automatic Reference Counting)自动管理引用计数,通过编译器的静态分析,自动插入引入计数的管理逻辑,从而避免繁杂的手动管理。

ARC是一种编译器特性,只是编译器在对应的时间给我们插入了内存管理的代码,其本质还是按照MRC的规则

标记-清除

在其他编程语言中,除了有引用计数之外,还有一种标记-清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。 标记阶段是把所有活动对象都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。通过这两个阶段,就可以令不能利用的内存空间重新得到利用

这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

可达性算法(Tracing GC)

此处不做过多说明。

相比之下,引用计数由于只记录了对象的被引用次数,实际上只是一个局部的信息,而缺乏全局信息,因此可能产生循环引用的问题,于是在代码层面就需要格外注意。

那么为什么 iOS 还要采用引用计数呢?

首先使用引用计数,对象生命周期结束时,可以立刻被回收,而不需要等到全局遍历之后再回首。其次,在内存不充裕的情况下,可达性算法 算法的延迟更大,效率反而更低,由于 iPhone 整体内存偏小,所以引用计数算是一种更为合理的选择。

Tagged Pointer

在说引用计数之前,先说一个"法外之徒":Tagged Pointer

  • 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumberNSDateNSString小对象的存储

  • 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

  • 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

  • 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

  • objc_msgSend能识别Tagged Pointer,比如NSNumberintValue方法,直接从指针提取数据,节省了以前的调用开销

  • 如何判断一个指针是否为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

根据打印发现str1NSTaggedPointerString类型,是不通过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

内存管理的经验总结: 当调用allocnewcopymutableCopy方法返回了一个对象,在不需要这个对象时,要调用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];
    }
}

关于copymutablCopy总结:

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,最后把这个entryweak表中删除,最后清理对象的记录。

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_autoreleasePoolPushobjc_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

以上若有错误,欢迎指正。转载请注明出处。