OC底层原理04—类、元类、根元类 与 isa的关联

iOS--OC底层原理文章汇总
如题,今天来探究下类的一些信息,isa指针的指向就是该类对象的元类,每一个类都是它的元类的对象,元类是对类对象的描述。首先来分析一下代码

//类对象内存存在个数为 1
void lgTestClassNum(){
    Class class1 = [LGPerson class];
    Class class2 = [LGPerson alloc].class;
    Class class3 = object_getClass([LGPerson alloc]);
    Class class4 = [LGPerson alloc].class;
    NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}

void lgTestNSObject(){
    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
}

打印结果分析之:

2020-09-14 23:24-isa分析[1763:92018] <LGPerson: 0x102055210>
2020-09-14 23:24-isa分析[1763:92018] 
0x100002588-
0x100002588-
0x100002588-
0x100002588
2020-09-14 23:24-isa分析[1763:92018] 
0x1005135b0 实例对象
0x7fffb1bd0140 类
0x7fffb1bd00f0 元类
0x7fffb1bd00f0 根元类
0x7fffb1bd00f0 根根元类

可以看到lgTestClassNum中打印的类对象地址都是同一个,说明:类对象存在个数仅有一份。在lgTestNSObject中实例对象地址与类的地址,以及元类、根元类、根根元类的地址是不一样的,其中元类、根元类、根根元类的地址则是一样的。这是为什么呢?
在系列文章前几章中我们了解了isa的结构,类的首地址就是isa,isa中的shiftcls存储着类的一些信息,isa在类中存在这非常重要的关系,继续通过LLDB分析之。

isa在类中用lldb分析

isa指向:对象->类对象->元类对象->根元类对象,根元类对象的isa指向根元类自身
元类可以存储类的自有信息,可供子类继承的类信息。
实例对象没有继承关系,只有类与类间才有继承关系;
根元类的父类为NSObject,NSObject 继承的地址没有,所以父类为nil。
经典isa走位图

实例对象的isa指向它的类对象Subclass,Subclass继承自它的父类Superclass,而Subclass的isa指向它的元类Subclass(meta),类和元类的地址是一样的。

拓展:objc_class vs objc_object 关系?

  • 类的底层编码是—— objc_class
  • 对象的根对象—— objc_object
  • objc_class 又继承自 objc_object ,所有class也是对象——万物皆对象。
  • 所有的对象,类,元类都有isa。
    结构体类型objc_class 继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性
    mian.cpp底层编译文件中,NSObject中的isa在底层是由Class 定义的,其中class的底层编码来自 objc_class类型,所以NSObject也拥有了isa属性
    NSObject 是一个类,用它初始化一个实例对象objc,objc 满足 objc_object 的特性(即有isa属性),主要是因为isa 是由 NSObject 从objc_class继承过来的,而objc_class继承自objc_object,objc_object 有isa属性。所以对象都有一个 isa,isa表示指向,来自于当前的objc_object
    objc_object(结构体) 是 当前的 根对象,所有的对象都有这样一个特性 objc_object,即拥有isa属性
    以下是objc-781源码objc_object定义:
    objc_object定义

objc_object 和实例对象的关系?

所有的对象都是继承NSObject,NSObject又继承自底层的objc_object(C/C++)结构体类型,底层的是没有对象而是结构体。

类的内存 + 对象的内存

先拓展:内存偏移

在探究类的内存信息时,需要用到内存偏移相关知识,所以先介绍下内存偏移。

  • 变量的地址差
    定义两个普通变量,打印它们以及变量地址
    普通变量及地址

    这个很好理解,内存上有一块数字未100的块,有两个变量都指向这个内存,虽然值相同,但是它们的变量的地址是不一样的,这是因为值发生了一个浅拷贝。但是观察到两个内存的地址偏差很小为4,这是一个int字节的长度,这个长度差取决于b的类型长度。这样对一个相同的值的指向,将其指针地址连续存储,是优化存储的一种小手段。
  • 对象的地址差
    定义一个Book类,实例化两个对象,打印它们的地址,前者为对象的指针地址,指向的事开辟的内存空间;后者的二级指针,存储的则是对象地址的地址(or指针的指针)。由于该类没有属性、成员变量,系统则将&book1,&book2存储在连续的地址,由于二级指针为纯指针,其字节长度就是isa的长度,所以两个二级指针差为8;通过这样的指针地址差是可以访问到两个相邻内存地址的。
    对象指针
  • 数组的指针差
    定义一个数组,然后定义一个指针指向这个数组
    数组指针

    &c:获取c的首地址
    &c[0]:获取c数组首个元素的地址
    &c[1]:获取c数组第2个元素的地址
    *d = c; 将c的首地址赋值给d,d+1则是对d的地址进行内存偏移,偏移1个单位则是对应于c的下一个元素地址。
    通过以上认识到:
    数组首个元素的指针地址作为数组的指针地址。*
    通过首地址的偏移,可挨个取出数组元素,偏移量则由元素类型的字节数决定。
    借用style_月月博主简单明了的一张图,能很好的理解了内存偏移
    数组指针内存偏移

类objc_object的结构信息

// 注意new 、old版本差,此版本为objc4-781
struct objc_class : objc_object {
    // Class ISA;  // 默认8字节
    Class superclass;  // 8字节
    cache_t cache;  // 8 + 4 + 2 + 2      // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    /*篇幅有限,省略N多代码,主要代码分析上面即可*/
};

提前剧透式的获取类的关键信息:class_data_bits_tclass_data_bits_t存储了类中的属性(property)、方法(method)、协议(protocol),这是类结构信息中的关键部分,通过之前的LLDB我们是可以获得类对象的首地址的,通过内存偏移的方式获取class_data_bits_t
进入cache_t源码分析之,它里面有相当多的代码,get其关键属性信息,则就是:struct cache_t的字节长度

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
    explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
    mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
    
#if __LP64__
    uint16_t _flags;  //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
    uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节

所以要想获取bits内存,就是要将类的首地址(同isa)进行内存偏移即可获取.So -> isa: 8字节 + superClass: 8字节 + cache_t( _buckets / _maskAndBuckets : 8 + _mask / _mask_unused: 4 + _flags :2 + _occupied :2 = 16)= 32字节(16进制:0x20)。

isa属性:继承自objc_object的isa,占 8字节
superclass 属性:Class类型,Class是由objc_object定义的,是一个指针,占8字节
cache属性:简单从类型class_data_bits_t目前无法得知,而class_data_bits_t是一个结构体类型,结构体的内存大小需要根据内部的属性来确定,而结构体指针才是8字节
bits属性:只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits

通过LLDB分析:
定义一个Person类

@interface Person : NSObject

@property (nonatomic,copy) NSString * name;

@property (nonatomic,copy) NSString * age;

-(void)sayHello;
+(void)sayHappy;

@end
  • 获取bits方法一:在源码中找 class_data_bits_t,打印bits探索其结构。
    源码打断点找bits
(lldb) p/x bits // 16进制打印bits内存地址
(class_data_bits_t) $0 = (bits = 0x0000000100002028)
(lldb) p $0->data()  // $0: 别名,调用data()方法
(class_rw_t *) $1 = 0x0000000100002028
  Fix-it applied, fixed expression was: 
    $0.data()
(lldb) p *$1
(class_rw_t) $2 = {
  flags = 129
  witness = 40
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 40
  }
  firstSubclass = nil
  nextSiblingClass = 0x0000000100000fa0
}
// 找到ro文件后续探索继续查看方法二找bits
  • 获取bits方法二:在调用Person * person =[Person alloc];之后下断点,通过打印类指针地址,利用内存偏移原理,加之上面分析的获取bits要在cls地址基础上偏移32字节(16进制:0x20),即在
(Class) $0 = 0x0000000100002250 LGPerson
(lldb)  p/x (class_data_bits_t *)0x0000000100002270 // 在$0基础上加0x20
(class_data_bits_t *) $3 = 0x0000000100002270 // 则可得到bits

方法二完整LLDB

(lldb) p/x person.class
(Class) $0 = 0x0000000100002250 LGPerson
(lldb)  p/x (class_data_bits_t *)0x0000000100002270 // 在$0寄出上加0x20,
(class_data_bits_t *) $3 = 0x0000000100002270
(lldb) p *$3
(class_data_bits_t) $4 = (bits = 4302628644)
(lldb) p $4->data()  /// $4: 别名,调用data()方法
(class_rw_t *) $5 = 0x000000010074e720
  Fix-it applied, fixed expression was: 
    $4.data()
(lldb) p *$5
(class_rw_t) $6 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975664
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $6.properties() // 打印第属性list
(const property_array_t) $7 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x00000001000021d8
      arrayAndFlag = 4294975960
    }
  }
}
(lldb) p $7.list
(property_list_t *const) $8 = 0x00000001000021d8
(lldb) p *$8
(property_list_t) $9 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 2
    first = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
  }
}
(lldb) p $9.get(1) // 打印第一个属性
(property_t) $10 = (name = "age", attributes = "T@\"NSString\",C,N,V_age")
(lldb) p $6.methods()
(const method_array_t) $11 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020f8
      arrayAndFlag = 4294975736
    }
  }
}
(lldb) p $11.list
(method_list_t *const) $12 = 0x00000001000020f8
(lldb) p *$12  // 获取了Person类的 method_list_t
(method_list_t) $13 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 6
    first = {
      name = "sayHello"
      types = 0x0000000100000f75 "v16@0:8"
      imp = 0x0000000100000cd0 (KCObjc`-[LGPerson sayHello])
    }
  }
}
(lldb) p $13.get(0) // 打印出对象方法 sayHello
(method_t) $14 = {
  name = "sayHello"
  types = 0x0000000100000f75 "v16@0:8"
  imp = 0x0000000100000cd0 (KCObjc`-[LGPerson sayHello])
}
(lldb) p $13.get(1)
(method_t) $15 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f75 "v16@0:8"
  imp = 0x0000000100000da0 (KCObjc`-[LGPerson .cxx_destruct])
}
(lldb) p $13.get(2)
(method_t) $16 = {
  name = "name"
  types = 0x0000000100000f89 "@16@0:8"
  imp = 0x0000000100000ce0 (KCObjc`-[LGPerson name])
}
(lldb) p $13.get(3)
(method_t) $17 = {
  name = "setName:"
  types = 0x0000000100000f91 "v24@0:8@16"
  imp = 0x0000000100000d10 (KCObjc`-[LGPerson setName:])
}
(lldb) p $13.get(4)
(method_t) $18 = {
  name = "age"
  types = 0x0000000100000f89 "@16@0:8"
  imp = 0x0000000100000d40 (KCObjc`-[LGPerson age])
}
(lldb) p $13.get(5)
(method_t) $19 = {
  name = "setAge:"
  types = 0x0000000100000f91 "v24@0:8@16"
  imp = 0x0000000100000d70 (KCObjc`-[LGPerson setAge:])
}
(lldb) p $13.get(6)
Assertion failed: (i < count), function get, file /Users/tl/逻辑教育/iOS底层/Day5/20200914-大师班第5天-类原理分析-资料/01--课堂代码/003-iskindof面试题/runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

p $13.get(6)执行时,发送了越界,方法list并没有类方法sayHappy(),这是由于对象方法存在类中,而类方法存在其元类中。下一节继续探索为什么。

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