1、原有系统的问题
假设我们要存储一个NSNumber对象,其值是一个整数,正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的,而指针类型的大小通常通常也与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节。
所以,一个普通的iOS程序,如果没有Tagged Pointer(标记指针)对象,从32位机器迁移到64位机器中后,虽然逻辑上没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。
2、Tagged Pointer介绍
由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31 = 2147483648,另外一位作为符号位),对于绝大多数情况都是可以处理的!
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"numberFFFF pointer is %p", numberFFFF);
image.png
我们将NSNumber类型的指针在64位CPU下直接输出,除去末尾的2和开头的0xb其他的数字刚好表示响应NSNumber的值。猜测:末尾的2和最开头0xb就是Tagged Pointer的特殊标记!?
我们继续验证,尝试方一个8字节长的整数到NSNumber实例中,这样的实例,Tagged Pointer无法将其按上面的压缩方式来保存:
NSNumber *bigNumber = @(0xFFFFFFFFFFFFFFFF);
NSLog(@"bigNumber pointer is %p", bigNumber);
打印结果:bigNumber pointer is 0x600000029520
Tagged Pointer特点:
- Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate。
- Tagged Pointer指针的值不再是地址,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象"皮"的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free!
- 在内存读取上有着以前3倍的效率,创建时比以前快106倍!
结论:
- 当8个字节可以承载用于表示的数值时,系统会以Tagged Pointer的方式生成指针,如果8个字节承载不了时,则又用以前的方式生产普通的指针!!
- 引入Tagged Pointer,不但减少了64位机器下程序的内存占用,还提高了运行效率,完美地解决了小内存对象在存储和访问效率上的问题!!
3、注意事项和实现细节
3.1 isa指针
Tagged Pointer的引入也带来了问题,即Tagged Pointer并不是真正的对象,而是一个伪对象,所以你如果完全把它当做对象来使用,可能会出问题,比如:所有对象都有isa指针,而Tagged Pointer其实是没有的,因为它不是真正的对象,以为不是真正的对象,所以你如果直接访问Tagged Pointer的isa成员的话,在编译时会有警告:
obj->isa
我们应该尽量避免上述写法,应该换成相应的方法调用,如isKindOfClass和object_getClass。只要避免在代码中直接访问对象的isa就可以了!
3.2 引用计数
对于64位设备,苹果除了引入Tagged Pointer来优化小的对象外,对于普通的对象,其isa指针也进行了优化和调整!
在32位环境下,对象的引用计数都保存在一个外部的表中,每一个对象的Retain操作,实际上包括如下5个步骤:
- 获得全局的记录引用计数的hash表
- 为了线程安全,给该hash表枷锁
- 查找到目标对象的引用计数值
- 将该引用计数值加1,写回hash表
- 给该hash表解锁
从上面步骤来看,为了保证线程安全,对引用计数的增减操作都要先锁定这个表,这从性能上看是非常差的!
而在64位环境下,isa指针也是64位的,实际作为指针部分只用到了33位,剩余31位苹果使用了类似Tagged Pointer的概念,其中19位将保存对象的引用计数,这样对引用计数的操作只需要修改这个指针即可。只有当引用计数超出19位,才会将引用计数保存到外部表,而这种情况是很少,所以这样引用计数的更改销量会更高!
在64位环境下,新的retain操作包括如下5个步骤:
- 检查isa指针上面的标记位,看引用计数是否保存在isa变量中,如果不是,则使用以前的步骤,否则执行第2步
- 检查当前对象是否正在释放,如果是,则不做任何事情
- 增加该对象的引用计数,但是并不是马上写回到isa变量中
- 检查增加后的引用计数的值是否能够被19位表示,如果不是,则切换成以前的办法,否则执行第5步
- 进行一个原子的写操作,将isa的值写回
由于没有了全局的加锁操作,所以引用计数的更改更快了!
3.3 isa的bit位
image.pngunion isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_NONPOINTER_ISA
# if __arm64__
# define ISA_MASK 0x00000001fffffff8ULL
# define ISA_MAGIC_MASK 0x000003fe00000001ULL
# define ISA_MAGIC_VALUE 0x000001a400000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
uintptr_t magic : 9;
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)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 14;
# define RC_ONE (1ULL<<50)
# define RC_HALF (1ULL<<13)
};
# else
// Available bits in isa field are architecture-specific.
# error unknown architecture
# endif
// SUPPORT_NONPOINTER_ISA
#endif
};
SUPPORT_NONPOINTER_ISA 用于标记是否支持优化的 isa 指针,其字面含义意思是 isa 的内容不再是类的指针了,而是包含了更多信息,比如引用计数,析构状态,被其他 weak 变量引用情况。判断方法也是根据设备类型:
#if !__LP64__ || TARGET_OS_WIN32 || TARGET_IPHONE_SIMULATOR || __x86_64__
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
我们可以看到,模拟器也是不支持Tagged Pointer的!
参考:isa 指针 和 IMP 指针
网友评论