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");
}
運行結果
- (void)setData:(NSArray *)data {
if (_data != data) {
[_data release];
_data = [data copy];
}
}
分析:因為
data
是copy
屬性,所以在其set
方法里先執行判斷,然后執行release
操作,最后執行copy
操作,變成了一個不可變
對象。
二 copy
- 拷貝的目的:產生一個副本對象,跟源對象互不影響
- 修改了源對象,不會影響副本對象
- 修改了副本對象,不會影響源對象
iOS提供了2個拷貝方法
-
copy
不可變拷貝,產生不可變副本 -
mutableCopy
可變拷貝,產生可變副本
深拷貝和淺拷貝
-
深拷貝
內容拷貝,產生新的對象 -
淺拷貝
指針拷貝,沒有產生新的對象
copy和mutableCopy 圖解
1.
copy
都是不可變拷貝
,產生不可變副本
。mutableCopy
都是可變拷貝
,產生可變副本
。
2.除了不可變對象
的copy
是淺拷貝
,其他都是深拷貝
。
三 引用計數的存儲
在64bit中,引用計數可以直接存儲在優化過的isa
指針中,也可能存儲在SideTable
類中
-
refcnts
是一個存放著對象引用計數的散列表
四 weak實現原理 - dealloc
- 當一個對象要釋放時,會自動調用
dealloc
,接下的調用軌跡是
1.dealloc
2._objc_rootDealloc
3.rootDealloc
4.object_dispose
5.objc_destructInstance
、free
五 自動釋放池
自動釋放池的主要底層數據結構是:
__AtAutoreleasePool
、AutoreleasePoolPage
調用了
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 自動釋放池
- 自動釋放池的主要底層數據結構是:
__AtAutoreleasePool
、AutoreleasePoolPage
- 調用了
autorelease
的對象最終都是通過AutoreleasePoolPage
對象來管理的
源碼分析
- clang重寫
@autoreleasepool
- objc4源碼:
NSObject.mm
變量說明
-
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
對象通過雙向鏈表
的形式連接在一起
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;
}
執行結果
- 因為只打印了一個
PAGE
,所以說明他們是在同一個AutoreleasePoolPage
,只是每次一個新的autoreleasepool
,都會插入一個POOL_BOUNDARY
。- 每次釋放對象時,都是從后往前釋放,直到遇到
POOL_BOUNDARY
為止。
代碼例子二
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
@autoreleasepool {
MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint();
}
return 0;
}
}
執行結果
代碼例子三
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;
}
執行結果
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__);
}
運行結果如下
- 得出結論,
autorelease
并不是根據對象的作用域
來決定釋放時機。- 實際上,
autorelease
釋放對象的依據是Runloop
,簡單說,runloop
就是iOS
中的消息循環機制,當一個runloop
結束時系統才會一次性清理掉被autorelease
處理過的對象,其實本質上說是在本次runloop迭代結束時
清理掉被本次迭代期間被放到autorelease pool
中的對象的。至于何時runloop結束并沒有固定的duration。- 本次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__);
}
通過打印結果可知,當
person
對象出了其作用域后就銷毀,即系統會在它出作用域的時候,自動調用其release
方法。
擴展
既然由runloop
來決定對象釋放時機而不是作用域,那么,在一個{}
內使用循環大量創建對象就有可能帶來內存上的問題,大量對象會被創建而沒有及時釋放,這時候就需要靠我們人工的干預autorelease
的釋放了。
上文有提到autorelease pool
,一旦一個對象被autorelease
,則該對象會被放到iOS的一個池:autorelease pool
,其實這個pool
本質上是一個stack
,扔到pool中的對象等價于入棧
。我們把需要及時釋放掉的代碼塊放入我們生成的autorelease pool
中,結束后清空這個自定義的pool
,主動地讓pool
清空掉,從而達到及時釋放內存的目的。優化代碼如下
@autoreleasePool{
//domeSomeThing;
}
什么時候用@autoreleasepool
根據 Apple的文檔 ,使用場景如下:
- 寫基于命令行的的程序時,就是沒有UI框架,如
AppKit
等Cocoa
框架時。 - 寫循環,循環里面包含了大量臨時創建的對象。(本文的例子)
- 創建了新的線程。(非
Cocoa
程序創建線程時才需要) - 長時間在后臺運行的任務。
autorelease 機制
基于UI framework
。因此寫非UI framework的程序時,需要自己管理對象生存周期。autorelease
觸發時機發生在下一次runloop
的時候。因此如何在一個大的循環里不斷創建autorelease對象
,那么這些對象在下一次runloop回來之前將沒有機會被釋放,可能會耗盡內存。這種情況下,可以在循環內部
顯式使用@autoreleasepool {}
將autorelease
對象釋放。- 自己創建的線程。
Cocoa
的應用都會維護自己autoreleasepool
。因此,代碼里spawn
的線程,需要顯式添加autoreleasepool
。注意:如果是使用POSIX API
創建線程,而不是NSThread
,那么不能使用Cocoa
,因為Cocoa
只能在多線程(multithreading)
狀態下工作。但可以使用NSThread創建一個馬上銷毀的線程,使得Cocoa進入multithreading狀態。
上述結論來自 guijiewan 的CSDN 博客 ,什么時候應該使用Autorelease
什么對象會加入Autoreleasepool中
- 使用
alloc
、new
、copy
、mutableCopy
的方法進行初始化時,由系統管理對象,在適當的位置release。 - 使用
array
會自動將返回值的對象
注冊到Autoreleasepool
。 -
__weak
修飾的對象,為了保證在引用時不被廢棄,會注冊到Autoreleasepool
中。 -
id的指針
或對象的指針
,在沒有顯示指定時會被注冊到Autoleasepool
中。
本文參考MJ底層原理教程,非常感謝
本文參考Autorelease Pool學習筆記,非常感謝。
優秀文章推薦 - Objective-C Autorelease Pool 的實現原理
- 多多點贊,打賞更好,您的支持是我寫作的動力。
項目連接地址 - MemoryManage-CADisplayLink+Timer
項目連接地址 - MemoryManage-Copy
項目連接地址 - MemoryManager_autorelease1
項目連接地址 - MemoryManager_autorelease