这是我参与更文挑战的第4天,活动详情查看: 更文挑战
对象的本质及拓展
什么是Clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。Clang是一个有Apple主导编写,基于LLVM的C/C++/Objective-C/Objective-C++编译器。它与GNU C语言规范几乎完全兼容,并在此基础上增加了额外的语法特性,比如C函数重载(通过__attribute__((overloadable))来修饰函数),其目标之一就是超越GCC。
Clang命令
接下来,我们来使用几个Clang命令
0、准备工作
新建一个类Person如下:
@interface Person : NSObject
@property (nonatomic, copy) NSString *nickName;
@end
@implementation Person
@end
1、clang命令
在Person.m文件所在目录下执行命令
clang -rewrite-objc Person.m -o Person.cpp 执行完毕之后,将会在目录下生成一个
Person.cpp文件
2、复杂的clang命令
我们再次使用上述命令,生成ViewController.m文件的cpp文件,结果如下:
错误显示,在ViewController.m文件中引入了UIKit库,那么单纯的Clang命令已经无法满足要求,此时我们需要如下命令
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.5 -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ ViewController.m 来生成
ViewController.m文件的cpp文件
3、xcrun命令
Xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了封装,更为好用
模拟器的xcrun命令
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
真机的xcrun命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64-iphoneos.cpp
用这两种xcrun命令都可生成.m文件对应的cpp文件
cpp文件
我们来研究一下
Person.m生成的cpp文件
对象在底层的本质
我们在cpp文件中查找nickName属性的位置,我们会定位到代码
#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif
extern "C" unsigned long OBJC_IVAR_$_Person$_nickName;
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString * _Nonnull _nickName;
};
我们可以发现类
Person的本质是一个结构体struct,此处是一个struct的伪继承,在C++中,结构体是可以继承的。
在OC层面,Person是继承自NSObject,而在下层, 他是一个objc_object类型的struct
我们继续搜索一下NSObject_IMPL,发现此处代码:
struct NSObject_IMPL {
Class isa;
};
我们发现,NSObject_IMPL就是Person父类NSObject中的isa:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
我们继续查看Class在底层的实现:
typedef struct objc_class *Class;
Class在底层是一个objc_class类型的结构体指针
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
我们常用的id,SEL等都是结构体指针
那么,nickName的getter和setter方法,在底层是什么情况呢:
// @implementation Person
static NSString * _Nonnull _I_Person_nickName(Person * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Person_setNickName_(Person * self, SEL _cmd, NSString * _Nonnull nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _nickName), (id)nickName, 0, 1); }
// @end
我们发现getter和setter方法都多了两个参数self和_cmd,这是方法的隐藏参数
联合体位域
结构体
我们看一个简单的结构体:
struct Car {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
struct Car car1;
NSLog(@"1--->%lu", sizeof(car1));
通过运行打印,我们发现这个结构体占用了4个字节的大小(每个BOOL占用一个字节)
0000 0000 0000 0000 0000 0000 0000 0000
在这样一个4字节,32位的空间内,我们仅仅只需要存放front,back,left,right四个方向值,极大的造成了空间的浪费,那么我们有没有办法优化空间呢,对我们来说4位大小的空间就已经能够满足我们四个方向的判断了,但是由于内存至少都是1字节的分配,那么我们有没有办法,让这个struct内存占用优化为1个字节呢?
接下来我们引入
位域的概念
位域
我们将上述结构体进行改造如下:
struct Car {
BOOL front: 1; // 规定front只占用1位
BOOL back: 1; // 规定back只占用1位
BOOL left: 1; // 规定left只占用1位
BOOL right: 1; // 规定right只占用1位
};
接下来,我们再来打印一下:
我们已经成功将原有的结构体4字节的占用内存,优化为了1个字节
0000 0000
这就是位域
但是,车子的四个方向不可能同时是都为真,他们中永远都只可能有一个方向是真,那么有没有方法,让他们中只有一个方向有值,其他方向都没有值呢?
接下来我们引入
联合体的概念
联合体
我们来看一个联合体
union Person {
char *name;
int age;
double height;
};
解析来,我们创建一个联合体,然后依次给成员赋值:
给name赋值:
age和height此时的内存是不被使用的,是脏内存,值也是脏数据
通过以上赋值流程,我们可以发现每一次给成员赋值,都会影响其他成员的值,这是因为,联合体的成员地址相同,占用了同一块内存,同一时间只有一个成员可被使用。
案例
我们来看一个案例:
Car.h文件
@interface Car : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
@end
Car.m文件
#define LGDirectionFrontMask (1 << 0)
#define LGDirectionBackMask (1 << 1)
#define LGDirectionLeftMask (1 << 2)
#define LGDirectionRightMask (1 << 3)
@interface Car (){
// 联合体
union {
char bits;
// 位域
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
}
@end
@implementation Car
- (instancetype)init {
self = [super init];
if (self) {
_direction.bits = 0b0000000000; // 二进制默认初始化
}
return self;
}
- (void)setFront:(BOOL)isFront {
if (isFront) {
_direction.bits |= LGDirectionFrontMask;
} else {
_direction.bits |= ~LGDirectionFrontMask;
}
NSLog(@"%s",__func__);
}
- (BOOL)isFront{
return _direction.front;
}
- (void)setBack:(BOOL)isBack {
_direction.back = isBack;
NSLog(@"%s",__func__);
}
- (BOOL)isBack{
return _direction.back;
}
可以针对运行结果更好的理解联合体:
结构体和联合体总结
- 结构体
struct中所有变量是共存的,可以理解为:有容乃大;缺点:是struct内存空间的分配是粗放的,不管用不用,全分配。 - 联合体
union中各变量是互斥的,不够包容;优点是union内存使用更为精细灵活,也节省了内存空间。
nonPointerIsa的分析
什么是nonPointerIsa?
我们经常使用的类,它的对象指针占用8字节,8字节等于64位,如果在这64位大小的空间之存放指针的话,那其实是很大的浪费,为了更好的使用内存空间,苹果把isa根据需要进行了区分,苹果提出了TaggedPointer和NonpointerIsa。对于小对象采用TaggedPointet方式来存放其值。对于占用内存比较大的对象采用NonpointerIsa来把isa按位使用,一部分用来存放实际的对象地址,一部分存放附加的其他信息。
nonPointerIsa的定义
还记得我们在研究objc的源码是,内存中的cls和我们的Person类是如何绑定的么?
通过
_class_createInstanceFromZone方法中的obj->initIsa(cls)把内存中生成的cls和Person进行了绑定 我们来看一下initIsa的具体实现
重要的是
isa_t,我们来看一下它的定义
isa_t是一个联合体,在联合体中有一个ISA_BITFIELD,这就是我们要找的nonPointerIsa
我们看一下它的定义
# 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__
# 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)
这里我们以__x86_64__架构来讲解:
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
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
既然如此,那么我们就能够根据isa来推导出类Class
isa推导class
我们以上位中使用的Car为例,来推导
其中
0x01000001005c55d9就是isa
接下来我们用isa与ISA_MASK做一下与操作
isa与ISA_MASK(掩码)的与运算结果就是Car.class
isa的位运算
根据nonPointerIsa的定义,我们可以知道,在其64位空间内,类的指针shiftcls占用44位,右边有3位的数据,左边有17位的数据(小端模式,数据从右往左读)如下图:
那么我们可以经过位运算,将左右数据全部清空,最后只留下shiftcls的44位数据留在原位。
右移
3位
左移
20位
右移
17位
最终我们清空了其他数据,只留下了类的指针
shiftcls
那么如何验证呢,我们通过代码来验证一下: