iOS底层探究-isa的初始化&走位分析

本文可能篇幅比较长,主要为了自己日后温习知识所用。如果有幸被你发现这篇文章,并且引起了你的阅读兴趣,希望这篇文章能对你有所帮助。如发现任何有误之处,肯请留言纠正,谢谢。️

一:isa是什么?

iOS底层探究-浅谈alloc,init,new 前面的文章中我们提到过,基类NSObject有个默认的属性isa:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

那么isa是什么呢?
苹果有段官方的描述: “A pointer to the class definition of which this object is an instance”。
isa其实是指一个实例对象指向该对象的类的指针。没错,早期的时候isa的确是单纯的一个指针,只不过后期苹果进行了优化,不但保存了指针的地址,另外也存储了一些类的信息。下面我们看下苹果isa的源代码:

union isa_t {
    //两个默认的构造函数
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

  //isa指向的类
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        //位域
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

可以看出isa其实是个union联合体,苹果这块用联合体优化内存占用,并且使用位域ISA_BITFIELD增加可读性。联合体和位域的结合使用,我会单独去写,请持续关注,我们先看下 ISA_BITFIELD 位域的定义和使用。
定义:

#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8

根据Union联合体的特性 isa_t 总共占用8个字节64位

其中:
nonpointer代表是否是纯指针 0 代表纯指针 1 代表不止是类对象地址,isa 中包含了类信息、对象的引用计数等
has_assoc
关联对象标志位
has_cxx_dtor
该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
shiftcls
存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针
magic
用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced
存储对象是否被指向或者曾经指向一个 ARC 的弱变量,
没有弱引用的对象可以更快释放。
deallocating
标志对象是否正在释放内存

has_sidetable_rc
当对象引用技术大于 10 时,则需要借用该变量存储进位
extra_rc
当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc

二:isa指针走位

前面我们已经看到了isa_t结构体保存了类的很多信息,其中就包括了类指针的值。下面我们就研究下isa的指针走位。先看一张大神画的图:

isa流程图

图中实线箭头代表类继承的走向,而虚线箭头是代表isa的走向

其实我第一次看这张图的时候是懵逼的,乱七八糟的线根本看不出是表达什么逻辑。后来随着对底层实现的了解和探索,才慢慢了解了图作者想表达的意思,下面我就和大家一起来研究一下

1:静态分析,我们要研究isa,先看下系统是怎样获取类对象的:

object_getClass()是系统获取类对象的一个函数,内部是这样实现的

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}


objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}

我们知道objc都是!isTaggedPointer的,可以直接定位到ISA()函数。

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
}

SUPPORT_INDEXED_ISA这个宏定义在iOS是0.

(Class)(isa.bits & ISA_MASK)这句话就是我们的重点了,系统通过&运算结合mask来得到isa类指针的值
最后(Class)做了步强转返回Class

2:动态调试,模拟静态代码的步骤,通过lldb工具印证我们的推测

我们通过objc最新的源码,建立一个target方便我们调试。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
      
        JSPerson *object = [JSPerson alloc];
        // 断点位置
    }
    return 0;
}
  • isa从对象指向类

添加断点,我们通过lldb来调试

(lldb) x/2xg object // 打印类的信息
0x101a4c8b0: 0x001d800100001101 0x0000000000000000

其中0x001d800100001101就是对象中的isa我们坐下运算

(lldb) p/x 0x001d800100001101 & 0x0000000ffffffff8
(long) $2 = 0x0000000100001100

然后对对象所属的类进行打印

(lldb) p/x object.class
(Class) $3 = 0x0000000100001100 JSPerson

我们看到了$2$3是相等的,也就印证了我们的第一步,对象和类是靠isa建立绑定的

那么类的isa会指向何处呢?我们先看下底层对类结构体的定义:

struct objc_class : objc_object // 类在底层其实是个继承于objc_object的结构体,万物皆对象在这里得到印证

// 我们看下objc_class这个结构体
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

说明objc_class第一个成员也是isa,那我们继续打印

  • isa从类指向元类
(lldb) x/2xg 0x0000000100001100
0x100001100: 0x001d8001000010d9 0x0000000100b00140

(lldb) p/x 0x001d8001000010d9 & 0x0000000ffffffff8
(long) $4 = 0x00000001000010d8

(lldb) po 0x00000001000010d8
JSPerson

我们看到输出的又是JSPerson,但是内存地址却不相同,其实他是JSPerson的元类(metaClass),metaClass 是 Class 对象的类,同样也是个对象。每个类都必须有一个唯一的 metaClass.

  • isa从元类指向根元类
(lldb) x/2xg 0x00000001000010d8
0x1000010d8: 0x001d800100b000f1 0x0000000100b000f0

(lldb) p/x 0x001d800100b000f1 & 0x0000000ffffffff8
(long) $6 = 0x0000000100b000f0

(lldb) po 0x0000000100b000f0
NSObject

输出了NSObject的元类也就是所有类的根元类

  • isa从根元类指向根根元类
(lldb)  x/2xg 0x0000000100b000f0
0x100b000f0: 0x001d800100b000f1 0x0000000100b00140

(lldb) p/x 0x001d800100b000f1 & 0x0000000ffffffff8
(long) $8 = 0x0000000100b000f0

(lldb) po 0x0000000100b000f0
NSObject

你会发现无论你打印多少次得到的结果都是NSObject内存地址也不会变,形成了循环。

三:总结

我们在把这张图拿过来,现在感觉是不是清晰了很多?

isa流程图

isa走位总结:实例对象->类->元类->根元类->根根元类(根元类本身)
继承关系 :Subclass->Superclass->Root class

下一篇文章 将对类的结构做跟细致的研究,如果觉得喜欢的话还请加个关注,我会经常保持更新。

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

推荐阅读更多精彩内容