Objective-C runtime机制(5)——iOS 内存管理

本文深入探讨了Objective-C在iOS中的内存管理机制,包括内存分区、Tagged Pointer的原理与优化,以及isa指针的优化。Tagged Pointer是一种节省内存的机制,用于存储小对象如NSNumber和NSString。文章详细解释了Tagged Pointer如何存储数据,以及如何通过位操作获取和管理对象的引用计数。此外,还介绍了SideTable数据结构,它是全局引用计数表,用于管理对象的强引用和弱引用计数。
摘要由CSDN通过智能技术生成

概述

当我们创建一个对象时:

SWHunter *hunter = [[SWHunter alloc] init];

上面这行代码在上创建了hunter指针,并在上创建了一个SWHunter对象。目前,iOS并不支持在上创建对象。

iOS 内存分区

iOS的内存管理是基于虚拟内存的。虚拟内存能够让每一个进程都能够在逻辑上“独占”整个设备的内存。关于虚拟内存,可以参考这里。

iOS又将虚拟内存按照地址由低到高划分为如下五个区:

这里写图片描述

  • 代码区: 存放APP二进制代码
  • 常量区:存放程序中定义的各种常量, 包括字符串常量,各种被const修饰的常量
  • 全局/静态区: 全局变量,静态变量就放在这里
  • 堆区:在程序运行时调用alloccopymutablecopynew会在堆上分配内存。堆内存需要程序员手动释放,这在ARC中是通过引用计数的形式表现的。堆分配地址不连续,但整体是地址从低到高地址分配
  • 栈区:存放局部变量,当变量超出作用域时,内存会被系统自动释放。栈上的地址连续分配,在内存地址由高向低增长

在程序运行时,代码区,常量区以及全局静态区的大小是固定的,会变化的只有栈和堆的大小。而栈的内存是有操作系统自动释放的,我们平常说所的iOS内存引用计数,其实是就堆上的对象来说的。

下面,我们就来看一下,在runtime中,是如何通过引用计数来管理内存的。

tagged pointer

首先,来想这么一个问题,在平常的编程中,我们使用的NSNumber对象来表示数字,最大会有多大?几万?几千万?甚至上亿?

我相信,对于绝大多数程序来说,用不到上亿的数字。同样,对于字符串类型,绝大多数时间,字符个数也在8个以内。

再想另一个方面,自2013年苹果推出iphone5s之后,iOS的寻址空间扩大到了64位。我们可以用63位来表示一个数字(一位做符号位),这是一个什么样的概念?231=2147483648,也达到了20多亿,而263这个数字,用到的概率基本为零。比如NSNumber *num=@10000的话,在内存中则会留下很多无用的空位。这显然浪费了内存空间。

苹果当然也发现了这个问题,于是就引入了tagged pointertagged pointer是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息

在引入tagged pointer 之前,iOS对象的内存结构如下所示(摘自唐巧博客):

这里写图片描述

显然,本来4字节就可以表示的数值,现在却用了8字节,明显的内存浪费。而引入了tagged pointer 后, 其内存布局如下

这里写图片描述

可以看到,利用tagged pointer后,“指针”又存储了对本身,也存储了和对象相关的标记。这时的tagged pointer里面存储的不是地址,而是一个数据集合。同时,其占用的内存空间也由16字节缩减为8字节。

我们可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:

  1. Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
  2. Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
  3. 在内存读取上有着3倍的效率,创建时比以前快106倍。

运行如下代码:

    NSMutableString *mutableStr = [NSMutableString string];
    NSString *immutable = nil;
	#define _OBJC_TAG_MASK (1UL<<63)
    char c = 'a';
    do {
        [mutableStr appendFormat:@"%c", c++];
        immutable = [mutableStr copy];
        NSLog(@"%p %@ %@", immutable, immutable, immutable.class);
    }while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);

输出为:

这里写图片描述

我们看到,字符串由‘a’增长到‘abcdefghi’的过程中,其地址开头都是0xa 而结尾也很有规律,是1到9递增,正好对应着我们的字符串长度,同时,其输出的class类型为NSTaggedPointerString。在字符串长度在9个以内时,iOS其实使用了tagged pointer做了优化的。

直到字符串长度大于9,字符串才真正成为了__NSCFString类型。

我们回头分析一下上面的代码。
首先,iOS需要一个标志位来判断当前指针是真正的指针还是tagged pointer。这里有一个宏定义_OBJC_TAG_MASK (1UL<<63) ,它表明如果64位数据中,最高位是1的话,则表明当前是一个tagged pointer类型。

然后,既然使用了tagged pointer,那么就失去了iOS对象的数据结构,但是,系统还是需要有个标志位表明当前的tagged pointer 表示的是什么类型的对象。这个标志位,也是在最高4位来表示的。我们将0xa转换为二进制,得到
1010,其中最高位1xxx表明这是一个tagged pointer,而剩下的3位010,表示了这是一个NSString类型。010转换为十进制即为2。也就是说,标志位是2的tagger pointer表示这是一个NSString对象。

在runtime源码的objc-internal.h中,有关于标志位的定义如下:

{
    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, 
    OBJC_TAG_RESERVED_7        = 7, 

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

    OBJC_TAG_RESERVED_264      = 264
};

最后,让我们再尝试分析一下NSString类型的tagged pointer是如何实现的。

我们前面已经知道,在总共64位数据中,高4位被用于标志tagged pointer以及对象类型标识。低1位用于记录字符串字符个数,那么还剩下59位可以让我们表示数据内容。

对于字符串格式,怎么来表示内容呢?自然的,我们想到了ASCII码。对应ASCII码,a用16进制ASCII码表示为0x61,b为0x62, 依次类推。在字符串长度增加到8个之前,tagged pointer的内容如下。可以看到,从最低2位开始,分别为61,62,63… 这正对应了字符串中字符的ASCII码。

这里写图片描述

直到字符串增加到7个之上,我们仍然可以分辨出tagged pointer中的标志位以及字符串长度,但是中间的内容部分,却不符合ASCII的编码规范了。
这里写图片描述

这是因为,iOS对字符串使用了压缩算法,使得tagged pointer表示的字符串长度最大能够达到9个。关于具体的压缩算法,我们就不再讨论了。由于苹果内部会对实现逻辑作出修改,因此我们只要知道有tagged pointer 的概念就好了。有兴趣的同学可以看采用Tagged Pointer的字符串,但其内容也有些过时了,和我们的实验结果并不一致。

我们顺便看一下NSNumber的tagged pointer实现:

	NSNumber *number1 = @(0x1);
    NSNumber *number2 = @(0x20);
    NSNumber *number3 = @(0x3F);
    NSNumber *numberFFFF = @(0xFFFFFFFFFFEFE);
    NSNumber *maxNum = @(MAXFLOAT);
    NSLog(@"number1 pointer is %p class is %@", number1, number1.class);
    NSLog(@"number2 pointer is %p class is %@", number2, number2.class);
    NSLog(@"number3 pointer is %p class is %@", number3, number3.class);
    NSLog(@"numberffff pointer is %p class is %@", numberFFFF, numberFFFF.class);
    NSLog(@"maxNum pointer is %p class is %@", maxNum, maxNum.class);

这里写图片描述

可以看到,对于MAXFLOAT,系统无法进行优化,输出的是一个正常的NSNumber对象地址。而对于其他的number值,系统采用了tagged pointer,其‘地址’都是以0xb开头,转换为二进制就是1011, 首位1表示这是一个tagged pointer,而011转换为十进制是3,参考前面tagged pointer的类型枚举,这是一个NSNumber类型。接下来几位,就是以16进制表示的NSNumber的值,而对于最后一位,应该是一个标志位,具体作用,笔者也不是很清楚。

isa

由于一个tagged pointer所指向的并不是一个真正的OC对象,它其实是没有isa属性的。

在runtime中,可以这样获取isa的内容:

#define _OBJC_TAG_SLOT_SHIFT 60
#define _OBJC_TAG_EXT_SLOT_MASK 0xff

inline Class 
objc_object::getIsa() 
{
	// 如果不是tagged pointer,则返回ISA()
    if (!isTaggedPointer()) return ISA();

	// 如果是tagged pointer,取出高4位的内容,查找对应的class
    uintptr_t ptr = (uintptr_t)this;
	  
    uintptr_t slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    return objc_tag_classes[slot];
    
}

在runtime中,还有专用的方法用于判断指针是tagged pointer还是普通指针:

#   define _OBJC_TAG_MASK (1UL<<63)
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr) 
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

isa 指针(NONPOINTER_ISA)

对象的isa指针,用来表明对象所属的类类型。
但是如果isa指针仅表示类型的话,对内存显然也是一个极大的浪费。于是,就像tagged pointer一样,对于isa指针,苹果同样进行了优化。isa指针表示的内容变得更为丰富,除了表明对象属于哪个类之外,还附加了引用计数extra_rc,是否有被weak引用标志位weakly_referenced,是否有附加对象标志位has_assoc等信息。

这里,我们仅关注isa中和内存引用计数有关的extra_rc 以及相关内容。

首先,我们回顾一下isa指针是怎么在一个对象中存储的。下面是runtime相关的源码:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

typedef struct objc_class *Class;

// ============ 注意!从这一行开始,其定义就和在XCode中objc.h看到的定义不一致,我们需要阅读runtime的源码,才能看到其真实的定义!下面是简化版的定义:============
struct objc_class : objc_object {
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

 
struct objc_object {
private:
    isa_t isa;
}

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        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)
    };
}

结合下面的图,我们可以更清楚的了解runtime中对象和类的结构定义,显然,类也是一种对象,这就是类对象的含义。

这里写图片描述

从图中可以看出,我们所谓的isa指针,最后实际上落脚于isa_t联合类型联合类型 是C语言中的一种类型,简单来说,就是一种n选1的关系。比如isa_t 中包含有clsbitsstruct三个变量,它们的内存空间是重叠的。在实际使用时,仅能够使用它们中的一种,你把它当做cls,就不能当bits访问,你把它当bits,就不能用cls来访问。

联合的作用在于,用更少的空间,表示了更多的可能的类型,虽然这些类型是不能够共存的。

将注意力集中在isa_t联合上,我们该怎样理解它呢?

首先它有两个构造函数isa_t(), isa_t(uintptr_value), 这两个定义很清晰,无需多言。

然后它有三个数据成员Class cls, uintptr_t bits, struct 。 其中uintptr_t被定义为typedef unsigned long uintptr_t,占据64位内存。

关于上面三个成员, uintptr_t bitsstruct 其实是一个成员,它们都占据64位内存空间,之前已经说过,联合类型的成员内存空间是重叠的。在这里,由于uintptr_t bitsstruct 都是占据64位内存,因此它们的内存空间是完全重叠的。而你将这块64位内存当做是uintptr_t bits 还是 struct,则完全是逻辑上的区分,在内存空间上,其实是一个东西。
uintptr_t bitsstruct 是一个东西的两种表现形式。

实际上在runtime中,任何对struct 的操作和获取某些值,如extra_rc,实际上都是通过对uintptr_t bits 做位操作实现的。uintptr_t bitsstruct 的关系可以看做,u

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值