OC底层原理--类结构分析

通过上一篇文章对isa的分析,我们知道了所有的对象都包含isa,并且isa存储了类的相关信息,所以这篇文章我们主要通过isa来引出类的底层结构以及一些信息

代码分析

创建对象

LYPerson *person = [[LYPerson alloc] init];

lldb分析

(lldb) x/4gx person
0x100513970: 0x001d80010000820d 0x0000000000000000
0x100513980: 0x0000000000000000 0x0000000000000000
(lldb) 

通过之前的分析,我们知道所有对象的第一个属性都是isa,故上面的0x001d80010000820disa的地址,我们继续通过打印isa的地址来看下

(lldb) po 0x001d80010000820d & 0x00007ffffffffff8ULL
LYPerson
(lldb) p/x 0x001d80010000820d & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100008208
(lldb) po 0x0000000100008208
LYPerson

这里需要& 0x00007ffffffffff8ULL主要是因为isa只有中间的44(或33)位才是类信息,所以需要把其它位置为0,通过上面的地址打印我们得出结论:对象的isa指向类信息,那么我们继续来打印下类的isa看看是什么

(lldb) x/4gx 0x0000000100008208
0x100008208: 0x00000001000081e0 0x00007fff98b0e118
0x100008218: 0x0000000100513990 0x0001802400000003
(lldb) p/x 0x00000001000081e0 & 0x00007ffffffffff8ULL
(unsigned long long) $5 = 0x00000001000081e0
(lldb) po 0x00000001000081e0
LYPerson

这里我们发现类的isa只想的信息,打印出来也是LYPerson,通过内存地址我们可以肯定此处的LYPerson跟上一步的LYPerson不是同一个,但是类在内存中又只会存在一份,因此我们引出了元类的概念,至此,我们得出结论:类的isa指向元类信息.
接下来我们给出类在内存中只存在一份的证明以及元类的说明

//MARK:--- 分析类对象内存 存在个数
void testClassNum(){
    Class class1 = [LYPerson class];
    Class class2 = [LYPerson alloc].class;
    Class class3 = object_getClass([LYPerson alloc]);
    NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
}
image.png
  • 元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类(类似于对象的归属是类)

  • 元类是类对象的类,每个类都有一个独一无二的元类用来存储类方法的相关信息。

  • 元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称

接下来我们继续分析下元类的isa

(lldb) x/4gx 0x00000001000081e0
0x1000081e0: 0x00007fff98b0e0f0 0x00007fff98b0e0f0
0x1000081f0: 0x00000001006471f0 0x0003e03500000007
(lldb) p/x 0x00007fff98b0e0f0 & 0x00007ffffffffff8ULL
(unsigned long long) $7 = 0x00007fff98b0e0f0
(lldb) po 0x00007fff98b0e0f0
NSObject

通过打印isa我们发现LYPerson的元类的isa指向了NSObject,那么此处的NSObject是不是就是我们的根类呢?我们来证明下

(lldb) p/x NSObject.class
(Class) $9 = 0x00007fff98b0e118 NSObject

很明显,两个NSObject并不是同一个类,因此我们认定上面的NSObject根元类,这里我们得出结论:元类的isa指向根元类.接下来我们继续打印根元类的isa

(lldb) x/4gx 0x00007fff98b0e0f0
0x7fff98b0e0f0: 0x00007fff98b0e0f0 0x00007fff98b0e118
0x7fff98b0e100: 0x00000001020081b0 0x0005e03100000007
(lldb) p/x 0x00007fff98b0e0f0 & 0x00007ffffffffff8ULL
(unsigned long long) $11 = 0x00007fff98b0e0f0
(lldb) po 0x00007fff98b0e0f0
NSObject

这里我们发现根元类的isa与它本身的地址相同,由此我们得出结论:根元类的isa指向它本身

isa & 继承链

通过上面的分析再加上我们的继承关系,于是就有了下面这个著名的图


image.png

上面这幅图,对于isa的走位我们已经通过代码证明过了,没有任何问题.那么关于集成链有两点需要注意下

  • 根元类的父类为根类
  • 根类的父类为nil

以上两点也说明了NSObject做为基类,是万物起源,我们可以看下底层编译代码

struct NSObject_IMPL {
    Class isa;
};

// 结构体
typedef struct objc_class *Class;

Class分析

源码分析

通过上面的分析我们来到了我们非常熟悉的Class,接下来我们就来具体分析下Class的定义objc_class的源码实现

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

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
    //....方法部分省略,未贴出
}

通过源码我们可以发现objc_class中包含的元素主要有ISA,superclass,cache,bits,对与ISA我们已经了解了,superclass很明显是父类,也没什么好分析的,那么剩下的cachebits,cache主要是缓存数据,我们可以放到后面,这里主要还是分析下bits里面有哪些内容,那么如何获取到bits呢,这里我们补充下个知识点--内存偏移

内存偏移

偏移地址是指段内相对于段起始地址的偏移值,
例如一个存储器的大小是1KB,可以把它分为4段,第一段的地址范围就是0—255,第二段的地址范围就是256-511,依次类推。

我们拿数组来举例

//数组指针
int c[4] = {1, 2, 3, 4};
int *d = c;
NSLog(@"%p -- %p - %p", &c, &c[0], &c[1]);
NSLog(@"%p -- %p - %p", d, d+1, d+2);
0x7ffeefbff560 -- 0x7ffeefbff560 - 0x7ffeefbff564
0x7ffeefbff560 -- 0x7ffeefbff564 - 0x7ffeefbff568

通过上面我们可以得出:数组的首地址即位第一个元素的地址,第二个元素的地址即为第一个元素的地址加上元素类型所占的字节大小,int占用4个字节,即+4,若为double+8

综上,内存偏移就可以理解为首地址加偏移量

class_data_bits_t bits分析

在了解了内存偏移后,我们来尝试下获取bits,首先ISAsuperclass都是Class类型,Class是结构体指针类型,占用8个字节,所以加一起是16个字节,接下来我们分析下cache,源码如下

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个字节

通过源码,我们得出cache占用16个字节,因此bits距首地址偏移32个字节,接下来我们通过lldb进行调试

// 打印LYPerson的内存地址
(lldb) x/4gx LYPerson.class
0x100008200: 0x00000001000081d8 0x000000010034c140
0x100008210: 0x000000010184dd90 0x0001802400000003
// 首地址为0x100008200,偏移32位获取class_data_bits_t
(lldb) p (class_data_bits_t *)0x100008220
(class_data_bits_t *) $30 = 0x0000000100008220
(lldb) 

通过objc_class源码我们可以发现bits中存储的信息为class_rw_t结构体类型,可以通过data()方法获取.源码如下

struct objc_class : objc_object {
    ...
    class_rw_t *data() const {
        return bits.data();
    }
    ...
}

我们继续通过lldb调试

(lldb) p $30->data()
(class_rw_t *) $31 = 0x0000000100645c60
(lldb) p *$31
(class_rw_t) $32 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000216
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}

接下来通过查看class_rw_t的源码,我们发现,class_rw_t存储了属性列表,方法列表,协议,ro(class_ro_t类型)等信息,如下图

image.png

接下来,继续通过lldb调试

// 获取属性列表
(lldb) p $32.properties()
(const property_array_t) $33 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x00000001000081a8
      arrayAndFlag = 4295000488
    }
  }
}
// 获取属性数组
(lldb) p $33.list
(property_list_t *const) $34 = 0x00000001000081a8
// 打印具体值
(lldb) p *$34
(property_list_t) $35 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
  }
}

(lldb) p $32.methods()
(const method_array_t) $36 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000080e0
      arrayAndFlag = 4295000288
    }
  }
}
(lldb) p $36.list
(method_list_t *const) $37 = 0x00000001000080e0
(lldb) p *$37
(method_list_t) $38 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 5
    first = {
      name = "sayName"
      types = 0x0000000100003f78 "v16@0:8"
      imp = 0x0000000100003d60 (KCObjc`-[LYPerson sayName])
    }
  }
}
(lldb) p $38.get(0)
(method_t) $39 = {
  name = "sayName"
  types = 0x0000000100003f78 "v16@0:8"
  imp = 0x0000000100003d60 (KCObjc`-[LYPerson sayName])
}
(lldb) p $38.get(1)
(method_t) $40 = {
  name = "saySex"
  types = 0x0000000100003f78 "v16@0:8"
  imp = 0x0000000100003d90 (KCObjc`-[LYPerson saySex])
}
(lldb) p $38.get(2)
(method_t) $41 = {
  name = ".cxx_destruct"
  types = 0x0000000100003f78 "v16@0:8"
  imp = 0x0000000100003e10 (KCObjc`-[LYPerson .cxx_destruct])
}
(lldb) p $38.get(3)
(method_t) $42 = {
  name = "name"
  types = 0x0000000100003f8c "@16@0:8"
  imp = 0x0000000100003dc0 (KCObjc`-[LYPerson name])
}
(lldb) p $38.get(4)
(method_t) $43 = {
  name = "setName:"
  types = 0x0000000100003f94 "v24@0:8@16"
  imp = 0x0000000100003de0 (KCObjc`-[LYPerson setName:])
}
(lldb) p $38.get(5)
Assertion failed: (i < count), function get, file /Users/LY/Desktop/LY/objc4_debug/objc4-781/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.
(lldb) 

通过上面获取到的properties我们得到了类的所有属性列表,但是却并没有找到对应的成员变量的列表,于是我们通过查看上面的ro的源码可以发现有一个ivars属性,同样的方式分析ro我们找到了所有的成员变量,这里不做赘述.大家可以自行尝试

通过上面获取到的methods我们得到了类的所有实例方法列表,但是却并没有找到对应的类方法的列表,根据我们最开始探索的isa指向,我们知道对象的isa指向类,类的isa指向元类.那么现在对象方法存在了类中,会不会类方法存在元类中呢,于是我们通过上面的步骤对元类进行调试,发现类方法确实存在元类中,这里不做赘述.大家可以自行尝试

通过上面的调试我们得出如下结论

  • 类中存储了类的所有属性列表,``方法列表,协议等信息在class_rw_t`中
  • 类的方法列表中除了实例方法还包括settergetter方法
  • 类方法并不存在类信息中而是存在元类中
  • 成员变量存在class_ro_tivars

至此,对于类的结构以及类中的class_data_bits_t存储了哪些信息我们已经基本都了解了,这篇文章就先到这里了,后续我们再对类的cache进行分析.

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