面试遇到的 OC 的 dealloc() 问题思考

0. 前言

最近面了一些试,某位面试官问了我一个有意思的问题:

dealloc 的时候增加引用计数,会防止对象销毁吗?

当时猜测了一下,没答很全面,今晚有空了,好好梳理一下 delloc 的流程。

1. 调用Dealloc之前的流程
2. Dealloc()
2.1 析构 CxxDestruct,以及到底是什么!
2.2 移除关联对象,复习关联对象实现
3. clearDeallocationg(),处理weak
4. free(obj)
5. 父类的 Dealloc 呢?

穿插研究复习几个问题,提供几个真正属于 runtime 的面试题:

1. customRR是什么?
2. SideTable里操作Weak在什么环节?
3. CxxDestruct是什么?
4. 源码里没有,那父类Dealloc如何调用的?
5. 一个Strong一个weak在delloc里面指向对象自己会怎么样?
6. 为什么自己写 dealloc 里面不需要写 [super dealloc]?

1. 调用Dealloc之前的流程

首先创造一个简单对象的 delloc 流程下符号断点 delloc :

{
      Fruit *banana = [[Fruit alloc] init];
}

可以捕获断点 bt 出来:

<Fruit: 0x100658430> (lldb) bt * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.2

    frame #0: 0x000000010031e3f0 libobjc.A.dylib` -[NSObject dealloc](self=0x0000000100658430, _cmd="dealloc") at NSObject.mm:2350:23

    frame #1: 0x00000001002c616b  libobjc.A.dylib`_objc_release [inlined]  objc_object::rootRelease(this=0x0000000100658430, performDealloc=true, =false)  at objc-object.h:701:9

    frame #2: 0x00000001002c5a98  libobjc.A.dylib`_objc_release [inlined] objc_object::rootRelease(this=0x0000000100658430) at objc-object.h:571

    frame #3: 0x00000001002c5a98 libobjc.A.dylib`_objc_release [inlined] objc_object::release(this=0x0000000100658430) at objc-object.h:550

    frame #4: 0x00000001002c5a09 libobjc.A.dylib`_objc_release(obj=0x0000000100658430) at NSObject.mm:1598

    frame #5: 0x0000000100317bef libobjc.A.dylib`objc_storeStrong( location=0x00007ffeefbff4f0, obj=0x0000000000000000) at NSObject.mm:256:5

  * frame #6: 0x0000000100000dfa `main(argc=1, argv=0x00007ffeefbff528) at main.m:17:9 [opt]

    frame #7: 0x00007fff67a7ecc9 libdyld.dylib`start + 1

    frame #8: 0x00007fff67a7ecc9 libdyld.dylib`start + 1

可以看到进入的流程:objc_storeStrong --> _objc_release --> release ......这就够了,最新的代码走起:

// PS: 如果不喜欢 bt 这个词的话,可以直接看:
// [Always Show Disassembly]
// 总之找到这个 dealloc 的入口是 objc_storeStrong() 就够了

->  0x100001b5f <+31>: movq   0x173a(%rip), %rdi        ; (void *)0x00000001000032e8: Fruit
    0x100001b66 <+38>: callq  0x100001d4a               ; symbol stub for: objc_alloc_init
    0x100001b6b <+43>: movq   %rax, -0x10(%rbp)
    0x100001b6f <+47>: leaq   -0x10(%rbp), %rdi
    0x100001b73 <+51>: xorl   %esi, %esi
    0x100001b75 <+53>: callq  0x100001d6e               ; symbol stub for: objc_storeStrong

调用了 NSObject.mmobjc_storeStrong()方法,传入的参数是 对象的地址以及 obj 为 nil,其实就是一个赋空值的过程。

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    // 判断上一个值跟目前的值是否同一地址
    if (obj == prev) {
        return;
    }
    // 两个值,retain新值,release旧的值
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

很明显这个 objc_retain(nil); 没什么作用,重点就是 objc_release(banana);

objc_release() 方法的实现很简单,第一判空返回,第二判TaggedPointer返回,第三调用obj->release()

__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

继续调用 objc_object::release()

// 等同于调用 [this release], 如果没有重写的话,就走一个捷径
inline void
objc_object::release()
{
    ASSERT(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}

插入去研究了 什么是 customRR 和 customCore:

//class 或 superclass 实现了默认的 retain/release/autorelease/retainCount/ _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法

//class 或 superclass 有默认的 new/self/class/respondsToSelector/isKindOfClass 方法
//源码:
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define RW_HAS_DEFAULT_RR     (1<<14)
// class or superclass has default new/self/class/respondsToSelector/isKindOfClass
#define RW_HAS_DEFAULT_CORE   (1<<13)

也就是说,👆这里 fastpath 大多数情况会调用 rootRelease,如果你的类自己实现了RR方法,就给 msgSend release 方法。

继续跟踪 rootRelease()

ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

rootRelease(true, false); 代码太多,看图里有详情,这里解释一下做的事情:

  1. 判 Tagged Pointer 返 false
  2. sideTable 和 extra_rc 的引用计数合并,如果都没有了,就通过 msgSend dealloc

另外这里提到:sidetable 使用的锁是自旋

问题1:Tagged Pointer 为啥这个时候返回False
TP不参与任何的引用计数,引用本身即是值,因为他很单纯,指针混淆、身体里大部分都是值。讲TP文章太多,在此不赘述了。

问题2:为什么会有 sideTable 和 extra_rc?
首先:nonpointerISA 对象的引用计数优先放在isa里面的ExtraRc里面,直到突破了那个区域的限制,就转到 SideTable 中的 两个Map中存储。
其次:而 pointerISA 直接在 SideTable 中的 两个Map中存储。

问题3:开篇的面试题
没有用,会继续释放,因为进入 dealloc 之前,问题2,已经判断了引用计数。

引发了另一个我想到的问题
一个Strong一个weak在delloc里面指向对象自己,会怎么样?稍后尝试!!!
我这里根据下面的推测猜测一下,strong 可能会有问题,甚至可能野指针,weak的话后面会释放,所以没关系。

2. Dealloc()

通过msgSend进入Dealloc

// NSObject.mm

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

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);
    obj->rootDealloc();
}
// objc-object.h
inline void
objc_object::rootDealloc()
{
    // fixme necessary? 源:有必要吗?
    if (isTaggedPointer()) return;

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

好如果你这个对象:是nonpointter的话,请问你是否有弱引用或者你是否有关联对象或者是否有析构函数或者是否你的引用计数太打了,存到sidetable里面去了

以上问题,如果你有一项的话,跟我们走一趟,调用下面的object_dispose((id)this)

如果以上你都没有,那好你是一个很单纯的对象,恭喜你,free() and go!

另外,去执行 object_dispose((id)this) 的对象也不要担心,看下面的实现,只是在 free(obj)之前多调用了一个 objc_destructInstance(obj) 而已。

/************************
* object_dispose
* fixme
* Locking: none
************************/
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

objc_destructInstance 这个是本篇的核心方法:

/************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
*************************************/
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.
        if (cxx) object_cxxDestruct(obj);  
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }
    return obj;
}

2.1 析构 CxxDestruct,以及到底是什么!

你看上面的源码说了,先执行析构函数还是先删除关联对象,先析构再移除关联。

object_cxxDestruct(obj) 往下走是 objc-class.mm 这个文件:两个方法,代码就不全粘了,核心就是一个for循环:

    // 源:先调用 cls 的析构,再不断调用父类的析构函数
    for ( ; cls; cls = cls->superclass) {
        if (!cls->hasCxxDtor()) return; 
        dtor = (void(*)(id))
            lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_impcache) {
            if (PrintCxxCtors) {
                _objc_inform("CXX: calling C++ destructors for class %s", 
                             cls->nameForLogging());
            }
            (*dtor)(obj);
        }
    }

看看这个for循环有意思,Cook多骚气,for ( ; cls; cls = cls->superclass),然后里面判断了hasCxxDtor,没有C++Dtor的话人家不写break,直接return了。

如果这个for循环找到了C++析构,就调用lookupMethodInClassAndLoadCache,方法寻址,这个方法有注释,仅在构造和析构的时候使用,也做了断言,很安全。

内部实现是一个方法寻址,找到的话填充缓存返回。

关于这个C++的析构函数,这里详细说明下,这里详细释放了对象的每一个实例变量!所以这个方法一点是 LLVM 的 clang 中的 代码生成模块搞出来的。

http://clang.llvm.org/doxygen/CodeGenModule_8cpp-source.html

它的方法实现核心就是对于这个对象所有实例变量,遍历调用objc_storeStrong(),然后这个实例变量就解除retain 了。

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

2.2 移除关联对象,复习关联对象实现

看到这里你要知道,我们通过API添加的关联对象,不管你ARC还是MRC,都没必要手动Remove。

objc-references.mm 里面

// 与设置/获取关联的引用不同,
// 此函数对性能敏感,
// 因为原始的isa对象(例如OS对象)
// 无法跟踪它们是否具有关联的对象。
void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }

    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

看删除就能看出来关联对象是如何管理的,这里复习一下:

  1. 有一个单例 AssociationsManager
  2. 通过 get() 获取了一个 AssociationsHashMap
  3. 这个 AssociationsHashMap 里面 K:V 是 disguise(obj)ObjectAssociation
  4. ObjectAssociation:结构体存储着:policyvalue

3. clearDeallocationg(),处理weak

继续前面的流程,完成2.1 2.2 之后 调用了 objc-object.h里面的 clearDeallocating方法:

// objc-object.h
inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

最后这里才去 side table 里面去处理指针和弱引用问题。

注意 isa.nonpointer 分开两个逻辑。

通过这个对象取出 Side Table,处理里面的 weakTable,调用 weak_clear_no_lock(&table.weak_table, (id)this)table.refcnts.erase(it)完成清除。

将所有weak引用指nil,就是在这里实现的。

最后有一个debug的断言,专门写一个函数来做debug的断言,这个也是非常谨慎的体现,粘出来纪念一下Cook做饭的谨慎:

// NSObject.mm
#if DEBUG
//DEBUG 模式下才启用 :用于  断言 side table 中不存在对象。
bool
objc_object::sidetable_present()
{
    bool result = false;
    SideTable& table = SideTables()[this];

    table.lock();

    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) result = true;

    if (weak_is_registered_no_lock(&table.weak_table, (id)this)) result = true;

    table.unlock();

    return result;
}
#endif

4. free(obj)

最后不管对象付不复杂,都会调用 free(obj)

Free 的实现在malloc的源码里面:

5. 父类的 Dealloc 呢?

至今看源码看不到任何调用父类dealloc 的地方,这个网上搜了一下,发现结果在这里!

http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html

clang 在 ARC 的 dealloc 结束的时候插入了如雷的dealloc 的调用。

通过 forward 给 superclass 调用 dealloc,才实现的[super dealloc]操作。

这个时候上面有一个隐藏的问题也就解释了,其实 msgSend 来调用 dealloc 方法,会先调用 Fruit 的 dealloc (如果有的话),然后因为ClangCodeGen的插手,才有的调用父类一直到根类的 dealloc。

所有dealloc不需要我们手动写 super dealloc。

6 总结图

最后放上来一张 dealloc 图,好久好久之前画的。

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