美文网首页
《跟我学》之OC对象isa结构分析

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

作者: 这货不是文熙 | 来源:发表于2020-09-11 04:49 被阅读0次

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

相关文章

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

    1. 前言 上一篇我们了解到了一个对象的属性内存分配和占用情况。并且额外引入了两个结构体做了对比。我们发现类跟结构...

  • Runtime

    oc对象 OC类对象元类对象编译后的结构如下结构体 isa arm64架构之前isa只是一个指针,指向类对象或者元...

  • isa结构分析

    isa结构分析 OC对象的本质 clang命令 把oc类编译成c++文件,对象在底层编译成struct 联合体(共...

  • isa指针结构分析

    OC对象中结构体中都有一个isa指针,我们都知道OC对象的isa指向他所属的类,那么这个isa是怎样的一种数据结构...

  • OC对象(三)-- isa结构分析

    OC对象(一)-- alloc和init底层到底在干嘛OC对象(二)-- 内存对齐和calloc中的16字节对齐O...

  • Runloop 本质是什么?

    Runloop 本质是什么?本质是一个OC对象,内部也有isa指针。Runloop 结构分析结构内部有2个重要的成...

  • isa的作用与内部结构(下)

    一、回顾 oc对象的本质的结构体,结构体内部存放了一个isa指针 二、源码分析 查看runtime源码发现 内部的...

  • iOS底层之类的结构分析

    从iOS底层之isa结构分析及关联类我们探究了类的实例对象的内存结构,对象指针的首地址存储了isa,也就是存储了类...

  • OC对象的本质

    OC对象的本质 一个对象的本质是一个结构体,结构体里面有isa指针、成员变量等,isa指针指向对象的类别,inst...

  • OC isa结构分析

    分析isa结构之前,先了解下OC对象 在终端中输入命令,将其转成C++代码 C++代码很长,找到main函数中的部...

网友评论

      本文标题:《跟我学》之OC对象isa结构分析

      本文链接:https://www.haomeiwen.com/subject/jocuektx.html