前序
在面向对象语言的世界里,万物皆对象,在我们的实际开发过程中,一直伴随着,那么OC对象的本质是什么?我们一起来探索下。
开始正文
对像的本质是什么?
首先我们来创建一个Demo工程,在main函数中,我们加入以下代码,如图:
在终端cd到Demo目录下,使用Clang(是由C、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器)命令编译。
命令:clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.5.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk main.m
(/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk这个是sdk路径,替换自己电脑相应的路径)
我们把main.m编译成cpp文件,即C++文件分析底层原理。
接下来我们打开main.cpp文件,搜索一下RoPerson,如下图
从上图,我们可以看到 RoPerson_IMPL是一个struct类型,里面有一个roName成员变量,跟第一张图正好吻合,也就是我们在main.m里定义的RoPerson类,NSObject_IVARS就是isa成员变量。我们搜下struct NSObject_IMPL,可以找到如下图:
这张图说明了NSObject_IVARS就是 isa成员变量。
我们再分析一下第二张图
typedef struct objc_object RoPerson这行代码可以看出来,在OC里面,我们知道所有的类的最终父类都是NSObject,而在objc中,NSObject由objc_object实现
我们从第三张图看到isa同Class类型,那么Class它的底层是什么类型,经过一系列的搜下,我们终于找到答案了,如图所示
在7849行,我们看到*typedef struct objc_class Class;这行代码,这说明Class是objc_class结构体指针,Class只不过是一个别名而已。
我们在实际开发过程中,当我们不确定对象的类型是,我们经常用id来代替,而不加,从7857行代码,我们可以看出,id是一个objc_object结构体指针的别名而已,所以不用加。
我们从上面的分析结果可以得出这个结论对象在底层的本质就是结构体。
nonPointerIsa的分析
在介绍nonPointerIsa之前,我们先介绍一下union(共用体)类型。
先贴代码便阐述
RoCar.h代码
#import
NS_ASSUME_NONNULL_BEGIN
@interface RoCar : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
@end
NS_ASSUME_NONNULL_END
RoCar.m代码
#import "RoCar.h"
#define LGDirectionFrontMask (1 << 0)
#define LGDirectionBackMask (1 << 1)
#define LGDirectionLeftMask (1 << 2)
#define LGDirectionRightMask (1 << 3)
@interface RoCar(){
// 联合体
union {
char bits;
// 位域
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
}
@end
@implementation RoCar
- (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;
}
@end
main.m代码
#import
#import
#import "RoCar.h"
struct RoCar1 {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
struct RoCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
struct RoTeacher1 {
char *name;
int age;
double height ;
};
union RoTeacher2 {
char *name;
int age;
double height ;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct RoTeacher1 teacher1;
teacher1.name = "Cooci";
teacher1.age = 18;
union RoTeacher2 teacher2;
teacher2.name = "Cooci";
teacher2.age = 18;
RoCar *car = [[RoCar alloc] init];
car.front = 1;
car.back = 0;
car.left = 0;
car.right = 1;
}
return 0;
}
RoCar1占用的内存是4字节,由四个BOOL(占用1字节)组成,占用4*8=32位,对于BOOL来讲,非0即1,我们可以用其中的4位来表示front,back,left,right,这样会造成内存的浪费,那么我们怎么进行优化呢,下面我们来讲一下。
我们可以用位域来解决
我们来看RoCar2
BOOL front: 1; (占1位)
BOOL back : 1;(占1位)
BOOL left : 1;(占1位)
BOOL right: 1;(占1位)
我们在main函数中加入
struct RoCar1 car1;
struct RoCar2 car2;
NSLog(@"%ld--%ld", sizeof(car1), sizeof(2));
看下结果
从上图可以看出,car1占用4字节,car2占用1字节,这样大大节省了内存。
下面我们再来分析RoTeacher1(struct类型)和RoTeacher2(union类型),
加入以下代码
struct RoTeacher1 teacher1;
teacher1.name = "Robert";
teacher1.age = 18;
union RoTeacher2 teacher2;
teacher2.name = "Robert";
teacher2.age = 18;
NSLog(@"%ld--%ld",sizeof(teacher1), sizeof(teacher2));
我们断点调试一下,如下图
可以看出struct的内部成员都能赋值。
我们再来看RoTeacher2,断点调试一下,如下图所示
从第7张和第8张图来看,当RoTeacher2中的name赋值时,其它变量的内存不被使用,当我们给age赋值是name的值不存在了,从这说明在同一个时刻只能用一个变量可以使用,所以union是共用内存的。
从RoTeacher1(struct类型)和RoTeacher2(union类型)对比来看,struct的内部成员是共存的,union的内部成员是互斥的,节省了内存。
弄明白了union特性后,我们再来分析isa以及nonPointerIsa。
我们打开objc源码,找到_class_createInstanceFromZone这个函数,找到
obj->initIsa(cls);代码,跟进去后,再initIsa(cls, false, false);再跟下这行代码,如下图所示
从上图我们发出一个isa_t,我们找下它的定义,如图
从这张图可以看出 isa_t是union类型,当我们来表现一个类的地址过程中,经常会出现nonPointerIsa这个名字,那么什么是nonPointerIsa呢?
OC中的类也是对象,也就是指针,也就是占用8 * 8=64(位),如果只用来存一个指针,这样就会造成资源浪费,所以苹果利用union的特性来进行优化,存储跟类相关的信息,比如引用计数,是否正在释放,weak,关联对象,析构函数
我们来看下ISA_BITFIELD的定义,如图
# 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
nonpointer:表示是否对 isa 指针开启指针优化
0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
has_assoc:关联对象标志位,0没有,1存在
has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,
如果没有,则可以更快的释放对象
shiftcls:
存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针
magic:⽤于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced:志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,
没有弱引⽤的对象可以更快释放。
deallocating:标志对象是否正在释放内存
has_sidetable_rc:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
extra_rc:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,
例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,
则需要使⽤到下⾯的 has_sidetable_rc。
以上就是nonPointerIsa 8 ** 8 = 64位的占用的情况分析。
结语
根据上文件的分析,OC对象在底层的本质就是结构体。nonPointerIsa就是利用union的特性优化,节省内存。