iOS Objective-C 内存管理初探

iOS Objective-C 内存管理

[TOC]

在iOS开发中我们常说内存有五大区,那么都是哪五大区呢?在iOS中,内存主要分为:栈区堆区全局区(静态区)常量区以及代码区这五大区。其实还分为内核区保留区只不过这两个区域跟我们程序的运行关系不大。详细的内存五大区的介绍请看我的这篇文章iOS底层原理之内存五大区

以4GB内存举例,其内存结构如下

iOS Objective-C 内存管理初探_第1张图片
image

下面我们将对iOS中的Tagged PointerNonpointer_isaSideTablesMRCARC做相关介绍。

1. TaggedPointer

参考文档

对象在内存中是对齐的,它们的地址总是指针大小的整数倍,通常为16的倍数。对象指针是一个64位的整数,而为了对齐,一些位将永远是零。

Tagged Pointer利用了这一现状,它使对象指针中非零位有了特殊的含义。在苹果的64位Objective-C实现中,若对象指针的最低有效位为1(即奇数),则该指针为Tagged Pointer。这种指针不通过解引用isa来获取其所属类,而是通过接下来三位的一个类表的索引。该索引是用来查找所属类是采用Tagged Pointer的哪个类。剩下的60位则留给类来使用。

针对Objective-C中的小对象通常使用TaggedPointer来减少内存的占用。Objective-C中的小对象有NSNumberNSDate、长度很小的NSString等。一般这类对象不会存储特别多的信息,比如就是一个数字1,这样如果在栈区给其开辟一个指针指向堆区的数据就有些耗费内存了。下面我们就一起看看TaggedPointer这项技术是如何实现的。

1.1 引入 TaggedPointer

对于NSString在底层有三种分别是NSCFConstantStringNSTargetPointerStringNSCFString

对于底层到底是使用哪种类型的区分如下:

  1. 如果使用stringWithString方法或者@""无论字符串长短都是NSCFConstantString类型
  2. 如果使用stringWithFormat方法
    1. 一般不超过8,9个英文字母或阿拉伯数字的时候是NSTargetPointerString,有时候11个也会使用NSTargetPointerString,这就跟编码有关系了包含ASCII、六位编码或五位编码
    2. 如果超过这个长度或者字符串总含有中文或特殊符号一般就是NSCFString

以上的原理就是Apple有一个编码表,如果字符串适合TargetPointer就会生成一个NSTargetPointerString。具体的信息请看如下文章

Tagged Pointer Strings或者它的中午版本【译】采用Tagged Pointer的字符串

1.2 小对象的引用计数

首先我们来看一段代码:

- (void)test1 {
    for (int i = 0; i<10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.nameStr = [NSString stringWithFormat:@"test"];
             NSLog(@"%@",self.nameStr);
        });
    }
}
- (void)test2 {
    for (int i = 0; i<10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.nameStr = [NSString stringWithFormat:@"这是一个taggedPointer测试代码"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

以上两个方法,看似没什么差别,但是test2会引起崩溃,崩溃原因如下:

iOS Objective-C 内存管理初探_第2张图片
image

我们可以看到崩溃的原因出现在objc_release函数中,那为什么只有test2中的代码会引起崩溃呢?这两段代码的区别就是字符串长短不一样,test1中的字符串是NSTargetPointerStringtest2中的是NSCFString,为了一探究竟,我们打开objc4-779.1的代码,找到objc_release函数来看看:

__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}


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


__attribute__((aligned(16), flatten, noinline))
id
objc_autorelease(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->autorelease();
}

通过以上代码我们可以看到,无论是objc_release还是objc_retain甚至是objc_autoreleaseTaggedPointer的处理就是直接返回。

所以说对于小对象来说基本就是不会进行retainrelease,所以上面的示例程序中使用TaggedPointer的字符串就不会崩溃。

我们知道小对象并不会指向堆区的内存,所以在这里没有引用计数也就很好理解了。那么小对象是如何存储的呢?我们进一步分析。

1.3 Tagged Pointer存储分析

通过上面章节的介绍我们大概知道了Tagged Pointer技术是将值存储到地址上,所以对于小对象的指针,是指针加值的组合,那么这项技术是如何实现的呢?我们来到objc4-779.1来寻找一下:

1.3.1 从源码看实现

我可能通过搜索Tagged pointer找到如下注释:

/***********************************************************************
* Tagged pointer objects.
*
* Tagged pointer objects store the class and the object value in the 
* object pointer; the "pointer" does not actually point to anything.
* 
* Tagged pointer objects currently use this representation:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  tag index
* 60 bits  payload
* (MSB)
* The tag index defines the object's class. 
* The payload format is defined by the object's class.
*
* If the tag index is 0b111, the tagged pointer object uses an 
* "extended" representation, allowing more classes but with smaller payloads:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  0b111
*  8 bits  extended tag index
* 52 bits  payload
* (MSB)
*
* Some architectures reverse the MSB and LSB in these representations.
*
* This representation is subject to change. Representation-agnostic SPI is:
* objc-internal.h for class implementers.
* objc-gdb.h for debuggers.
**********************************************************************/

从注释中我们可以得到如下信息:

  1. Tagged pointer对象存储了类信息和对象实际的值,此时的指针不指向任何东西;
  2. Tagged pointer目前使用以下表示
    1. 使用最低位作为标记位,如果是标签指针对象就标记为1,如果是普通对象类型就标记为0;
    2. 接下来的三位是标签索引位
    3. 余下的60位作为有效负载位,标签索引位定义了标签对象代表的对象的真实类型,负载的格式由实际的类定义;
  3. 如果标签位是0b111,表示该对象使用了是被扩展的标签对象,这种扩展的方式可以允许更多的类使用标签对象来表示,同时负载的有效位数变小。此时:
    1. 1位还是标记位
    2. 紧接着三位是0b111
    3. 接下来8位是扩展标记位
    4. 余下的52位作为有效的负载位
  4. 并不是所有架构都如以上的位置分配一样,有些事颠倒了MSB和LSB的,比如iOS,就是通过使用高位作为标记位的。
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
    // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

1.3.2 TaggedPointer标签

在上面我们提到了三位标签以及八位扩展位,那么这些都表示什么呢?经过一番的探索我们找到如下代码:

// Tagged pointer layout and usage is subject to change on different OS versions.

// Tag indexes 0..<7 have a 60-bit payload.
// Tag index 7 is reserved.
// Tag indexes 8..<264 have a 52-bit payload.
// Tag index 264 is reserved.

#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,
    OBJC_TAG_NSColor           = 16,
    OBJC_TAG_UIColor           = 17,
    OBJC_TAG_CGColor           = 18,
    OBJC_TAG_NSIndexSet        = 19,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

从上面的定义中我们可以看到60-bit52-bit时的一些标签定义:

  • 当处于60-bit时使用三位最多表示8中,其中有我们常见的2(NSString)3(NSNumber)4(NSIndexPath)6(NSDate)
  • 当处于52-bit时也会有16(NSColor)17(UIColor)18(CGColor)19(NSIndexSet)这几个我们熟悉的
  • 后面我们还可以看到First60BitPayload = 0Last60BitPayload = 6说明60-bit时第一个是0,最后一个是6,First52BitPayload = 8Last52BitPayload = 263说明52-bit时第一个是8,最后一个是263
  • 最后我们还可以看到保留264。最大值264是由3位表示8个,8位表示256个,由于52-bit是从8作为第一个值的,所以最后可以取值到264。

1.3.3 initializeTaggedPointerObfuscator

当初我们在类加载的时候就提到了Tagged Pointer,下面我们来到objc4-779.1中的_read_images函数中,这里面会调用initializeTaggedPointerObfuscator函数,初始化TaggedPointer,下面我们就来到这个函数中一探究竟:

/***********************************************************************
* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
*
* The tagged pointer obfuscator is intended to make it more difficult
* for an attacker to construct a particular object as a tagged pointer,
* in the presence of a buffer overflow or other write control over some
* memory. The obfuscator is XORed with the tagged pointers when setting
* or retrieving payload values. They are filled with randomness on first
* use.
**********************************************************************/
static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}
  • 该函数的左右是用随机性初始化objc_debug_taggedpointer_obfuscator
  • 我们可以看到在MacOS10.14iOS12之前或者禁用TargetPointer的时候会将objc_debug_taggedpointer_obfuscator置为0
  • 其余的情况会生成一个随机数赋值给objc_debug_taggedpointer_obfuscator并与上_OBJC_TAG_MASK取反的值,其实这里就是一次混淆操作,目的是让攻击者在缓冲区溢出或对某些内存进行其他写控制的情况下,更难以构造一个特定对象作为标记指针

_OBJC_TAG_MASK

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL

可以说objc_debug_taggedpointer_obfuscator的值每次启动应用都是不一样的,就像该函数注释中说的一样:标记指针混淆器的目的是让攻击者在缓冲区溢出或对某些内存进行其他写控制的情况下,更难以构造一个特定对象作为标记指针。当设置或检索有效负载值时,混淆器会使用带标记的指针。它们在第一次使用时就充满了随机性。异或时也是根据不同架构从高位还是地位开始用作标记进行异或不同的_OBJC_TAG_MASK,那么初始化的这个objc_debug_taggedpointer_obfuscator是用来做什么的呢?

1.3.4 TaggedPointer 编码与解码

我们通过搜索objc_debug_taggedpointer_obfuscator可以找到它的应用点,其实它的应用点就是对TaggedPointer类型的指针做编码与解码操作,这里的编码与解码就是个异或操作,我们知道两次异或得到的就是其本身。实现代码如下:

extern uintptr_t objc_debug_taggedpointer_obfuscator;

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

1.3.5 _objc_makeTaggedPointer

那么我们是在何时进行编码的呢?我们通过搜索_objc_encodeTaggedPointer来到如下代码处:

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    // PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
    // They are reversed here for payload insertion.

    // ASSERT(_objc_taggedPointersEnabled());
    if (tag <= OBJC_TAG_Last60BitPayload) {
        // ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
        uintptr_t result =
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    } else {
        // ASSERT(tag >= OBJC_TAG_First52BitPayload);
        // ASSERT(tag <= OBJC_TAG_Last52BitPayload);
        // ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
        uintptr_t result =
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    }
}

函数分析:

  • 首先判断tag是否小于等于60bit时的最后一个值也就是6
    • 如果小于就是60bit中的值,进行一系列|和位移操作,详见代码吧,最后进行编码返回
    • 如果不是就是52bit中的值同样也是一系列|和位移操作后进行编码返回

以上位移和|的值在不同情况是不一样的,感兴趣的可以自己跟一下源码。同样还需要判断是否开启了TaggedPointer优化,因为有些情况是没有TaggedPointer优化的。

1.4 验证TaggedPointer的实现

说了这么多关于TaggedPointer的底层实现,下面我们来验证一番

1.4.1 准备条件

为了得到小对象的真实指针,我们需要对其地址进行解码操作,为了方便使用添加如下代码:

#import 

extern uintptr_t objc_debug_taggedpointer_obfuscator;

uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

1.4.2 验证NSString

测试代码:

    NSString *str1 = [NSString stringWithFormat:@"a"];
    NSString *str2 = [NSString stringWithFormat:@"b"];
    NSString *str3 = [NSString stringWithFormat:@"abcdefghi"];
    NSString *str4 = [NSString stringWithFormat:@"cdefghijklm"];
    

//    NSLog(@"%p-%@",str1,str1);
//    NSLog(@"%p-%@",str2,str2);
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str1));
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str3));
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str4));

打印结果:

iOS Objective-C 内存管理初探_第3张图片
image

通过打印结果我们可以看到:

  • 61aASCII码,62bASCII
  • 如果多个字符会用ASCII码依次进行存储的
  • 对于多个字符不能使用TaggedPointer优化的则开辟地址进行存储
  • 以上举例前四个都是支持TaggedPointer优化的,对于字符串的个数在上面的介绍中有所提及,Apple为了做到极致的优化采用压缩算法,根据字符编码和使用频率进行优化,目前最多也就11个字符,最终有一个字符集为:
eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX
  • 其中最后一位的1是表示字符的个数。
  • 0xa是标签,这里是16进制,转换成2进制为010也就是2,在上面我们介绍标签的时候2代表NSString

1.4.2 验证NSNumber

验证代码:

NSNumber *number1 = @1;
NSNumber *number2 = @1;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;
NSNumber *number5 = @-1;
    
NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number2));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number4));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number5));

打印结果:

image

通过打印结果我们可以看到:

  • 对于1和2都在第位上显示出来
  • 目前最后一位的作用还不得而知
  • 对于小数即浮点数不能通过TaggedPointer优化
  • 对于负数直接从最大数开始表示,也就是补码
  • 0xb是标签,这里是16进制,转换成2进制为011也就是3,在上面我们介绍标签的时候3代表NSNumber

PS:对于最后一位,虽然在上面的打印中我们并不知道它代表着什么,如果多打印几个类型就知道了,其中0表示char类型,1表示short类型,2表示整形,3表示长整型,4表示单精度类型,5表示双精度类型.

1.4.3 验证NSDate

验证代码:

NSDate *date1 = [NSDate dateWithTimeIntervalSince1970:-1];
NSDate *date2 = [NSDate dateWithTimeIntervalSince1970:0];
NSDate *date3 = [NSDate dateWithTimeIntervalSince1970:1];
NSDate *date4 = [NSDate dateWithTimeIntervalSince1970:0xd27e440000000 / 0x800000 + 0x20000000000000 / 0x1000000];
NSDate *date5 = [NSDate dateWithTimeInterval:2 sinceDate:date4];
NSDate *date6 = [NSDate date];
    
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"];
    
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date1],object_getClass(date1),_objc_decodeTaggedPointer_(date1));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date2],object_getClass(date2),_objc_decodeTaggedPointer_(date2));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date3],object_getClass(date3),_objc_decodeTaggedPointer_(date3));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date4],object_getClass(date4),_objc_decodeTaggedPointer_(date4));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date5],object_getClass(date5),_objc_decodeTaggedPointer_(date5));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date6],object_getClass(date6),_objc_decodeTaggedPointer_(date6));

打印结果:

iOS Objective-C 内存管理初探_第4张图片
image

通过打印结果我们可以看到:

  • 0xe是标签,这里是16进制,转换成2进制为110也就是6,在上面我们介绍标签的时候3代表NSDate
  • 在2001-01-01 00:00:00 这个特殊的时间点应该是一个坐标系原点,所有的时间都在这个时间点的基础上做相对偏移。具体如何体现比较复杂这里就不进一步分析了

1.5 小结

  • TaggedPointer是一种内存优化技术,对于小对象类型,譬如NSNumberNSDate短NSStringNSIndexPath
  • 对于这些小对象的指针不在是简单的指针地址,而是标签+值的存储方式,这样就不用开辟堆内存以节约内存,也就不远mallocfree
  • 可以直接读取数据,对于读取速度也是一种提升,在内存读取上有着3倍的效率,创建时比以前快106倍。
  • TaggedPointer不会retainrelease,减少了内存管理
  • 在iOS中TaggedPointer的64位地址的前四位代码类型,后四位对于不同类型有着不同的意义

2. NONPOINTER_ISA

既然谈到了NONPOINTER_ISA那还是先回顾一下iOS Objective-C isa吧。

回顾完之后我们可以知道NONPOINTER_ISA就是表示是否对 isa 指针开启指针优化,在isa上占一位,0代表纯isa指针,1不仅是类对象的的地址,isa中还包含了类信息,对象的引用计数等信息。

PS:这里本来打算在objc4-779.1中通过x/4gx打印一下对象的4段内存地址,看看isa的信息,结果发现在外部打印和内部真正操作的是不一样的地址,每当对象retain的时候会修改isa,前提是NONPOINTER_ISA。这里为什么不一样可能是为了保证对象初始化后展示给外界的地址信息一致吧,如果随意更改就不好了。

后面写了个iOS工程,使用真机运行,不断传递一个属性到下一个控制器中我发现:

  • 在真机中arm64也是占用8位存储,不知道是不新的isa修改了extra_rc的占位数量
  • 8位就是256,到了256就会将has_sidetable_rc标记为1,然后减少一半的存储,其他的存储到散列表中
  • 当在次增加引用计数还是优先增加在extra_rc
  • 然后我又换了模拟器,模拟器竟然看不到引用计数。。。。无论跳转多少次,都是000000000
  • 然后我又换了iOS12.1.1的iPadPro,发现extra_rc确实是占用19位,可能是iOS14以后修改了吧,暂时还看不到新的开源的objc源码,19位524288个引用计数,就没试,内存估计不够。。。

这里说一下,对于NONPOINTER_ISA

  • 会将一部分引用计数信息存储到extra_rc中,arm64占用19位,x86_64占用8位(iphone XS Max iOS 14.2 Xcode 12.2 也占有8位(不知道是不是修改了,下载了781.2看了看没修改)我又换了iOS12.1.1的iPadPro 发现确实是19位)
  • 表示该对象的引用计数值,实际上是引用计数值减 1,因为初始化对象时这里是0,获取引用计数会自动加1,当认为引用计数为0会销毁对象
  • 如果 extra_rc 不足以存储引用计数则会标记has_sidetable_rc为1,将引用计数存储到散列表中。

后面在objc4 818.2找到了这段代码,此时19位怎么来的就清楚了。

iOS Objective-C 内存管理初探_第5张图片
image

3.散列表

NONPOINTER_ISA中我们提到,当extra_rc存储不下引用计数的时候就会存储到散列中,下面我们就来看看散列表存储。散列表并不只是一张,从目前的源码中可以看到在iOS真机中的散列表是8张。

相对于数组和链表散列表的优缺点:

  1. 数组:优点在于读取,可以直接通过下标读取,但是增加和删除都要找位置后才能增删
  2. 链表:优点在于增删方便,直接修改指针即可,但是查找就比较麻烦了,需要从指定节点(一般都是头结点)开始遍历
  3. 散列表:散列也就是哈希,本质还是哈希表,这里实际用的是哈希链表,利用哈希值做下标方便查找,又有链表的特性方便增/删(retain/release),哈希表是苹果特别爱用的一种存储结构,在@synchronized中也是用的这种结构的表。(解决哈希冲突的一种方式->拉链法)

所以散列表就如下图所示的样子:

iOS Objective-C 内存管理初探_第6张图片
image

在后续的章节介绍中我们会通过代码中的使用继续说说散列表。

3.MRC & ARC

3.1 MRC & ARC简介

MRC 即手动内存管理,ARC 即自动内存管理

MRC:

  • MRC时代对象被创建时引用计数为1(默认加的1)
  • 每当被其他指针引用时需要调用retain方法使其引用计数+1
  • 反之当指针变量不在使用该对象时需要调用release方法来释放对象,没调用一次release会使引用计数-1
  • 当一个对象的引用计数为0时,系统就会销毁这个对象。

ARC:

  • ARC模式是WWDC2011,即iOS5开始引入的
  • ARC模式是LLVMRuntime的结果
  • ARC模式中禁止手动调用retainreleaseretainCountdealloc
  • ARC模式中新加了weakstrong属性关键字
  • ARC模式下不需要手动retainreleaseautorelease,编译器会在适当的地方插入以上方法的调用

3.2 源码分析

针对上面提到的一些方法,无论是MRC的手动编写调用还是ARC的自动插入调用,最终都会调用的,下面我们就来看看这些方法的具体作用,以及它们都是怎么实现的。

3.2.1 retainCount

我们首先来看看引用计数的统计,我们在以上的介绍中多次提到,对象初始化引用计数即为1,那么为什么为1呢?是在对象初始化的时候自动调用了retain吗?

我们可以来到alloc的源码中看看,alloc的流程图(objc4-756.2的流程,跟其他objc源码中的也是大同小异)如下:

iOS Objective-C 内存管理初探_第7张图片
image

其实在alloc的流程中重要的方法也就是_class_createInstanceFromZoneinitIsa两个方法,我们顺着流程并着重在这两个方法中探索,并没有发现对象在初始化的时候主动调用retain相关方法,那么所说的对象创建的引用计数就为1是怎么来的呢?所以我们就只能去retainCount源码中去看看了。

打印引用计数源码:

#import 

NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

打印结果:

iOS Objective-C 内存管理初探_第8张图片
image

retainCount的调用流程如下:

[NSObject retainCount]->_objc_rootRetainCount->objc_object::rootRetainCount

主要实现在objc_object::rootRetainCount中,其源码如下:


- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}

uintptr_t
_objc_rootRetainCount(id obj)
{
    ASSERT(obj);

    return obj->rootRetainCount();
}

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

打印isa结构:

iOS Objective-C 内存管理初探_第9张图片
image

我们可以看到在源码中是默认给引用计数加 1 的,下面我们断点打印一下isa,我们可以看到extra_rc的值是 0,所以对象初始的时候引用计数为1是系统默认加的,因为当引用计数为0就会销毁对象,不能一创建就被销毁释放,所以默认+1操作就是为了这个作用。

3.2.2 retain

下面我们来看看retain是如何增加引用计数的,首先我们还是来到objc4-779.1源码中查找retain。经过分析我们可以看到retain的调用流程如下:objc_retain->retain->rootRetain

objc_retain 源码:

id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

源码分析:

  • objc_retain函数就是一层简单的调用封装
  • 首先判断对象非空,如果空就直接返回
  • 然后判断是否是TaggedPointer,如果是也就直接返回,这个在TaggedPointer章节有详细介绍
  • 接下来就是调用retain函数进一步处理

retain 源码:

// Equivalent to calling [this retain], with shortcuts if there is no override
inline id 
objc_object::retain()
{
    ASSERT(!isTaggedPointer());

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

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}

源码分析:

  • 这里再次判断了一次TaggedPointer
  • 然后通过hasCustomRR方法检查类(包括其父类)中是否含有默认的方法
  • 如果有则通过消息发送调用自定义方法(一般都没有)
  • 如果没有则调用rootRetain进一步处理

rootRetain 源码:

ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false);
}

ALWAYS_INLINE bool 
objc_object::rootTryRetain()
{
    return rootRetain(true, false) ? true : false;
}

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

源码分析:

这里还包含一层快捷调用,两个参数全部传false,以及封装了一个rootTryRetain函数,尝试进行retain

  • rootRetain中首先还是判断了一次TaggedPointer,并初始化了几个变量
  • 这里是个do while循环,循环的判断是新旧isa的替换是否成功
  • 如果不是nonpointerisa
    • 如果是元类直接返回
    • 如果不是尝试tryRetain并且散列表处于锁定状态,则调用sidetable_unlock函数解锁散列表
    • 如果是尝试retain则调用sidetable_tryRetain函数尝试retainretain成功返回当前对象,不成功返回nil
    • 不是尝试reatin则调用sidetable_retain函数进行retain操作
  • 如果是尝试retain,并且当前对象正在析构deallocating
    • 清理isa的内存
    • 判断!tryRetain && sideTableLocked则调用sidetable_unlock解锁散列表,这里应该是调用不到的,进入该判断说明tryRetain,现在又判断!tryRetain,则不成立
    • 最后返回nil
  • 然后调用addc函数增加引用计数RC_ONE就是1左移45位和56位的区别,因为在arm64x86_64同架构下,引用计数extra_rc分别占用19位和8位,如果引用计数+1,正好分别左移45位和56位。addc是一层封装调用,内部实现在开源代码中并未找到,其实无非就是对isa联合体进行操作加上一个引用计数extra_rc++
  • 通过addc函数会取到一个carry的值,如果carry有值说明newisa.extra_rc++ overflowed溢出,存满了
    • 判断是否处理溢出,如果不处理则调用ClearExclusive函数清理isa,调用rootRetain_overflow返回,其实就包装了一层这次调用函数是传值处理溢出
    • 判断不是尝试retain,散列表没加锁,则调用sidetable_lock给散列表加锁
    • 加上锁后则标记散列表加锁,标记向散列表写入数据
    • extra_rc中的数据设置为满值的一半RC_HALF就是1左移7位和18位,也是对应arm64x86_64架构中extra_rc分别占用8位和19位。
  • 跳出循环后判断transcribeToSideTable,向散列表写入数据,则调用sidetable_addExtraRC_nolock函数向散列表中写入extra_rc满值的一半的引用计数
  • 判断!tryRetain && sideTableLocked)不是尝试,散列表加锁,后调用sidetable_unlock将桑列表解锁,以供其他增加引用计数的地方使用散列表
  • 返回当前对象

以上提到的函数源码:

sidetable_loc k& sidetable_unlock 源码:

void 
objc_object::sidetable_lock()
{
    SideTable& table = SideTables()[this];
    table.lock();
}

void 
objc_object::sidetable_unlock()
{
    SideTable& table = SideTables()[this];
    table.unlock();
}

void unlock() { slock.unlock(); }

spinlock_t slock;

以上就是散列表加锁和解锁的函数及其相关的代码,这里使用了spinlock_t,对于spinlock_t可以看我以前的iOS 中的锁(3)

sidetable_tryRetain 源码:

bool
objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    // NO SPINLOCK HERE
    // _objc_rootTryRetain() is called exclusively by _objc_loadWeak(), 
    // which already acquired the lock on our behalf.

    // fixme can't do this efficiently with os_lock_handoff_s
    // if (table.slock == 0) {
    //     _objc_fatal("Do not call -_tryRetain.");
    // }

    bool result = true;
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_RC_ONE);
    auto &refcnt = it.first->second;
    if (it.second) {
        // there was no entry
    } else if (refcnt & SIDE_TABLE_DEALLOCATING) {
        result = false;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        refcnt += SIDE_TABLE_RC_ONE;
    }
    
    return result;
}

// Inserts key,value pair into the map if the key isn't already in the map.
  // The value is constructed in-place if the key is not in the map, otherwise
  // it is not moved.
  template 
  std::pair try_emplace(KeyT &&Key, Ts &&... Args) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // Otherwise, insert the new element.
    TheBucket =
        InsertIntoBucket(TheBucket, std::move(Key), std::forward(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }
  • 简单来说就是调用try_emplace函数判断表中有没有存这个对象的引用计数,存了就继续增加引用计数,没有就在哈希表中新增一个进行存储。
  • 判断链表中有没有第二个值,有的话result依旧为true
  • 如果正在析构result为false
  • 如果不是固定的则引用计数+1

sidetable_retain 源码:

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

没啥说的,就是引用计数表中对引用计数+1,其中+1时会加锁和解锁,处理完返回当前对象。

addc 源码:

static ALWAYS_INLINE uintptr_t 
addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
{
    return __builtin_addcl(lhs, rhs, carryin, carryout);
}

这就是一层封装,后续的__builtin_addcl暂时找不到,也没继续探索。

rootRetain_overflow 源码:

NEVER_INLINE id 
objc_object::rootRetain_overflow(bool tryRetain)
{
    return rootRetain(tryRetain, true);
}

正如上文所说的,只是封装了一次。

sidetable_addExtraRC_nolock 源码:

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];

    size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}
  • 一些判断
  • 存储传入大小的引用计数到散列表中

关于散列表的扩展:

在上面探索时,我们可以找到散列表的定义如下:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

如何获得散列表呢?是通过SideTablesMap.get()函数

static objc::ExplicitInit> SideTablesMap;

static StripedMap& SideTables() {
    return SideTablesMap.get();
}

关于引用计数的散列表倒地有多少张,可以在StripedMap的源码定义中看到:

template
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];
    
    `
    `
    `
}

可以看到引用计数表在iOS真机和模拟器中有8张,else 64 张。


关于散列表的下标:

static unsigned int indexForPointer(const void *p) {
    uintptr_t addr = reinterpret_cast(p);
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

StripedMap类中我们找打了上面的函数,内部是通过哈希计算得到散列表中的下标的。


关于散列表的可以使用[]:


 public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast>(this)[p]; 
    }

通过以上函数我们可以考的这里是重载了操作符。

到此我们就基本对retain操作进行近乎完整的分析,下面我们以一幅retain流程图来总结一下。

iOS Objective-C 内存管理初探_第10张图片
retain 主要流程图

3.2.2 release

下面我们来看看release在底层的实现,release的主要流程是objc_release->release->rootRelease,这里的设计与retain的设计思想是一致的,源码如下:

void objc_release(id obj) { [obj release]; }


inline void
objc_object::release()
{
    ASSERT(!isTaggedPointer());

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

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


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

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

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return false;
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.        
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // Really deallocate.

    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

对于一些调用封装与retain一致,就不过多分析了,下面我们对rootRelease做详细的分析:

  • 首先还是判断TaggedPointer,如果是则返回false,并初始化一些局部变量
  • 进入retry部分,同样还是一个do while循环,循环的条件与retain相同,同样是新旧isa是否替换成功
  • do里面还是判断是否是nonpointerisa
    • 元类返回false
    • 散列表加锁就解锁
    • 调用sidetable_release函数直接处理散列表返回
  • 不是nonpointerisa调用subc函数对引用计数进行-1操作
  • 如果-1时出现了获取到的carry没有值则解锁散列表以供其他release进行操作
  • 如果获取到的carry有值则goto underflow
  • 进入underflow部分,首先判断has_sidetable_rc,如果为true说明存储用到了散列表
    • 同样是判断溢出操作,如果没有则调用包装了一层rootRelease_underflow进行处理
    • 如果散列表没加锁则进行加锁,跳转到retry,最终还会跳转到underflow部分
    • 接下来则尝试调用sidetable_subExtraRC_nolock函数从散列表中借用RC_HALF最大值一半的引用计数,相当于不够减了则向高位借位操作
    • 如果散列表借不来,说明都没有了,则进入dealloc操作,如果散列表中能借来,即大于0,则把借来的-1存储到newisa.extra_rc
    • 然后将新旧isa进行内联更新操作,如果操作成功,则解锁散列表,返回false,如果操作失败则做一些判断进行再次内联更新操作
    • 如果还是内联更新失败则调用sidetable_addExtraRC_nolock函数将借来的加回去,再goto retry
  • 以下就是散列表也没有了,则判断是否正在析构释放,
    • 如果是则解锁散列表,调用overrelease_error函数crash这就是多次release导致的崩溃
    • 如不没有正在析构释放,则标记为正在析构释放,将空isa与isa进行内联更新,也就是将isa置空,如果内联失败则goto retry
    • 如果内联更新成功则解锁散列表
    • 操作原子线程栅栏(我不知道这是干啥的)
    • 发送消息调用dealloc函数(按照此流程performDealloc传入时即为true
  • 最后返回true(析构释放销毁了返回true,正常减引用计数返回false

几处上面提到的源码:

rootRelease_underflow 源码:

NEVER_INLINE uintptr_t
objc_object::rootRelease_underflow(bool performDealloc)
{
    return rootRelease(performDealloc, true);
}

一层封装,跟retain也是一致的,没啥可说的


sidetable_release 源码:

// return uintptr_t instead of bool so that the various raw-isa 
// -release paths all return zero in eax
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
    auto &refcnt = it.first->second;
    if (it.second) {
        do_dealloc = true;
    } else if (refcnt < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        refcnt |= SIDE_TABLE_DEALLOCATING;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        refcnt -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return do_dealloc;
}

retain差不多,这里就不过多介绍了


sidetable_subExtraRC_nolock 源码:

size_t 
objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];

    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()  ||  it->second == 0) {
        // Side table retain count is zero. Can't borrow.
        return 0;
    }
    size_t oldRefcnt = it->second;

    // isa-side bits should not be set here
    ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT);
    ASSERT(oldRefcnt > newRefcnt);  // shouldn't underflow
    it->second = newRefcnt;
    return delta_rc;
}
  • 首先还是判断了一次nonpointerisa,并获取到散列表
  • 这里判断链表头尾相等或者第二个元素=0(存储个数的?),说明散列表中是空的了返回0
  • 如果不是则初始化oldRefcnt并且判断是否正在析构,是不是弱引用
  • 取出一半的引用计数,判断旧值是不是大于新取出来到,并将新值赋值给it->second
  • 最后返回delta_rc

至此我们对release也做了近乎完整的分析,同样总结一张流程图:

iOS Objective-C 内存管理初探_第11张图片
release 流程

3.2.2 dealloc

release流程中如果引用计数为0了就会走dealloc流程,下面我们就来看看dealloc在底层的实现,dealloc主要流程是:dealloc->_objc_rootDealloc->rootDealloc,源码如下:

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


void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}


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

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

相对于retainrelease来说dealloc代码就简单多了:

  • 首先还是判断了TaggedPointer
  • 然后判断是不是nonpointerisa并且
    • 没有弱引用
    • 没有关联对象
    • 没有c++析构函数
    • 引用计数表没有存储引用计数
    • 满足以上条件后还断言了一次是否存在副引用计数表
    • 然后free该对象
  • 如果不满以上任一条件则调用object_dispose进行处理

这里也就验证了一些我们以前提到过的,没有弱引用,没有关联对象,没有C++析构函数就会更快的释放对象的内存。

下面我们来看看object_dispose这个函数是怎么释放这些那些不满足条件的对象的。

object_dispose 源码:

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

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

通过源码我们可以看到:

  • 首先判断对象是否为空
  • 然后调用objc_destructInstance函数进行处理
  • 接着free对象并返回nil

那么我们接着探索objc_destructInstance

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

通过源码我们可以看到:

  • 首先还是对对象进行了非空判断
  • 这里面获取了是否有c++的析构和关联对象
  • 如果有则调用object_cxxDestruct函数处理C++析构
  • 调用_object_remove_assocations函数处理关联对象
  • 最后调用clearDeallocating函数进一步处理

下面我们就来看看c++析构函数是怎么处理的:

object_cxxDestruct 源码:

/***********************************************************************
* object_cxxDestruct.
* Call C++ destructors on obj, if any.
* Uses methodListLock and cacheUpdateLock. The caller must hold neither.
**********************************************************************/
void object_cxxDestruct(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    object_cxxDestructFromClass(obj, obj->ISA());
}

/***********************************************************************
* object_cxxDestructFromClass.
* Call C++ destructors on obj, starting with cls's 
*   dtor method (if any) followed by superclasses' dtors (if any), 
*   stopping at cls's dtor (if any).
* Uses methodListLock and cacheUpdateLock. The caller must hold neither.
**********************************************************************/
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; 
        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);
        }
    }
}

object_cxxDestruct函数中最终是调用object_cxxDestructFromClass进行处理的,在这个函数中:

  • 通过不断向父类去遍历,直到没有C++析构
  • 然后通过lookupMethodInClassAndLoadCache找到这个析构函数
  • 然后判断这个函数不是_objc_msgForward_impcache,就调用一下找到的函数

接下来我们看看关联对象是怎么处理的:

_object_remove_assocations 源码:

// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
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();
    }
}

通过源码我们可以看到:

  • 关联对象还是通过AssociationsManager去处理
  • 这里就是找到该对象的关联对象所在的关联表
  • 然后判断此关联对象是否是最后一个
  • 如果不是则进行一些处理
  • 最后遍历这个关联表调用releaseHeldValue函数释放关联对象

下面我们看看clearDeallocating内部都处理了什么?

clearDeallocating 源码:

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

通过源码我们可以知道:

  • 如果不是nonpointerisa则调用sidetable_clearDeallocating进行处理
  • 如果是并且有弱引用或者引用计数表存储了引用计数,则调用clearDeallocating_slow函数进行处理

sidetable_clearDeallocating 源码:

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    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);
    }
    table.unlock();
}

这里的源码是针对非nonpointerisa进行处理的:

  • 首先是拿到引用计数表,并加锁
  • 然后判断是否包含说引用,包含就处理弱引用
  • 最后清理该对象的引用计数
  • 最后解锁

clearDeallocating_slow 源码:

// Slow path of clearDeallocating() 
// for objects with nonpointer isa
// that were ever weakly referenced 
// or whose retain count ever overflowed to the side table.
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

这里是针对nonpointerisa的处理:

  • 首先还是做了一些判断
  • 然后获取到引用计数表
  • 如果有弱引用就清理该对象的弱引用
  • 如果使用了引用计数表就清理引用计数表
  • 最后解锁

关于弱引用的处理代码如下:

weak_clear_no_lock 源码:

/** 
 * Called by dealloc; nils out all weak pointers that point to the 
 * provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

通过以上代码我们可以知道:

  • 首先是根据弱引用id找到弱引用表
  • 然后获取到弱引用数量和弱引用链表
  • 遍历这个弱引用链表,将其引用置空
  • 最后将其移除出弱引用表

至此我们对dealloc做了比较详细的分析,联调其内部的一些函数也进行了相关的解读,下面我们通过一个流程图来总结一下:

iOS Objective-C 内存管理初探_第12张图片
image

你可能感兴趣的:(iOS Objective-C 内存管理初探)