iOS开发:retain、release、dealloc

一、Tagged Pointer细节探究

苹果为了提高执行效率和节省内存,引入了Tagged Pointer的概念,对于64位程序来说可以达到3倍的访问速度和100多倍的创建销毁的速度。支持Tagged Pointer的类型以某种方式创建后便是Tagged Pointer指针,这种特殊的指针包括了数据内容和附加信息,访问的时候可以通过指针地址解码获得。

objc源码中定义了全部的支持Tagged Pointer的类型,常用的类型摘录如下,如NSStringNSNumberNSIndexPathNSDateUIColorNSIndexSet等:

...// 60-bit payloads
OBJC_TAG_NSString          = 2, 
OBJC_TAG_NSNumber          = 3, 
OBJC_TAG_NSIndexPath       = 4, 
OBJC_TAG_NSManagedObjectID = 5, 
OBJC_TAG_NSDate            = 6,
...// 52-bit payloads
OBJC_TAG_UIColor           = 17,
OBJC_TAG_CGColor           = 18,
OBJC_TAG_NSIndexSet        = 19,
OBJC_TAG_NSMethodSignature = 20,

先来看如下代码的打印结果:

NSString *str1 = @"abcd";
NSString *str2 = [[NSString alloc] initWithString:str1];
NSString *str3 = [[NSString alloc] initWithFormat:@"%@", str1];
NSString *str4 = [[NSString alloc] initWithFormat:@"%@-%@-%@", str1,str1,str1];
NSLog(@"str1=%@, ptr=%p, class=%@;", str1, str1, [str1 class]);
NSLog(@"str2=%@, ptr=%p, class=%@;", str2, str2, [str2 class]);
NSLog(@"str3=%@, ptr=%p, class=%@;", str3, str3, [str3 class]);
NSLog(@"str4=%@, ptr=%p, class=%@;", str4, str4, [str4 class]);

打印结果:

str1=abcd, ptr=0x1020fc9d0, class=__NSCFConstantString;
str2=abcd, ptr=0x1020fc9d0, class=__NSCFConstantString;
str3=abcd, ptr=0xa1e53d6849de69de, class=NSTaggedPointerString;
str4=abcd-abcd-abcd, ptr=0x283195de0, class=__NSCFString;
  • 打印结果中str3的真实类型为NSTaggedPointerStringstr1str2的真实类型为__NSCFConstantStringstr4的真实类型为__NSCFString。通过打印superclass找到了他们之间的继承关系,其中NSTaggedPointerStringNSString的子类。
    NSString.png
  • 打印结果可以看出,是否支持Tagged Pointer跟创建的方式和初始化的内容长度等也有关系。

先从这里入口,如何判断一个对象是不是支持Tagged Pointer?

static inline bool  _objc_isTaggedPointer(const void * _Nullable ptr){
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

//而_OBJC_TAG_MASK的定义与架构平台相关,真机的_OBJC_TAG_MASK = (1UL<<63) ,也就是高1位是1就是Tagged Pointer指针。

#if __arm64__
#   define OBJC_SPLIT_TAGGED_POINTERS 1  //64位真机
#else
#   define OBJC_SPLIT_TAGGED_POINTERS 0
#endif

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)   //真机:指针最高位为1
#elif OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
# endif

为了避免开发人员直接从指针地址上直接获取到内容,我们直接通过指针取地址获取到的都是encode之后的,要想拿到真实的信息需要decode:

static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr){
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return (void *)ptr;
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
    return (void *)value;
}

static inline uintptr_t_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr){
    uintptr_t value = (uintptr_t)ptr;
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return value;
#endif
    return value ^ objc_debug_taggedpointer_obfuscator;
}

encode的时候通过objc_debug_taggedpointer_obfuscator与指针地址按位异或,decodeobjc_debug_taggedpointer_obfuscator 按位异或encode之后的值。而objc_debug_taggedpointer_obfuscator则是在应用启动时_read_images()->initializeTaggedPointerObfuscator()初始化的。在按位异或中相同为0,不同为1,如果objc_debug_taggedpointer_obfuscator的值为0encode/decode前后的值应该相同。DisableTaggedPointerObfuscation==YES的时候初始化的objc_debug_taggedpointer_obfuscator=0,可以在scheme的环境变量中配置OBJC_DISABLE_TAG_OBFUSCATION==YES之后在调试更方便。
设置环境变量操作如下:

设置环境变量.png

设置之后再次打印如上数据,其中str3的打印结果发生变化:str3=abcd, ptr=0x8000003231b130a2, class=NSTaggedPointerString;接下来按照官方说的方式去还原一下数据:
还原字符串过程.png

常用类型总结:

变量 地址 二进制地址 说明
[NSString stringWithFormat:@"abcd"] 0x8000003231b130a2 0B1-0000000000000000000000000110
0100011000110110001001100001-0100-010
最高位为1,最低3位2代表NSString类型,4-7位为4表示字符串长度
[NSString stringWithFormat:@"abcdefg"] 0xb3b332b231b130ba 0B1-0110011101100110011001010110
0100011000110110001001100001-0111-010
最高位为1,最低3位2代表NSString类型,4-7位为7表示字符串长度
[NSNumber numberWithChar:1] 0x8000000000000083 0B1-0000000000000000000000000000
0000000000000000000000000001-0000-011
最高位为1,最低3位3代表NSNumber类型,4-7位为0表示char型
[NSNumber numberWithShort:3]] 0x800000000000018b 0B1-0000000000000000000000000000
0000000000000000000000000011-0001-011
最高位为1,最低3位3代表NSNumber类型,4-7位为1表示short型
[NSNumber numberWithInt:7] 0x8000000000000393 0B1-0000000000000000000000000000
0000000000000000000000000111-0010-011
最高位为1,最低3位3代表NSNumber类型,4-7位为2表示int型
[NSNumber numberWithLong:52] 0x8000000000001a1b 0B1-0000000000000000000000000000
0000000000000000000000110100-0011-011
最高位为1,最低3位3代表NSNumber类型,4-7位为3表示long型
[NSIndexPath indexPathWithIndex:5] 0x8000000000002874 0B1-0000000000000000000000000000
0000000000000000000001010000-1110-100
最高位为1,最低3位4代表NSIndexPath类型
[NSDate date] 0x969db1df206a00a6 0B1-0010110100111011011000111011
1110010000001101010000000001-0100-110
最高位为1,最低3位6代表NSDate类型

采用Tagged Pointer存储的小对象,需要在类型、创建方式、内容长度等方面满足要求,简单老说就是数据内容、标识位和扩展信息需要在2^64位中能存储完整完整。我们在实际开发中不应该依赖这些细节,这些内容不同平台不一样,而且可能会经常改变。

二、retain/release的流程梳理

先看retain:


retain核心流程梳理.png

retain核心流程梳理如下:

  • 1.首次进入rootRetain(tryRetain, variant):参数tryRetain=falsevariant=FastOrMsgSend
  • 2.如果是isTaggedPointer,则直接return this;反之继续3.
  • 3.variant=FastOrMsgSend,执行objc_msgSend(this, @selector(retain)),继续4。
  • 4.二次进入rootRetain(tryRetain, variant):参数tryRetain=falsevariant=Fast
  • 5.如果isa.nonpointer==0,执行sidetable_retain():引用计数全部全部存储在sidetable中;直接根据当前对象找到存储该对象的table,然后找到原有的refcntStorage+=SIDE_TABLE_RC_ONE即可。
  • 6.如果isa.nonpointer==1,应用计数存储在isa.extra_rcsidetable中。引用计数+1会优先添加到isa.extra_rc上。如果存满了,则先保存一半RC_HALFextra_rc中,并标记has_sidetable_rc=true已使用引用计数表,处理完isa后更新isa的数据。再将另一半RC_HALF追加到sidetable中,保存到side_table的流程与2同。
  • 7.retain的最后返回this指针。

源码中release的核心流程objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)中,源码较多,这里梳理核心流程如下:
release核心流程梳理如下:

  • 1.首次进入rootRelease(performDealloc, variant):参数performDealloc=true, variant= FastOrMsgSend
  • 2.如果是isTaggedPointer,则直接return false;反之继续3
  • 3.variant=FastOrMsgSend,执行objc_msgSend)(this, @selector(release)),继续4。
  • 4.二次进入rootRelease(performDealloc, variant):参数performDealloc=true, variant= Fast
  • 5.如果isa.nonpointer==0,执行sidetable_release():引用计数全部存在sidetable中;根据当前的对象找到存储该对象引用计数的table,然后找到原有的refcnt -= SIDE_TABLE_RC_ONE;即可。满足dealloc条件的,继续执行dealloc流程。
  • 6.如果isa.nonpointer==1,应用计数存储在isa.extra_rcsidetable中。引用继续-1会先从isa.extra_rc上减。如果不够减了,会进入underflow流程7.
  • 7.如果该对象有has_sidetable_rc,执行rootRelease_underflow流程,三次进入rootRelease(performDealloc, variant):参数performDealloc=true, variant= Full
  • 8.执行auto borrow = sidetable_subExtraRC_nolock(RC_HALF);也就是问sidetableRC_HALF,返回借到的数量和剩余的数量。
  • 9.如果借到了则将借到的数量-1保存到isa.extrac_rc中。如果sidetable中剩余为0则标记isa.has_sidetable_rc=0,再存储新的isa.bits的数据。处理存储失败的情况。
  • 10.如果没有借到或者根本就没有再sidetable中存储则执行dealloc相关流程。

小结:如果是Tagged Pointer小对象,没有占用堆空间分配内存,无需引用计数的管理,小对象的释放随着栈空间的回收而释放。常规对象,先判断有没开启isa优化(isa.nonpointer==0),没有开启则对象的引用计数都存储在sidetable中,无论retain还是release都操作的是sidetable中的计数+1-1,如果是release,则当计数为0的时候执行dealloc操作。如果开启了isa优化(isa.nonpointer==1),则对象的引用计数存储在isa.extra_rc和sidetable中,优先操作isa.extra_rcretain操作isa.extra_rc++,当isa.extra_rc中存满255个后,就会分一半(1<<7)到sidetable中,并在isa中标记isa.has_sidetable_rc=1。而release操作isa.extra_rc--,当isa.extra_rc中不够减了,则会从sidetable中尝试借1<<7个,如果sidetable中被借了之后没有了会设置isa.has_sidetable_rc=0,借到的数据会加到isa.extra_rc中,方便后续使用。如果没有借到则执行dealloc操作。

三、dealloc流程梳理

先看看底层objc_object::rootDealloc函数:

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

    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc)){
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

从这里可以看出如果一个对象是isTaggedPointer,那么这里什么事情都不做。然后再判断是否有开启isa优化、是否没有被弱引用、是否没有关联对象、是否没有C++构造/析构函数、是否在sidetable中没有引用计数了,如果这些都是是,则直接调用底层free(this)回收内存;反之则调用object_dispose(this)

接着进入object_dispose函数:

id object_dispose(id obj){
    if (!obj) return nil;
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}

先执行objc_destructInstance(obj),再执行free(obj)。结合上下文推测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.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }
    return obj;
}

objc_destructInstance函数中,做了三个事情:

  • 1、如果有c++析构方法,则调用object_cxxDestruct(obj),内部继续调用object_cxxDestructFromClass()方法,最终会从子类到父类依次调用析构函数(如果存在的话),具体调用析构函数做什么,还需要进一步探究。
  • 2.如果有关联对象,则调用_object_remove_assocations(obj, true),将该对象相关的关联记录擦除,同时如果关联对象存在且引用计数策略是OBJC_ASSOCIATION_SETTER_RETAIN,则像改对象发送objc_release(_value)消息。
  • 3.调用obj->clearDeallocating()函数,进行弱引用对象、引用计数相关的处理:如果没有开启isa.nonpointer,则调用sidetable_clearDeallocating()。如果该对象被弱引用或者再sidetable中存储了引用计数则调用clearDeallocating_slow()
sidetable_clearDeallocating()和clearDeallocating_slow()做的事情是一致的,核心代码如下:
//3.1
void objc_object::sidetable_clearDeallocating(){
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
}
//3.2
NEVER_INLINE void objc_object::clearDeallocating_slow(){
    SideTable& table = SideTables()[this];
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
}
  • 4.weak_clear_no_lock进行弱引用关系的处理:相关细节标记在如下代码中
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) {
    objc_object *referent = (objc_object *)referent_id;  //当前对象referent
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);//从weak_table取出entry
    if (entry == nil) {
        return;//如果weak_table中没有,则直接return
    }

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

推荐阅读更多精彩内容