27-內(nèi)存管理之copy+weak+autorease原理

1.下面代碼執(zhí)行結(jié)果如何
// Person.h
@interface Person : NSObject
@property (copy, nonatomic) NSMutableArray *data;
@end

// 調(diào)用
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    Person *p = [[Person alloc] init];

    p.data = [NSMutableArray array];
    [p.data addObject:@"jack"];
    [p.data addObject:@"rose"];
    NSLog(@"end");
}

運行結(jié)果

image.png
- (void)setData:(NSArray *)data {
    if (_data != data) {
        [_data release];
        _data = [data copy];
    }
}

分析:因為datacopy屬性郎哭,所以在其set方法里先執(zhí)行判斷狂鞋,然后執(zhí)行release操作,最后執(zhí)行copy操作劫狠,變成了一個不可變對象拴疤。

二 copy
  • 拷貝的目的:產(chǎn)生一個副本對象,跟源對象互不影響
  • 修改了源對象嘉熊,不會影響副本對象
  • 修改了副本對象遥赚,不會影響源對象
iOS提供了2個拷貝方法
  • copy 不可變拷貝,產(chǎn)生不可變副本
  • mutableCopy可變拷貝阐肤,產(chǎn)生可變副本

深拷貝和淺拷貝

  • 深拷貝 內(nèi)容拷貝,產(chǎn)生新的對象
  • 淺拷貝 指針拷貝,沒有產(chǎn)生新的對象

copy和mutableCopy 圖解

image.png

1.copy都是不可變拷貝孕惜,產(chǎn)生不可變副本愧薛。mutableCopy都是可變拷貝,產(chǎn)生可變副本衫画。
2.除了不可變對象copy淺拷貝毫炉,其他都是深拷貝

三 引用計數(shù)的存儲

在64bit中削罩,引用計數(shù)可以直接存儲在優(yōu)化過的isa指針中瞄勾,也可能存儲在SideTable類中

image.png
  • refcnts是一個存放著對象引用計數(shù)的散列表
四 weak實現(xiàn)原理 - dealloc
  • 當一個對象要釋放時,會自動調(diào)用dealloc弥激,接下的調(diào)用軌跡是

1.dealloc
2._objc_rootDealloc
3.rootDealloc
4.object_dispose
5.objc_destructInstance进陡、free

image.png
五 自動釋放池
  • 自動釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePoolAutoreleasePoolPage

  • 調(diào)用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的

  • __AtAutoreleasePool結(jié)構(gòu)體

 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 構(gòu)造函數(shù)微服,在創(chuàng)建結(jié)構(gòu)體的時候調(diào)用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }

    ~__AtAutoreleasePool() { // 析構(gòu)函數(shù)趾疚,在結(jié)構(gòu)體銷毀的時候調(diào)用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

    void * atautoreleasepoolobj;
 };

下面將代碼進行轉(zhuǎn)換

@autoreleasepool {
    Person *p4 = [[[MJPerson alloc] init] autorelease];
}

將上述代碼轉(zhuǎn)成C++代碼

 {
    __AtAutoreleasePool __autoreleasepool;
    MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
 }

去除一些不必要的代碼后變成下面這個樣子

 {
    __AtAutoreleasePool __autoreleasepool;
   MJPerson *person = [[[MJPerson alloc] init] autorelease];
 }

又因為__AtAutoreleasePool是一個結(jié)構(gòu)體,所以創(chuàng)建時會調(diào)用其構(gòu)造函數(shù)__AtAutoreleasePool()以蕴,當離開其作用域后糙麦,會調(diào)用其析構(gòu)函數(shù)~__AtAutoreleasePool(),所以上面的代碼又可以轉(zhuǎn)換成下面的代碼

atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);

5.0 自動釋放池
  • 自動釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePool丛肮、AutoreleasePoolPage
  • 調(diào)用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的

源碼分析

  • clang重寫@autoreleasepool
  • objc4源碼:NSObject.mm
image.png

變量說明

  • magic 用來校驗 AutoreleasePoolPage 的結(jié)構(gòu)是否完整
  • next指向最新添加的 autoreleased 對象的下一個位置赡磅,初始化時指向 begin()
  • thread 指向當前線程
  • parent指向父結(jié)點,第一個結(jié)點的 parent 值為 nil
  • child 指向子結(jié)點宝与,最后一個結(jié)點的 child 值為 nil
  • depth 代表深度焚廊,從 0 開始,往后遞增 1
  • hiwat 代表 high water mark
5.1 AutoreleasePoolPage的結(jié)構(gòu)
  • 每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存伴鳖,除了用來存放它內(nèi)部的成員變量节值,剩下的空間用來存放autorelease對象的地址
  • 所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起
image.png
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);

上圖的執(zhí)行步驟說明

  • 調(diào)用push方法會將一個POOL_BOUNDARY入棧,并且返回其存放的內(nèi)存地址榜聂,即返回給atautoreleasepoolobj搞疗。
  • 調(diào)用pop方法時傳入一個POOL_BOUNDARY的內(nèi)存地址,會從最后一個入棧的對象開始發(fā)送release消息须肆,直到遇到這個POOL_BOUNDARY
  • id *next指向了下一個能存放autorelease對象地址的區(qū)域

代碼例子如下

extern void _objc_autoreleasePoolPrint(void);

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()
            for (int i = 0; i < 5; i++) {
                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;
}

執(zhí)行結(jié)果

image.png
  1. 因為只打印了一個PAGE匿乃,所以說明他們是在同一個AutoreleasePoolPage,只是每次一個新的autoreleasepool豌汇,都會插入一個POOL_BOUNDARY幢炸。
  2. 每次釋放對象時,都是從后往前釋放拒贱,直到遇到POOL_BOUNDARY為止宛徊。

代碼例子二

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()
        @autoreleasepool {
            MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }
}

執(zhí)行結(jié)果

image.png

代碼例子三

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()

        MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
        MJPerson *p2 = [[[MJPerson alloc] init] autorelease];

        @autoreleasepool { // r2 = push()
            for (int i = 0; i < 600; i++) {
                MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
            }

            @autoreleasepool { // r3 = push()
                MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
                _objc_autoreleasePoolPrint();
            } // pop(r3)

        } // pop(r2)

    } // pop(r1)
    return 0;
}

執(zhí)行結(jié)果

image.png
image.png
image.png
5.2 Runloop和Autorelease

iOS在主線程的Runloop中注冊了2個Observer

  • 第1個Observer監(jiān)聽了kCFRunLoopEntry事件佛嬉,會調(diào)用objc_autoreleasePoolPush()

  • 第2個Observer

<1> 監(jiān)聽了kCFRunLoopBeforeWaiting事件,會調(diào)用objc_autoreleasePoolPop()闸天、objc_autoreleasePoolPush()
<2> 監(jiān)聽了kCFRunLoopBeforeExit事件暖呕,會調(diào)用objc_autoreleasePoolPop()

5.3 autorelease對象在什么時機會被調(diào)用release

實踐內(nèi)容可以參考 你真的懂iOS的autorelease嗎?

代碼例子如下

  • MRC環(huán)境下
- (void)viewDidLoad {
    [super viewDidLoad];

    // 這個Person什么時候調(diào)用release,是由RunLoop來控制的
    // 它可能是在某次RunLoop循環(huán)中苞氮,RunLoop休眠之前調(diào)用了release
    MJPerson *person = [[[MJPerson 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__);
}

運行結(jié)果如下

image.png
  1. 得出結(jié)論湾揽,autorelease并不是根據(jù)對象的作用域來決定釋放時機。
  2. 實際上笼吟,autorelease釋放對象的依據(jù)是Runloop库物,簡單說,runloop就是iOS中的消息循環(huán)機制贷帮,當一個runloop結(jié)束時系統(tǒng)才會一次性清理掉被autorelease處理過的對象戚揭,其實本質(zhì)上說是在本次runloop迭代結(jié)束時清理掉被本次迭代期間被放到autorelease pool中的對象的。至于何時runloop結(jié)束并沒有固定的duration皿桑。
  3. 本次runloop迭代休眠之前調(diào)用了objc_autoreleasePoolPop()方法毫目,然后調(diào)用release,從而釋放Person對象诲侮。
5.4 方法里有局部對象镀虐, 出了方法后會立即釋放嗎
  • ARC環(huán)境下
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person = [[Person alloc] init];
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    NSLog(@"%s", __func__);
}

image.png

通過打印結(jié)果可知,當person對象出了其作用域后就銷毀沟绪,即系統(tǒng)會在它出作用域的時候刮便,自動調(diào)用其release方法。

擴展

既然由runloop來決定對象釋放時機而不是作用域绽慈,那么恨旱,在一個{}內(nèi)使用循環(huán)大量創(chuàng)建對象就有可能帶來內(nèi)存上的問題,大量對象會被創(chuàng)建而沒有及時釋放坝疼,這時候就需要靠我們?nèi)斯さ母深A(yù)autorelease的釋放了搜贤。

上文有提到autorelease pool,一旦一個對象被autorelease钝凶,則該對象會被放到iOS的一個池:autorelease pool仪芒,其實這個pool本質(zhì)上是一個stack,扔到pool中的對象等價于入棧耕陷。我們把需要及時釋放掉的代碼塊放入我們生成的autorelease pool中掂名,結(jié)束后清空這個自定義的pool,主動地讓pool清空掉哟沫,從而達到及時釋放內(nèi)存的目的饺蔑。優(yōu)化代碼如下

@autoreleasePool{
    //domeSomeThing;
}

什么時候用@autoreleasepool

根據(jù) Apple的文檔 ,使用場景如下:

  • 寫基于命令行的的程序時嗜诀,就是沒有UI框架猾警,如AppKitCocoa框架時孔祸。
  • 寫循環(huán),循環(huán)里面包含了大量臨時創(chuàng)建的對象肿嘲。(本文的例子)
  • 創(chuàng)建了新的線程融击。(非Cocoa程序創(chuàng)建線程時才需要)
  • 長時間在后臺運行的任務(wù)筑公。
  1. autorelease 機制基于 UI framework雳窟。因此寫非UI framework的程序時,需要自己管理對象生存周期匣屡。
  2. autorelease 觸發(fā)時機發(fā)生在下一次runloop的時候封救。因此如何在一個大的循環(huán)里不斷創(chuàng)建autorelease對象,那么這些對象在下一次runloop回來之前將沒有機會被釋放捣作,可能會耗盡內(nèi)存誉结。這種情況下,可以在循環(huán)內(nèi)部顯式使用@autoreleasepool {}autorelease對象釋放券躁。
  3. 自己創(chuàng)建的線程惩坑。Cocoa的應(yīng)用都會維護自己autoreleasepool。因此也拜,代碼里spawn的線程以舒,需要顯式添加autoreleasepool。注意:如果是使用POSIX API 創(chuàng)建線程慢哈,而不是NSThread蔓钟,那么不能使用Cocoa,因為Cocoa只能在多線程(multithreading)狀態(tài)下工作卵贱。但可以使用NSThread創(chuàng)建一個馬上銷毀的線程滥沫,使得Cocoa進入multithreading狀態(tài)。

上述結(jié)論來自 guijiewan 的CSDN 博客 键俱,什么時候應(yīng)該使用Autorelease

什么對象會加入Autoreleasepool中
  • 使用alloc兰绣、newcopy编振、mutableCopy的方法進行初始化時缀辩,由系統(tǒng)管理對象,在適當?shù)奈恢胷elease党觅。
  • 使用array會自動將返回值的對象注冊到Autoreleasepool雌澄。
  • __weak修飾的對象,為了保證在引用時不被廢棄杯瞻,會注冊到Autoreleasepool中镐牺。
  • id的指針對象的指針,在沒有顯示指定時會被注冊到Autoleasepool中魁莉。

本文參考:
路飛_Luck (http://www.reibang.com/p/07f7b96bb03f)
以及借鑒MJ的教程視頻
非常感謝.


本文參考Autorelease Pool學習筆記睬涧,非常感謝募胃。
優(yōu)秀文章推薦 - Objective-C Autorelease Pool 的實現(xiàn)原理

項目連接地址 - MemoryManage-CADisplayLink+Timer
項目連接地址 - MemoryManage-Copy
項目連接地址 - MemoryManager_autorelease1
項目連接地址 - MemoryManager_autorelease

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市畦浓,隨后出現(xiàn)的幾起案子痹束,更是在濱河造成了極大的恐慌,老刑警劉巖讶请,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祷嘶,死亡現(xiàn)場離奇詭異,居然都是意外死亡夺溢,警方通過查閱死者的電腦和手機论巍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來风响,“玉大人嘉汰,你說我怎么就攤上這事∽辞冢” “怎么了鞋怀?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長持搜。 經(jīng)常有香客問我密似,道長,這世上最難降的妖魔是什么朵诫? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任辛友,我火速辦了婚禮,結(jié)果婚禮上剪返,老公的妹妹穿的比我還像新娘废累。我一直安慰自己,他們只是感情好脱盲,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布邑滨。 她就那樣靜靜地躺著,像睡著了一般钱反。 火紅的嫁衣襯著肌膚如雪掖看。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天面哥,我揣著相機與錄音哎壳,去河邊找鬼。 笑死尚卫,一個胖子當著我的面吹牛归榕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吱涉,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼刹泄,長吁一口氣:“原來是場噩夢啊……” “哼外里!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起特石,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤盅蝗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后姆蘸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體墩莫,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年乞旦,在試婚紗的時候發(fā)現(xiàn)自己被綠了贼穆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡兰粉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出顶瞳,到底是詐尸還是另有隱情玖姑,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布慨菱,位于F島的核電站焰络,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏符喝。R本人自食惡果不足惜闪彼,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望协饲。 院中可真熱鬧畏腕,春花似錦、人聲如沸茉稠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽而线。三九已至铭污,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間膀篮,已是汗流浹背嘹狞。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留誓竿,地道東北人磅网。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像烤黍,于是被迫代替她去往敵國和親知市。 傳聞我的和親對象是個殘疾皇子傻盟,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容