在分析isa与类关联的原理之前先了解clang
编译器
Clang
clang
是一个由Apple
主导编写,基于LLVM
的C/C++/OC
的编译器
主要是用于底层编译,将一些文件``输出成c++
文件,例如main.m
输出成main.cpp
,其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。
clang输出一下(clang -rewrite-objc main.m -o main.cpp)
也可以用一下指令
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
输出之后的.mm太长了, 截取重要的信息:
//NSObject
#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif
struct NSObject_IMPL {
Class isa;
};
//Person
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;//isa 继承至nsobject
NSString *_name;
};
// @property (nonatomic, copy) NSString *name;
/* @end */
// @implementation LGPerson
static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _name), (id)name, 0, 1); }
// @end
可以看出底层实现为结构体(struct)有一个属性name;
_I_LGPerson_name
和_I_LGPerson_setName_
为属性name
的set
和get
方法
objc_setProperty
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
再看
reallySetProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);//新值 retain
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot]; //原子性, 加锁
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);// 释放旧值
}
isa
根据源跟踪, 我们可以到这一步
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#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
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// 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的底层是isa_t
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
提供了两个成员,cls 和 bits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式
1. 通过cls初始化,bits无默认值
-2. 通过bits初始化,cls有默认值
还提供了一个结构体
定义的位域
,用于存储类信息
及其他信息
,结构体
的成员ISA_BITFIELD
,这是一个宏定义,有两个版本 __arm64__
(对应ios 移动端) 和 __x86_64__
(对应macOS),以下是它们的一些宏定义,如下图所示
-
nonpointer有两个值,表示自定义的类等,占1位
- 0:纯isa指针
- 1:不只是类对象地址,isa中包含了类信息、对象的引用计数等
-
has_assoc表示关联对象标志位,占1位
- 0:没有关联对象
- 1:存在关联对象
-
has_cxx_dtor 表示该对象是否有C++/OC的析构器(类似于dealloc),占1位
- 如果有析构函数,则需要做析构逻辑
- 如果没有,则可以更快的释放对象
-
shiftclx表示存储类的指针的值(类的地址), 即类信息
- arm64中占 33位,开启指针优化的情况下,在arm64架构中有33位用来存储类指针
- x86_64中占 44位
magic 用于调试器判断当前对象是真的对象 还是 没有初始化的空间,占6位
weakly_refrenced是 指对象是否被指向 或者 曾经指向一个ARC的弱变量
没有弱引用的对象可以更快释放deallocating 标志对象是是否正在释放内存
has_sidetable_rc表示 当对象引用计数大于10时,则需要借用该变量存储进位
extra_rc(额外的引用计数) --- 导尿管表示该对象的引用计数值,实际上是引用计数值减1
如果对象的引用计数为10,那么extra_rc为9
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# 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 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 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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 deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
调试initIsa 方法的时候犯了错误 😁
想查看newisa信息,
po newisa
什么信息都没有, 因这部分源码是c++, 并没有description方法, 不能用po查看
应该用 p newisa
magic = 59
?magic
是59
是由于将isa
指针地址转换为二进制
,从47
(因为前面有4个位域,共占用47位,地址是从0开始)位开始读取6
位,再转换为十进制
,如下图所示
isa 与 类 的关联
cls
与 isa
关联原理
就是isa
指针中的shiftcls
位域中存储了类信息
,其中initInstanceIsa
的过程是将 calloc
指针 和当前的 类cls
关联起来,有以下几种验证方式:
【方式一】通过
initIsa
方法中的newisa.shiftcls = (uintptr_t)cls >> 3;
验证【方式二】通过
isa指针地址
与ISA_MSAK
的值&
来验证【方式三】通过runtime的方法
object_getClass
验证【方式四】通过
位运算
验证
方式一:通过 initIsa 方法
-
运行至
newisa.shiftcls = (uintptr_t)cls >> 3;
前一步,其中shiftcls
存储当前类的值信息
此时查看
cls
,是LGPerson
类-
shiftcls
赋值的逻辑是将LGPerson
进行编码后,右移3
位
-
执行lldb命令
p (uintptr_t)cls
,结果为(uintptr_t) $2 = 4294975720
,再右移三位,有以下两种方式(任选其一),将得到536871965
存储到newisa
的shiftcls
中p (uintptr_t)cls >> 3
-
通过上一步的结果
$2
,执行lldb命令p $2 >> 3
-
继续执行程序到
isa = newisa;
部分,此时执行p newisa
与
bits赋值
结果的对比
,bits的位域
中有两处变化cls
由默认值
,变成了LGPerson
,将isa与cls完美关联
-
shiftcls
由0
变成了536871965
所以isa
中通过初始化
后的成员
的值变化过程
,如下图所示
为什么在shiftcls赋值时需要类型强转?
因为内存
的存储不能存储字符串
,机器码
只能识别 0 、1
这两种数字,所以需要将其转换为uintptr_t
数据类型,这样shiftcls
中存储的类信息
才能被机器码理解
, 其中uintptr_t
是long
为什么需要右移3位?
主要是由于shiftcls
处于isa
指针地址的中间
部分,前面还有3
个位域,为了不影响前面的3个位域
的数据,需要右移
将其抹零
。
方式二:通过 isa & ISA_MSAK
在方式一后,继续执行,回到
_class_createInstanceFromZone
方法,此时cls 与 isa已经关联完成
,执行po objc
执行
x/4gx obj
,得到isa
指针的地址0x001d8001000020e9
-
将
isa
指针地址 &ISA_MASK
(处于macOS
,使用x86_64
中的宏
定义),即po 0x001d8001000020e9 & 0x00007ffffffffff8
,得出LGPerson
arm64
中,ISA_MASK 宏定义的值为0x0000000ffffffff8ULL
-
x86_64
中,ISA_MASK 宏定义的值为0x00007ffffffffff8ULL
方式三:通过 object_getClass
通过查看object_getClass
的源码实现,同样可以验证isa与类关联的原理,有以下几步:
main中导入#import <objc/runtime.h>
通过
runtime
的api,即object_getClass
函数获取类信息