前言:

本次分享會先介紹Dealloc,對常見的用法分析,然後初步深入瞭解Dealloc的機制。

一、Dealloc 是什麼

Deallocates the memory occupied by the receiver.

摘自 官方文檔 ,文檔描述,dealloc 其實就是NSObject的一個 方法 ,當對象被銷燬的時候,系統就會回調這個方法,用來釋放內存佔用。 當然當iOS開發的應該都知道,這個方法默認是沒有寫的,因爲系統會自動處理了,至於處理了什麼,以及怎麼處理,這個後文會繼續分析。

二、Dealloc 怎麼寫

推薦的代碼組織方式是將dealloc方法放在實現文件的最前面(直接在@synthesize以及@dynamic之後)

摘自《 禪與Objective-C編程藝術 》一書,書中提到了dealloc的方法實現建議放的位置。

順便提一下, @synthesize 的語義是如果你沒有手動實現 setter 方法和 getter 方法,那麼編譯器會自動爲你加上這兩個方法,當然定義一個屬性的時候,系統會默認編寫了,而 @dynamic 的用法則相反,屬性的 setter 與 getter 方法由用戶自己實現,不自動生成。

那dealloc方法裏面,我們要處理什麼?

在dealloc方法中通常需要做的有移除通知或監聽操作,或對於一些非Objective-C對象也需要手動清空,比如CoreFoundation中的對象。

MRC下就要手動釋放、置空變量等操作後還需要調用父類的dealloc,而ARC下除了CoreFoundation的對象需要手動釋放以及KVO監聽移除外(NSNotification 在iOS9之後也不需要手動移除了),基本就沒了,當然ARC的內存銷燬具有一定的滯後性,也可將一些變量手動置空,也就是告訴系統這些變量已經使用完畢可以釋放了,當然也可以不做任何操作, 系統會自動釋放這些成員變量或者屬性

三、Dealloc 容易犯的錯誤

前面簡單介紹了dealloc的定義以及用法,那我們平時開發的時候,會容易出現什麼樣的錯誤呢?下面會列舉四種錯誤用法分析:

1、dealloc在什麼線程中被調用

很多人都認爲是在主線程調用的,其實並不是,而是取決於最後在什麼線程中release後觸發,這個稍後結合第四個錯誤點來分析會比較清楚。

Runtime 源碼 objc-object.h 源碼中可以得到上述結論。

ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) 

...

if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}

2、dealloc中置空操作

常見的操作:

- (void)dealloc {
NSLog(@"BaseModel dealloc");
self.baseName = nil;
}

這個表面看上去沒什麼問題,在《 Effective Objective-C 2.0 》一書中第7條提到:

在對象內部儘量直接訪問實例變量,而在初始化方法和dealloc方法中,總是應該直接通過實例變量來讀寫數據。

除了文中所說的加快訪問速度之外,但是如果用法不巧當的話,會出現不必要的崩潰問題。下面舉個簡單的例子分析一下:

定義了一個BaseModel 基類,基類中演示了使用 self.baseName = nil

@interface BaseModel : NSObject

@property (nonatomic, copy) NSString * _Nullable baseName;

@end

@implementation BaseModel

- (void)dealloc {
NSLog(@"BaseModel dealloc");
self.baseName = nil;
}

- (void)setBaseName:(NSString *)baseName {
_baseName = baseName;
NSLog(@"BaseModel setBaseName:%@", baseName);
}

@end

同時定義了一個子類SubModel繼承自BaseModel,子類中重寫了baseName 的setter方法,並獲取baseName進行其他操作

@implementation SubModel// 繼承自BaseModel

- (void)dealloc {
NSLog(@"SubModel dealloc");
}

- (void)setBaseName:(NSString *)baseName {
[super setBaseName:baseName];
NSLog(@"SubModel setBaseName:%@", [NSString stringWithString:baseName]);
}

@end

當SubModel作爲一個臨時變量生成後賦值baseName,變量使用完後系統會自動回收,此時大家可以想想會發什麼什麼問題?

不難想到,此時會出現崩潰現象,原因是 [NSString stringWithString:baseName] 這裏,baseName是nil,而這個方法是不允許傳nil參數的,當然,這個業務處理上肯定需要一個判空操作,我們先來分析一下爲什麼會是nil

子類SubModel被釋放會調用子類的dealloc方法,然後會調用父類BaseModel的dealloc方法,此時父類中通過setter方法來賦值nil,而子類SubModel重寫了,子類拿到nil來處理導致崩潰問題

究竟屬性是否需要手動置空釋放?實際上來說,是不需要手動釋放的,因爲dealloc中 .cxx_destruct 會處理。當然因爲執行是有一定延遲性,爲了節省資源,在確保屬性沒利用價值的時候可以手動清空,這個後文會分析dealloc的處理邏輯。

3、dealloc中使用__weak

舉個簡單例子來模擬實際複雜業務場景:

- (void)dealloc {
NSLog(@"SubModel dealloc");
[self performSelectorWhenDealloc];
}

- (void)performSelectorWhenDealloc {
__weak typeof(self) weakSelf = self;
// 模擬複雜的block結構,需要弱引用解除循環引用
void (^block)(void) = ^ {
[weakSelf test];
};
block();
}

當SubModel這個類被釋放,調用dealloc的時候會出現崩潰,崩潰信息如下: Cannot form weak reference to instance (0x2813c4d90) of class xxx. It is possible that this object was over-released, or is in the process of deallocation.

崩潰原因我們來分析一下:

先了解一下 __weak 到底做了什麼操作,通過clang 轉換的代碼是這樣的 __attribute__((objc_ownership(weak))) typeof(self) weakSelf = self; 這樣還是看不出問題,我們看回堆棧,堆棧崩在 objc_initWeak 函數中,我們可以看看 Runtime 源碼 objc_initWeak 函數的定義是怎麼樣的:

id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

可以留意到,內部調用了 storeWeak 函數,其中有個模板名稱是 DontCrashIfDeallocating 不難猜到,當調用到了 storeWeak 函數的時候,如果釋放過程中存儲,那就會crash,函數最終會調用register函數 id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating)

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id, bool crashIfDeallocating)
{
...
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
...
}

在這就找到了崩潰時打印出信息了。通過上面的分析,我們也知道, __weak 其實就是會最終調用 objc_initWeak 函數進行註冊。抱着求學的態度,可以在 clang 8.7objc_initWeak 函數描述中找到答案:

object is a valid pointer which has not been registered as a __weak object. value is null or a pointer to a valid object. If value is a null pointer or the object to which it points has begun deallocation, object is zero-initialized. Otherwise, object is registered as a __weak object pointing to value. Equivalent to the following code:

4、dealloc中使用GCD

GCD相信大家平時用得不少,但在Dealloc方法裏面使用GCD大家有沒有注意呢,先來舉個簡單例子,我們在主線程中創建一個定時器,然後類被釋放的時候銷燬定時器,相關代碼如下。

- (void)dealloc {
[self invalidateTimer];
}

- (void)fireTimer {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
if (!weakSelf.timer) {
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"TestDeallocModel timer:%p", timer);
}];
[[NSRunLoop currentRunLoop] addTimer:weakSelf.timer forMode:NSRunLoopCommonModes];
}
});
}

- (void)invalidateTimer {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.timer) {
NSLog(@"TestDeallocModel invalidateTimer:%p model:%p", self->_timer, self);
[self.timer invalidate];
self.timer = nil;
}
});
}

補充說明一下,定時器的釋放和創建必須在同一個線程,這個也是比較容易犯的錯誤點,官方描述如下:

Stops the timer from ever firing again and requests its removal from its run loop.
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point. If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

簡單解釋一下,當前的定時器銷燬只能從啓動定時器的Runloop中移除,然後Runloop和線程是一一對應的,因此需要確保銷燬和創建在同一個線程中處理,否則可能會出現釋放不了的情況。

說回正題,當定時器所在類被釋放後,此時調用 invalidateTimer 方法去銷燬定時器的時候就會出現崩潰情況。

崩潰報錯: Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

出現訪問野指針問題了,原因其實不難想到程序代碼默認是在主線程主隊列中執行,而dealloc中異步執行主隊列中釋放定時器釋放,GCD會強引用 self ,此時dealloc已經執行完成了,那麼 self 其實已經被free釋放掉了,此時銷燬內部再調用 self 就會訪問野指針。

我們來繼續分析一下,GCD爲啥會強引用 self ,以及簡單分析一下GCD的調用時機問題。

強引用問題,我們可以通過Clang去查看一下底層源碼實現,簡單轉換如下代碼:

- (void)dealloc {
dispatch_async(dispatch_queue_create("Kong", 0), ^{
[self test];
});
}

轉換後如下:

struct __TestModel__dealloc_block_impl_0 {
struct __block_impl impl;
struct __TestModel__dealloc_block_desc_0* Desc;
TestModel *const __strong self;
__TestModel__dealloc_block_impl_0(void *fp, struct __TestModel__dealloc_block_desc_0 *desc, TestModel *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __TestModel__dealloc_block_func_0(struct __TestModel__dealloc_block_impl_0 *__cself) {
TestModel *const __strong self = __cself->self; // bound by copy

((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("test"));
}


static void __TestModel__dealloc_block_copy_0(struct __TestModel__dealloc_block_impl_0*dst, struct __TestModel__dealloc_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}



static void __TestModel__dealloc_block_dispose_0(struct __TestModel__dealloc_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}



static struct __TestModel__dealloc_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestModel__dealloc_block_impl_0*, struct __TestModel__dealloc_block_impl_0*);
void (*dispose)(struct __TestModel__dealloc_block_impl_0*);
} __TestModel__dealloc_block_desc_0_DATA = { 0, sizeof(struct __TestModel__dealloc_block_impl_0), __TestModel__dealloc_block_copy_0, __TestModel__dealloc_block_dispose_0};

static void _I_TestModel_dealloc(TestModel * self, SEL _cmd) {
dispatch_async(dispatch_queue_create("Kong", 0), ((void (*)())&__TestModel__dealloc_block_impl_0((void *)__TestModel__dealloc_block_func_0, &__TestModel__dealloc_block_desc_0_DATA, self, 570425344)));
}

轉換後的代碼量有點多,我們可以只抓重點來查看一下,具體實現細節本文就不過多分析。

可以發現, dealloc 方法 轉換成 static void _I_TestModel_dealloc(TestModel * self, SEL _cmd) 內部調用的 dispatch 方法變化不大,主要是看Block的傳遞,可以留意到 __TestModel__dealloc_block_impl_0 這個結構體地址參數,看其代碼實現可以發現, TestModel *const __strong self; 就是這個 __strong 使得Block 會對 self 進行強引用。順帶說一下,結構體中有個 struct __block_impl impl; 成員變量,而這個結構體內部有個 FuncPtr 成員,Block的調用實際上就是通過 FuncPtr 來實現的。

以上就通過一個簡單的例子,解釋了dealloc 中使用GCD中出現的問題,實際上,GCD還會出現很多種搭配情況,這裏簡單畫了一個圖:

原理是一樣的,簡單總結一下: GCD任務底層通過鏈表管理,隊列任務遵循FIFO模式,那麼任務執行肯定就會有延遲性,同一時刻只能執行一個任務,只要dealloc任務執行先,那麼此時block使用self就會訪問野指針,因爲dealloc內會有free操作。

四、Dealloc 源碼分析

上文解答了dealloc 中的幾種使用情況,具體源碼實現還沒分析,下面我們來分析一下源碼到底是如何實現的。本文的分析源碼版本是 objc4-756.2.tar.gz

我們通過問題來閱讀源碼:

1、爲什麼dealloc 中使用GCD 會容易訪問野指針?

出現野指針訪問,那肯定就有free操作,我們查一下這個free是在哪一步執行:

dealloc 在NSObject.mm 文件中的實現

// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}

最終調用在 objc-object.h 的 inline void objc_object::rootDealloc() 函數中

inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return;  // fixme necessary?

if (fastpath(isa.nonpointer  &&  // 是否是優化過的isa
!isa.weakly_referenced  &&  // 不包含或者不曾經包含weak指針
!isa.has_assoc  &&  // 沒有關聯對象
!isa.has_cxx_dtor  &&  // 沒有c++析構方法
!isa.has_sidetable_rc))// 引用計數沒有超出上限的時候可以快速釋放,rootRetain(bool tryRetain, bool handleOverflow) 中設置爲true
{
assert(!sidetable_present());
free(this);
} 
else {
object_dispose((id)this);
}
}

可以看到滿足一定條件下,對象指針會直接free釋放掉,實際很多情況下都會走 object_dispose((id)this) 函數,這個函數是在objc-runtime-new.mm文件,下面繼續分析這個函數實現

id 
object_dispose(id obj)
{
if (!obj) return nil;

objc_destructInstance(obj);
/// 釋放內存
free(obj);

return nil;
}

終於看到了大概調用結構了, objc_destructInstance 函數後面分析,可以發現 dealloc 內部最終都會走 free 操作,而這個操作就會導致野指針訪問問題

2、爲什麼屬性或者說成員變量會自動釋放?

開篇說了系統會自動釋放屬性或者成員變量,其實就是 objc_destructInstance 函數的處理,其定義如下:

void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
// 對象擁有成員變量時編譯器會自動插入.cxx_desctruct方法用於自動釋放,可打印方法名證明;
if (cxx) object_cxxDestruct(obj);
// 移除關聯對象
if (assoc) _object_remove_assocations(obj);
// weak->nil
obj->clearDeallocating();
}

return obj;
}

代碼上我已經部分註釋了,我們直接看 object_cxxDestruct 函數實現,後面 _object_remove_assocations 是移除關聯對象,就是我們分類中通過 objc_setAssociatedObject 函數新增的就是關聯對象,而 clearDeallocating 則是把對應weak哈希表的置空

void object_cxxDestruct(id obj)
{
if (!obj) return;
// 如果是isTaggedPointer,不處理

///爲了節省內存和提高執行效率,蘋果提出了Tagged Pointer的概念,避免32位機器遷移到64位機器內存翻倍 https://blog.devtang.com/2014/05/30/understand-tagged-pointer
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);

// Call cls's dtor first, then superclasses's dtors.
// 按繼承鏈釋放
for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
/// .cxx_destruct是編譯器生成的代碼,在.cxx_destruct進行形如objc_storeStrong(&ivar, null)的調用後,對應的實例變量就被release和設置成nil了
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
// 進行過動態方法解析後會標記IMP爲_objc_msgForward_impcache,進行緩存後會進行消息分發
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s", 
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}

看最終實現可以發現,內部就是通過繼承鏈遍歷調用 lookupMethodInClassAndLoadCache 函數來進行後續釋放,實際上是通過 SEL_cxx_destruct 來執行C++的析構方法

我們可以通過 watchpoint 來監控對象屬性的釋放,監控堆棧如下: 可以看出,最終實際上就是調用了 objc_storeStrong 函數來做釋放操作,可以查看源碼如下

void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}

簡單總結一下dealloc的處理邏輯

五、Dealloc 用法總結

1、dealloc中儘量直接訪問實例變量來置空。

2、dealloc中切記不能使用__weak self。

3、dealloc中切線程操作儘量避免使用GCD,可利用performSelector,確保線程操作先於dealloc完成。

六、Dealloc 機制應用

從源碼中可以知道,dealloc在對象置nil以及free之前,會進行關聯對象釋放,那麼可以利用關聯對象銷燬監聽dealloc完成,做一些自動釋放操作,例如通知監聽釋放等,實際上網上也是有一些例子了。

簡單的源碼演示:

@interface TestDeallocAssociatedObject : NSObject

- (instancetype)initWithDeallocBlock:(void (^)(void))block;

@end
@implementation TestDeallocAssociatedObject {
void(^_block)(void);
}

- (instancetype)initWithDeallocBlock:(void (^)(void))block {

if (self = [super init]) {
self->_block = [block copy];
}
return self;
}

- (void)dealloc {
if (self->_block) {
self->_block();
}
}

@end

然後在需要監聽的地方創建關聯對象,Block內處理即可,此時要注意Block引用的問題。

// 添加關聯對象
TestDeallocAssociatedObject *object = [[TestDeallocAssociatedObject alloc] initWithDeallocBlock:^{
NSLog(@"TestDeallocAssociatedObject dealloc");
}];
objc_setAssociatedObject(self, &KTestDeallocAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

七、結語

通過幾個常見案例,逐步分析dealloc的底層代碼實現,本文篇幅有點多,如果有描述錯誤或者不當的地方,歡迎指正~喜歡的可以點個贊哈,謝謝!

相關文章