iOS-底層原理(27)-內存管理之copy+weak+autorease原理

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

// 調用
- (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");
}

運行結果

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

分析:因為datacopy屬性,所以在其set方法里先執行判斷,然后執行release操作,最后執行copy操作,變成了一個不可變對象。

二 copy
  • 拷貝的目的:產生一個副本對象,跟源對象互不影響
  • 修改了源對象,不會影響副本對象
  • 修改了副本對象,不會影響源對象
iOS提供了2個拷貝方法
  • copy 不可變拷貝,產生不可變副本
  • mutableCopy可變拷貝,產生可變副本

深拷貝和淺拷貝

  • 深拷貝 內容拷貝,產生新的對象
  • 淺拷貝 指針拷貝,沒有產生新的對象

copy和mutableCopy 圖解

image.png

1.copy都是不可變拷貝,產生不可變副本mutableCopy都是可變拷貝,產生可變副本
2.除了不可變對象copy淺拷貝,其他都是深拷貝

三 引用計數的存儲

在64bit中,引用計數可以直接存儲在優化過的isa指針中,也可能存儲在SideTable類中

image.png
  • refcnts是一個存放著對象引用計數的散列表
四 weak實現原理 - dealloc
  • 當一個對象要釋放時,會自動調用dealloc,接下的調用軌跡是

1.dealloc
2._objc_rootDealloc
3.rootDealloc
4.object_dispose
5.objc_destructInstancefree

image.png
五 自動釋放池
  • 自動釋放池的主要底層數據結構是:__AtAutoreleasePoolAutoreleasePoolPage

  • 調用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的

  • __AtAutoreleasePool結構體

 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 構造函數,在創建結構體的時候調用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 析構函數,在結構體銷毀的時候調用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };

下面將代碼進行轉換

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

將上述代碼轉成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是一個結構體,所以創建時會調用其構造函數__AtAutoreleasePool(),當離開其作用域后,會調用其析構函數~__AtAutoreleasePool(),所以上面的代碼又可以轉換成下面的代碼

atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
5.0 自動釋放池
  • 自動釋放池的主要底層數據結構是:__AtAutoreleasePoolAutoreleasePoolPage
  • 調用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的

源碼分析

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

變量說明

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

上圖的執行步驟說明

  • 調用push方法會將一個POOL_BOUNDARY入棧,并且返回其存放的內存地址,即返回給atautoreleasepoolobj
  • 調用pop方法時傳入一個POOL_BOUNDARY的內存地址,會從最后一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY
  • id *next指向了下一個能存放autorelease對象地址的區域

代碼例子如下

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

執行結果

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

執行結果

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

執行結果

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

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

  • 第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()

  • 第2個Observer

<1> 監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
<2> 監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()

5.3 autorelease對象在什么時機會被調用release

實踐內容可以參考 你真的懂iOS的autorelease嗎?

代碼例子如下

  • MRC環境下
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 這個Person什么時候調用release,是由RunLoop來控制的
    // 它可能是在某次RunLoop循環中,RunLoop休眠之前調用了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__);
}

運行結果如下

image.png
  1. 得出結論,autorelease并不是根據對象的作用域來決定釋放時機。
  2. 實際上,autorelease釋放對象的依據是Runloop,簡單說,runloop就是iOS中的消息循環機制,當一個runloop結束時系統才會一次性清理掉被autorelease處理過的對象,其實本質上說是在本次runloop迭代結束時清理掉被本次迭代期間被放到autorelease pool中的對象的。至于何時runloop結束并沒有固定的duration。
  3. 本次runloop迭代休眠之前調用了objc_autoreleasePoolPop()方法,然后調用release,從而釋放Person對象。
5.4 方法里有局部對象, 出了方法后會立即釋放嗎
  • ARC環境下
- (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

通過打印結果可知,當person對象出了其作用域后就銷毀,即系統會在它出作用域的時候,自動調用其release方法。

擴展

既然由runloop來決定對象釋放時機而不是作用域,那么,在一個{}內使用循環大量創建對象就有可能帶來內存上的問題,大量對象會被創建而沒有及時釋放,這時候就需要靠我們人工的干預autorelease的釋放了。

上文有提到autorelease pool,一旦一個對象被autorelease,則該對象會被放到iOS的一個池:autorelease pool,其實這個pool本質上是一個stack,扔到pool中的對象等價于入棧。我們把需要及時釋放掉的代碼塊放入我們生成的autorelease pool中,結束后清空這個自定義的pool,主動地讓pool清空掉,從而達到及時釋放內存的目的。優化代碼如下

@autoreleasePool{
    //domeSomeThing;
}
什么時候用@autoreleasepool

根據 Apple的文檔 ,使用場景如下:

  • 寫基于命令行的的程序時,就是沒有UI框架,如AppKitCocoa框架時。
  • 寫循環,循環里面包含了大量臨時創建的對象。(本文的例子)
  • 創建了新的線程。(非Cocoa程序創建線程時才需要)
  • 長時間在后臺運行的任務。
  1. autorelease 機制基于 UI framework。因此寫非UI framework的程序時,需要自己管理對象生存周期。
  2. autorelease 觸發時機發生在下一次runloop的時候。因此如何在一個大的循環里不斷創建autorelease對象,那么這些對象在下一次runloop回來之前將沒有機會被釋放,可能會耗盡內存。這種情況下,可以在循環內部顯式使用@autoreleasepool {}autorelease對象釋放。
  3. 自己創建的線程。Cocoa的應用都會維護自己autoreleasepool。因此,代碼里spawn的線程,需要顯式添加autoreleasepool。注意:如果是使用POSIX API 創建線程,而不是NSThread,那么不能使用Cocoa,因為Cocoa只能在多線程(multithreading)狀態下工作。但可以使用NSThread創建一個馬上銷毀的線程,使得Cocoa進入multithreading狀態。

上述結論來自 guijiewan 的CSDN 博客 ,什么時候應該使用Autorelease

什么對象會加入Autoreleasepool中
  • 使用allocnewcopymutableCopy的方法進行初始化時,由系統管理對象,在適當的位置release。
  • 使用array會自動將返回值的對象注冊到Autoreleasepool
  • __weak修飾的對象,為了保證在引用時不被廢棄,會注冊到Autoreleasepool中。
  • id的指針對象的指針,在沒有顯示指定時會被注冊到Autoleasepool中。

本文參考MJ底層原理教程,非常感謝
本文參考Autorelease Pool學習筆記,非常感謝。
優秀文章推薦 - Objective-C Autorelease Pool 的實現原理


  • 多多點贊,打賞更好,您的支持是我寫作的動力。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容