《跟我学》之OC对象isa结构分析

1. 前言

上一篇我们了解到了一个对象的属性内存分配和占用情况。并且额外引入了两个结构体做了对比。我们发现结构体好像有什么相似的地方。那到底有什么相似的呢。话不多说,肝着。

1.1Clang

首先clang是一个由Apple主导编写,基于LLVMC/C++/OC的编译器,这货干啥的呢?
主要用途可以将你编写的类输出成较为低一级别的代码,第一天玩人(Person)。第二天玩狗(Dog),今天我们来当许仙。一起来玩蛇(Snake)🐍,例如将你Snake.m 输出为Snake.cpp,这样一来就可以更直观的观察到代码还做了哪些你不知道的事情。直接上码

@interface Snake ()
@property (nonatomic, copy) NSString *name;
@end

@implementation Snake
@end

通过终端,利用 clangSnake.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,代表普通的指针,存储着 ClassMeta-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)的掩码和位域.。

图1

isa的存储情况如图所示

图2

现在也就理解刚才代码中newisa赋值都是干啥的了吧。
1、clsisa关联原理就是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做以下操作

图3

聪明的你是不是已经发现,我们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_月月

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容