1. 前言
上一篇我们了解到了一个对象的属性内存分配和占用情况。并且额外引入了两个结构体
做了对比。我们发现类
跟结构体
好像有什么相似的地方。那到底有什么相似的呢。话不多说,肝着。
1.1Clang
首先clang
是一个由Apple
主导编写,基于LLVM
的C/C++/OC
的编译器,这货干啥的呢?
主要用途可以将你编写的类输出成较为低一级别的代码,第一天玩人(Person)
。第二天玩狗(Dog)
,今天我们来当许仙。一起来玩蛇(Snake)
🐍,例如将你Snake.m
输出为Snake.cpp
,这样一来就可以更直观的观察到代码还做了哪些你不知道的事情。直接上码
@interface Snake ()
@property (nonatomic, copy) NSString *name;
@end
@implementation Snake
@end
通过终端,利用 clang
将 Snake.m
编译成 Snake.cpp
,有以下几种编译命令,这里使用的是第一种
//1、将 Snake.m 编译成 Snake.cpp
clang -rewrite-objc Snake.m -o Snake.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
**这里要注意`iPhoneSimulator13.7`这个目录一定要跟你本地的目录对应上**
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
//以下两种方式是通过指定架构模式的命令行,使用 `xcode` 工具 `xcrun`
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc Snake.m -o Snake-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Snake.m -o Snake- arm64.cpp
之后我们会在同级文件看到Snake.cpp
文件。打开之后是不是很惊喜有上万行代码。惊不惊喜,意不意外。
我们全局搜索只看我们关心部分。
extern "C" unsigned long OBJC_IVAR_$_Snake$_name;
struct Snake_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
/* @end */
// @interface Snake ()
// @property (nonatomic, copy) NSString *name;
/* @end */
// @implementation Snake
//手动添加的注释,对应name的geet方法
static NSString * _I_Snake_name(Snake * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Snake$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
//手动添加的注释,对应name的set方法
static void _I_Snake_setName_(Snake * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Snake, _name), (id)name, 0, 1); }
// @end
1.2分析
我们刚才OC
代码定义的Snake
类以及属性居然被注释了,等价的被替换成了C++
代码。并且我们的类变成了结构体,我们都知道万物皆NSObject
,我们的这个Snake
类也是继承NSObject
,但是定义的 Snake
类只有一个name
属性,为什么结构体里还有 NSObject_IMPL
的结构体呢?
其实这样的定义同OC
,也是继承自 NSObject
的意思 ,属于伪继承
,伪继承
的方式是直接将 NSObject
结构体定义为 Snake
中的第一个属性,意味着 Snake
拥有 NSObject
中的所有成员变量
Snake
中的第一个属性 NSObject_IVARS
等效于 NSObject
中的 isa
我们多次听到了这个 isa
。这个 isa
到底是做啥的,平时开发好像也没怎么用到它,为什么会被多次提及,引用大佬的一句话简单来说就是很重要
,装逼的来说不要试图去理解它。试着去感受它
。
还记得我们提及过 alloc
三大核心方法的核心之一的 initInstanceIsa
方法吗?忘记了没关系,上祖传代码
obj->initInstanceIsa(cls, hasCxxDtor);
-------------------------------------------------
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 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;
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 = newisa;
}
}
我们看到这个方法有点懵逼。那我们一层脱下它的衣服。看看它里面穿了啥
1、 通过cls
初始化isa
2、如果是非 nonpointer
,代表普通的指针,存储着 Class
、Meta-Class
对象的内存地址信息。
3、然后就发现 定义了一个newisa
,然后对它疯狂赋值。足已证明它多重要了。我们看看里面是什么
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
};
1.3 结构体(struct)&&联合体(union)
构造数据类型的方式有以下两种:
- 结构体(
struct
) - 联合体(
union
,也称为共用体)
之前我们已经讲过struct
,现在又出现一种union
我们来好好科普一下这两个东西
结构体
结构体
是指把不同的数据组合成一个整体,其变量
是共存
的,变量
不管是否使用
,都会分配内存
。
缺点:所有属性都分配内存,比较浪费内存,假设有 `4` 个 `int` 成员,一共分配了 `16` 字节的内存,但是在使用时,你只使用了 `4` 字节,剩余的 `12` 字节就是属于内存的浪费
优点:存储容量较大,包容性强,且成员之间不会相互影响
联合体
联合体
也是由不同的数据类型组成,但其变量是互斥
的,所有的成员共占一段内存。而且共用体采用了内存覆盖
技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉
缺点:包容性弱
优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
两者的区别
1、内存占用情况
结构体的各个成员会占用不同的内存,互相之间没有影响
共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
2、内存分配大小
结构体内存 >= 所有成员占用的内存总和(成员之间可能会有缝隙)
共用体占用的内存等于最大的成员占用的内存
我们刚才的那个isa_t
就是一个union
,为什么使用它来定义。通过刚才优缺点也自然不言而喻了。我们来分析一下isa_t
这个里面定义了什么?
-
cls
:是Class
类型的指针变量,指向的是对象的类。 -
bits
:是结构体位域指针。 -
ISA_BITFIELD
:宏 ISA_BITFIELD,用来定义位域,用于存储类信息及其他信息。
ISA_BITFIELD
ISA_BITFIELD
宏在内部分别定义了arm64
位架构(iOS
)和x86_64
架构(macOS
)的掩码和位域.。
其isa
的存储情况如图所示
现在也就理解刚才代码中newisa
赋值都是干啥的了吧。
1、cls
与 isa
关联原理就是isa
指针中的shiftcls
位域中存储了类信息,
2、initInstanceIsa
的过程是将创建对象的指针和当前的 类cls
关联起来
最后
说了这么多。我们是否能装逼反响验证一波上面所说的呢?
1、【方式一】通过initIsa
方法中的newisa.shiftcls = (uintptr_t)cls >> 3
来验证
2、【方式二】通过isa
指针地址与ISA_MSAK
的值 & 来验证
3、【方式三】通过runtime
的方法object_getClass
验证
4、【方式四】通过位运算验证
方式一:通过 initIsa 方法
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
我们用源代码在这两行代码加入断点。确保调用传递进来的cls是我们要研究的Snake
类
运行至此时。在lldb
做以下操作
聪明的你是不是已经发现,我们p (uintptr_t)cls
,结果为(uintptr_t) $5 = 4294976016
,再右移三位,p (uintptr_t)cls >> 3
得到(uintptr_t) $6 = 536872002
,我们再试将$5的值右移3位p 4294976016 >> 3
,得到也是536872002
,最后从左边变量看shiftcls
还是我们来直接暴力的看一下p newisa.shiftcls
得到也是536872002
cls
也变成了我们的Snake
方式二:通过 isa & ISA_MSAK
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
我们走完刚才的方法返回到这里时在要return obj
的之前的地方打个断点。执行x/4gx obj
,得到isa指针的地址0x001d800100002215
,再将isa
指针地址 & ISA_MASK
(处于macOS
,使用x86_64
中的宏定义),即 po 0x001d800100002215 & 0x00007ffffffffff8
,得出Snake
-
arm64
中,ISA_MASK
宏定义的值为0x0000000ffffffff8ULL
-
x86_64
中,ISA_MASK
宏定义的值为0x00007ffffffffff8ULL
方式三:通过 object_getClass
通过查看·object_getClass·的源码实现,最终发现核心处理与我们的方法二一样。这里就不过多复述
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
}
方式四:通过位运算
我们用方法二在返回obj
之前断点执行如下操作
1、将isa
地址右移3
位:p/x 0x001d800100002215 >> 3
,得到0x0003b00020000442
2、再将得到的0x0003b00020000442
左移20
位:p/x 0x0003b00020000442 << 20
,得到0x0002000044200000
3、将得到的0x0002000044200000
再右移17
位:p/x 0x0002000041d00000 >> 17
得到新的0x0000000100002210
我们之所以左移右移,是因为知道shiftcls
所在位于的位置。所有的操作都是为了精准读取到shiftcls
那为什么是左移20
位?因为先右移了3
位,相当于向右偏移了3
位,而左边需要抹零的位数有17
位,所以一共需要移动20
位
获取cls
的地址,或者直接po
与上面的进行验证 得到
p/x cls
0x0000000100002210 `Snake`
po 0x0000000100002210 `Snake`
(注:部分图片来自“style_月月”的博客) 传送门->Style_月月