【IOS開發進階系列】Objective-c內存管理學習總結

weak的生命周期:具體實現方法

http://www.cocoachina.com/ios/20150605/11990.html

(試讀)Objective-C高級編程:iOS與OS X多線程和內存管理

http://book.2cto.com/201305/23841.html


1 內存管理概論

1.1 內存管理思考方式

1、自己生成的對象,自己持有,以alloc、new、copy、mutablecopy開頭的方法;

2、非自己生成的對象,自己也能持有,通過retain方法;

3、不再需要自己持有的對象時,將其釋放,用release方法;

4、非自己持有的對象,無法釋放;


1.2 Autorelease方法

????????Autoreleasepool的作用是:避免頻繁申請/釋放內存。

????????調用autorelease方法,可使取得的對象存在,但自己不持有。在對象超出指定生存范圍時能夠自動并正確地釋放——通過將對象注冊到autoreleasepool中實現,在最近的pool結束時,自動調用release方法釋放對象。

? ??????特別說明:在函數返回值時,調用autorelease方法后,返回值對象的釋放機制——在每一次事件觸發時(即一個運行循環runloop),系統會自動生成一個autoreleasepool,在事件響應結束時,通過釋放此pool,來釋放那些所調用的函數產生的對象。

? ? ? ?網友解釋:在Iphone項目中,大家會看到一個默認的Autoreleasepool,程序開始時創建,程序退出時銷毀,按照對Autorelease的理解,豈不是所有autoreleasepool里的對象在程序退出時才release, 這樣跟內存泄露有什么區別?

????????答案是,對于每一個Runloop, 系統會隱式創建一個Autoreleasepool, 這樣所有的releasepool會構成一個象CallStack一樣的一個棧式結構,在每一個Runloop結束時,當前棧頂的Autoreleasepool會被銷毀,這樣這個pool里的每個Object會被release。

????????那什么是一個Runloop呢? 一個UI事件,Timercall,delegate call, 都會是一個新的Runloop。

2 具體實現

2.1 alloc/retain/release/dealloc實現

????????接下來,以Objective-c 內存管理中使用的alloc/retain/release/dealloc 方法為基礎,通過實際操作來理解內存管理。

????????OS X、iOS 中的大部分作為開源軟件公開在Apple Open Source上。雖然想讓大家參考NSObject 類的源代碼,但是很遺憾,包含NSObject 類的Foundation 框架并沒有公開。不過,Foundation 框架使用的Core? Foundation 框架的源代碼,以及通過調用NSObject 類進行內存管理部分的源代碼是公開的。但是,沒有NSObject 類的源代碼,就很難了解NSObject 類的內部實現細節。為此,我們首先使用開源軟件GNUstep來說明。

????????GNUstep 是Cocoa 框架的互換框架。也就是說,GNUstep 的源代碼雖不能說與蘋果的Cocoa實現完全相同,但是從使用者角度來看,兩者的行為和實現方式是一樣的,或者說非常相似。理解了GNUstep 源代碼也就相當于理解了蘋果的Cocoa 實現。

????????我們來看看GNUstep 源代碼中NSObject 類的alloc 類方法。為明確重點,有的地方對引用的源代碼進行了摘錄或在不改變意思的范圍內進行了修改。

id obj = [NSObject alloc];

????????上述調用NSObject 類的alloc 類方法在NSObject.m 源代碼中的實現如下。

GNUstep/modules/core/base/Source/NSObject.m

+(id)alloc

{

?? ?return [self allocWithZone: NSDefaultMallocZone()];

}

+ (id) allocWithZone: (NSZone*) z

{

?? ?return NSAllocateObject(self, 0, z);

}

????????通過allocWithZone :類方法調用NSAllocateObject 函數分配了對象。下面我們來看看NSAllocateObject 函數。

GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject?? ▼

struct obj_layout {

?? ?NSUInteger retained;

};

inline id

NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)

{

?? ?int size =計算容納對象所需內存大小;

?? ?id new = NSZoneMalloc(zone,size);

?? ?memset(new, 0, size);

?? ?new =(id)&((struct obj_layout *)new)[1];

}

????????NSAllocateObject 函數通過調用NSZoneMalloc 函數來分配存放對象所需的內存空間,之后將該內存空間置0,最后返回作為對象而使用的指針。

????????專欄區域NSDefaultMallocZone、NSZoneMalloc等名稱中包含的NSZone是什么呢?它是為防止內存碎片化而引入的結構。對內存分配的區域本身進行多重化管理,根據使用對象的目的、對象的大小分配內存,從而提高了內存管理的效率。

????????但是,如同蘋果官方文檔《Programming With ARC Release Notes》中所說,現在的運行時系統只是簡單地忽略了區域的概念。運行時系統中的內存管理本身已極具效率,使用區域來管理內存反而會引起內存使用效率低下以及源代碼復雜化等問題。

????以下是去掉NSZone 后簡化了的源代碼:

GNUstep/modules/core/base/Source/NSObject.m alloc簡化版?? ▼

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

}

????????alloc 類方法用struct obj_layout 中的retained 整數來保存引用計數,并將其寫入對象內存頭部,該對象內存塊全部置0 后返回。以下用圖示來展示有關GNUstep 的實現,alloc 類方法返回的對象。

????????對象的引用計數可通過retainCount 實例方法取得。

id obj = [[NSObject alloc] init];

NSLog(@"retainCount=%d", [obj retainCount]);

/*

?*顯示retainCount=1

*/

? ? ? ? 執行alloc 后對象的retainCount 是“1”。下面通過GNUstep 的源代碼來確認。

GNUstep/modules/core/base/Source/NSObject.m retainCount ▼

- (NSUInteger) retainCount

{

?? ?return NSExtraRefCount(self) + 1;

}

inline NSUInteger

NSExtraRefCount (id anObject)

{

?? ?return ((struct obj_layout *) anObject)[-1].retained;

}

? ? ? ? 由對象尋址找到對象內存頭部,從而訪問其中的retained 變量。因為分配時全部置0,所以retained 為0。由NSExtraRefCount(self) + 1 得出,retainCount 為1。

? ? ? ? 可以推測出,retain 方法使retained 變量加1,而release 方法使retained 變量減1。

[obj retain];

? ? ? ?下面來看一下像上面那樣調用出的retain 實例方法。

GNUstep/modules/core/base/Source/NSObject.m 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: @"NSIncrementExtraRefCount()asked to increment too far"];

?? ?((struct obj_layout *)anObject)[-1].retained++;

}

? ? ? ? 雖然寫入了當retained 變量超出最大值時發生異常的代碼,但實際上只運行了使retained 變量加1 的retained++ 代碼。同樣地,release 實例方法進行retained-- 并在該引用計數變量為0 時做出處理。下面通過源代碼來確認。

[obj release];

? ? 以下為此release 實例方法的實現。

GNUstep/modules/core/base/Source/NSObject.m? release?? ▼

- (void) release

{

?? ?if (NSDecrementExtraRefCountWasZero(self))

?? ?????[self dealloc];

}

BOOL NSDecrementExtraRefCountWasZero (idanObject)

{

?? ?if (((struct obj_layout *)anObject)[-1].retained== 0) {

?? ?????return YES;

?? ?} else {

?? ?????((struct obj_layout *)anObject)[-1].retained--;

???????return NO;

?? ?}

}

? ? ? ? 同預想的一樣,當retained 變量大于0 時減1,等于0 時調用dealloc 實例方法,廢棄對象。以下是廢棄對象時所調用的dealloc 實例方法的實現。

GNUstep/modules/core/base/Source/NSObject.m? dealloc?? ▼

- (void) dealloc

{

?? ?NSDeallocateObject (self);

}

inline void NSDeallocateObject (id anObject)

{

?? ?struct obj_layout *o = &((structobj_layout *)anObject)[-1];

?? ?free(o);

}

? ? ? ? 上述代碼僅廢棄由alloc 分配的內存塊。

? ? ? ? 以上就是alloc/retain/release/dealloc 在GNUstep 中的實現。具體總結如下:

1、在Objective-C 的對象中存有引用計數這一整數值。

2、調用alloc 或是retain 方法后,引用計數值加1。

3、調用release 后,引用計數值減1。

4、引用計數值為0 時,調用dealloc 方法廢棄對象。


2.2 Weak實現

? ? ? ? 我們以下面這行代碼為例:

代碼清單1:示例代碼

{

??? id__weak obj1 = obj;

}

? ? ? ? 當我們初始化一個weak變量時,runtime會調用objc_initWeak函數。這個函數在Clang中的聲明如下:

id objc_initWeak(id *object, id value);

? ? ? ? 其具體實現如下:

id objc_initWeak(id *object, id value)

{

??? *object= 0;

??? return?objc_storeWeak(object, value);

}

? ? ? ?示例代碼輪換成編譯器的模擬代碼如下:

id obj1;

objc_initWeak(&obj1, obj);

????????因此,這里所做的事是先將obj1初始化為0(nil),然后將obj1的地址及obj作為參數傳遞給objc_storeWeak函數。

????????objc_initWeak函數有一個前提條件:就是object必須是一個沒有被注冊為__weak對象的有效指針。而value則可以是null,或者指向一個有效的對象。

????????如果value是一個空指針或者其指向的對象已經被釋放了,則object是zero-initialized的。否則,object將被注冊為一個指向value的__weak對象。而這事應該是objc_storeWeak函數干的。objc_storeWeak的函數聲明如下:

id objc_storeWeak(id *location, id value);

????????其具體實現如下:

id?objc_storeWeak(id?*location,?id?newObj)

{

??? id oldObj;

??? SideTable *oldTable;

??? SideTable *newTable;

??? ......

??? //?Acquire?locks?for?old?and?new?values.

??? // Order by lock address to prevent?lock?ordering?problems.

??? //Retry if the old value changes underneath us.

??? retry:

???????oldObj = *location;

???????oldTable = SideTable::tableForPointer(oldObj);

???????newTable = SideTable::tableForPointer(newObj);

???????......

???????if(*location != oldObj) {

???????????OSSpinLockUnlock(lock1);

???????#if?SIDE_TABLE_STRIPE?>?1

???????? if(lock1 != lock2)OSSpinLockUnlock(lock2);

#endif

??????????goto retry;

??? }

??? if(oldObj) {

???????weak_unregister_no_lock (&oldTable->weak_table, oldObj, location);

??? }

??? if(newObj) {

???????newObj = weak_register_no_lock (&newTable->weak_table, newObj,location);

???????// weak_register_no_lock returns NULL if weak store should be rejected

??? }

??? // Do not set

*location anywhere else. That would introduce a race.

??? *location= newObj;

??? ......

??? return?newObj;

}

????????我們撇開源碼中各種鎖操作,來看看這段代碼都做了些什么。在此之前,我們先來了解下weak表和SideTable。

????????weak表是一個弱引用表,實現為一個weak_table_t結構體,存儲了某個對象相關的的所有的弱引用信息。其定義如下(具體定義在objc-weak.h中):

struct weak_table_t{

??? weak_entry_t *weak_entries;

??? size_t num_entries;

??? ......

};

????????其中weak_entry_t是存儲在弱引用表中的一個內部結構體,它負責維護和存儲指向一個對象的所有弱引用hash表。其定義如下:

struct weak_entry_t {

??? DisguisedPtr referent;

??? union{

???????struct{

???????????weak_referrer_t *referrers;

???????????uintptr_t out_of_line : 1;

???????????......

???????};

???????struct{

//out_of_line=0 is LSB of one of these (don't care which)

???????????weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];

???????};

??? };

};

????????其中referent是被引用的對象,即示例代碼中的obj對象。下面的union即存儲了所有指向該對象的弱引用。由注釋可以看到,當out_of_line等于0時,hash表被一個數組所代替。另外,所有的弱引用對象的地址都是存儲在weak_referrer_t指針的地址中。其定義如下:

typedef objc_object **weak_referrer_t;


????????SideTable是一個用C++實現的類,它的具體定義在NSObject.mm中,我們來看看它的一些成員變量的定義:

class SideTable{

private:

??? static uint8_t table_buf[SIDE_TABLE_STRIPE *SIDE_TABLE_SIZE];

public:

??? RefcountMap refcnts;

??? weak_table_t weak_table;

??? ......

}

????????RefcountMap refcnts,大家應該能猜到這個做什么用的吧?看著像是引用計數什么的。哈哈,貌似就是啊,這東東存儲了一個對象的引用計數的信息。當然,我們在這里不去探究它,我們關注的是weak_table。這個成員變量指向的就是一個對象的weak表。

????????了解了weak表和SideTable,讓我們再回過頭來看看objc_storeWeak。首先是根據weak指針找到其指向的老的對象:

oldObj = *location;

????????然后獲取到與新舊對象相關的SideTable對象:

oldTable = SideTable::tableForPointer(oldObj);

newTable = SideTable::tableForPointer(newObj);

????????下面要做的就是在老對象的weak表中移除指向信息,而在新對象的weak表中建立關聯信息:

if(oldObj){

??? weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);

}

if(newObj){

??? newObj= weak_register_no_lock (&newTable->weak_table, newObj, location);

??? // weak_register_no_lock returns NULL if weak store should be rejected

}


????接下來讓弱引用指針指向新的對象:

*location = newObj;

????最后會返回這個新對象:

return newObj;

???????objc_storeWeak的基本實現就是這樣。當然,在objc_initWeak中調用objc_storeWeak時,老對象是空的,所有不會執行weak_unregister_no_lock操作。

????????而當weak引用指向的對象被釋放時,又是如何去處理weak指針的呢?當釋放對象時,其基本流程如下:

????1、調用objc_release

????2、因為對象的引用計數為0,所以執行dealloc

????3、在dealloc中,調用了_objc_rootDealloc函數

????4、在_objc_rootDealloc中,調用了object_dispose函數

????5、調用objc_destructInstance

????6、最后調用objc_clear_deallocating

????????我們重點關注一下最后一步,objc_clear_deallocating的具體實現如下:

void objc_clear_deallocating(id obj)

{

??? ......

??? SideTable*table = SideTable::tableForPointer(obj);

??? //?clear?any?weak table?items

??? //?clear?extra?retain?count?and?deallocating?bit

??? //?(fixme?warn?or?abort?if?extra?retain?count?==?0??)

??? OSSpinLockLock(&table->slock);

??? if?(seen_weak_refs){

??????? arr_clear_deallocating(&table->weak_table,?obj);

??? }

??? ......

}

????????我們可以看到,在這個函數中,首先取出對象對應的SideTable實例,如果這個對象有關聯的弱引用,則調用arr_clear_deallocating來清除對象的弱引用信息。我們來看看arr_clear_deallocating具體實現:

PRIVATE_EXTERN void arr_clear_deallocating(weak_table_t*weak_table, id referent){

??? {

???????weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);

???????if (entry == NULL){

???????????......

???????????return;

???????}

???????//?zero?out?references

???????for?(int i = 0; i < entry->referrers.num_allocated; ++i){

???????????id *referrer = entry->referrers.refs[i].referrer;

???????????if?(referrer){

??????????????? if?(*referrer ==referent){

??????????????????? *referrer = nil;

??????????????? }

??????????????? else if?(*referrer){

??????????????????? _objc_inform ("__weak?variable @ %p holds %p instead of %p\n", referrer, *referrer, referent);

??????????????? }

???????????}

???????}

???????weak_entry_remove_no_lock(weak_table, entry);

???????weak_table->num_weak_refs--;

??? }

}

????????這個函數首先是找出對象對應的weak_entry_t鏈表,然后挨個將弱引用置為nil。最后清理對象的記錄。

????????通過上面的描述,我們基本能了解一個weak引用從生到死的過程。從這個流程可以看出,一個weak引用的處理涉及各種查表、添加與刪除操作,還是有一定消耗的。所以如果大量使用__weak變量的話,會對性能造成一定的影響。那么,我們應該在什么時候去使用weak呢?《Objective-C高級編程》給我們的建議是只在避免循環引用的時候使用__weak修飾符。

????????另外,在clang中,還提供了不少關于weak引用的處理函數。如objc_loadWeak, objc_destroyWeak, objc_moveWeak等,我們可以在蘋果的開源代碼中找到相關的實現。等有時間,我再好好研究研究。

3 參考鏈接

ARC下需要注意的內存管理

http://www.lxweimin.com/p/556ba33fa498


iOS內功篇:內存管理

http://www.cocoachina.com/ios/20160411/15892.html?utm_source=tuicool&utm_medium=referral


《Objective-C高級編程》1.4: __weak修飾符


Clang 3.7 documentation – Objective-C?Automatic Reference Counting (ARC)


apple opensource – NSObject.mm

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容