Objective-C isa指针探秘

稍微精深一点的IOS开发都听说过isa指针。它在OC的类中起到了指示自身类型的作用,是runtime实现的基础。那么isa指针到底是如何实现的呢,让我们从源码的层面进行分析。

NSObject -> Class -> objc_class -> objc_object

新建一个最简单的空类:

@interface Person : NSObject
@end

@implementation Person
@end

command点击NSObejct,我们可以看到cls的实现:

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

那么这个Class又是什么数据呢?

typedef struct objc_class *Class;

我们可以看到Class实际上是objc_class结构体的指针。而objc_class,就是OC类的元类

objc_class在objc源码中有三处定义:

// in runtime.h
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    .....
} OBJC2_UNAVAILABLE;


#if __OBJC2__
#include "objc-runtime-new.h"
#else
#include "objc-runtime-old.h"
#endif

// in objc-runtime-old.h
struct objc_class : objc_object {}

// in objc-runtime-new.h
struct objc_class : objc_object {}

我们当前使用的版本多是 objc2,所以起作用的是 最后一个 objc-runtime-new.h中定义的objc_classobjc_class则继承了objc_object

struct objc_class : objc_object 

objc_object同样在两个位置有定义:

in objc.h

#if !OBJC_TYPES_DEFINED
....
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
#endif


in objc-private.h

#ifdef _OBJC_OBJC_H_
#error include objc-private.h before other headers
#endif

#define OBJC_TYPES_DEFINED 1

struct objc_object {
private:
    isa_t isa;
.....
}

根据objc-private.h中的宏定义我们可以看到,起作用的实际上是objc-private.h中的定义。

对一个继承了NSObject的类而言,isa最后能定位到objc_object中。而用来存储类信息的,是objc_objectisa_t isa;

isa_t

** isa_t是一个union**聚合体。聚合体和结构体最大的不同是它的内存存储方式。对聚合体而言,所有的成员变量的起始地址相同,对每一个成员的修改都会影响所有的变量。这样做最大的好处就是可以节约内存空间。但是也会导致每次可用的变量只有一个。

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

    Class cls; // 初始化是不会使用的
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // 这才是关键
    };
#endif
};

isa_t 是一个联合体,这里所占空间为 8字节,共64位 ,内存布局从低位到高位情况如下图:


    struct {
        ISA_BITFIELD;  // 这才是关键
    };

#   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

注意 在 struct 的变量A后面加 :(数字n), 是指定该变量A在内存中占用 n 位。

  • nonpointer(存储在第0字节) : 是否为优化isa标志。0代表是优化前的isa,一个纯指向类或元类的指针;1表示优化后的isa,不止是一个指针,isa中包含类信息、对象的引用计数等。现在基本上都是优化后的isa。

  • has_assoc (存储在第1个字节): 关联对象标志位。对象含有或者曾经含有关联引用,0表示没有,1表示有,没有关联引用的可以更快地释放内存。

  • has_cxx_dtor(存储在第2个字节): 析构函数标志位,如果有析构函数,则需进行析构逻辑,如果没有,则可以更快速地释放对象。

  • shiftcls :(存储在第3-35字节)存储类的指针,其实就是优化之前 isa 指向的内容。在arm64架构中有33位用来存储类指针。x86_64架构有44位。

  • magic(存储在第36-41字节):判断对象是否初始化完成, 是调试器判断当前对象是真的对象还是没有初始化的空间。

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

  • deallocating(存储在第43字节):标志对象是否正在释放内存。

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

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

isa的初始化

对OC类而言,isa的初始化是在 alloc中完成的。具体来说是在

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls,...) {

...
id obj; // id 是objc_object的指针

    // 3: ?
    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        obj->initIsa(cls);
    }
...
}

initInstanceIsainitIsa最后都会统一到一个函数里:

(去除一些宏定义,断言以及条件判断等,我们直接将代码减少到它执行的代码)

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)  {
          isa_t newisa(0);

        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;

        isa = newisa;
}
  1. 首先对整个bits进行赋值,传入 ISA_MAGIC_VALUE ,在arm64架构下,该值为
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

将该值转换为2进制

image.png

对应 isa_t 中内存布局的位置,可以看出对 bits 赋值 就是对 nonpinter 和 magic 赋值的过程。

2.其次对has_cxx_dtor赋值。

  1. 最后对shifcls赋值

shiftcls详解

shiftcls 里面存放着类的指针。众所周知指针是64位的,那么为什么可以放置在33位或或者44位的shiftcls中呢?在需要指针时又如何还原呢?

首先我们看下shiftcls的赋值:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
....
        isa_t newisa(0);
....
        newisa.shiftcls = (uintptr_t)cls >> 3;
....
        isa = newisa;
    }
}

首先我们将cls的尾部三位去掉,这是因为对类而言,它至少包含一个isa指针,那么它的长度一定是8的整数倍。这里我们可以使用runtime计算下Person的长度来认证下:

#import <objc/runtime.h>
size_t insSize = class_getInstanceSize([Person class]);
NSLog(@"PP Size:%zd",insSize);

然后,我们会看到源码中shiftcls的宏定义后面有一个数据 MACH_VM_MAX_ADDRESS:

 uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \

这是最大虚拟寻址空间,也就是内存空间能够分配的最大地址。以ARM32为例,它最大的虚拟寻址空间为 0x10,0000,0000。而33 + 3 (右移的三位) = 36 = 4 * 9。正好是虚拟寻址空间的位数 - 1。正好可以覆盖虚拟寻址空间。也就是说64位的数据中,前面64 - 36 = 18位是无用的。

因此,只要存储64位指针中间的33位就可以表示了。只要在使用的时候与上mask:0x0000000ffffffff8,就可以得到对应的类指针。

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