iOS内存管理指北

文章目录

一.内存管理准则
二.属性内存管理修饰符全解析
三.block中的weak和strong
四.weak是怎么实现的
五.autoreleasepool实现方式

一.内存管理准则

OC中使用自动引用计数(ARC)的方式实现内存管理,说是自动引用计数,其实遵循的还是iOS5以前的手动引用计数(MRC)的逻辑,不过是编译器隐式为我们实现了retain,release,autorelease那一套东西。我们先引用《iOS与OS X多线程和内存管理》中的类比来认识一下什么是自动引用计数:
假设办公室里的照明设备只有一台,上班进入办公室的人需要照明,所以要把灯打开。而对于下班离开办公室的人来说,已经不需要照明了,所以要把灯关掉。若是很多人上下班,每个人都开灯或是关灯,就会造成最早下班的人关了灯,办公室里还没走的人处于一片黑暗之中的情况。解决这一问题的办法是使办公室在还有至少1人的情况下保持开灯状态,在无人时保持关灯状态。为判断是否还有人在办公室,这里导入计数功能来计算“需要照明的人数”:
1.第一个人进入办公室,“需要照明人数”加1,计数值从0变成1,因此要开灯。
2.之后每当有人进入办公室,“需要照明的人数”就加1...
3.每当有人下班离开办公室,“需要照明的人数”就减1...
4.最后一个人下班离开办公室时,“需要照明的人数”减1,计数值从1变成0,因此需要关灯。
这样就能在不需要照明的时候保持关灯状态,办公室中仅有的照明设备得到了很好的管理。那么OC中的对象就好比办公室的照明设备,当创建某个对象的时候,其引用计数由0变1,当增加强引用指向时,计数加1;强引用不再指向该对象时,计数减1;当引用计数变为0时,说明当前对象已经没有人需要了。那么对象销毁,系统回收内存。

内存管理准则总结起来就下面4条:

  • 自己生成的对象,自己所持有
  • 非自己生成的对象,自己也能持有
  • 不再需要自己持有的对象时释放
  • 非自己持有的对象无法释放

对象操作与Objective-C方法的对应

对象操作 OC方法
生成并持有对象 alloc/new/copy/mutableCopy 方法
持有对象 retain 方法
释放对象 release 方法

这些有关Objective-C内存管理的方法,实际上不包括在该语言中,而是包含在Cocoa框架中用于OS X,iOS应用开发。Cocoa框架中Foundation框架类库的NSObject类担负内存管理的职责。上述的alloc/retain/release/dealloc方法分别指代NSObject类的alloc类方法,retain实例方法,release实例方法和dealloc实例方法。
平时我们使用一个实例对象的时候一般都像这样:

 - (void)test {
    //自己生成并持有对象
    id obj = [[NSObject alloc] init];
    。。。
    //编译器自动添加
    // [obj release];
}

实际上是编译器在test方法结束之前,自动给我们添加了[obj release]这行代码。其实该方法的实现逻辑就是将obj对象的引用计数减1,然后检查引用计数是否为零,如果为零,则调用[obj dealloc]。关于retainrelease,和dealloc方法的实现,后面会具体讲到。
非自己生成的对象,自己也能持有是什么情况呢?比如我们常用的类方法创建实例对象:

- (void)test {
    //取得对象的存在,但自己不持有对象
    id obj = [NSMutableArray array];
    
    //编译器自动添加
    //自己持有对象
    //[obj retain];
    ...
    ...
    //编译器自动添加
    //释放对象
    //[obj release];
}

使用alloc/retain/release/dealloc以外的方法获得的对象,都不是自己持有的,编译器会为我们添加retain方法(引用计数+1),以持有对象,保证在test方法范围内该对象一直存在。最后在test方法结束之前,还需要调用release释放该对象。当然这只是大体的意思,实际编译器针对成对出现的retain/release会有优化策略,这里先不展开说了。
其实说到这里,内存管理的基本原则大概已经说完了,总结起来就是:当创建一个实例对象的时候将其引用计数初始化为1,如果有其他强引用指向的话(实际调用了retain方法),引用计数加1;强引用取消的话(实际调用release方法),引用计数减1;每次减少引用计数都会去检查该对象的引用计数是否为零,如果为零,则内部调用dealloc方法,析构对象,回收内存。关于属性的内存管理,请看第二部分。


二.属性内存管理修饰符全解析

属性的修饰符分为内存管理(strong/weak/assign/copy),读写权限(readwrite/readonly),是否原子性(atomic/nonatomic),getter方法(getter=method)四类。这一节主要分析一下内存管理语义。

strong
strong修饰符表示指向并持有该对象,即所谓的强引用,当某个对象有强引用指向时,其引用计数加1。一般都是用来修饰对象类型。

weak
weak 修饰符指向但是并不持有该对象,即所谓的弱引用,引用计数也不会加1。在 Runtime 中对该属性进行了相关操作,当指向的对象销毁时,所有的弱引用可以自动置空(如何实现的请看第五节)。weak用来修饰对象,多用于避免循环引用的地方,最常见的就是delegate属性使用该修饰符。weak 不可以修饰基本数据类型。

assign
assign主要用于修饰基本数据类型,
例如NSInteger,CGFloat,存储在栈中,内存不用程序员管理。assign是可以修饰对象的,跟weak的区别就是,当指向的对象销毁时,assign修饰的指针不会自动置空,容易引起野指针问题。

copy
copy关键字和 strong类似,都是强引用指向对象。copy除了用来修饰block外, 多用于修饰有可变类型的不可变对象,如NSString,NSArray,NSDictionary上,保证封装性。这个问题用测试代码比较好说明。

@interface ViewController ()

@property (nonatomic, strong) NSString *testString;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSMutableString *ms = [NSMutableString stringWithString:@"test"];
    self.testString = ms;
    NSLog(@">>>>>%@",self.testString);
    
    。。。
    。。。
    
    [ms appendString:@"hello"];
    NSLog(@">>>>>%@",self.testString);
}
@end

运行打印结果:

2018-11-14 11:44:17.391568+0800 ZZTest[4330:96375] >>>>>test
2018-11-14 11:44:17.391698+0800 ZZTest[4330:96375] >>>>>testhello

如果用strong修饰NSString,赋值的是一个NSMutableString对象,如果该对象后续有修改,会影响到testString,这可能并不是我想要的结果。如果换成copy修饰的话就可以避免这个问题,因为testString指向的是一个全新的副本,原对象的修改对它不会有任何影响,测试代码为证。

@interface ViewController ()

@property (nonatomic, copy) NSString *testString;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSMutableString *ms = [NSMutableString stringWithString:@"test"];
    self.testString = ms;
    NSLog(@">>>>>%@",self.testString);
    
    
    
    [ms appendString:@"hello"];
    NSLog(@">>>>>%@",self.testString);
    NSLog(@">>>>>%@",ms);
}
@end

打印结果:

2018-11-14 11:53:29.309521+0800 ZZTest[4510:105814] >>>>>test
2018-11-14 11:53:29.309636+0800 ZZTest[4510:105814] >>>>>test
2018-11-14 11:53:29.309700+0800 ZZTest[4510:105814] >>>>>testhello

所以引申一下copy关键字的一个作用就是多用于修饰有可变类型的不可变对象


三.block中的weak和strong

关于block中的__weak__strong转换,相信用到block的地方都少不了要注意他们的使用。就像SDWebImage中随意截出来的一段代码一样:

        //摘自SDWebImage
        __weak __typeof(self)wself = self;
        SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            wself.sd_imageProgress.totalUnitCount = expectedSize;
            wself.sd_imageProgress.completedUnitCount = receivedSize;
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            if (!sself) { return; }
            ...
        }];

其实关于block的强弱引用转换,在我之前的解读SDWebImage源码的文章中就提过一次。不过这次是专门的内存管理篇,block的__weak__strong不得不提:

  • 1.先weak后strong到底会不会增加引用计数?
  • 2.如果会增加引用计数,那么跟直接使用strong有什么不同?

回答第一个问题之前,我们可以用代码测试一下:

@interface ViewController ()
{
    __weak typeof(NSObject *) _obj;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self test];
    NSLog(@">>>>%@", _obj);

}

- (void)test {
    NSObject *obj = [[NSObject alloc] init];
    _obj = obj;
    NSLog(@">>>>%@", _obj);
}

@end

打印结果如下:

2018-11-12 19:20:55.447117+0800 ZZTest[13659:492278] >>>><NSObject: 0x6000009030d0>
2018-11-12 19:20:55.447220+0800 ZZTest[13659:492278] >>>>(null)

因为test方法中创建的自动变量obj在方法的{}之内是有效的,所以第一个打印有值;出了test方法后,obj只有弱引用指向,所以被释放了。第二个打印为null,这个是很好理解的。
接下来将代码稍作修改,如下:

@interface ViewController ()
{
    __strong typeof(NSObject *) _obj;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self test];
    NSLog(@">>>>%@", _obj);

}

- (void)test {
    NSObject *obj = [[NSObject alloc] init];
    __weak typeof(NSObject *)weakObj = obj;
    _obj = weakObj;
    NSLog(@">>>>%@", _obj);
}

@end

打印结果:

2018-11-12 19:31:06.321660+0800 ZZTest[13856:504099] >>>><NSObject: 0x6000035c4a00>
2018-11-12 19:31:06.321828+0800 ZZTest[13856:504099] >>>><NSObject: 0x6000035c4a00>

结果很好的回答了上面的第一个问题,先weak后strong引用一个对象,会增加该对象的引用计数。那么既然转了一圈还是会增加引用计数,为啥还要“多此一举”呢?其实这就涉及到block的实现原理了,我们知道block会捕获其定义时使用的自动变量。如果block定义时直接使用当前对象的话,那么它捕获的就是默认__strong修饰的对象,而先将其用__weak转一下的话,它捕获的就是对象的弱引用,那么这就打破了所谓的引用循环,避免了内存泄漏。
既然弱引避免了内存泄漏,那么block内部的__strong转换又是什么目的呢?其实这样再转换一次,就是为了增加对象的引用计数,避免其被提前释放(尤其在多线程切换时),否则后续的访问会出现野指针错误!那么一句话回答上面两个问题就是:先weak是为了打破引用循环,避免内存泄漏;后strong是为了保证在block内部该对象一直存在,避免野指针错误。


四.weak是怎么实现的

前面说到当有强引用指向某对象时,该对象的引用计数加1,当强引用取消时,引用计数减1;那么底层是怎么实现计数的加1减1呢?还有weak修饰的属性,当指向的对象被释放时,该指针会自动置空,这又是怎么实现的呢?
为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,它是一个全局Hash表,里面装的都是SideTable结构体。其定义在NSObject.mm的源码中:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

可以看到SideTable有三个成员变量:

1.一把自旋锁spinlock_t slock
百度百科是这么解释的:“何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。”
自旋锁适用于锁使用者保持锁时间比较短的情况,对于引用计数的操作速度其实是非常快的,所以这里使用自旋锁恰到好处。

2.引用计数器RefcountMap refcnts
RefcountMap的定义是这样的

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

其实就是个C++的Map,那么这个Map里面存储的又是什么呢?从这里可以看到:

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

那么size_t的定义是typedef __darwin_size_t size_t;,再进一步看它的定义是unsigned long,在32位和64位操作系统中,它分别占用32和64个bit。这里使用的是bit mask技术。在SideTable结构体定义的上面,定义了这么几个数:

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

1UL<<0的意思是将“1”放到最右侧,然后左移0位(就是原地不动),以32位为例的话就是:0b0000 0000 0000 0000 0000 0000 0000 0001,同理1UL<<1就是:0b0000 0000 0000 0000 0000 0000 0000 0010。上面的定义其实可以这样理解:一个32位的数,其右边第一位SIDE_TABLE_WEAKLY_REFERENCED表示是否有弱引用指向这个对象,如果为1的话,在对象释放的时候需要把所有指向它的弱引用都置为nil;右边第二位SIDE_TABLE_DEALLOCATING表示对象是否正在释放,1正在释放,0没有;左边第一位即最高位SIDE_TABLE_RC_PINNED,其实没有特殊的含义,就是随着对象的引用计数不断变大,如果这一位都变成1了,表示引用计数已经达到了能够存储的最大值。最后SIDE_TABLE_RC_ONE其实定义的就是增加一个引用计数,size_t实际增加的值,因为末尾两位是被占用的,所以引用计数加1,size_t实际加的是4。

3.维护weak指针的结构体weak_table_t weak_table
weak_table_t定义在objc-weak.h文件中:

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

weak_entries是一个数组,num_entries用来维护数组始终有一个合适的size,比如当数组中的元素数量超过3/4时,将数组大小乘以2。
weak_entry_t也定义在objc-weak.h中:

#define WEAK_INLINE_COUNT 4

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 {
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
}

其中三个成员比较重要:referent,被指对象的地址;referrers,可变数组,里面保存着所有指向这个对象的弱引用的地址,如果弱引用指针超过4个的话,将会存在这个数组中;inline_referrers,只有4个元素的数组,默认情况下用它存储弱引用的指针,超过4个的时候存储到referrers中。
先总结一下SideTables的数据结构,如下图所示:

sidetable结构图解.jpg

接着再梳理一下流程,当系统调用retain方法时,最终调用的是NSObject.mm中的这个方法:

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

即取到对应SideTable的refcnts,然后以当前对象地址为key,找到real count,将其增加SIDE_TABLE_RC_ONE,相应的引用计数就加了1。

当系统调用release方法时,最终调用的是NSObject.mm中的这个方法:

uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) {
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

release相比retain多了最终是否需要调用dealloc的判断,大概逻辑是1.遍历变量是否存在,如果不存在就将do_dealloc置为true;2.如果存在再判断是否小于SIDE_TABLE_DEALLOCATING,如果小于也将do_dealloc置为true;3.否则就减去前面说过的SIDE_TABLE_RC_ONE;4.判断是否需要实际调用dealloc。
调用了dealloc方法后,最终会调用到sidetable_clearDeallocating方法:

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

这里加了遍历有值 和 存在弱引用 两个判断条件,如果满足的话就会调用weak_clear_no_lock方法,其定义在objc-weak.mm文件中:

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) {
        /// 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;
    }
    
    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);
}

会先判断最后的遍历数组是referrers数组取还是最大容量为4的inline_referrers数组,在这一步,将每一个weak指针置为了nil。


五.autoreleasepool实现方式

在大部分情况下,我们不需要手动提供autoreleasepool,因为从每个App的入口main函数可以看到,系统默认用了一个自动释放池将我们的代码包含。即所有在主线程创建的非自己持有的对象都添加到了这个autoreleasepool里面。但是我们知道主线程是默认开启runloop的,runloop往简单了说就是一个do while 循环,那么只要这个循环还在执行,main函数里面的autoreleasepool就没办法走到后面这个花括号},那这个自动释放池到底什么时候释放呢?答案是当前runloop迭代结束的时候释放,因为系统在每个runloop迭代中都加入了autoreleasepool的pushpop。具体原理可以深究runloop源码。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    } 
}

文章开头也说了,在ARC环境下,以alloc/new/copy/mutableCopy开头的方法的返回值取得的对象是自己持有的,其他情况下便是取得非自己持有的对象,此时对象的持有者就是autoreleasepool。我们可以用以下代码来验证一下:

#import <Foundation/Foundation.h>
@interface MyObject : NSObject
+ (id)testObject;
@end
@implementation MyObject
+ (id)testObject {
    id obj = [[MyObject alloc] init];
    return obj;
}
+ (id)allocObject {
    id obj = [[MyObject alloc] init];
    return obj;
}
@end

extern void _objc_autoreleasePoolPrint ();

int main(int argc, char * argv[]) {
    __weak id a;
    @autoreleasepool {
        a = [MyObject testObject];
//         a = [MyObject allocObject];
        _objc_autoreleasePoolPrint();
        NSLog(@"in:%@",a);
    }
    NSLog(@"out:%@",a);
}

需要说明的是,其中的_objc_autoreleasePoolPrint方法是非公开的调试方法,需要声明是外部实现的,否则无法使用。运行打印的结果如下:

autoreleasepool打印结果1.jpg

可以看到autoreleasepool持有了对象TestObject,这也验证了生成非自己持有的对象,其真正的持有者是autoreleasepool这一说法。我们将a = [MyObject testObject];这行注释,打开下面一行,运行打印结果如下:

autoreleasepool打印结果2.jpg

可以看到这一次autoreleasepool并没有持有TestObject对象,说明以alloc开头的方法生成的对象是自己持有的。而且,由于a是__weak修饰的,返回的对象由于无人持有,赋值以后立即被释放掉了;所以in:后面打印就是null了。同时编译器已经给出了警告⚠️Assigning retained object to weak variable; object will be released after assignment。应用autoreleasepool这一特性,可以在我们的项目中for in遍历处理大量对象的时候,在循环体内部用autoreleasepool将代码包含,降低应用内存峰值,类似这样:

摘自 SDWebImageCoderHelper.m
for (size_t i = 0; i < frameCount; i++) {
        @autoreleasepool {
            SDWebImageFrame *frame = frames[i];
            float frameDuration = frame.duration;
            CGImageRef frameImageRef = frame.image.CGImage;
            NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
            CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
        }
    }

当然,按照sunnyxx这篇文章最后提到的一个知识点,使用容器的block版本的枚举器时,内部会自动添加一个autoreleasepool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部@autoreleasepool包围着
}];

由于笔者写了测试代码,用“clang -rewrite-objc”命令重写为C++实现后,并没有找到block版本枚举器内部会自动添加autoreleasepool的蛛丝马迹;同时也查看了帮助文档,这个方法的说明也没有提到相关事项。还望知道怎么得出这个结论的朋友指点一下。
那么autoreleasepool的内部实现是怎么样的呢?可以随便写段测试代码,用“clang -rewrite-objc”命令重写为C++一探究竟。测试代码如下:

@implementation ZZTestObject

- (void)test {
    NSArray *arr = @[@"1", @"one", @"2", @"two", @"three", @"3"];
    for (NSString *str in arr) {
        @autoreleasepool {
            NSLog(@">>>>>>>>>>%@", str);
        }
    }
}

终端cd到ZZTestObject.m这一层,运行命令“clang -rewrite-objc ZZTestObject.m”,就会得到一个ZZTestObject.cpp文件,打开后全局搜索@implementation ZZTestObject,可以看到这段代码:

// @implementation ZZTestObject
static void _I_ZZTestObject_test(ZZTestObject * self, SEL _cmd) {
    NSArray *arr = ((NSArray *(*)(Class, SEL, ObjectType  _Nonnull const * _Nonnull, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(6U, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_1, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_2, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_3, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_4, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_5).arr, 6U);
    {
    NSString * str;
    struct __objcFastEnumerationState enumState = { 0 };
    id __rw_items[16];
    id l_collection = (id) arr;
    _WIN_NSUInteger limit =
        ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
        ((id)l_collection,
        sel_registerName("countByEnumeratingWithState:objects:count:"),
        &enumState, (id *)__rw_items, (_WIN_NSUInteger)16);
    if (limit) {
    unsigned long startMutations = *enumState.mutationsPtr;
    do {
        unsigned long counter = 0;
        do {
            if (startMutations != *enumState.mutationsPtr)
                objc_enumerationMutation(l_collection);
            str = (NSString *)enumState.itemsPtr[counter++]; {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_6, str);
        }
    };
    __continue_label_1: ;
        } while (counter < limit);
    } while ((limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
        ((id)l_collection,
        sel_registerName("countByEnumeratingWithState:objects:count:"),
        &enumState, (id *)__rw_items, (_WIN_NSUInteger)16)));
    str = ((NSString *)0);
    __break_label_1: ;
    }
    else
        str = ((NSString *)0);
    }

}

// @end

我们注意这一行:

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_6, str);
        }

我们再全局搜索一下__AtAutoreleasePool,最终会找到这里:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

发现autoreleasepool最终会变成 objc_autoreleasePoolPushobjc_autoreleasePoolPop 两个方法的调用,这里显示这两个方法是外部定义的,那么我们去哪里找这两个方法的实现呢?答案是runtime源码!
这里说一下怎么下载runtime源码:先打开这个网址https://opensource.apple.com/,然后选择你电脑对应的macOS版本,目前我电脑是10.13.6,然后com+F搜索objc4,我这里搜到的是objc4-723,点击下载。打开之后的目录是这样的:

objc源码目录.jpg

我们打开NSObject.mm文件查看,全局搜索objc_autoreleasePoolPush,发现是这样:

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

那就直接看AutoreleasePoolPage这个类的实现:

class AutoreleasePoolPage 
{
    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    ...
}

AutoreleasePoolPage是一个C++实现的类:

  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
  • id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

我们注意这一行:

#   define POOL_BOUNDARY nil

定义了一个POOL_BOUNDARY的宏,值为nil,待会会用到。
看看AutoreleasePoolPage的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;
    }

由于源码太多,就不一一贴码了。总结流程图如下:

autoreleasepoolpage流程图.jpg

首先会判断DebugPoolAllocation标志位,是否需要为每个pool都生成一个新page,为真就走autoreleaseNewPage方法,否则,执行autoreleaseFast方法.
在autoreleaseFast方法中,如果存在page且未满,则直接添加;
如果不存在page,会响应autoreleaseNoPage;
如果当前page已满,则响应autoreleaseFullPage方法;
autoreleaseNoPage和autoreleaseFullPage会生成新的page,然后向该page中添加对象.
而autoreleaseNewPage方法,如果当前存在page,则执行autoreleaseFullPage方法,否则响应autoreleaseNoPage方法,然后就同上了,去执行添加方法。那么具体怎么样添加呢?
每当进行一次push调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,即前面说的POOL_BOUNDARY宏,值为0(也就是个nil),那么这一个page就变成了下面的样子:

pooladd.jpg

objc_autoreleasePoolPush 的返回值就是这个哨兵对象的地址,同时当作 objc_autoreleasePoolPop 的入参:

  1. 根据传入的哨兵对象地址找到哨兵对象所处的page
  2. 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
  3. kill掉空page

pop之后就变成了这样:

pool_pop.jpg

总结一下autoreleasepool的用法:在非UI框架,或者辅助线程中,或者处理大量的临时变量时,需要使用@autoreleasepool {}。编译器会将其转为push和pop两个操作,中间是我们自己的业务逻辑。push时是向AutoReleasePoolPage添加一个值为nil的哨兵对象,并作为该方法的返回值,也是pop方法的入参。pop时根据哨兵对象的地址获取到当前page,然后在当前page中,将晚于哨兵对象添加的对象都发送一次release命令,并更新next指针位置,最后kill掉空page。autoreleasepool允许多层嵌套,逻辑如上,不过是一个个的套娃,一层层的剥离罢了。


结语:内存管理是iOS开发或者面试永远绕不开的一个坎儿,想要完全跨越它,必须一步一个脚印,慢慢攻克。理解这些原理性的东西,实际编程的时候才有理论指导,不至于两眼一抹黑。路漫漫其修远兮,吾将上下而求索。。。


求索.jpg

参考文章:
《iOS与OS X多线程和内存管理》
http://www.cocoachina.com/ios/20170410/19030.html
https://juejin.im/entry/58a178060ce463005644ee4a
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容