前言
对象的本质是什么? 我们经常听说对象的本质就是结构体,哪怎么去验证呢?我们经常听说类里面有isa ,哪 NONPOINTER_ISA 又是什么呢?带着疑问,我们用编译器编译OC代码看看,编译完成后的代码是否能找到一些线索。
编译器介绍
Clang 编译器
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。Clang 将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。Clang有Apple主导编写,基于LLVM的C/C++/Objective-c编译器。
Clang 的使用
clang -rewrite-objc main.m -o main.cpp 把目标文件编译成c++文件
如果是命令行工程则没有问题,顺利编译成功!
如果是iOS工程则会报错:
解决方案,就要指定SDK路径(iPhoneSimulator14.2.sdk 要修改为自己机器的对应版本)
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk main2.m -o main2.cpp
也可以用Xcode自带的命令xcrun编译:
#模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main2.m -o main.cpp
#真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
编译前的代码和编译后的代码对比
编译前的代码
@interface WJPerson : NSObject
@property (nonatomic, strong) NSString *sName;
@property (nonatomic, copy) NSString *cName;
@end
@implementation WJPerson
@end
编译后的代码
//省略上面配置代码
#ifndef _REWRITER_typedef_WJPerson
#define _REWRITER_typedef_WJPerson
typedef struct objc_object WJPerson;
typedef struct {} _objc_exc_WJPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_WJPerson$_sName;
extern "C" unsigned long OBJC_IVAR_$_WJPerson$_cName;
struct WJPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_sName;
NSString *_cName;
};
// @property (nonatomic, strong) NSString *sName;
// @property (nonatomic, copy) NSString *cName;
/* @end */
// @implementation WJPerson
static NSString * _I_WJPerson_sName(WJPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_sName)); }
static void _I_WJPerson_setSName_(WJPerson * self, SEL _cmd, NSString *sName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_sName)) = sName; }
static NSString * _I_WJPerson_cName(WJPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_cName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_WJPerson_setCName_(WJPerson * self, SEL _cmd, NSString *cName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct WJPerson, _cName), (id)cName, 0, 1); }
// @end
//省略...
分析过程
1、原本 WJPerson 继承 NSObject 变成了继承 objc_object
typedef struct objc_object WJPerson;
2、WJPerson 类变成了 WJPerson_IMPL 结构体,所以类的本质就是结构体, 申明的属性也帮忙我们自动生成了带下划线的成员变量。
struct WJPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_sName;
NSString *_cName;
};
// @property (nonatomic, strong) NSString *sName;
// @property (nonatomic, copy) NSString *cName;
3、WJPerson_IMPL 中的成员变量 struct NSObject_IMPL NSObject_IVARS; 我们并没申明,是继承下来的成员变量。在编译完成的代码中可以找到结构体 objc_object 和 NSObject_IMPL 里面有一个成员变量是 Class isa,
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
struct NSObject_IMPL {
Class isa;
};
4、WJPerson.m 我没有申明任何方法,但是编译完成后的代码多出4个方法,分别是 sName、cName的setter 、getter 函数实现。仔细看会发现 sName、cName实现方法不同,sName是通过内存平移去读写的,而cName 是通过 objc_setProperty 是去实现的。sName、cName都是属性,只是修饰的关键字不一样,sName 是 strong 修饰的, cName是 copy修饰的,所以我们可以得出修饰关键字会改变底层实现方式。
// @implementation WJPerson
static NSString * _I_WJPerson_sName(WJPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_sName)); }
static void _I_WJPerson_setSName_(WJPerson * self, SEL _cmd, NSString *sName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_sName)) = sName; }
static NSString * _I_WJPerson_cName(WJPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_cName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_WJPerson_setCName_(WJPerson * self, SEL _cmd, NSString *cName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct WJPerson, _cName), (id)cName, 0, 1); }
// @end
NONPOINTER_ISA
探索 NONPOINTER_ISA 之前我们先来了解一下 结构体、联合体、位域
结构体、位域、联合体之间的比较
结构体
struct Car1 {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
struct Car1 car1;
NSLog(@"%ld",sizeof(car1));
上面输出car1的内存大小是 4个字节,如果有不清楚结构体的内存大小怎么算,可以看我之前写的探索结构体内存对齐。
位域
// 互斥
struct Car2 {
BOOL front: 1; //后面的 1 代表用一个位
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
struct Car2 car2;
NSLog(@"%ld",sizeof(car2));
BOOL类型 不是 1 就是 0,所以用一个位就能满足需求,car2一共用了4个位,只就是半个字节,而最小单位是一个字节,所以上面输出car2的内存大小是 1个字节。比结构体省了3个字节。
联合体
// 结构体
struct Person1 {
char *name;
int age;
double height ;
};
// 联合体
union Person2 {
char *name;
int age;
double height ;
};
struct Person1 person1;
person1.name = "person1";
person1.age = 18;
person1.height = 180;
union Person2 person2;
person2.name = "person2";
person2.age = 18;
person2.height = 180;
NSLog(@"%ld-%ld",sizeof(person1),sizeof(person2));
//打印了:24-8
每次单步,person1 的属性都赋值成功了,且前面赋值的属性也没有变化。
(lldb) p person1
(Person1) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p person1
(Person1) $1 = (name = "person1", age = 0, height = 0)
(lldb) p person1
(Person1) $2 = (name = "person1", age = 18, height = 0)
(lldb) p person1
(Person1) $3 = (name = "person1", age = 18, height = 180)
(lldb)
每次单步,person2 的属性都赋值成功了,但是其他属性要么是脏数据、要么是空,只有当前赋值的属性是正常的值。
(lldb) p person2
(Person2) $4 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p person2
(Person2) $5 = (name = "person2", age = 15952, height = 2.1220036723004548E-314)
(lldb) p person2
(Person2) $6 = (name = "", age = 18, height = 2.1219957998584539E-314)
(lldb) p person2
(Person2) $7 = (name = "", age = 0, height = 180)
(lldb)
所以我们可以得出一个结论,
联合体是互斥的,共用了一个内存地址,会影响到其他成员变量结构体是共存的,成员变量是各占各的内存,互补影响- 位域(
bit field),C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,利用位域(bit field)能够用较少的位数存储数据,苹果就是利用位域和联合体去优化类的内存
NONPOINTER_ISA
ISA分为纯的ISA和NONPOINTER_ISA,NONPOINTER_ISA除了包含一个纯指针外,还包含了类的一些信息。
在前面iOS源码探索之alloc一文中,最终会通过obj->initIsa(cls)将从堆申请的结构体指针和当前的class绑定在一起。
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
//省略部分代码
}
//共用体
union isa_t {
//构造方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//变量
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// 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);
};
ISA_BITFIELD在arm64及x86_64架构下的定义
# if __arm64__
// 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)
# 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
# elif __x86_64__
//macOS
# 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)
# else
# error unknown architecture for packed isa
# endif
-
nonpointer:表示是否对 isa 指针开启指针优化,0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等 -
has_assoc:关联对象标志位,0没有,1存在 -
has_cxx_dtor:该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 -
shiftcls:存储类指针的值。开启指针优化的情况下,在arm64架构中有 33 位用来存储类指针 -
-magic:用于调试器判断当前对象是真的对象还是没有初始化的空间 -
weakly_referenced:志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放 -
unused:是否未被使用 -
has_sidetable_rc:散列表,当对象引用技术大于10时,则需要借用该变量存储进位 -
extra_rc:当表示该对象的引用计数值,实际上是引用计数值减1, 例如,如果对象的引用计数为10,那么extra_rc为9。如果引用计数大于10, 则需要使用到下面的has_sidetable_rc