iOS內(nèi)存管理指北

文章目錄

一.內(nèi)存管理準(zhǔn)則
二.屬性內(nèi)存管理修飾符全解析
三.block中的weak和strong
四.weak是怎么實(shí)現(xiàn)的
五.autoreleasepool實(shí)現(xiàn)方式

一.內(nèi)存管理準(zhǔn)則

OC中使用自動引用計(jì)數(shù)(ARC)的方式實(shí)現(xiàn)內(nèi)存管理,說是自動引用計(jì)數(shù),其實(shí)遵循的還是iOS5以前的手動引用計(jì)數(shù)(MRC)的邏輯,不過是編譯器隱式為我們實(shí)現(xiàn)了retain,release,autorelease那一套東西。我們先引用《iOS與OS X多線程和內(nèi)存管理》中的類比來認(rèn)識一下什么是自動引用計(jì)數(shù):
假設(shè)辦公室里的照明設(shè)備只有一臺,上班進(jìn)入辦公室的人需要照明,所以要把燈打開。而對于下班離開辦公室的人來說,已經(jīng)不需要照明了,所以要把燈關(guān)掉。若是很多人上下班,每個(gè)人都開燈或是關(guān)燈,就會造成最早下班的人關(guān)了燈,辦公室里還沒走的人處于一片黑暗之中的情況。解決這一問題的辦法是使辦公室在還有至少1人的情況下保持開燈狀態(tài),在無人時(shí)保持關(guān)燈狀態(tài)。為判斷是否還有人在辦公室,這里導(dǎo)入計(jì)數(shù)功能來計(jì)算“需要照明的人數(shù)”:
1.第一個(gè)人進(jìn)入辦公室,“需要照明人數(shù)”加1,計(jì)數(shù)值從0變成1,因此要開燈。
2.之后每當(dāng)有人進(jìn)入辦公室,“需要照明的人數(shù)”就加1...
3.每當(dāng)有人下班離開辦公室,“需要照明的人數(shù)”就減1...
4.最后一個(gè)人下班離開辦公室時(shí),“需要照明的人數(shù)”減1,計(jì)數(shù)值從1變成0,因此需要關(guān)燈。
這樣就能在不需要照明的時(shí)候保持關(guān)燈狀態(tài),辦公室中僅有的照明設(shè)備得到了很好的管理。那么OC中的對象就好比辦公室的照明設(shè)備,當(dāng)創(chuàng)建某個(gè)對象的時(shí)候,其引用計(jì)數(shù)由0變1,當(dāng)增加強(qiáng)引用指向時(shí),計(jì)數(shù)加1;強(qiáng)引用不再指向該對象時(shí),計(jì)數(shù)減1;當(dāng)引用計(jì)數(shù)變?yōu)?時(shí),說明當(dāng)前對象已經(jīng)沒有人需要了。那么對象銷毀,系統(tǒng)回收內(nèi)存。

內(nèi)存管理準(zhǔn)則總結(jié)起來就下面4條:

  • 自己生成的對象,自己所持有
  • 非自己生成的對象,自己也能持有
  • 不再需要自己持有的對象時(shí)釋放
  • 非自己持有的對象無法釋放

對象操作與Objective-C方法的對應(yīng)

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

這些有關(guān)Objective-C內(nèi)存管理的方法,實(shí)際上不包括在該語言中,而是包含在Cocoa框架中用于OS X,iOS應(yīng)用開發(fā)。Cocoa框架中Foundation框架類庫的NSObject類擔(dān)負(fù)內(nèi)存管理的職責(zé)。上述的alloc/retain/release/dealloc方法分別指代NSObject類的alloc類方法,retain實(shí)例方法,release實(shí)例方法和dealloc實(shí)例方法。
平時(shí)我們使用一個(gè)實(shí)例對象的時(shí)候一般都像這樣:

 - (void)test {
    //自己生成并持有對象
    id obj = [[NSObject alloc] init];
    。。。
    //編譯器自動添加
    // [obj release];
}

實(shí)際上是編譯器在test方法結(jié)束之前,自動給我們添加了[obj release]這行代碼。其實(shí)該方法的實(shí)現(xiàn)邏輯就是將obj對象的引用計(jì)數(shù)減1,然后檢查引用計(jì)數(shù)是否為零,如果為零,則調(diào)用[obj dealloc]。關(guān)于retainrelease,和dealloc方法的實(shí)現(xiàn),后面會具體講到。
非自己生成的對象,自己也能持有是什么情況呢?比如我們常用的類方法創(chuàng)建實(shí)例對象:

- (void)test {
    //取得對象的存在,但自己不持有對象
    id obj = [NSMutableArray array];
    
    //編譯器自動添加
    //自己持有對象
    //[obj retain];
    ...
    ...
    //編譯器自動添加
    //釋放對象
    //[obj release];
}

使用alloc/retain/release/dealloc以外的方法獲得的對象,都不是自己持有的,編譯器會為我們添加retain方法(引用計(jì)數(shù)+1),以持有對象,保證在test方法范圍內(nèi)該對象一直存在。最后在test方法結(jié)束之前,還需要調(diào)用release釋放該對象。當(dāng)然這只是大體的意思,實(shí)際編譯器針對成對出現(xiàn)的retain/release會有優(yōu)化策略,這里先不展開說了。
其實(shí)說到這里,內(nèi)存管理的基本原則大概已經(jīng)說完了,總結(jié)起來就是:當(dāng)創(chuàng)建一個(gè)實(shí)例對象的時(shí)候?qū)⑵湟糜?jì)數(shù)初始化為1,如果有其他強(qiáng)引用指向的話(實(shí)際調(diào)用了retain方法),引用計(jì)數(shù)加1;強(qiáng)引用取消的話(實(shí)際調(diào)用release方法),引用計(jì)數(shù)減1;每次減少引用計(jì)數(shù)都會去檢查該對象的引用計(jì)數(shù)是否為零,如果為零,則內(nèi)部調(diào)用dealloc方法,析構(gòu)對象,回收內(nèi)存。關(guān)于屬性的內(nèi)存管理,請看第二部分。


二.屬性內(nèi)存管理修飾符全解析

屬性的修飾符分為內(nèi)存管理(strong/weak/assign/copy),讀寫權(quán)限(readwrite/readonly),是否原子性(atomic/nonatomic),getter方法(getter=method)四類。這一節(jié)主要分析一下內(nèi)存管理語義。

strong
strong修飾符表示指向并持有該對象,即所謂的強(qiáng)引用,當(dāng)某個(gè)對象有強(qiáng)引用指向時(shí),其引用計(jì)數(shù)加1。一般都是用來修飾對象類型。

weak
weak 修飾符指向但是并不持有該對象,即所謂的弱引用,引用計(jì)數(shù)也不會加1。在 Runtime 中對該屬性進(jìn)行了相關(guān)操作,當(dāng)指向的對象銷毀時(shí),所有的弱引用可以自動置空(如何實(shí)現(xiàn)的請看第五節(jié))。weak用來修飾對象,多用于避免循環(huán)引用的地方,最常見的就是delegate屬性使用該修飾符。weak 不可以修飾基本數(shù)據(jù)類型。

assign
assign主要用于修飾基本數(shù)據(jù)類型,
例如NSInteger,CGFloat,存儲在棧中,內(nèi)存不用程序員管理。assign是可以修飾對象的,跟weak的區(qū)別就是,當(dāng)指向的對象銷毀時(shí),assign修飾的指針不會自動置空,容易引起野指針問題。

copy
copy關(guān)鍵字和 strong類似,都是強(qiáng)引用指向?qū)ο?。copy除了用來修飾block外, 多用于修飾有可變類型的不可變對象,如NSString,NSArray,NSDictionary上,保證封裝性。這個(gè)問題用測試代碼比較好說明。

@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

運(yùn)行打印結(jié)果:

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,賦值的是一個(gè)NSMutableString對象,如果該對象后續(xù)有修改,會影響到testString,這可能并不是我想要的結(jié)果。如果換成copy修飾的話就可以避免這個(gè)問題,因?yàn)閠estString指向的是一個(gè)全新的副本,原對象的修改對它不會有任何影響,測試代碼為證。

@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

打印結(jié)果:

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關(guān)鍵字的一個(gè)作用就是多用于修飾有可變類型的不可變對象。


三.block中的weak和strong

關(guān)于block中的__weak__strong轉(zhuǎn)換,相信用到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; }
            ...
        }];

其實(shí)關(guān)于block的強(qiáng)弱引用轉(zhuǎn)換,在我之前的解讀SDWebImage源碼的文章中就提過一次。不過這次是專門的內(nèi)存管理篇,block的__weak,__strong不得不提:

  • 1.先weak后strong到底會不會增加引用計(jì)數(shù)?
  • 2.如果會增加引用計(jì)數(shù),那么跟直接使用strong有什么不同?

回答第一個(gè)問題之前,我們可以用代碼測試一下:

@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

打印結(jié)果如下:

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)

因?yàn)?code>test方法中創(chuàng)建的自動變量obj在方法的{}之內(nèi)是有效的,所以第一個(gè)打印有值;出了test方法后,obj只有弱引用指向,所以被釋放了。第二個(gè)打印為null,這個(gè)是很好理解的。
接下來將代碼稍作修改,如下:

@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

打印結(jié)果:

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>

結(jié)果很好的回答了上面的第一個(gè)問題,先weak后strong引用一個(gè)對象,會增加該對象的引用計(jì)數(shù)。那么既然轉(zhuǎn)了一圈還是會增加引用計(jì)數(shù),為啥還要“多此一舉”呢?其實(shí)這就涉及到block的實(shí)現(xiàn)原理了,我們知道block會捕獲其定義時(shí)使用的自動變量。如果block定義時(shí)直接使用當(dāng)前對象的話,那么它捕獲的就是默認(rèn)__strong修飾的對象,而先將其用__weak轉(zhuǎn)一下的話,它捕獲的就是對象的弱引用,那么這就打破了所謂的引用循環(huán),避免了內(nèi)存泄漏。
既然弱引避免了內(nèi)存泄漏,那么block內(nèi)部的__strong轉(zhuǎn)換又是什么目的呢?其實(shí)這樣再轉(zhuǎn)換一次,就是為了增加對象的引用計(jì)數(shù),避免其被提前釋放(尤其在多線程切換時(shí)),否則后續(xù)的訪問會出現(xiàn)野指針錯(cuò)誤!那么一句話回答上面兩個(gè)問題就是:先weak是為了打破引用循環(huán),避免內(nèi)存泄漏;后strong是為了保證在block內(nèi)部該對象一直存在,避免野指針錯(cuò)誤。


四.weak是怎么實(shí)現(xiàn)的

前面說到當(dāng)有強(qiáng)引用指向某對象時(shí),該對象的引用計(jì)數(shù)加1,當(dāng)強(qiáng)引用取消時(shí),引用計(jì)數(shù)減1;那么底層是怎么實(shí)現(xiàn)計(jì)數(shù)的加1減1呢?還有weak修飾的屬性,當(dāng)指向的對象被釋放時(shí),該指針會自動置空,這又是怎么實(shí)現(xiàn)的呢?
為了管理所有對象的引用計(jì)數(shù)和weak指針,蘋果創(chuàng)建了一個(gè)全局的SideTables,它是一個(gè)全局Hash表,里面裝的都是SideTable結(jié)構(gòu)體。其定義在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有三個(gè)成員變量:

1.一把自旋鎖spinlock_t slock
百度百科是這么解釋的:“何謂自旋鎖?它是為實(shí)現(xiàn)保護(hù)共享資源而提出一種鎖機(jī)制。其實(shí),自旋鎖與互斥鎖比較類似,它們都是為了解決對某項(xiàng)資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時(shí)刻,最多只能有一個(gè)保持者,也就說,在任何時(shí)刻最多只能有一個(gè)執(zhí)行單元獲得鎖。但是兩者在調(diào)度機(jī)制上略有不同。對于互斥鎖,如果資源已經(jīng)被占用,資源申請者只能進(jìn)入睡眠狀態(tài)。但是自旋鎖不會引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名?!?br> 自旋鎖適用于鎖使用者保持鎖時(shí)間比較短的情況,對于引用計(jì)數(shù)的操作速度其實(shí)是非常快的,所以這里使用自旋鎖恰到好處。

2.引用計(jì)數(shù)器RefcountMap refcnts
RefcountMap的定義是這樣的

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

其實(shí)就是個(gè)C++的Map,那么這個(gè)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;,再進(jìn)一步看它的定義是unsigned long,在32位和64位操作系統(tǒng)中,它分別占用32和64個(gè)bit。這里使用的是bit mask技術(shù)。在SideTable結(jié)構(gòu)體定義的上面,定義了這么幾個(gè)數(shù):

#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”放到最右側(cè),然后左移0位(就是原地不動),以32位為例的話就是:0b0000 0000 0000 0000 0000 0000 0000 0001,同理1UL<<1就是:0b0000 0000 0000 0000 0000 0000 0000 0010。上面的定義其實(shí)可以這樣理解:一個(gè)32位的數(shù),其右邊第一位SIDE_TABLE_WEAKLY_REFERENCED表示是否有弱引用指向這個(gè)對象,如果為1的話,在對象釋放的時(shí)候需要把所有指向它的弱引用都置為nil;右邊第二位SIDE_TABLE_DEALLOCATING表示對象是否正在釋放,1正在釋放,0沒有;左邊第一位即最高位SIDE_TABLE_RC_PINNED,其實(shí)沒有特殊的含義,就是隨著對象的引用計(jì)數(shù)不斷變大,如果這一位都變成1了,表示引用計(jì)數(shù)已經(jīng)達(dá)到了能夠存儲的最大值。最后SIDE_TABLE_RC_ONE其實(shí)定義的就是增加一個(gè)引用計(jì)數(shù),size_t實(shí)際增加的值,因?yàn)槟┪矁晌皇潜徽加玫?,所以引用?jì)數(shù)加1,size_t實(shí)際加的是4。

3.維護(hù)weak指針的結(jié)構(gòu)體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是一個(gè)數(shù)組,num_entries用來維護(hù)數(shù)組始終有一個(gè)合適的size,比如當(dāng)數(shù)組中的元素?cái)?shù)量超過3/4時(shí),將數(shù)組大小乘以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];
        };
    };
}

其中三個(gè)成員比較重要:referent,被指對象的地址;referrers,可變數(shù)組,里面保存著所有指向這個(gè)對象的弱引用的地址,如果弱引用指針超過4個(gè)的話,將會存在這個(gè)數(shù)組中;inline_referrers,只有4個(gè)元素的數(shù)組,默認(rèn)情況下用它存儲弱引用的指針,超過4個(gè)的時(shí)候存儲到referrers中。
先總結(jié)一下SideTables的數(shù)據(jù)結(jié)構(gòu),如下圖所示:

sidetable結(jié)構(gòu)圖解.jpg

接著再梳理一下流程,當(dāng)系統(tǒng)調(diào)用retain方法時(shí),最終調(diào)用的是NSObject.mm中的這個(gè)方法:

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;
}

即取到對應(yīng)SideTable的refcnts,然后以當(dāng)前對象地址為key,找到real count,將其增加SIDE_TABLE_RC_ONE,相應(yīng)的引用計(jì)數(shù)就加了1。

當(dāng)系統(tǒng)調(diào)用release方法時(shí),最終調(diào)用的是NSObject.mm中的這個(gè)方法:

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多了最終是否需要調(diào)用dealloc的判斷,大概邏輯是1.遍歷變量是否存在,如果不存在就將do_dealloc置為true;2.如果存在再判斷是否小于SIDE_TABLE_DEALLOCATING,如果小于也將do_dealloc置為true;3.否則就減去前面說過的SIDE_TABLE_RC_ONE;4.判斷是否需要實(shí)際調(diào)用dealloc。
調(diào)用了dealloc方法后,最終會調(diào)用到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();
}

這里加了遍歷有值 和 存在弱引用 兩個(gè)判斷條件,如果滿足的話就會調(diào)用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);
}

會先判斷最后的遍歷數(shù)組是referrers數(shù)組取還是最大容量為4的inline_referrers數(shù)組,在這一步,將每一個(gè)weak指針置為了nil。


五.autoreleasepool實(shí)現(xiàn)方式

在大部分情況下,我們不需要手動提供autoreleasepool,因?yàn)閺拿總€(gè)App的入口main函數(shù)可以看到,系統(tǒng)默認(rèn)用了一個(gè)自動釋放池將我們的代碼包含。即所有在主線程創(chuàng)建的非自己持有的對象都添加到了這個(gè)autoreleasepool里面。但是我們知道主線程是默認(rèn)開啟runloop的,runloop往簡單了說就是一個(gè)do while 循環(huán),那么只要這個(gè)循環(huán)還在執(zhí)行,main函數(shù)里面的autoreleasepool就沒辦法走到后面這個(gè)花括號},那這個(gè)自動釋放池到底什么時(shí)候釋放呢?答案是當(dāng)前runloop迭代結(jié)束的時(shí)候釋放,因?yàn)橄到y(tǒng)在每個(gè)runloop迭代中都加入了autoreleasepool的pushpop。具體原理可以深究runloop源碼。

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

文章開頭也說了,在ARC環(huán)境下,以alloc/new/copy/mutableCopy開頭的方法的返回值取得的對象是自己持有的,其他情況下便是取得非自己持有的對象,此時(shí)對象的持有者就是autoreleasepool。我們可以用以下代碼來驗(yàn)證一下:

#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方法是非公開的調(diào)試方法,需要聲明是外部實(shí)現(xiàn)的,否則無法使用。運(yùn)行打印的結(jié)果如下:

autoreleasepool打印結(jié)果1.jpg

可以看到autoreleasepool持有了對象TestObject,這也驗(yàn)證了生成非自己持有的對象,其真正的持有者是autoreleasepool這一說法。我們將a = [MyObject testObject];這行注釋,打開下面一行,運(yùn)行打印結(jié)果如下:

autoreleasepool打印結(jié)果2.jpg

可以看到這一次autoreleasepool并沒有持有TestObject對象,說明以alloc開頭的方法生成的對象是自己持有的。而且,由于a是__weak修飾的,返回的對象由于無人持有,賦值以后立即被釋放掉了;所以in:后面打印就是null了。同時(shí)編譯器已經(jīng)給出了警告??Assigning retained object to weak variable; object will be released after assignment。應(yīng)用autoreleasepool這一特性,可以在我們的項(xiàng)目中for in遍歷處理大量對象的時(shí)候,在循環(huán)體內(nèi)部用autoreleasepool將代碼包含,降低應(yīng)用內(nèi)存峰值,類似這樣:

摘自 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);
        }
    }

當(dāng)然,按照sunnyxx這篇文章最后提到的一個(gè)知識點(diǎn),使用容器的block版本的枚舉器時(shí),內(nèi)部會自動添加一個(gè)autoreleasepool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 這里被一個(gè)局部@autoreleasepool包圍著
}];

由于筆者寫了測試代碼,用“clang -rewrite-objc”命令重寫為C++實(shí)現(xiàn)后,并沒有找到block版本枚舉器內(nèi)部會自動添加autoreleasepool的蛛絲馬跡;同時(shí)也查看了幫助文檔,這個(gè)方法的說明也沒有提到相關(guān)事項(xiàng)。還望知道怎么得出這個(gè)結(jié)論的朋友指點(diǎn)一下。
那么autoreleasepool的內(nèi)部實(shí)現(xiàn)是怎么樣的呢?可以隨便寫段測試代碼,用“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這一層,運(yùn)行命令“clang -rewrite-objc ZZTestObject.m”,就會得到一個(gè)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;
};

發(fā)現(xiàn)autoreleasepool最終會變成 objc_autoreleasePoolPushobjc_autoreleasePoolPop 兩個(gè)方法的調(diào)用,這里顯示這兩個(gè)方法是外部定義的,那么我們?nèi)ツ睦镎疫@兩個(gè)方法的實(shí)現(xiàn)呢?答案是runtime源碼!
這里說一下怎么下載runtime源碼:先打開這個(gè)網(wǎng)址https://opensource.apple.com/,然后選擇你電腦對應(yīng)的macOS版本,目前我電腦是10.13.6,然后com+F搜索objc4,我這里搜到的是objc4-723,點(diǎn)擊下載。打開之后的目錄是這樣的:

objc源碼目錄.jpg

我們打開NSObject.mm文件查看,全局搜索objc_autoreleasePoolPush,發(fā)現(xiàn)是這樣:

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

那就直接看AutoreleasePoolPage這個(gè)類的實(shí)現(xiàn):

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是一個(gè)C++實(shí)現(xiàn)的類:

  • AutoreleasePool并沒有單獨(dú)的結(jié)構(gòu),而是由若干個(gè)AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應(yīng)結(jié)構(gòu)中的parent指針和child指針)
  • AutoreleasePool是按線程一一對應(yīng)的(結(jié)構(gòu)中的thread指針指向當(dāng)前線程)
  • AutoreleasePoolPage每個(gè)對象會開辟4096字節(jié)內(nèi)存(也就是虛擬內(nèi)存一頁的大小),除了上面的實(shí)例變量所占空間,剩下的空間全部用來儲存autorelease對象的地址
  • id *next指針作為游標(biāo)指向棧頂最新add進(jìn)來的autorelease對象的下一個(gè)位置
  • 一個(gè)AutoreleasePoolPage的空間被占滿時(shí),會新建一個(gè)AutoreleasePoolPage對象,連接鏈表,后來的autorelease對象在新的page加入

我們注意這一行:

#   define POOL_BOUNDARY nil

定義了一個(gè)POOL_BOUNDARY的宏,值為nil,待會會用到。
看看AutoreleasePoolPage的push方法是怎么實(shí)現(xiàn)的:

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;
    }

由于源碼太多,就不一一貼碼了??偨Y(jié)流程圖如下:

autoreleasepoolpage流程圖.jpg

首先會判斷DebugPoolAllocation標(biāo)志位,是否需要為每個(gè)pool都生成一個(gè)新page,為真就走autoreleaseNewPage方法,否則,執(zhí)行autoreleaseFast方法.
在autoreleaseFast方法中,如果存在page且未滿,則直接添加;
如果不存在page,會響應(yīng)autoreleaseNoPage;
如果當(dāng)前page已滿,則響應(yīng)autoreleaseFullPage方法;
autoreleaseNoPage和autoreleaseFullPage會生成新的page,然后向該page中添加對象.
而autoreleaseNewPage方法,如果當(dāng)前存在page,則執(zhí)行autoreleaseFullPage方法,否則響應(yīng)autoreleaseNoPage方法,然后就同上了,去執(zhí)行添加方法。那么具體怎么樣添加呢?
每當(dāng)進(jìn)行一次push調(diào)用時(shí),runtime向當(dāng)前的AutoreleasePoolPage中add進(jìn)一個(gè)哨兵對象,即前面說的POOL_BOUNDARY宏,值為0(也就是個(gè)nil),那么這一個(gè)page就變成了下面的樣子:

pooladd.jpg

objc_autoreleasePoolPush 的返回值就是這個(gè)哨兵對象的地址,同時(shí)當(dāng)作 objc_autoreleasePoolPop 的入?yún)ⅲ?/p>

  1. 根據(jù)傳入的哨兵對象地址找到哨兵對象所處的page
  2. 在當(dāng)前page中,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次- release消息,并向回移動next指針到正確位置
  3. kill掉空page

pop之后就變成了這樣:

pool_pop.jpg

總結(jié)一下autoreleasepool的用法:在非UI框架,或者輔助線程中,或者處理大量的臨時(shí)變量時(shí),需要使用@autoreleasepool {}。編譯器會將其轉(zhuǎn)為push和pop兩個(gè)操作,中間是我們自己的業(yè)務(wù)邏輯。push時(shí)是向AutoReleasePoolPage添加一個(gè)值為nil的哨兵對象,并作為該方法的返回值,也是pop方法的入?yún)?。pop時(shí)根據(jù)哨兵對象的地址獲取到當(dāng)前page,然后在當(dāng)前page中,將晚于哨兵對象添加的對象都發(fā)送一次release命令,并更新next指針位置,最后kill掉空page。autoreleasepool允許多層嵌套,邏輯如上,不過是一個(gè)個(gè)的套娃,一層層的剝離罷了。


結(jié)語:內(nèi)存管理是iOS開發(fā)或者面試永遠(yuǎn)繞不開的一個(gè)坎兒,想要完全跨越它,必須一步一個(gè)腳印,慢慢攻克。理解這些原理性的東西,實(shí)際編程的時(shí)候才有理論指導(dǎo),不至于兩眼一抹黑。路漫漫其修遠(yuǎn)兮,吾將上下而求索。。。


求索.jpg

參考文章:
《iOS與OS X多線程和內(nèi)存管理》
http://www.cocoachina.com/ios/20170410/19030.html
https://juejin.im/entry/58a178060ce463005644ee4a
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容