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_月月
