OC对象在C/C++底层的本质
- 本文使用的 iOS底层系列01-- objc4-781源码的编译与调试文章中的源码工程,创建继承自NSObject的YYPerson类,创建YYStudent其继承自YYPerson类;
@interface YYPerson : NSObject
@property(nonatomic,copy)NSString *name;
@end
@interface YYStudent : YYPerson
@property(nonatomic,assign)NSInteger age;
@end
Snip20210209_114.png
-
首先了解一下Clang编译器,详细内容见 iOS逆向04 -- 编译过程
- clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器;
- 主要是用于底层编译,将一些文件输出成C++文件,例如将main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构及实现的逻辑,方便理解底层原理;
-
常见的Clang命令如下所示:
-
clang -rewrite-objc main.m -o main.cpp
将main.m文件编译成main.cpp文件; -
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
将ViewController.m文件编译成ViewController.cpp文件 因为涉及UIKit框架SDK 所以要输入SDK的路径; -
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
使用xcode工具 xcrun 模拟器文件编译 main.m文件编译成main.cpp文件; -
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
使用xcode工具 xcrun 真机文件编译 main.m文件编译成main.cpp文件;
-
-
打开终端 cd 指定文件路径 执行
clang -rewrite-objc YYPerson.m -o YYPerson.cpp
将
YYPerson.m文件编译成YYPerson.cpp文件,可以看到本地生成了一个YYPerson.cpp文件,双击打开YYPerson.cpp文件,全局搜索YYPerson定位如下所示:
- NSObject的C++底层如下:
- 可以看出YYPerson在C++底层被转化成
YYPerson_IMPL
结构体,其内部有两个成员分别是struct NSObject_IMPL
和_name
,其中struct NSObject_IMPL
是NSObject在C++底层的结构体,因为YYPerson继承自NSObject,所以其YYPerson_IMPL
结构体成员会包含NSObject的底层结构体struct NSObject_IMPL
,并且是放在第一个成员的位置; - 终端执行
clang -rewrite-objc YYStudent.m -o YYStudent.cpp
将
YYStudent.m文件编译成YYStudent.cpp文件;
-
YYStudent继承自YYPerson,其C++底层结构体
struct YYStudent_IMPL
中的数据成员包含了YYPerson的底层结构体YYPerson_IMPL
,其是放在数据成员的第一个位置; -
总结:
- OC类Class在C++底层的本质就是
结构体
; - OC类Class之间的继承关系,在C++底层表现为
子类的结构体成员包含父类的结构体
,并将父类的结构体放在数据成员的第一个位置
; - NSObject的C++底层结构体有一个Class类型的isa数据成员,那么所有OC对象都会存在一个isa属性成员;
- OC类Class在C++底层的本质就是
isa的结构分析
- 在Arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class的内存地址;
- 从Arm64架构开始,对isa进行了优化,如果要获取到Class、Meta-Class的内存地址,需要将isa与其掩码ISA_MASK 作位与运算 即 (isa & ISA_MASK) 才能得到Class、Meta-Class的内存地址;
- isa变成了一个共用体(union),并且使用
位域来存储更多的信息
; - 在 iOS底层系列03 -- alloc init new方法的探索这片文章提到OC对象在alloc时底层调用
_class_createInstanceFromZone
函数,在最后会创建一个isa_t共用体变量
,最后赋值给实例对象的isa成员
;
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
//创建一个isa_t对象
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
//isa_t属性与class的绑定关系建立
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
//将创建isa_t赋值给对象的isa属性,实现了对象isa的初始化,即实现了对象与class的绑定
isa = newisa;
}
}
- 首先我们来分析一下isa_t这个共用体的结构,定位到其结构代码如下:
- 共用体的特性是数据成员之间是互斥的,能极大的节约内存;
- isa_t有两个数据成员分别为
cls
和bits
,两者之间是互斥的; - 还提供了一个
利用结构体定义的位域
,用于存储类的信息,结构体的成员ISA_BITFIELD
,这是一个宏定义,存在两个版本 arm64(对应iOS移动端)和 x86_64(对应macOS),以下是它们的一些宏定义,如下图所示(这里引用的是他人的解析图片) -
uintptr_t bits
就是ISA的存储数据占8个字节,64个二进制位,用它的二进制位来表示类的相关信息,实现原理可见最后面自定义位域实现;
-
uintptr_t
等价于unsigned long
即无符号长整型,占8个字节,64个二进制位; -
ISA_BITFIELD
位域总共有64个二进制位,不同的二进制位或者二进制位区域表示不同的含义
,下面详细解释每一个位域参数的含义; -
nonpointer
:表示是否对isa开启指针优化,0代表是纯isa指针,1代表除了地址外,还包含了类的其他信息,占一个二进制位
; -
has_assoc
:表示关联对象标志位,0表示没有关联对象,1表示关联对象,占一个二进制位
; -
has_cxx_dtor
:表示该对象是否有C++/OC的析构器(类似于dealloc)如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象,占一个二进制位
; -
shiftcls
:表示类的地址,在开启指针优化的情况下,在arm64中占33位,在x86_64中占44位
; -
magic
:判断当前对象是真的对象还是一段没有初始化的空间,占6个二进制位
; -
weakly_refrenced
:表示是否被弱引用指向,占1个二进制位
; -
deallocating
:表示对象是是否正在释放内存,占1个二进制位
; -
has_sidetable_rc
:表示是否使用 sidetable散列表来存储对象的引用计数,占1个二进制位
; -
extra_rc
:存储对象的引用计数值,若extra_rc存储已满,则开始使用sidetable散列表来存储对象的引用计数,占19个二进制位,详细原理可见 iOS内存管理07 -- retain, release, dealloc与retainCount的源码分析 - isa位域图如下所示:
- 根据上面创建isa_t共用体的函数
objc_object::initIsa()
,当代码执行到newisa.bits = ISA_MAGIC_VALUE;
这里时,断点停下,在控制台上输入命令p newisa
可以看到以下的结果:
- 当过掉当前断点,完成bits成员的赋值,此时bits赋值的是一个默认值为
ISA_MAGIC_VALUE = 0x001d800000000001ULL
然后控制台再次输入p newisa
,看到结果如下:
- 看到newisa的成员已经被初始化了,并且位域上的有些位也被赋值了;
- bits赋值默认值后,位域magic的值为59;首先
cls = 0x001d800000000001
是16进制,计算位域时需要转成二进制,再者此代码调试都是在Mac x86_64
平台上进行的,由上面的位域图可知magic的位域为[47,52]占6个二进制位,具体换算如下所示:
- 断点接着往下走,我们来继续分析,如下图所示:
- 首先我们已经知道在isa指针中的shiftcls位域中存储的是类信息;
-
newisa.shiftcls = (uintptr_t)cls >> 3
其中cls就是当前类YYPerson,从这里就可以看出newisa开始与Class类建立关联了; -
newisa.shiftcls = (uintptr_t)cls >> 3
赋值完成之后,看到newisa的cls成员已经被赋值为YYPerson,说明newisa已经与Class类建立了关联; - 在控制台上输入
p (uintptr_t)cls
;然后在输入p (uintptr_t)cls >> 3
如下所示:
Snip20210210_133.png - 这就解释了shiftcls成员为什么等于536872031;
- 在计算shiftcls成员时,cls右移了三位是因为由上面的位域图知道在shiftcls之前还存在三个二进制;
- 下面附一张newisa内部成员变化流程图:
-
上面的代码流程执行完,那么实例对象isa指针完成初始化并且与类Class建立了关联;
-
下面提供几种方式来证明实例对象的isa是指向实例对象的类的:
-
第一种方式:
Snip20210210_136.png通过isa指针地址与ISA_MSAK 的值 & 来验证
当实例对象的isa完成初始化并绑定类之后,会接着执行_class_createInstanceFromZone
函数下面的逻辑,在下面的逻辑代码中打下断点如下所示:
-
ISA_MASK isa指针的掩码在x86_64架构平台上其值为:
0x00007ffffffffff8ULL
,通过isa指针的值与isa指针的掩码做位与
运算,最后得到isa所指向的类YYPerson; -
第二种方式:
通过object_getClass()
函数
object_getClass()
函数`底层实现就是采用第一种方式,通过isa指针的值与isa指针的掩码做位与运算,最后得到isa所指向的类;
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
inline Class
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
- 最终的代码执行:
(Class)(isa.bits & ISA_MASK)
自定义实现系统的ISA指针类型 -- 位域
第一个版本
- 代码实现如下:
#import <Foundation/Foundation.h>
@interface YYPerson : NSObject
@property(nonatomic,assign)BOOL tall;
@property(nonatomic,assign)BOOL rich;
@property(nonatomic,assign)BOOL handsome;
@end
#import "YYPerson.h"
#define YYTallMask (1<<0)
#define YYRichMask (1<<1)
#define YYHandsomeMask (1<<2)
@interface YYPerson ()
{
char _tallRichHandsome;
}
//0000 0001
//倒数第一位 表示tall
//倒数第二位 表示rich
//倒数第三位 表示handsome
@end
@implementation YYPerson
- (instancetype)init{
self = [super init];
if (self) {
_tallRichHandsome = 0b00000011;
}
return self;
}
- (void)setTall:(BOOL)tall{
if (tall) {
_tallRichHandsome |= YYTallMask;
}else{
_tallRichHandsome &= ~YYTallMask;
}
}
- (BOOL)tall{
return !!(_tallRichHandsome & YYTallMask);
}
- (void)setRich:(BOOL)rich{
if (rich) {
_tallRichHandsome |= YYRichMask;
}else{
_tallRichHandsome &= ~YYRichMask;
}
}
- (BOOL)rich{
return !!(_tallRichHandsome & YYRichMask);
}
- (void)setHandsome:(BOOL)handsome{
if (handsome) {
_tallRichHandsome |= YYHandsomeMask;
}else{
_tallRichHandsome &= ~YYHandsomeMask;
}
}
- (BOOL)handsome{
return !!(_tallRichHandsome & YYHandsomeMask);
}
@end
- 测试代码:
#import <Foundation/Foundation.h>
#import "YYPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
YYPerson *person = [[YYPerson alloc]init];
NSLog(@" tall = %d -- rich = %d -- handsome = %d",person.tall,person.rich,person.handsome);
person.handsome = YES;
NSLog(@" tall = %d -- rich = %d -- handsome = %d",person.tall,person.rich,person.handsome);
}
return 0;
}
- 调试结果如下:
- 看到确实能实现用一个二进制位表示一个BOOL属性,能达到节约内存的目的;
第二个版本
-
YYPerson.h
保持不变
#import "YYPerson.h"
@interface YYPerson ()
{
//结构体 + 位域
struct{
char tall : 1;
char rich : 1;
char handsome : 1;
}_tallRichHandsome;
}
@end
@implementation YYPerson
- (instancetype)init{
self = [super init];
if (self) {
}
return self;
}
- (void)setTall:(BOOL)tall{
_tallRichHandsome.tall = tall;
}
- (BOOL)tall{
return !!_tallRichHandsome.tall;
}
- (void)setRich:(BOOL)rich{
_tallRichHandsome.rich = rich;
}
- (BOOL)rich{
return !!_tallRichHandsome.rich;
}
- (void)setHandsome:(BOOL)handsome{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)handsome{
return !!_tallRichHandsome.handsome;
}
@end
- 测试代码:
#import <Foundation/Foundation.h>
#import "YYPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
YYPerson *person = [[YYPerson alloc]init];
person.tall = YES;
person.rich = NO;
person.handsome = NO;
NSLog(@" tall = %d -- rich = %d -- handsome = %d",person.tall,person.rich,person.handsome);
}
return 0;
}
- 调试结果如下:
- 使用结构体 + 位域 的技术 实现一个二进制位表示一个BOOL属性;
第三个版本
#import <Foundation/Foundation.h>
@interface YYPerson : NSObject
- (void)setTall:(BOOL)tall;
- (BOOL)tall;
- (void)setRich:(BOOL)rich;
- (BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)handsome;
@end
#define YYTallMask (1<<0)
#define YYRichMask (1<<1)
#define YYHandsomeMask (1<<2)
#import "YYPerson.h"
@interface YYPerson ()
{
//共用体
union{
char bits;
struct{
char tall : 1;
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
}
//0000 0001
//倒数第一位 表示tall
//倒数第二位 表示rich
//倒数第三位 表示handsome
@end
@implementation YYPerson
- (instancetype)init{
self = [super init];
if (self) {
}
return self;
}
- (void)setTall:(BOOL)tall{
if (tall) {
_tallRichHandsome.bits |= YYTallMask;
}else{
_tallRichHandsome.bits &= ~YYTallMask;
}
}
- (BOOL)tall{
return !!(_tallRichHandsome.bits & YYTallMask);
}
- (void)setRich:(BOOL)rich{
if (rich) {
_tallRichHandsome.bits |= YYRichMask;
}else{
_tallRichHandsome.bits &= ~YYRichMask;
}
}
- (BOOL)rich{
return !!(_tallRichHandsome.bits & YYRichMask);
}
- (void)setHandsome:(BOOL)handsome{
if (handsome) {
_tallRichHandsome.bits |= YYHandsomeMask;
}else{
_tallRichHandsome.bits &= ~YYHandsomeMask;
}
}
- (BOOL)handsome{
return !!(_tallRichHandsome.bits & YYHandsomeMask);
}
@end
- 测试代码如下:
#import <Foundation/Foundation.h>
#import "YYPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
YYPerson *person = [[YYPerson alloc]init];
person.tall = YES;
person.rich = NO;
person.handsome = NO;
NSLog(@" tall = %d -- rich = %d -- handsome = %d",person.tall,person.rich,person.handsome);
}
return 0;
}
- 调试结果如下:
- 使用
共用体 + 位域
的技术 实现一个二进制位表示一个BOOL属性; - 结构体struct已经形同虚设,主要作用在于展示不同位所表示的信息而已,增加可读性;
-
char bits
是共用的数据对象,占一个字节8个二进制位,不同的二进制位表示不同的信息数据;
网友评论