iOS Runtime 一: isa的内部结构与初始化
下载Apple的源码
在macOS下面有objc4. 每当系统更新,这些源码都会更新,源码版本和系统版本以及Xcode不适配会编译出错.
在线Apple源码
一.isa_t 的定义
isa_t定义在objc-private.h中
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
首先这是个C的共用体,共用体可以定义很多个成员,但是同时只能表达一种数据类型,
所占内存空间等于其中需要内存空间最大的成员.
这个共用体有两个构造函数:
isa_t() { } 和 isa_t(uintptr_t value) : bits(value) { }
isa_t(uintptr_t value) : bits(value) { }是C++构造函数和初始化的组合写法,后面是给成员bits赋值,参数就是value
然后它有三个成员,bits,cls和一个结构体struct
bits和构体struct是等价的,取出来就是一个64位/32位二进制数;
Class cls;是一个别名
typedef struct objc_class *Class;
而objc_class继承自objc_object,所以Class是一个结构体指针,在64位机器上是8个字节64位,在32机器上是4个字节32位;
结构体struct是匿名的,可以认为它的名称和共同体一样, 定义类似这样:
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
位域
在结构体成员后面加上冒号和数字,数字是声明该成员所占的位数.结构体的成员在内存分布是连续的,所以这些成员就占据着不同的区段表示着不同的含义.
这里面全是uintptr_t类型的成员,intptr_t其实就是整形,uintptr_t是无符号整形,但是它没有固定的内存长度,冒号右边是声明的长度,
比如nonpointer声明了一位,shiftcls声明了44位,上面那个结构体总共占用64位,不同的区段,对应结构体不同的成员.同样下面那个加起来是32位.
并且64位的ISA_BITFIELD在arm64和x86_64还不一样.
不同架构的isa_t的struct
不同环境的isa_t定义的struct不同,一共有四种,不同的定义成员不同,实现也不同,看源码的时候相当折磨
首先插一嘴如何查看当前运行环境的cpu架构,lldb打印TARGET_CPU_XXX,会给出补全,如果是这个环境,会输出1.
(lldb) po TARGET_CPU_X86_64
1
(lldb) po TARGET_CPU_ARM64
<nil>
1.这段是arm64e下的定义,A12芯片以上是arm64e架构,ptrauth_calls是这种架构才支持的编译器选项.
相对于arm64相对于arm64增加了一系列功能,其中最重要的一个是:指针验签
并且注释中说arm64的模拟器也使用这种定义,是指M系芯片mac上的模拟器,当然前提是使用arm64环境运行,而不是x86_64.
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
2.这段是arm64其他环境的定义,从iPhone5s开始到iPhone8
else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
3.这段是x86_64环境的定义,后面的内容都使用这个架构来学习的.
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
4.这段是某几种特殊环境下的定义,ARM_ARCH_7K >= 2 是指watch的几种架构,arm64 是arm64架构,LP64是64位,
就是说如果是某几种watch或者32位的arm64即armv7k or arm64_32时SUPPORT_INDEXED_ISA为1,否则为0.
当为1的时候,runtime维护一个类表,isa的结构体会有一个indexcls,它表示在类在类表中的偏移.
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
# if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
// armv7k or arm64_32
# define ISA_INDEX_IS_NPI_BIT 0
# define ISA_INDEX_IS_NPI_MASK 0x00000001
# define ISA_INDEX_MASK 0x0001FFFC
# define ISA_INDEX_SHIFT 2
# define ISA_INDEX_BITS 15
# define ISA_INDEX_COUNT (1 << ISA_INDEX_BITS)
# define ISA_INDEX_MAGIC_MASK 0x001E0001
# define ISA_INDEX_MAGIC_VALUE 0x001C0001
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t indexcls : 15; \
uintptr_t magic : 4; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 7
# define RC_ONE (1ULL<<25)
# define RC_HALF (1ULL<<6)
struct成员的具体功能
nonpointer: 1表示isa优化,0表示单纯指针
has_assoc: 有没有关联对象,1表示有
has_cxx_dtor: 有没有C++析构函数,1表示有
indexcls: 在类表中的偏移
shiftcls: 类的信息
magic: 用于标记是否初始化
weakly_referenced: 表示是否被别的对象弱引用
extra_rc: 对象的引用计数
nonpointer
nonpointer意思是非指针,指针的值就是一个地址,结构体是64位,指针也是64位,直接就填充满了.
当nonpointer为1时,表示它不是一个指针,此时结构体成员的含义生效.
当在SUPPORT_INDEXED_ISA = 1的情况时,isa_t的cls会被赋值,此时它表示一个指针,nonpointer为0.
但是isa并非只能在指针和非指针之间作出选择,指针的值也可以变向存在isa结构体,即便nonpointer是1,它也能取出一个指针.下面会详细讲到.
二.init isa
了解struct其他成员的作用,需要先了解isa_t的初始化.
只有一个数据类型里面使用了isa_t类型的成员,那就是objc_object,也定义在objc-private.h中
//objc4-818版本
struct objc_object {
private:
isa_t isa;
public:
// initIsa() should be used to init the isa of new objects only.
// If this object already has an isa, use changeIsa() for correctness.
// initInstanceIsa(): objects with no custom RR/AWZ
// initClassIsa(): class objects
// initProtocolIsa(): protocol objects
// initIsa(): other objects
void initIsa(Class cls /*nonpointer=false*/);
void initClassIsa(Class cls /*nonpointer=maybe*/);
void initProtocolIsa(Class cls /*nonpointer=maybe*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
//...
//objc4-841版本
private:
char isa_storage[sizeof(isa_t)];
isa_t &isa() { return *reinterpret_cast<isa_t *>(isa_storage); }
const isa_t &isa() const { return *reinterpret_cast<const isa_t *>(isa_storage); }
//...
objc_object只有一个成员变量isa.有很多函数,重点看isa的初始化相关的.
isa的初始化函数有这么四个,initIsa()用于对象,initClassIsa()用于类对象,initProtocolIsa()用于协议,initInstanceIsa()用于实例,
这里面提到了custom RR和custom AWZ, custom RR指的是custom retain/release methods,custom AWZ,指的是custom allocWithZone methods.
实际上这几个方法里面的实现都指向了下面这个友元函数
#if !SUPPORT_INDEXED_ISA && !ISA_HAS_CXX_DTOR_BIT
#define UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT __attribute__((unused))
#else
#define UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT
#endif
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
这个就是初始化isa_t的真正函数,首先这里有一个宏定义UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT,
SUPPORT_INDEXED_ISA已上面已经说了,
ISA_HAS_CXX_DTOR_BIT在arm64模拟器或者arm64e的情况下是0,其他都是1,
意思是在一些这种情况下添加了编译指令attribute((unused)),作用是不传hasCxxDtor这个参数也不会报警告,因为这种环境下定义的isa_t的结构体里面没有has_cxx_dtor成员.
接下来是一个断言,ASSERT(!isTaggedPointer()); 如果是tagged Pointer object就会触发,说明tagged Pointer不用这个函数.
在接下来初始化 isa_t newisa(0); bits是0,也就是初始化成64个0;
之后是
if (!nonpointer) {
newisa.setClass(cls, this);
如果是一个纯指针,那么直接给cls赋值.稍后再看setClass方法.
再往下如果是SUPPORT_INDEXED_ISA,类的信息由 newisa.indexcls = (uintptr_t)cls->classArrayIndex();来设置;
如果是非SUPPORT_INDEXED_ISA,类的信息也是setClass.
最后把引用计数设置为1.
另外还设置了bits = ISA_MAGIC_VALUE,除了这个,还有另外两个有特定值的宏需要关注.
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
setClass
indexcls就不看了,重点是setClass
// Set the class field in an isa. Takes both the class to set and
// a pointer to the object where the isa will ultimately be used.
// This is necessary to get the pointer signing right.
//
// Note: this method does not support setting an indexed isa. When
// indexed isas are in use, it can only be used to set the class of a
// raw isa.
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
// Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
// No signing, just use the raw pointer.
uintptr_t signedCls = (uintptr_t)newCls;
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
// We're only signing Swift classes. Non-Swift classes just use
// the raw pointer
uintptr_t signedCls = (uintptr_t)newCls;
if (newCls->isSwiftStable())
signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
// We're signing everything
uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# else
# error Unknown isa signing mode.
# endif
shiftcls_and_sig = signedCls >> 3;
#elif SUPPORT_INDEXED_ISA
// Indexed isa only uses this method to set a raw pointer class.
// Setting an indexed class is handled separately.
cls = newCls;
#else // Nonpointer isa, no ptrauth
shiftcls = (uintptr_t)newCls >> 3;
#endif
}
最上面注释中指出: 在isa中设置类的信息,需要设置类和指向最终将使用isa的对象的指针.
这个方法传了两个参数,一个是class指针,一个是对象指针; objc_object在初始化isa的时候是newisa.setClass(cls, this),传的是自己.
前面说了,SUPPORT_INDEXED_ISA用indexcls设置类,不走这里,所以这里对应了另外三种环境.
除此之外arm64e还需要区分三种签名模式,NONE,ONLY_SWIFT和ALL.实质还是不同的环境有不同的值,签名就是指针验签
这段代码生成了一个signedCls,如果需要签名,就用签名算法,并且签名算法需要对象指针作为参数.
如果不需要签名,signedCls就直接取class的地址,
然后signedCls右移三位赋值给shiftcls_and_sig,如果是x86_64就赋值给shiftcls.
isa如何存储类的信息
class指针是64位,右移3位不还是有61位吗,shiftcls只有三十多到五十多位,怎么放得下.
实际上64位虚拟地址范围极大,类对象的地址,64位二进制,其实有效位数只有后面三十多位,前面全是0.
这篇文章讲了最初引入64位时的一些变化
Although pointers are 64 bits, not all of those bits are really used. Mac OS X on x86-64, for example, only uses 47 bits of a pointer. iOS on ARM64 uses even less, with only 33 bits of a pointer currently being used. As long as the extra bits are masked off before the pointer is used, they can be used to store other data. This leads to one of the most significant internal changes in the Objective-C runtime in the language's history.
虽然指针是64位,但并非所有这些位都被真正使用。例如,x86-64上的Mac OS X仅使用47位指针。ARM64上的iOS使用更少,目前只使用33位指针。只要在使用指针之前屏蔽掉额外的位,它们就可以用来存储其他数据。这导致了语言历史上Objective-C运行时最重要的内部变化之一.
不过这文章也有很老了,具体多少位也看环境,不过可以验证一下这个说法.
创建一个Command line tool的project
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "MyObjc.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObjc *my = [[MyObjc alloc]init];
NSNumber *num = @2;
MyObjc *my2 = [[MyObjc alloc]init];
NSLog(@"Hello, World!");
}
return 0;
}
断点在log,具体内容用lldb查看.
(lldb) po TARGET_CPU_X86_64
1
(lldb) p/t MyObjc.class
(Class) $0 = 0b 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000 1000 0000 1110 1000 MyObjc
(lldb) p/t my->isa
(Class) $1 = 0b 0000 0001 0001 1101 1000 0000 0000 0001 0000 0000 0000 0000 1000 0000 1110 1001 MyObjc
(lldb) p/t my
(MyObjc *) $2 = 0b 0000 0000 0000 0000 0000 0000 0000 0001 0000 1000 1110 1001 1000 0001 0000 0000
首先要明确这里输出的是什么,p输出的是值,如果目标是一个指针,那么输出的就是指针的值,指针的值是一个地址.
而isa的值不一定是一个地址,或者说通常都不是一个地址.
但是他们都是64位,因为指针和isa_t都是8字节.
其次说明一下LSB,MSB,大端和小端
MSB,最高有效位,位于二进制数的最左侧,LSB,最低有效位,位于二进制数的最右侧.
大端表示将高位存放在低地址,小端表示将高位存放在高地址.
可以用共用体检查一下当前是大端还是小端
union {
int i;
char c;
}un;
un.i = 1;
if(un.c == 1) {
NSLog(@"小端");
}else {
NSLog(@"大端");
}
当前的x86_64环境输出是小端.
另外在isa_t的struct定义的地方注释说明nonpointer must be the LSB,nonpointer是struct的第一位.
所以右边第一个是第1位,对应地址的低位,但是对应bits的第一位,它就是nonpointer.
其实我们可以明确MyObjc.class和my都是指针,数一下都是33位,
my->isa它的最后一位,从内存上来说是最低位,对应nonpointer,是1,说明不是真实的指针;
然后可以看到MyObjc.class的第4位到33位,和my->isa是一样的,这部分就是shiftcls的一部分.
get class
既然把类的信息放进isa,就得能再取出来
inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
return cls;
#else
uintptr_t clsbits = bits;
# if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
// Most callers aren't security critical, so skip the
// authentication unless they ask for it. Message sending and
// cache filling are protected by the auth code in msgSend.
if (authenticated) {
// Mask off all bits besides the class pointer and signature.
clsbits &= ISA_MASK;
if (clsbits == 0)
return Nil;
clsbits = (uintptr_t)ptrauth_auth_data((void *)clsbits, ISA_SIGNING_KEY, ptrauth_blend_discriminator(this, ISA_SIGNING_DISCRIMINATOR));
} else {
// If not authenticating, strip using the precomputed class mask.
clsbits &= objc_debug_isa_class_mask;
}
# else
// If not authenticating, strip using the precomputed class mask.
clsbits &= objc_debug_isa_class_mask;
# endif
# else
clsbits &= ISA_MASK;
# endif
return (Class)clsbits;
#endif
}
结构和setclass一样,arm64e签名了的,在这里要解签,
关键代码就一行: clsbits &= ISA_MASK
clsbits就是isa_t,ISA_MASK是一个宏,定义在isa_t的结构体的位置,类似0x00007ffffffffff8ULL
clsbits和ISA_MASK按位与,就得到了类的地址.
可以验证一下
(lldb) p/x my->isa
(Class) $0 = 0x011d8001000080e9 MyObjc
(lldb) p/x MyObjc.class
(Class) $1 = 0x00000001000080e8 MyObjc
(lldb) p/x 0x011d8001000080e9 & 0x00007ffffffffff8
(long) $2 = 0x00000001000080e8
0x00007ffffffffff8是x86_64定义的ISA_MASK.
可以看到运算结果和真实地址是一样的.
这里有一点需要关注,掩码ISA_MASK的最后一位是8,也就是1000,
正好和前面setClass的时候,右移三位对上了,也就是说类的地址,最后一位一定是0或者8.