探究ARC下dealloc實現(xiàn)

我是前言

目前正在看 oc 底層的東西坛悉,看了許多大牛的博客席纽,發(fā)現(xiàn)有一些小問題:

  • runtime 的版本可能跟作者當(dāng)時寫的版本不一致
  • 許多方法一筆帶過瓤湘,因為基礎(chǔ)知識的薄弱看不懂。。濒憋。
  • 沒有標(biāo)明蘋果文檔的出處

所以我打算解決上面的一些問題,然后重新發(fā)一版陶夜,當(dāng)然大部分的內(nèi)容還是原作者寫的 凛驮。runtime 的源碼為 objc4-646.tar.gz版本

進(jìn)入正題

在 ARC 環(huán)境下,我們不需要主動的調(diào)用系統(tǒng)的析構(gòu)函數(shù) dealloc 就能夠完成將對象以及父類的成員變量內(nèi)存釋放掉的操作:

- (void)dealloc
{
    // ... //
    // 非Objc對象內(nèi)存的釋放条辟,如CFRelease(...)
    // ... //
}

問題來了:

  1. 這個對象成員變量(ivars)的釋放操作去哪兒了黔夭?
  2. 沒有主動調(diào)用 [super dealloc],那么是什么時候調(diào)用這個方法的羽嫡?

ARC文檔中對dealloc過程的解釋

clang [ARC文檔](http://clang.llvm.org/docs/AutomaticReferenceCounting.html#dealloc

A class may provide a method definition for an instance method named dealloc. This method will be called after the final release of the object but before it is deallocated or any of its instance variables are destroyed. The superclass’s implementation of dealloc will be called automatically when the method returns.

大概意思是:dealloc 方法在最后一次 release 后被調(diào)用本姥,但此時實例變量(ivars)并未釋放,父類的dealloc的方法將在子類dealloc方法返回后自動調(diào)用

The instance variables for an ARC-compiled class will be destroyed at some point after control enters the dealloc method for the root class of the class. The ordering of the destruction of instance variables is unspecified, both within a single class and between subclasses and superclasses.

ARC下對象的實例變量在根類 [NSObject dealloc] 中釋放(通常root class都是NSObject)厂僧,變量釋放順序各種不確定(一個類內(nèi)的不確定扣草,子類和父類間也不確定,也就是說不用care釋放順序)


所以颜屠,我們不需要主動調(diào)用 [super dealloc] 辰妙,系統(tǒng)會自動調(diào)用,后面我們再講這是怎么實現(xiàn)的甫窟。接下來我們來探究在根類 NSObject 析構(gòu)時發(fā)生了什么

NSObject的析構(gòu)過程

通過 runtime 源碼密浑,我們可以發(fā)現(xiàn) NSObject 調(diào)用 dealloc 時會調(diào)用 _objc_rootDealloc(NSObject.mm 2071行) 繼而調(diào)用object_dispose(objc-object.h 301行) 隨后調(diào)用objc_destructInstance(objc-runtime-new.mm 6838行), 下面講一下rootDeallocobjc_destructInstance函數(shù)

inline void objc_object::rootDealloc()
{
    assert(!UseGC);
    if (isTaggedPointer()) return;

    if (isa.indexed  &&  
        !isa.weakly_referenced  &&  
        !isa.has_assoc  &&  
        !isa.has_cxx_dtor)
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

64位下,isa 指針的結(jié)構(gòu):

// ...
struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
        uintptr_t magic             : 9;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
// ...
  • indexed(1 bit) 0 表示普通的 isa 指針粗井,1 表示使用優(yōu)化尔破,即Tagged Pointer存儲引用計數(shù)
  • has_assoc(1 bit) 表示該對象是否包含 associated object,如果沒有浇衬,則析構(gòu)(釋放內(nèi)存)時會更快
  • has_cxx_dtor(1 bit) 表示該對象是否有 C++ 或 ARC 的析構(gòu)函數(shù)懒构,如果沒有,則析構(gòu)(釋放內(nèi)存)時更快
  • shiftcls(30 bits) 類的指針
  • magic(9 bits) 固定值為 0xd2耘擂,用于在調(diào)試時分辨對象是否未完成初始化胆剧。
  • weakly_referenced(1 bit) 表示該對象是否有過 weak 對象,如果沒有醉冤,則析構(gòu)(釋放內(nèi)存)時更快
  • deallocating(1 bit) 表示該對象是否正在析構(gòu)
  • has_sidetable_rc(1 bit) 表示該對象的引用計數(shù)值是否過大無法存儲在 isa 指針
  • extra_jc(19 bits) 表示引用計數(shù)值減一后的結(jié)果秩霍。例如,如果對象引用計數(shù)為4蚁阳,則extra_jc為3
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = !UseGC && obj->hasAssociatedObjects();
        bool dealloc = !UseGC;

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        if (dealloc) obj->clearDeallocating();
    }

    return obj;
}

objc_destructInstance干了三件事情:

  1. 執(zhí)行了一個 object_cxxDestruct 函數(shù)
  2. 執(zhí)行_object_remove_assocations函數(shù)去除和這個對象 assocate 的對象(常用于類目中添加的屬性 )
  3. 執(zhí)行clearDeallocating铃绒, 清空引用計數(shù)并清除弱引用表,將所有使用__weak修飾的指向該對象的變量置為nil

所以螺捐,ARC 自動釋放實例變量的地方就在 object_cxxDestruct 這個方法里面沒跑了颠悬。

探究 object_cxxDestruct

上面找到的名為object_cxxDestruct的方法最終成為下面的調(diào)用

static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);

    // Call cls's dtor first, then superclasses's dtors.

    for ( ; cls != NULL; cls = _class_getSuperclass(cls)) {
        if (!_class_hasCxxStructors(cls)) return;
        dtor = (void(*)(id))
            lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_internal) {
            if (PrintCxxCtors) {
                _objc_inform("CXX: calling C++ destructors for class %s",
                             _class_getName(cls));
            }
            (*dtor)(obj);
        }
    }
}

代碼的大致意思是通過繼承鏈(isa)向上遞歸調(diào)用 SEL_cxx_destruct這個函數(shù)的函數(shù)實現(xiàn)
這篇文章提到:

ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

和《Effective Objective-C 2.0》中的:

When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

可以了解到cxx_destruct方法原本是為了 C++ 對象析構(gòu)的矮燎,ARC 借用了這個方法插入代碼實現(xiàn)了自動釋放的工作。

通過實驗找出 .cxx_destruct

@interface Father : NSObject
@property (nonatomic, copy) NSString *name;
@end

@interface Son : Father
@property (nonatomic, copy) NSArray *toys;
@end

只有兩個簡單的屬性椿疗,找個地方寫簡單的測試代碼:

// start
{
    // before new
    Son *son = [Son new];
    son.name = @"sark";
    son.toys = @[@"sunny", @"xx"];
    // after new
}
// gone

當(dāng)過了大括號的作用域漏峰,son 對象就會被釋放糠悼。所以在after new這行son對象初始化完成届榄,在gone這行son對象被dealloc。

本次實驗使用 NSObject+DLIntrospection 這個擴(kuò)展來作用調(diào)試工具倔喂,通過它可以輕松打出一個類的方法铝条,成員變量等。
將這個擴(kuò)展引入工程席噩,在 after new 處設(shè)置一個斷點(diǎn)班缰,在這里打印出 Son 類所有的方法名:

po [[Son class] instanceMethods]
<__NSArrayI 0x280982520>(
- (void)setToys:(id)arg0 ,
- (id)toys,
- (void).cxx_destruct
)

發(fā)現(xiàn)出現(xiàn)了.cxx_destruct這個方法,經(jīng)過幾次實驗悼枢,發(fā)現(xiàn):

  1. 只有在ARC下這個方法才會出現(xiàn)(試驗代碼的情況下)
  2. 只有當(dāng)前類擁有實例變量時(不論是不是用property)這個方法才會出現(xiàn)埠忘,且父類的實例變量不會導(dǎo)致子類擁有這個方法
  3. 出現(xiàn)這個方法和變量是否被賦值,賦值成什么沒有關(guān)系

使用 watchpoint 定位內(nèi)存釋放時刻

依然在 after new 斷點(diǎn)處馒索,輸入 lldb 命令:

watchpoint set variable son->_name

name的變量加入watchpoint莹妒,當(dāng)這個變量被修改時會觸發(fā)trigger:
從中可以看出,在這個時刻绰上,_name 從 0x0000000104ac5048 變成了0x0000000000000000旨怠,也就是nil,趕緊看下調(diào)用棧:
發(fā)現(xiàn)果然跟到了.cxx_destruct方法蜈块,而且是在objc_storeStrong方法中釋放

image

刨根問底.cxx_destruct

知道了ARC環(huán)境下鉴腻,對象實例變量的釋放過程在 .cxx_destruct 內(nèi)完成,但這個函數(shù)內(nèi)部發(fā)生了什么百揭,是如何調(diào)用 objc_storeStrong 釋放變量的呢爽哎?
從上面的探究中知道,.cxx_destruct 是編譯器生成的代碼器一,那它很可能在clang前端編譯時完成课锌,這讓我聯(lián)想到clang的Code Generation,因為之前曾經(jīng)使用clang -rewrite-objc xxx.m時查看過官方文檔留下了些印象盹舞,于是google:
.cxx_destruct site:clang.llvm.org

結(jié)果發(fā)現(xiàn)clang的 doxygen 文檔中 CodeGenModule 模塊正是這部分的實現(xiàn)代碼产镐,cxx相關(guān)的代碼生成部分源碼在
http://clang.llvm.org/doxygen/CodeGenModule_8cpp-source.html
位于1827行,刪減掉離題部分如下:

/// EmitObjCIvarInitializations - Emit information for ivar initialization
/// for an implementation.
void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D)
{
    DeclContext* DC = const_cast<DeclContext*>(dyn_cast<DeclContext>(D));
    assert(DC && "EmitObjCIvarInitializations - null DeclContext");
    IdentifierInfo *II = &getContext().Idents.get(".cxx_destruct");
    Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
    ObjCMethodDecl *DTORMethod = ObjCMethodDecl::Create(getContext(),
                                                        D->getLocation(),
                                                        D->getLocation(), cxxSelector,
                                                        getContext().VoidTy, 0,
                                                        DC, true, false, true,
                                                        ObjCMethodDecl::Required);
   D->addInstanceMethod(DTORMethod);
   CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, DTORMethod, false);
}

這個函數(shù)大概作用是:獲取到 .cxx_destruct 的selector踢步,創(chuàng)建 Method癣亚,然后加入到這個類的方法列表中,最后一行的調(diào)用才是真的創(chuàng)建這個方法的實現(xiàn)获印。這個方法位于http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 1354行述雾,包含了構(gòu)造和析構(gòu)的 cxx 方法,繼續(xù)跟隨 .cxx_destruct,最終調(diào)用 emitCXXDestructMethod 函數(shù)玻孟,代碼如下:

static void emitCXXDestructMethod(CodeGenFunction &CGF, ObjCImplementationDecl *impl)
{
   CodeGenFunction::RunCleanupsScope scope(CGF);

   llvm::Value *self = CGF.LoadObjCSelf();

   const ObjCInterfaceDecl *iface = impl->getClassInterface();
   for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin(); ivar; ivar = ivar->getNextIvar())
   {
     QualType type = ivar->getType();

     // Check whether the ivar is a destructible type.
     QualType::DestructionKind dtorKind = type.isDestructedType();
     if (!dtorKind) continue;

     CodeGenFunction::Destroyer *destroyer = 0;

     // Use a call to objc_storeStrong to destroy strong ivars, for the
     // general benefit of the tools.
     if (dtorKind == QualType::DK_objc_strong_lifetime) {
       destroyer = destroyARCStrongWithStore;

     // Otherwise use the default for the destruction kind.
     } else {
       destroyer = CGF.getDestroyer(dtorKind);
     }

     CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);
     CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
                                          cleanupKind & EHCleanup);
   }

   assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
}

分析這段代碼以及其中調(diào)用后發(fā)現(xiàn):它遍歷當(dāng)前對象所有的實例變量(Ivars)唆缴,調(diào)用objc_storeStrong,從clang的ARC文檔上可以找到 objc_storeStrong 的示意代碼實現(xiàn)如下:

void objc_storeStrong(id *object, id value) {
  id oldValue = *object;
  value = [value retain];
  *object = value;
  [oldValue release];
}

在 .cxx_destruct 進(jìn)行形如 objc_storeStrong(&ivar, null) 的調(diào)用后黍翎,這個實例變量就被release和設(shè)置成nil了

自動調(diào)用[super dealloc]的實現(xiàn)

按照上面的思路面徽,自動調(diào)用 [super dealloc] 也一定是 CodeGen 的工作了,位于http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 492行 StartObjCMethod 方法中:

if (ident->isStr("dealloc"))
   EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind());

上面代碼可以得知在調(diào)用dealloc方法時被插入了代碼匣掸,由FinishARCDealloc結(jié)構(gòu)定義:

struct FinishARCDealloc : EHScopeStack::Cleanup {
   void Emit(CodeGenFunction &CGF, Flags flags) override {
     const ObjCMethodDecl *method = cast<ObjCMethodDecl>(CGF.CurCodeDecl);

     const ObjCImplDecl *impl = cast<ObjCImplDecl>(method->getDeclContext());
     const ObjCInterfaceDecl *iface = impl->getClassInterface();
     if (!iface->getSuperClass()) return;

     bool isCategory = isa<ObjCCategoryImplDecl>(impl);

     // Call [super dealloc] if we have a superclass.
     llvm::Value *self = CGF.LoadObjCSelf();

     CallArgList args;
     CGF.CGM.getObjCRuntime().GenerateMessageSendSuper(CGF, ReturnValueSlot(),
                                                       CGF.getContext().VoidTy,
                                                       method->getSelector(),
                                                       iface,
                                                       isCategory,
                                                       self,
                                                       /*is class msg*/ false,
                                                       args,
                                                       method);
   }
};

上面代碼基本上就是向父類轉(zhuǎn)發(fā)dealloc的調(diào)用趟紊,實現(xiàn)了自動調(diào)用[super dealloc]方法。

總結(jié)

  • ARC下對象的成員變量在編譯器插入的.cxx_desctruct方法自動釋放
  • ARC下[super dealloc]方法也由編譯器自動插入
  • 所謂編譯器插入代碼過程需要進(jìn)一步了解碰酝,還不清楚其運(yùn)作方式

  • ARC環(huán)境霎匈,對象的實例變量將在根類 NSObject 的 dealloc 方法中釋放內(nèi)存
  • Father 的實例變量(如果有)將在它的 .cxx_desctruct方法中被釋放,而 Son 的實例變量(如果有)將在它的 .cxx_desctruct方法中被釋放
  • 子類在調(diào)用 dealloc 方法時會被插入代碼送爸,自動調(diào)用父類的 dealloc 方法

引用

ARC下dealloc過程及.cxx_destruct的探究
iOS內(nèi)存管理之二

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末铛嘱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子袭厂,更是在濱河造成了極大的恐慌墨吓,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嵌器,死亡現(xiàn)場離奇詭異肛真,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)爽航,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門蚓让,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人讥珍,你說我怎么就攤上這事历极。” “怎么了衷佃?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵趟卸,是天一觀的道長。 經(jīng)常有香客問我氏义,道長锄列,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任惯悠,我火速辦了婚禮邻邮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘克婶。我一直安慰自己筒严,他們只是感情好丹泉,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鸭蛙,像睡著了一般摹恨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上娶视,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天晒哄,我揣著相機(jī)與錄音,去河邊找鬼歇万。 笑死揩晴,一個胖子當(dāng)著我的面吹牛勋陪,可吹牛的內(nèi)容都是我干的贪磺。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼诅愚,長吁一口氣:“原來是場噩夢啊……” “哼寒锚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起违孝,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤刹前,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后雌桑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喇喉,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年校坑,在試婚紗的時候發(fā)現(xiàn)自己被綠了拣技。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡耍目,死狀恐怖膏斤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情邪驮,我是刑警寧澤莫辨,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站毅访,受9級特大地震影響沮榜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜喻粹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一蟆融、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧磷斧,春花似錦振愿、人聲如沸捷犹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽萍歉。三九已至,卻和暖如春档桃,著一層夾襖步出監(jiān)牢的瞬間枪孩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工藻肄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔑舞,地道東北人。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓嘹屯,卻偏偏與公主長得像攻询,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子州弟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容