概述
当我们创建一个对象时:
SWHunter *hunter = [[SWHunter alloc] init];
上面这行代码在栈
上创建了hunter
指针,并在堆
上创建了一个SWHunter对象
。目前,iOS并不支持在栈
上创建对象。
iOS 内存分区
iOS的内存管理是基于虚拟内存的。虚拟内存能够让每一个进程都能够在逻辑上“独占”整个设备的内存。关于虚拟内存,可以参考这里。
iOS又将虚拟内存按照地址由低到高划分为如下五个区:
- 代码区: 存放APP二进制代码
- 常量区:存放程序中定义的各种常量, 包括字符串常量,各种被const修饰的常量
- 全局/静态区: 全局变量,静态变量就放在这里
- 堆区:在程序运行时调用
alloc
,copy
,mutablecopy
,new
会在堆上分配内存。堆内存需要程序员手动释放,这在ARC中是通过引用计数的形式表现的。堆分配地址不连续,但整体是地址从低到高地址分配 - 栈区:存放局部变量,当变量超出作用域时,内存会被系统自动释放。栈上的地址连续分配,在内存地址由高向低增长
在程序运行时,代码区,常量区以及全局静态区的大小是固定的,会变化的只有栈和堆的大小。而栈的内存是有操作系统自动释放的,我们平常说所的iOS内存引用计数,其实是就堆上的对象来说的。
下面,我们就来看一下,在runtime
中,是如何通过引用计数来管理内存的。
tagged pointer
首先,来想这么一个问题,在平常的编程中,我们使用的NSNumber对象来表示数字,最大会有多大?几万?几千万?甚至上亿?
我相信,对于绝大多数程序来说,用不到上亿的数字。同样,对于字符串类型,绝大多数时间,字符个数也在8个以内。
再想另一个方面,自2013年苹果推出iphone5s之后,iOS的寻址空间扩大到了64位。我们可以用63位来表示一个数字(一位做符号位),这是一个什么样的概念?231=2147483648,也达到了20多亿,而263这个数字,用到的概率基本为零。比如NSNumber *num=@10000
的话,在内存中则会留下很多无用的空位。这显然浪费了内存空间。
苹果当然也发现了这个问题,于是就引入了tagged pointer
。tagged pointer
是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。
在引入tagged pointer
之前,iOS对象的内存结构如下所示(摘自唐巧博客):
显然,本来4字节就可以表示的数值,现在却用了8字节,明显的内存浪费。而引入了tagged pointer
后, 其内存布局如下
可以看到,利用tagged pointer
后,“指针”又存储了对本身,也存储了和对象相关的标记。这时的tagged pointer里面存储的不是地址,而是一个数据集合。同时,其占用的内存空间也由16字节缩减为8字节。
我们可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:
- Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
- Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
- 在内存读取上有着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
中包含有cls
,bits
, struct
三个变量,它们的内存空间是重叠
的。在实际使用时,仅能够使用它们中的一种,你把它当做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 bits
和 struct
其实是一个成员,它们都占据64位内存空间,之前已经说过,联合类型的成员内存空间是重叠的。在这里,由于uintptr_t bits
和 struct
都是占据64位内存,因此它们的内存空间是完全重叠的。而你将这块64位内存当做是uintptr_t bits
还是 struct
,则完全是逻辑上的区分,在内存空间上,其实是一个东西。
即uintptr_t bits
和 struct
是一个东西的两种表现形式。
实际上在runtime中,任何对struct
的操作和获取某些值,如extra_rc
,实际上都是通过对uintptr_t bits
做位操作实现的。uintptr_t bits
和 struct
的关系可以看做,u