iOS关于64位指针做了些什么

2,175 阅读7分钟

ARM64

在iPhone5S,苹果就已经转向ARM64位处理器了。通常说处理器多少位,一般涉及到整数寄存器大小以及指针的宽度。而现代CPU这两个大小一般也是相等的。所以64位一般也就是说CPU有64位整数寄存器和64位宽的指针了。

不过我还不是很明白,为什么这两个大小要设计的一样大。寄存器个数和宽度翻番都是明显可以提升性能的。而翻倍的指针宽度好像只是浪费更多的内存了?虽然提高了地址空间,但真的是提高的太多了。32位CPU,可使用的地址空间有2^32,即4G个,对于每个存储单元1byte说,就是最大支持4GB的内存。可直到今年的iPhone11 Pro发布才刚超过这个大小。64位宽的指针,内存地址空间达到2^64个,也就是17,179,877,980G个地址,远远远远远远超过需要的地址空间。指针大小的翻倍,使程序运行需要不必要的更多内存。

所以,尽管指针有64 bit,但并不是所有的bit都真正使用了。Mac OS X 上用了47位,而手机iOS上更是只用了33位。那么64位这么有富余的指针地址,苹果都玩出来了什么花

//内存类似这么一个数组,地址从0到0X01 FFFFFFFF
//每个地址里是一个存储单元,一个存储单元是8位,一个byte
//所以33位的8GB = 8G * 1byte
memory = [
                0x00 00 00 00 00 00 00 00:[0,1,0,1,0,0,0,0],
                0x00 00 00 00 00 00 00 01:[0,1,0,1,0,0,0,0],
                0x00 00 00 00 00 00 00 02:[0,1,0,1,0,0,0,0],
                0x00 00 00 00 00 00 00 03:[0,1,0,1,0,0,0,0],
                0x00 00 00 00 00 00 00 04:[0,1,0,1,0,0,0,0],
                0x00 00 00 00 00 00 00 05:[0,1,0,1,0,0,0,0],
                0x00 00 00 00 00 00 00 06:[0,1,0,1,0,0,0,0],
                0x00 00 00 00 00 00 00 07:[0,1,0,1,0,0,0,0],
                0x00 00 00 00 00 00 00 08:[0,1,0,1,0,0,0,0],
                ...
                ...
                0x00 00 00 01 FF FF FF F9:[0,1,0,1,0,0,0,0],
                0x00 00 00 01 FF FF FF FA:[0,1,0,1,0,0,0,0],
                0x00 00 00 01 FF FF FF FB:[0,1,0,1,0,0,0,0],
                0x00 00 00 01 FF FF FF FC:[0,1,0,1,0,0,0,0],
                0x00 00 00 01 FF FF FF FD:[0,1,0,1,0,0,0,0],
                0x00 00 00 01 FF FF FF FE:[0,1,0,1,0,0,0,0],
                0x00 00 00 01 FF FF FF FF:[0,1,0,1,0,0,0,0]

]

内存地址空间大致分布如下,地址仅代表从低向高方法,栈地址使用由高向低,堆由低向高:

0x000000 00 00 00 00 00 保留
0x000000 00 00 00 F0 00 _Text
0x000000 00 00 0F 00 00 _Data
0x000000 00 0F 00 00 00 字符串常量
0x000000 00 0F 0F 00 00 未初始化数据
0x000000 00 0F F0 00 00 初始化数据
0x000000 00 0F FF 00 00 堆区
0x000000 00 FF 00 F0 00 栈区
0x000000 01 FF FF FF FF 内核区

isa

在Objective-C里,什么是对象?

typedef struct objc_object *id;

struct objc_object {
private:
    isa_t isa;
...
    void initInstanceIsa(Class cls, bool hasCxxDtor);//对象初始化
}

OC里用id表示ObjC对象,这里可以看到id指针就是一个指向含isa字段的结构体。isa在32位CPU上实际就是它的类结构体地址,类结构体里有父类地址以及方法地址等。

而在ARM64上,我们说了一个地址实际只用了33位,如果isa完整的保存这个64位地址0x00000001 A5FC91F0,首先高位的28位肯定是0,后面三位也肯定是0(因为对象是8位对齐的,CPU并不是任意地址都会直接访问,为了效率,只会访问被4整除的地址),显然每个对象里保存一样的31个0完全是浪费。

define ISA_MASK        0x0000000ffffffff8ULL
union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t indexed           : 1;//为0则是直接地址,1则是含有其他信息
        uintptr_t has_assoc         : 1;//1显示该对象objc_setAssociate对象
        uintptr_t has_cxx_dtor      : 1;//1为有c++销毁函数
        uintptr_t shiftcls          : 33; //类地址
        uintptr_t magic             : 6;//判断是否已初始化
        uintptr_t weakly_referenced : 1;//1为该对象有弱指针
        uintptr_t deallocating      : 1;//该对象正在释放
        uintptr_t has_sidetable_rc  : 1;//判断该对象引用计数是否过大
        uintptr_t extra_rc          : 19;//该对象额外的指针引用
    };
}

isa类型是isa_t,上面可以看到isa_t是个union联合体类型。联合体是指这里面的字段指的是同一段内存空间,只是以不同的字段看待不同理解而已。

我们可以看到33个bit长度的shiftcls,这就是类的真正地址。它为什么会在第4个位置呢?因为对象地址是8位对齐的,也就是最低三位地址肯定是0的,用低三个位存储特殊bit,要获取真实地址时,和低三位为0的ISA_MASK进行操作可以直接获得真实地址。

我们可以看到普通对象有一个isa,也就是8个字节,接下来会存储类属性值。那么一个NSNumber对象存一个数字123使要用多少空间呢?下面图可以看到至少要20个字节,因为对象实际是以16个字节为最小单位递增分配的,实际上至少是24个字节。

image

有点费空间,那么苹果对这个情况是怎么省着点用的呢

Tagged Pointer

苹果的回答是标记指针。我们先看使用标记指针的结果是多少,8个字节。

image

从上面这个地址我们可以看到,这明显不是一个正常的内存地址。28个高位不再是0了。是的,这就是标记指针,指针用本身表示了这个对象,也就不用在堆里分配空间创建对象了。

  //ObjC原生 Tagged Pointer支持的类,index偏移
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6, 

我们现在有一个值为1的NSNumber对象,打印地址得到0x91f75f31d1a0f417,63位为1表示是TaggedPointer,60到62位表示类型,0到59位是data

关于类对象

在ObjC里,类也是一种对象,也就是类对象。

typedef struct objc_class *Class;

struct objc_class : objc_object {
    // Class ISA;  //8byte
    Class superclass;  //8byte
    cache_t cache;      // 方法缓存,16byte
    uintptr_t bits;    //8byte,class_rw_t,方法、属性、协议等
}

struct cache_t {
    struct bucket_t *_buckets;
    uint32_t _mask; //分配用来缓存bucket的总数
    uint32_t _occupied; //目前实际占用的缓存bucket的个数。
}
struct bucket_t {
private:
    uintptr_t _key; 
    IMP _imp;
}

从上面的定义,就可以看出来,类对象继承自objc_object。另含有父类,方法缓存,及bit三个字段。总大小为40个字节。

NSLog(@"NSObject对象实际需要的内存大小: %zd", class_getInstanceSize(NSObject.class));//8
NSLog(@"NSObject对象实际分配的内存大小: %zd", malloc_size((__bridge const void *)(fatherClas)));//16

Class objectMeta = objc_getMetaClass("NSObject");
NSLog(@"Object类实际需要的内存大小: %zd", class_getInstanceSize(objectMeta));//40

类对象是它对应的Meta Class的实例,所以我们打印MetaClass的实例大小,可以获取类的大小,打印出来的也正是40。

参考资料

1.神经病院 Objective-C Runtime 入院第一天—— isa 和 Class

2.解读objc_msgSend

3.Mike Ash:Let's Build Tagged Pointers

4.Introduction to 64-Bit Transition Guide