iOS对象原理探究:isa结构分析

Clang

我们知道iOS中对象对应到底层都是结构体。那么我们怎么才能分析底层的对象源码呢。所以就引入了Clang

那么什么是Clang呢?

Clang是一个C语言、C++、Objective-C语言的轻量级编译器。它采用了LLVM作为其后端,而且由LLVM2.6开始,一起发布新版本。源代码发布于BSD协议下。

Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C的编译器。

Clang的目标是提供一个GUN编译器套装(GCC)的替代品,支持了GUN编译器大多数的编译设置以及非官方语言的扩展。(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异,并在此基础上增加了额外的语法特性,比如C函数重载 (通过attribute((overloadable))来修饰函数),其目标(之一)就是超越GCC。

常用的Clang指令

// 把目标文件编译成c++文件
clang -rewrite-objc main.m -o main.cpp 
编译后的文件夹结构
编译后的文件对比

通过编译后,我们可以看出,Person对象,其实对应到底层中其实就是一个结构体,那么我们加些属性,来验证是否真的是这样的。

增加属性编译后的文件对比

通过对比,我们可以看出:

  • Person类中的属性其实就是结构体中的数据成员。
  • Person类中属性的方法,对应到c++中都是对应的静态方法。
  • Person类中的setter方法其实都是统一调用了runtime中的objc_setProperty方法。

那么Person对象对应结构体中的第一个成员(struct NSObject_IMPL NSObject_IVARS;)是什么呢?其实它就是我们今天的重点isa了。

isa 结构

isa初始化流程

我们在iOS对象原理探究:alloc & init & new中,曾提到过一个方法initInstanceIsa

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, 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;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        isa = newisa;
    }
}

initInstanceIsa方法中直接调用了initIsa

isa_t

通过initIsa的源码可以知道,无论是那个条件中,都是isa_t的结构体。

  • !nonpointer 条件下:isa = isa_t((uintptr_t)cls);
  • nonpointer 条件下:isa_t newisa(0);

同时根据isa_t的结构我们也可以知道其实isa_t中的 clsbits 其实是互斥的。当我们的类型是nonpointer时,bits会有值,当不是nonpointer时,会直接返回cls

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
};

那么 ISA_BITFIELD 是什么呢?ISA_BITFIELD 其实就是我们NONPOINTER_ISA结构

NONPOINTER_ISA 结构(ISA_BITFIELD)

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)
arm64的isa分析图
  • nonpointer:表示是否对 isa 指针开启指针优化(0:纯 isa 指针;1:不止是类对象地址,isa中包含了类信息、对象的引用计数等)
  • has_assoc:关联对象标志位;0没有,1存在
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器(dealloc等),如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic:用于调试器判断当前对象是真的还是没有初始化的空间
  • weakly_referenced:指对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快的释放。
  • deallocating:标志对象是否正在释放内存。
  • has_sidetable_rc:当对象引用计数大于 10 时,则需要借用该变量存储进位。(是否有外挂的散列表)
  • extra_rc:额外的引用计数;表示该对象的引用计数值,实际上是引用计数值减1。(eg:如果对象的引用计数为10,那么 extra_rc 为9。如果应用计数大于 10,则需要使用到 has_sidetable_rc)。
__x86_64__ bits赋值

当使用 ISA_MAGIC_VALUE 设置 isa_t 结构体之后,实际上只是设置了 nonpointer 和 magic 这两部分的值。

shiftcls

在设置了 nonpointer、magic、和has_cxx_dtor 之后,我们要将对象对应的类指针存入到 isa 结构体中。

newisa.shiftcls = (uintptr_t)cls >> 3;
类的指针地址

将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。

_class_createInstanceFromZone 方法中打印了调用这个方法传入的类指针:

打印指针

可以看到,这里打印出来的所有类指针十六进制地址的最后一位都为 8 或者 0。也就是说,类指针的后三位都为 0,所以,我们在上面存储 Class 指针时右移三位是没有问题的。

而在此结构中,我们可以知道shiftcls其实就是存储类指针的值,那么我们来验证下到底是否是这样呢?

方式一:验证shiftcls

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;
        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;
    }
}

通过initIsa的源码分析,我们可以知道当是nonpointer指针是,我们会先给bits赋值(ISA_MAGIC_VALUE),通过注释其实我们可以知道,magicnonpointer都是ISA_MAGIC_VALUE的一部分,最后我们将cls右移三位就得到了shiftcls

接下来我们尝试将 NSObject 的类指针和对象的 isa 打印出来,具体分析一下:

类指针和isa

代码中打印出来的 isa 结构体中的内容如下:

isa的内容

其中红色部分为类指针,与上面的 [NSObject class] 指针右移三位的结果是完全相同的,这也就验证了我们之前对于初始化 isa 时对 initIsa 方法的分析是正确的。它设置了 indexed、magic 以及 shiftcls。

方式二:验证shiftcls

反推结果

通过图中的lldb调试,其实我们可以知道通过二进制的偏移,可以得出对象的指针值其实就是shiftcls中存储的值。(x86_64的架构)

那么这只是我们的反推结果,Apple是怎么实现的呢?

源码获取isa

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
}

通过源码可以知道,主要是通过 isa.bitsISA_MASK 进行了位与。那么 ISA_MASK 是什么呢,通过NONPOINTER_ISA 结构(ISA_BITFIELD)中我们可以看出 ISA_MASK 的定义。

arm64分析

#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

那么0x0000000ffffffff8ULL是什么呢?为何这个算法这么牛皮呢?

来来来,分析一波!!!

ISA()

0x0000000ffffffff8ULL 其实就是一个转换成二进制是

111111111111111111111111111111111000

其中前三位是0,中间的33位是1,前面都是0了

对应到我们的isa结构中,进行位与,其实就是将前三位和后17位进行了抹零留下了中间的33位。那么这33位其实就是我们的shiftcls的值了,通过上面的分析,其实我们知道类指针默认后三位都是0,所以不需要我们在右移三位就已经是我们得到的类指针了。

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