这是我参与更文挑战的第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
那么如何验证呢,我们通过代码来验证一下: