探究ARC下dealloc实现

我是前言

目前正在看 oc 底层的东西,看了许多大牛的博客,发现有一些小问题:

  • runtime 的版本可能跟作者当时写的版本不一致
  • 许多方法一笔带过,因为基础知识的薄弱看不懂。。。
  • 没有标明苹果文档的出处

所以我打算解决上面的一些问题,然后重新发一版,当然大部分的内容还是原作者写的 。runtime 的源码为 objc4-646.tar.gz版本

进入正题

在 ARC 环境下,我们不需要主动的调用系统的析构函数 dealloc 就能够完成将对象以及父类的成员变量内存释放掉的操作:

- (void)dealloc
{
    // ... //
    // 非Objc对象内存的释放,如CFRelease(...)
    // ... //
}

问题来了:

  1. 这个对象成员变量(ivars)的释放操作去哪儿了?
  2. 没有主动调用 [super dealloc],那么是什么时候调用这个方法的?

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 后被调用,但此时实例变量(ivars)并未释放,父类的dealloc的方法将在子类dealloc方法返回后自动调用

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),变量释放顺序各种不确定(一个类内的不确定,子类和父类间也不确定,也就是说不用care释放顺序)


所以,我们不需要主动调用 [super dealloc] ,系统会自动调用,后面我们再讲这是怎么实现的。接下来我们来探究在根类 NSObject 析构时发生了什么

NSObject的析构过程

通过 runtime 源码,我们可以发现 NSObject 调用 dealloc 时会调用 _objc_rootDealloc(NSObject.mm 2071行) 继而调用object_dispose(objc-object.h 301行) 随后调用objc_destructInstance(objc-runtime-new.mm 6838行), 下面讲一下rootDeallocobjc_destructInstance函数

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 指针的结构:

// ...
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 表示使用优化,即Tagged Pointer存储引用计数
  • has_assoc(1 bit) 表示该对象是否包含 associated object,如果没有,则析构(释放内存)时会更快
  • has_cxx_dtor(1 bit) 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构(释放内存)时更快
  • shiftcls(30 bits) 类的指针
  • magic(9 bits) 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。
  • weakly_referenced(1 bit) 表示该对象是否有过 weak 对象,如果没有,则析构(释放内存)时更快
  • deallocating(1 bit) 表示该对象是否正在析构
  • has_sidetable_rc(1 bit) 表示该对象的引用计数值是否过大无法存储在 isa 指针
  • extra_jc(19 bits) 表示引用计数值减一后的结果。例如,如果对象引用计数为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. 执行了一个 object_cxxDestruct 函数
  2. 执行_object_remove_assocations函数去除和这个对象 assocate 的对象(常用于类目中添加的属性 )
  3. 执行clearDeallocating, 清空引用计数并清除弱引用表,将所有使用__weak修饰的指向该对象的变量置为nil

所以,ARC 自动释放实例变量的地方就在 object_cxxDestruct 这个方法里面没跑了。

探究 object_cxxDestruct

上面找到的名为object_cxxDestruct的方法最终成为下面的调用

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)向上递归调用 SEL_cxx_destruct这个函数的函数实现
这篇文章提到:

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++ 对象析构的,ARC 借用了这个方法插入代码实现了自动释放的工作。

通过实验找出 .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

当过了大括号的作用域,son 对象就会被释放。所以在after new这行son对象初始化完成,在gone这行son对象被dealloc。

本次实验使用 NSObject+DLIntrospection 这个扩展来作用调试工具,通过它可以轻松打出一个类的方法,成员变量等。
将这个扩展引入工程,在 after new 处设置一个断点,在这里打印出 Son 类所有的方法名:

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

发现出现了.cxx_destruct这个方法,经过几次实验,发现:

  1. 只有在ARC下这个方法才会出现(试验代码的情况下)
  2. 只有当前类拥有实例变量时(不论是不是用property)这个方法才会出现,且父类的实例变量不会导致子类拥有这个方法
  3. 出现这个方法和变量是否被赋值,赋值成什么没有关系

使用 watchpoint 定位内存释放时刻

依然在 after new 断点处,输入 lldb 命令:

watchpoint set variable son->_name

name的变量加入watchpoint,当这个变量被修改时会触发trigger:
从中可以看出,在这个时刻,_name 从 0x0000000104ac5048 变成了0x0000000000000000,也就是nil,赶紧看下调用栈:
发现果然跟到了.cxx_destruct方法,而且是在objc_storeStrong方法中释放

image

刨根问底.cxx_destruct

知道了ARC环境下,对象实例变量的释放过程在 .cxx_destruct 内完成,但这个函数内部发生了什么,是如何调用 objc_storeStrong 释放变量的呢?
从上面的探究中知道,.cxx_destruct 是编译器生成的代码,那它很可能在clang前端编译时完成,这让我联想到clang的Code Generation,因为之前曾经使用clang -rewrite-objc xxx.m时查看过官方文档留下了些印象,于是google:
.cxx_destruct site:clang.llvm.org

结果发现clang的 doxygen 文档中 CodeGenModule 模块正是这部分的实现代码,cxx相关的代码生成部分源码在
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);
}

这个函数大概作用是:获取到 .cxx_destruct 的selector,创建 Method,然后加入到这个类的方法列表中,最后一行的调用才是真的创建这个方法的实现。这个方法位于http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 1354行,包含了构造和析构的 cxx 方法,继续跟随 .cxx_destruct,最终调用 emitCXXDestructMethod 函数,代码如下:

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?");
}

分析这段代码以及其中调用后发现:它遍历当前对象所有的实例变量(Ivars),调用objc_storeStrong,从clang的ARC文档上可以找到 objc_storeStrong 的示意代码实现如下:

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

在 .cxx_destruct 进行形如 objc_storeStrong(&ivar, null) 的调用后,这个实例变量就被release和设置成nil了

自动调用[super dealloc]的实现

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

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

上面代码可以得知在调用dealloc方法时被插入了代码,由FinishARCDealloc结构定义:

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

上面代码基本上就是向父类转发dealloc的调用,实现了自动调用[super dealloc]方法。

总结

  • ARC下对象的成员变量在编译器插入的.cxx_desctruct方法自动释放
  • ARC下[super dealloc]方法也由编译器自动插入
  • 所谓编译器插入代码过程需要进一步了解,还不清楚其运作方式

  • ARC环境,对象的实例变量将在根类 NSObject 的 dealloc 方法中释放内存
  • Father 的实例变量(如果有)将在它的 .cxx_desctruct方法中被释放,而 Son 的实例变量(如果有)将在它的 .cxx_desctruct方法中被释放
  • 子类在调用 dealloc 方法时会被插入代码,自动调用父类的 dealloc 方法

引用

ARC下dealloc过程及.cxx_destruct的探究
iOS内存管理之二

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容