自動引用計數(ARC)是指內存管理中對引用採取自動計數的技術。
使用ARC,就無需再次鍵入retain或者release代碼,這降低了程序崩潰,內存泄漏等風險的同時,很大程度上減少了開發程序的工作量。ARC技術使得編譯器清楚目標對象,並能立刻釋放那些不再被使用的對象。如此一來,應用程序將具有可預測性,且能流暢運行,速度也將大幅提升。
MRC
人工引用計數(Manual Reference Counting)
內存管理的思考方式:
- 自己生成的對象,自己持有
- 非自己生成的對象,自己也能持有
- 不需要自己持有的對象時釋放
- 無法釋放非自己持有的對象
對象操作 | oc方法 |
---|---|
生成並持有對象 | alloc/new/copy/mutableCopy 等方法 |
持有對象 | retain 方法 |
釋放對象 | release 方法 |
廢棄對象 | dealloc 方法 |
自己生成的對象,自己持有
- alloc
- new
- copy
- mutablecopy
id obj = [[NSObject alloc] init];
id obj = [NSObject new];
// 兩者完全一致,生成並持有對象
// NSCopying和NSMutableCopying
非自己生成的對象,自己也能持有
id obj = [NSMutableArray array];
// 取得的對象存在,但自己不持有
[obj retain];
// 自己持有對象
不再需要自己持有的對象時釋放
自己持有的對象,一旦不再需要,持有者有義務釋放該對象。
id obj = [NSMutableArray array];
// 取得的對象存在,但自己不持有
[obj retain];
// 自己持有對象
[obj release];
// 釋放對象
// 對象不可再被訪問
array方法的實現
- (id) object
{
id obj = [[NSObject alloc] init];
// 自己持有對象
[obj autorelease];
// 釋放後取得的對象存在,但自己不持有該對象
return obj;
}
release方法時調用後立即釋放,而autorelease方法則是不立即釋放而是註冊到autoreleasepool中,在超出指定的生存範圍時能夠自動並正確釋放(調用release)。
id obj1 = [obj0 object];
// 取得對象存在但不持有
[obj1 retain];
// 自己持有對象
無法釋放非自己持有的對象
- 已經釋放過的對象再一次釋放。
id obj = [NSObject alloc] init];
[obj release];
[obj release];
- 自己還未持有的對象進行釋放
id obj1 = [obj0 object];
[obj1 release];
alloc/retain/release/dealloc的實現
id obj = [NSObject alloc];
+(id)alloc
{
return [self allocWithZone: NSDefaultMallocZone()];
}
+(id)allocWithZone:(NSZone*)z
{
return NSAllocateObject(self, 0, z);
}
//
struct obj_layout
{
NSUInteger retained;
};
inline id NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
int size = 計算容納對象所需內存大小;
id new = NSZoneMalloc(zone, self);
memset(new, o, size);
new = (id)&((struct obj_layput *)new)[1];
}
NSDefaultMallocZone和NSZoneMalloc中包含的NSZone時防止內存碎片化而引入的結構,對內存分配的區域本身進行多重化管理,根據使用對象的目的,對象的大小分配內存,從而提高了內存管理的效率。
去掉NSZone後代碼簡化版
struct obj_layout
{
NSUInteger retained;
};
+(id) alloc
{
int size = sizeof(struct obj_layout) + 對象大小;
struct obj_layout *p = (struct obj_layout *)calloc(1, size);
return (id)(p+1);
}
方法中的retain整數用來保持引用計數並將其寫入對象內存頭部。
對象的引用計數通過retainCount來實現
id obj = [[NSObject alloc] init];
[obj retainCount];
- (NSUInteger)retainCount
{
return NSExtraRefCount(self) + 1;
}
inline NSUInteger NSExtraRefCount(id anObject)
{
return ((struct obj_layout *)anObject)[-1].retained;
}
通過retain方法可使retained變量加1,通過release方法可使retained變量減1
retain方法的實現:
-(id)retain
{
NSIncrementExtraRefCount(self);
return self;
}
inline void NSIncrementExtraRefCount(id anObject)
{
if (((struct obj_layout *) anObject)[-1].retained == UINT_MAX - 1)
[NSException raise: NSInternalInconsistencyException format: @"....."];
((struct obj_layout *) anObject)[-1].retained++;
}
release方法的實現
- (void)release
{
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL NSDecrementExtraRefCountWasZero (id anObject)
{
if ((struct obj_layout *) anObject)[-1].retained == 0) {
return YES;
}
else
{
((struct obj_layout *)anObject)[-1].retained--;
return NO;
}
}
dealloc方法的實現
-(void)dealloc
{
NSDeallocateObject(self);
}
inline void NSDeallocateObject (id anObject)
{
struct obj_layout *o = &((struct obj_layout *) anObject)[-1];
free(o);
}
// 廢棄alloc分配的內存塊
實際上蘋果是通過散列表的形式實現引用計數的,散列表的鍵值爲內存塊地址的散列值。
散列表存儲信息爲引用計數以及內存塊地址。
autorelease
autorelease會類似c語言中的自動變量來對待對象實例,超出作用域時則將對象釋放。
{
int a;
}
autorelease就是類似 { }
的作用,使用方法如下:
- 生成並持有NSAutoreleasePool對象
- 調用已分配對象的autorelease實例方法。
- 廢棄NSAutoreleasePool對象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; //等同於[obj release]
在Cocoa框架中,相當於程序主循環的NSRunLoop或者在其他程序可運行的地方,對NSAutoreleasePool 對象進行生成、持有和廢棄處理。因此開發者不一定非得使用NSAutoreleasePool對象來進行開發工作。
但是在大量產生autorelease對象時,只有不廢棄NSAutoreleasePool對象,那麼生成的對象就不能被釋放,因此可能出現內存不足的情況。例如:
for (int i = 0; i < 圖像數; ++i)
{
/*
* 讀入圖像
* 大量產生autorelease對象
* 沒有廢棄NSAutoreleasePool 對象
* 導致最終內存不足
*/
[pool drain];
/*
* 通過drain方法autorelease對象被release
*/
}
Cocoa框架中也有很多類方法返回autorelease對象,比如NSMutableArray類的arrayWithCapacity類方法。
id array = [NSMutableArray arrayWithCapacity:1];
等同於
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];
autorelease的實現
[obj autorelease]
-(id)autorelease
{
[NSAutoreleasePool addObject:self];
}
+(void)addObject:(id)anObj
{
NSAutorelease *pool = 取得正在使用的NSAutorelease對象;
if (pool != nil)
{
[pool addObject:anObj];
}
else
{
NSLog(@"AutoreleasePool 對象非存在狀態下調用");
}
}
如果嵌套生成或持有多個NSAutoreleasePool對象,則會使用最內側的對象。
[pool drain]
- (void)drain
{
[self dealloc];
}
- (void) dealloc
{
[self emptyPool];
[array release];
}
- (void) emtpyPool
{
for (id obj in array)
{
[obj release];
}
}
ARC
與MRC的引用計數式內存管理在本質上沒有太大變化,只是自動幫我們處理引用計數的部分。
通過__strong修飾符可以不必再次鍵入retain或者release,自動地實現了上述的四種內存管理思考方式。
但引用計數式內存管理會發生"循環引用"的問題。
{
id test0 = [[Test alloc] init]; /* 對象A */
/*
* test0 持有Test對象A的強引用
*/
id test1 = [[Test alloc] init]; /* 對象B */
/*
* test1 持有Test對象B的強引用
*/
[test0 setObject:test1];
/*
* 此時持有Test對象B的強引用變量爲test1和對象A的obj_成員變量
*/
[test1 setObject:test0];
/*
* 此時持有Test對象A的強引用變量爲test0和對象B的_obj變量
*/
}
/*
* 因爲test0變量超出其作用域,強引用失效,所以自動釋放對象A
*
* 因爲test1變量超出其作用域,強引用失效,所以自動釋放對象B
*
* 此時持有對象A的強引用變量爲對象B的_obj,持有對象B的強引用變量爲對象A的_obj
*
* 發生內存泄漏!(應當廢棄的對象在超出其生命週期後依然存在)
*/
同時像下面的情況,雖然只有一個對象,但對象持有其自身時也會發生循環引用。
id test = [[Test alloc] init];
[test setObject:test];
使用弱引用__weak可以實現,弱引用不持有對象,只是持有該對象的強引用,如果該對象的所有強持有者都釋放,則對象廢棄,即使此時仍有弱持有者。
_unsafe_unretained 和weak的不同之處在於前者在賦值給帶有strong修飾符的變量必須確保被賦值對象確實存在,不然其不會像weak一樣被設置爲nil而是造成懸浮指針。(存在是歷史遺留問題,有了weak後基本不用它了)
strong修飾符類似於c++中的std::shared_ptr指針,而weak修飾符類似於c++中的std::weak_ptr,在c++中沒有strong,weak強烈推薦使用這兩個指針。
ARC,MRC區別
說了這麼多總結一下ARC,MRC的區別吧。MRC類似於c++中的普通指針,程序猿要手動的進行生成持有釋放廢棄操作,但是可以將其註冊到autoreleasepool中(類似c++的作用域),在作用域消失時也就是autoreleasepool對象釋放時會釋放所有註冊在它裏面的對象。而ARC則是類似於c++的智能指針,不需要顯示的對對象實例進行釋放,出現了strong,weak等修飾符,採用自動引用計數的方法,使得當一個對象實例在強引用計數爲0時,則廢棄這個對象實例,釋放其所佔的內存塊。
ARC的規則:
- 在ARC有效時編譯代碼一定要遵守以下規則,
- 不能使用NSAllocateObject/NSDeallocateObject
- 須遵守內存管理的命名規則
- init返回的必須是實例對象
- 不要顯式地調用dealloc
- 使用@autorelease塊代替NSAutoreleasePool
- 不能使用NSZone
- 對象型變量不能作爲C語言結構體成員
- 顯式轉換id和void*
- 不能強制轉換
- 如果只是想單純地賦值可以通過__bridge轉換,但可能造成懸空指針問題,不推薦。
ARC中的屬性:
屬性聲明 | 所有權修飾符 |
---|---|
assign | _unsafe__unretained修飾符 |
copy | __strong修飾符 |
retain | __strong修飾符 |
strong | __strong修飾符 |
unsafe_unretained | _unsafe__unretained修飾符 |
weak | __weak修飾符 |
-
**assign:**對應到__unsafe_unretained, 表明setter僅做賦值,不增加對象的引用計數,用於基本數據類型
-
__weak修飾符賦值後不會註冊到autoreleasepool中,只會在使用時註冊,而且每次使用都會註冊一次,在autoreleasepool結束後會全部釋放。
-
**copy:**對應到__strong,但是賦值操作比較特殊:賦值時進行copy而非retain操作,原來的值可變則深拷貝,不可變則淺拷貝。
ARC的實現
__strong
先看如下一段代碼:
{
// ARC中默認會在對象前添加一個修飾符__strong
id obj = [[NSObject alloc] init];
//<==>等價於
id __strong obj = [[NSObject alloc] init];
}
根據runtime特性,它的實際調用如下:
{
// 消息轉發
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 編譯器在obj作用域結束時自動插入release
objc_release(obj);
}
當然這裏是以alloc/new/copy/mutableCopy
生成的對象,這種對象會被當前的變量所持有,引用計數會加1.那如果不是用被持有的方式生成對象呢?
看下面這段代碼:
{
id obj = [NSMutableArray array];
}
這種方式生成的對象不會被obj持有,通常情況下會被註冊到autoreleasepool
中.但也有特殊情況,上面的代碼可以轉換成如下代碼:
{
// 消息轉發
id obj = objc_msgSend(NSMutableArray,@selector(array));
// 調用objc_retainAutoreleasedReturnValue函數
objc_retainAutoreleasedReturnValue(obj);
// 編譯器在obj作用域結束時自動插入release
objc_release(obj);
}
這裏介紹兩個相關函數:
- objc_retainAutoreleasedReturnValue():這個函數的作用是返回註冊在
autoreleasepool
當中的對象. - objc_retainAutoreleaseReturnValue():這個函數一般是和
objc_retainAutoreleasedReturnValue()
成對出現的.目的是註冊對象到autoreleasepool
中.但不僅限於此
.
爲何說不僅限於此
呢?原因在於,objc_retainAutoreleaseReturnValue()
函數在發現對象調用了方法或者函數之後又調用了objc_retainAutoreleasedReturnValue()
,那麼就不會再把返回的對象註冊到autoreleasepool
中了,而是直接把對象傳遞過去.
這樣的好處顯而易見:不用再去autoreleasepool
中取出對象,傳遞出去,而是越過autoreleasepool
直接傳遞,提升了性能.
__weak
weak
修飾符想必大家都非常熟悉,它有一個衆所周知的特性:用weak修飾的對象在銷燬後會被自動置爲nil
.另外還補充一點:凡是用weak修飾過的對象,必定是註冊到autoreleasepool中的對象
.
看下面的代碼:
{
// obj默認有__strong修飾
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
}
實際過程如下:
{
// 省略obj的實現
id obj1;
// 通過objc_initWeak初始化變量
objc_initWeak(&obj1,obj);
// 通過objc_destroyWeak釋放變量
objc_destroyWeak(&obj1);
}
objc_initWeak()
函數的作用是將obj1初始化爲0,然後將obj作爲參數傳遞到這個函數中objc_storeWeak(&obj1,obj)
objc_destroyWeak()
函數則將0作爲參數來調用:objc_storeWeak(&obj1,0)
objc_storeWeak()
函數的作用是以第二個參數(obj || 0)
作爲key,第一個參數(&obj1)
作爲value,將第一個參數的地址註冊到weak表中.當key爲0,即從weak表中刪除變量地址.
那麼weak表中的對象是如何被釋放的呢?
- 從weak表中獲取廢棄對象的鍵值記錄.
- 將記錄中所有包含__weak的變量地址,賦值爲nil.
- 從weak表中刪除該記錄.
- 從引用計數表中刪除對應的記錄.
這就是__weak修飾的變量會在釋放後自動置爲nil的原因.同時,因爲weak修飾之後涉及到註冊到weak表等相關操作,如果大量使用weak可能會造成不必要的CPU資源浪費,所以書裏指出儘量在循環引用中使用weak
.
這裏不得不提到另外一個和__weak相近的屬性:__unsafe_unretained
,它與weak的區別在於,釋放對象後不會對其置爲nil,在某些特定的場合下,需要延遲釋放的時候,可以考慮用這個屬性修飾.
好了,下一個問題,看如下代碼:
{
id __weak obj1 = obj;
// 這裏使用了obj1這個用weak修飾的變量
NSLog(@"%@",obj1);
}
在weak變量被使用的情況下,實際過程如下:
{
id obj1;
objc_initWeak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&obj1);
}
從這段實現代碼中我們可以看出如下幾點:
- 當我們使用weak修飾的對象時,實際過程中產生了一個
tmp
對象,因爲objc_loadWeakRetained()
函數會從weak表中取出weak修飾的對象,所以tmp會對這個取出的對象進行一次強引用. - 因爲上述原因,weak修飾的對象在當前變量作用域結束前都可以放心使用.
objc_autorelease()
會將tmp對象也註冊到autoreleasepool
中.所以當大量使用weak對象的時候,註冊到autoreleasepool
的對象會大量增加.解決方案就是用一個__strong修飾的臨時變量來使用.
{
id __weak obj1 = obj;
id tmp = obj1;
// 後面使用tmp即可
}
延伸一下:爲什麼有循環引用block內用weakObject的時候最好能在block內套一層strongObject?
- 在異步線程中weakObject可能會被銷燬,所以需要套一層strong.
- 如果內部有耗時的循環語句,頻繁使用weakObject也會增加內存損耗.
!!! 爲什麼訪問weak修飾的對象就會訪問註冊到自動釋放池的對象呢?
因爲weak不會引起對象的引用計數器變化,因此,該對象在運行過程中很有可能會被釋放。所以,需要將對象註冊到自動釋放池中並在autoreleasePool銷燬時釋放對象佔用的內存。
__autoreleasing
它的主要作用就是將對象註冊到autoreleasepool
中.沒啥好說的.
最後補充幾種在ARC環境下獲取引用計數的方法,但並不一定準確:ARC的一些引用計數優化,以及多線程的中的競態條件問題,有興趣的可以自己去了解一下
.
(1) 使用_objc_rootRetainCount()私有函數
OBJC_EXTERN int _objc_rootRetainCount(id);
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",_objc_rootRetainCount(obj));
}
@end
(2) 使用KVC
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",[[obj valueForKey:@"retainCount"] integerValue]);
}
@end
(3) 使用CFGetRetainCount()
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",CFGetRetainCount((__bridge CFTypeRef)(obj)));
}
@end