提前准备
为了探究对象在底层的实现方式,此次使用clang
来进行探究。相关语句clang -rewrite-objc main.m -o main.cpp
,在终端中进入工程目录main.m
所在的路径中,执行上面的语句,就会生成一个main.cpp
的C++文件,该文件就是main.m
文件的底层实现方式。
main.cpp探究
通过clang之后的main.cpp 文件可以看到如下代码:
struct YKPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
可以看出我们创建的YKPerson
类在底部被编译成了YKPerson_IMPL
的结构体,name
属性成为结构体的属性。所以我们可以得到第一个结论,对象在底层被编译成了结构体,对象在底层是以结构体的形式存在
,而NSObject_ IVARS
是在内部默认全部都有的属性isa
。
联合体位域
在日常开发中少不了在类中声明一些布尔或其他类型的属性用于判断。为了更高的利用内存,我们可以合理使用联合体位域的方式来减少对内存的占用。
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
union {
char bits;
struct {
char front : 1;
char back : 2;
char left : 3;
char right : 4;
}
} _direction;
例如以上代码,日常开发中一般会使用第一段代码声明(front,back,left,right)
四个属性来做相应的方向判断,并且在相应的setter,getter方法中进行相应的操作,因为BOOL
类型是4字节类型,所以它们需要占用4*4 = 16字节 = 128位
,只是为了做一个方向判断时,这个内存占用量其实也是不少的。所以这个时候我们可以用到联合体位域
。
在第二段代码中,因为char
类型的数据为1字节,所以总共占用了5字节。每一个属性后面的数字代表的是所在的位置(例如:0000 1111,第一位代表front,第二位代表back,第三位代表3,第四位代表right)。使用联合体位域时需要配合相应的方法来对联合体位域中的属性进行相应设置。
- 结构体
优点:所有的变量是“共存”的,全面;
缺点:struct内存空间的分配是粗放的,不管用不用,全分配。 - 联合体
优点:各个变量是“互斥”的,内存使用更为精细灵活,节省了内存空间;
缺点:不够“包容”
isa分析
之前我们已经分析了对象在alloc时调用instanceSize
计算内存空间,calloc
像系统申请内存,接下来就是讲开辟的内存与对象类进行关联。通过源码分析,调用initInstanceIsa
后在方法initIsa
中进行处理,因此initIsa
为核心方法。在该方法中主要是对isa
的赋值操作,接下来对该方法做主要分析。属性isa
为isa_t
类型,在该方法中对联合体isa_t isa
中的相关属性进行了一些赋值操作。
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
}
以上为联合体isa_t
的定义,属性cls
与bits
互斥,只有当nonpointer
不存在时cls
才会被赋值,当nonpointer
存在时,讲给bit
赋值一个宏定义的ISA_INDEX_MAGIC_VALUE 0x001d800000000001ULL
。重点在ISA_BITFIELD
,以下是以x86架构下的关于相关定义,并对内部结构进行解读。
# 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)
-
nonpointer
:表示是否对isa
指针开启指针优化,0:纯isa指针,1:不止是类对象地址,isa包含了类信息、对象的引用计数等 -
has_assoc
: 关联对象标志位,0没有,1存在 -
has_cxx_dtor
: 该对象是否有C++或者OBJC的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象 -
shiftcls
:在x86架构下占用44位内存。存储类指针的值。开启指针优化的情况下,arm64架构下占用33位内存。 -
magic
:用于调试器判断当前对象是真的对象还是没有初始化的空间。 -
weakly_referenced
:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。 -
deallocating
:标志对象是否正在释放内存。 -
has_sidetable_rc
:当对象引用计数大于10时,则需要借用该变量存储进位。 -
extra_rc
:当表示该对象的引用计数时,实际上是引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc
为9.如果引用计数大于10,则需要使用到上面的has_sidetable_rc
。
在之前的文章中讲到对象调用alloc
时,在_class_createInstanceFromZone
中与类进行关联,通过调试obj
为返回结果,0x001d8001000020f1
为isa
,我们将isa
进行位移计算,目的是得到isa_t
中的shiftcls
,再以16进制打印传入的cls
时我们发现此时的shiftcls
就是我们的类,代表此时我们自定义的类已经被系统关联,并初始化了内存空间。
拓展 (clang)
-
clang -rewrite-objc main.m -o main.cpp
把目标文件编译成c++文件 - UIKit报错问题
-
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platfroms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m
通过找到当前版本Xcode下对应的SDK来进行生成,一般在头文件中包含UIKit
时出现该问题
-
-
xcode
安装的时候顺带安装了xcrun
命令,xcrun
命令在clang
的基础上进行了 一些封装,要更好用一些 -
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
(模拟器) xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp (手机)
结论
从以上可以得到在调用initIsa时是将系统为类创建的内存以及设置一些类的信息并与当前类进行关联。所以在isa
中保存了类的所有信息。