OC底层原理05 - isa与对象的关联

对象在alloc的时候有重要三步。

  • 计算需要开辟的内存空间大小
  • 开辟指定大小的空间
  • 将对象与isa指针关联起来

本文重点分析,如何将对象与isa指针关联起来

OC对象的本质

在探索对象与isa关联之前,需要了解到底什么是OC对象。那如何探索OC对象呢?

探索前准备

在探索OC对象的本质之前,先了解一个编译器:Clang

  • Clang是一个由Apple主导编写的,基于LLVMC/C++/OC的编译器

  • Clang主要用于底层编译,可以将OC文件转化为C++文件,这使得我们可以更好的观察底层的结构实现逻辑

  • 常用的Clang编译指令

//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

开始探索

  • 自定义一个类LGPerson,为了方便观察,在自定义类中增加一个属性name。
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LGPerson
@end
  • 在main函数中定义一个LGPerson对象。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        NSLog(@"%@",person);
    }
    return 0;
}
  • 使用clang将main函数编译成c++文件
clang -rewrite-objc main.m -o main.cpp
  • 打开编译好的main.cpp,找到LGPerson的定义
//NSObject的定义
typedef struct objc_class *Class;
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//NSObject 的底层编译
struct NSObject_IMPL {
    Class isa;
};

//LGPerson的底层编译
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
    NSString *_name;
};

由上面的代码可以看到,LGPerson在底层会被编译成结构体

  1. LGPerson_IMPL中的第一个成员是一个嵌套的结构体NSObject_IMPL,这个结构体的成员只有一个,那就是isa
  2. LGPerson_IMPL中的第二个成员就是定义的属性name。

总结

  1. 对象的本质一个含有isa的结构体

  2. isaClass类型

  3. Class是一个objc_class结构体类型的指针

探索isa

object_class结构在Object.mm中。定义如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

struct objc_object {
private:
    isa_t isa;
}

由源码可知,objc_class结构体继承于objc_object结构体,,而objc_object中只有一个私有成员isa_t isa

接下来我们一项一项分析。

isa_t isa

arm64之前isa就是一个普通的指针,只存储类对象元类对象指针,但arm64之后isa做了优化,采用了联合体的形式,这使得8字节的内存可以存储更多的内容。

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对8字节内存的位域进行了宏定义,方便不同架构下代码统一。

isa位域.png

接下来以arm64架构对isa的位域进行说明

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    
    struct {
        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
     }
};
  • nonpointer(bit0):是否对isa指针开启指针优化

    • 0 - 纯isa指针;
    • 1 - 不止是类对象地址,还包含了类信息、对象引用计数等。
  • has_assoc(bit1):关联对象标志位

    • 0 - 没有关联对象;
    • 1 - 存在关联对象;
  • has_cxx_dtor(bit2):该对象是否有C++或者Objc的析构器,如果有则做析构逻辑,如果没有则可以更快的释放对象。

    • 0 - 没有C++或者Objc的析构器;
    • 1 - 有C++或者Objc的的析构器;
  • shiftcls(bit3 - bit35):存储类的指针,开启指针优化的情况下在arm64架构下有33位来存储类的指针

  • magic(bit36 - bit41):判断当前对象是否初始化完成。调试器用来判断当前对象是真的对象还是没有初始化的空间。

  • weakly_referenced(bit42):对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放(dealloc的底层代码有体现)

  • deallocating(bit43):标志对象是否正在释放内存

  • has_sidetable_rc(bit44):判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。

  • extra_rc(bit45 - bit63):存放该对象的引用计数值减1后的结果。对象的引用计数超过 1,会存在这个里面,如果引用计数为 10,extra_rc 的值就为 9。

initIsa

了解了isa的结构之后,接下来看一下是如何将isa与类关联起来的。

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);
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
    }
}

这个函数主要做三件事:

  • 定义一个newisa结构体,并初始化为全0
  • 向newisa结构体中的各位赋值
  • 把newisa赋值给objc_object的成员变量

这个过程最重要的就是第二步,向newisa结构体中的各位赋值

  1. newisa.bits = ISA_MAGIC_VALUE;
    newisa.bits初始化为ISA_MAGIC_VALUE
    ISA_MAGIC_VALUE被宏定义为0x000001a000000001ULL。转换为二进制就是
    ISA_MAGIC_VALUE

从这个对应着isa_t的64个位域,可以看到这是对nonpointermagic赋值。

  1. newisa.has_cxx_dtor = hasCxxDtor;
    对has_cxx_dtor赋值

  2. newisa.shiftcls = (uintptr_t)cls >> 3
    cls右移3位,然后赋值给newisa.shiftcls
    重点来了,这里为什么要右移3位。

shiftcls为什么是(uintptr_t)cls >> 3

cls是Class类型,定义如下:

typedef struct objc_class *Class;

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    // 省略 ...
}

再继续看cache_tclass_data_bits_t发现都是结构体,然后看里面的成员变量,大部分都是uintptr_t类型的,查看定义

typedef unsigned long           uintptr_t;

根据内存对齐原则,可知Class肯定是8字节对齐的,同样的,cls的指向地址(也既开始地址)肯定是8的倍数,转换成二进制后低三位肯定是000

再联想联合体的说明,共用内存,可见苹果设计优化节省内存的良苦用心。

赋值shiftcls的时候既没有改变cls的值,也最大的优化了内存使用

至此,isa与类就关联起来,接下来就来验证了。

验证之前分析

定义一个LGPerson对象,然后再进入到initIsa函数中,断点停下。

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);
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
    }
}
  • 当执行isa_t newisa(0);时,通过lldb调试输出newisa的值。
(lldb) p newisa
(isa_t) $1 = {
  cls = nil
  bits = 0
   = {
    nonpointer = 0
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 0
    magic = 0
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}

此时newisa内容为全为0;

  • 当执行完位域赋值后再查看newisa的值
(lldb) p newisa
(isa_t) $2 = {
  cls = LGPerson
  bits = 8303516107965037
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 1
    shiftcls = 536875085
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}

此时可以看到,isashiftcls已经与类关联起来了。
isashiftcls已经与类关联起来之后,回到obj->initInstanceIsa(cls, hasCxxDtor);这里。
通过lldb输出obj的内容

(lldb) x/4gx obj
0x101233f30: 0x001d80010000826d 0x0000000000000000
0x101233f40: 0x0000000000000000 0x0000000000000000

根据我们的分析,对象是结构体,且第一个成员为isa。此时0x001d80010000826d就是isa

如何验证该isa就是LGPerson类呢?

我们前面分析isa的shiftcls就是类,那如何取出shiftcls的值呢?

这里仍然是前面的分析,shiftclsisabit3 - bit35

接下来通过lldb取出isabit3 - bit35

//两种方式取出bit3 - bit35
//1. 通过移位的方式 
(lldb) p 0x001d80010000826d >> 3
(long) $14 = 1037939513495629
(lldb) p 1037939513495629 << 30
(long) $15 = 576465233028055040
(lldb) p 576465233028055040 >> 27
(long) $16 = 4295000680
(lldb) po 4295000680
LGPerson

//2. 通过掩码的方式
(lldb) po 0x001d80010000826d & 0x0000000ffffffff8ULL
LGPerson

果然,此时isabit3 - bit35存的就是LGPerson类信息。这也验证了我们之前的分析。

疑问

前面提到newisa.shiftcls = (uintptr_t)cls >> 3;,即将cls右移了3位之后再赋值给shiftcls。那为何取出来的时候直接是Class而不需要左移3位呢?

解答

  1. 前面有讲到,为什么右移3位
    答:因为Class是8字节对齐cls指向地址(也既开始地址)肯定是8的倍数,转换为二进制后3位就是0,出于优化的考虑,在赋值的时候无需将无用的后3位也赋值过去。

  2. 赋值之后如何取?
    我们来看苹果是如何取出Class的。

objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
    return (Class)(isa.bits & ISA_MASK);
}

这里直接将isa.bits与掩码ISA_MASK进行与操作后,强转成Class类型。
为什么这样就可以取出Class呢?
答:因为进行与操作可以直接将isa.bitsbit3 - bit35保留外,其余的位都置0,此时就相当于bit3 - bit35Class右移3位的值,而bit0 - bit2Class的后三位。这样就与Class结构对应了。因此可以通过强转获得Class信息。

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