OC底层原理九:类的原理分析

OC底层原理 学习大纲

上一节我们了解了isa内部结构,了解了结构的关系。

  • 现在,我们用代码来探究下isa的指针指向与的关系

1. 类与isa指针的关系

objc4源码中,加入测试代码。在NSLog打印出加上断点

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson * person = [[HTPerson alloc]init];
        
        NSLog(@"%@", person);
    }
    return 0;
}
  • 准备好isa位运算的MASK(遮罩)。
    image.png

p person打印,x/4gx打印,p/x&与上ISA_MASK、打印获取到的isa中的shiftcls地址。成功找到HTPerson

image.png

如果看不到,需要回看上一节。了解isa内部构造和获取类地址的方法。

  • 我们猜想,既然拿到了HTPerson类的isa地址,那HTPerson类的isa指针指向哪里呢?
    image.png

我们发现,0x00000001000026300x0000000100002608都打印出来是HTPerosn的。2个地址不一样为什么打印出来结果一样?

  • 类地址不应该是唯一的吗?

  • 这个问题我们保留。下面再一起回答。

  • 现在,我想继续顺着这根藤(isa指针方向),看可以摸到哪个类去。

NSObject

我们发现,一直顺着摸,摸到NSObject后,内存地址不再发生变化。

为了解答上面2个不同地址都是打印了HTPerson的问题。我们需要先了解一个新东西: 👇

2. 元类(Meta)

元类的定义和创建都由系统控制,由编译器自动完成,不受我们管理

对象的isa来自于也是对象。那isa指向哪里呢?

  • 答案: 元类 。类的归属来自于元类。

类既然是对象,就需要管理方法属性的存储和归属。而这个管理者,就是元类(Meta)

上面2个不同地址都打印HTPerson的问题,实际上打印路径是: HTPerosn -> HTPerson元类 -> NSObject

问题: 既然你说打印到了元类, 那HTPerson元类NSObject之后,为什么就结束了? 不应该再打印一次NSObject元类吗?

  • 我们顺着上面代码。打印一次p/x [NSObject class]:
image.png

发现[NSObject class]打印的地址与之前打印的地址不一致

  • 因为 [NSObject class]打印的是NSObject本类,而HTPerson元类的父类是NSObject元类。所以地址不一样。
  • 我们验证一下。
image.png

果然,NSObjectisa指针指向了NSObject元类。 地址和HTPerson元类isa指针地址一致。

那么,类在内存中会存在多份(多地址)吗?

#import <objc/runtime.h>
#import "HTPerson.h"

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        Class class1 = [HTPerson class];
        Class class2 = [HTPerson alloc].class;
        Class class3 = object_getClass([HTPerson alloc]);
        Class class4 = [HTPerson alloc].class;
        
        NSLog(@"%p", class1);
        NSLog(@"%p", class2);
        NSLog(@"%p", class3);
        NSLog(@"%p", class4);
        
    }
    return 0;
}
image.png

我们多种方式读取类,通过打印可以发现,所有地址都一样。

  • 类的信息在内存中永远只存在一份
问题: 不直接继承NSObject类,Isa是如何指向的呢?
@interface HTMan : HTPerson
@end

@implementation HTMan
@end

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTMan * man = [[HTMan alloc]init];
        
        NSLog(@"%p", man);
        
    }
    return 0;
}

代码中HTMan继承自HTPerson,而HTPerson继承自NSObject

image.png

但是isa的指向,却是HTMan->HTMan元类->NSObject元类

总结
  • 类的信息在内存中永远只存在一份
  • 类的isa指针首先指向自己的元类,再直接指向NSObject元类
    (自己类->自己元类->NSObject元类)
  • NSObject元类的isa也指向NSObject元类
  • NSObject元类是所有元类的始祖。所以也叫根元类
isa指向

误区

  • 上述是isa的指针指向,并非类的继承关系。
  • 继承关系,实例对象无继承关系。
  • NSObject没有父类(父类为Null)。
    所以我们类的继承,溯源只需要找到NSObject。
    OC语言中:NSObject是对象的始祖。万物皆对象
  • NSObject根元类isa指针是直接指向NSObject根元类

3. OC对象的本质

首先了解2个结构体: objc_object (根对象)和 objc_class(根类)

我们打开objc4源码,搜索struct objc_object

objc_object部分代码

搜索struct objc_class

objc_class部分代码

源码搜索时,注意看结构体尾部的声明。 UNAVAILABLE已废弃的不要耗费精力了。

image.png

我们发现,objc_class继承自objc_object

  1. object_object拥有isa属性,所以objc_class也拥有isa

  2. 万物皆对象(object)。

使用方法:
  • struct objc_object * Object 以objc_object为模板,定义一个对象
  • struct objc_class * Class 以objc_class为模板,定义一个类

3. 类的结构分析

老规矩,先从案例下手,看懂了再总结

#import <Foundation/Foundation.h>
#import "HTPerson.h"

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson * person = [[HTPerson alloc]init];
        
        NSLog(@"%p", person);
        
    }
    return 0;
}
  • p/x打印HTPerson类地址。 -> x/4gx 打印内存信息
image.png

请问:

1. 为什么第一个地址是HTPeroson?第二个是NSObject

  • objc_class第一个地址存放的是isa地址,第二个存放的是superclass类地址
    (参考上面objc_object部分代码objc_class部分代码图。)

2. 第二个地址打印的是NSObject类还是元类?

image.png

  • NSObject.class打印的地址与第二个地址打印的一致。 与接着打印的NSObject元类地址不一致。 说明第二个地址打印的是NSObjetc自身类。

3. 内存偏移

老规矩,先从案例开始,在main.m文件中加入测试代码:

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        int f[4] = {10,20,30,40};

        NSLog(@"%p", &f);
        NSLog(@"%p", &f[0]);
        NSLog(@"%p", &f[1]);
        NSLog(@"%p", &f[2]);
        NSLog(@"%p", &f[3]);

    }
    return 0;
}

在打印的尾部加入断点

image.png

我们发现:

    1. &f&f[0]内存地址一样。 证明对象内存地址就是使用内部首元素的地址
    1. fint类型的数组,内部元素每位占用4字节。 所以每个元素内存偏移值4

是不是瞬间有个小想法:

我们是否可以根据数据类型,确定内存占用空间的大小,通过内存值偏移,就可定位下一元素的指针地址。然后直接取出这个指针地址指向的

小拓展: 如何通过地址取出对应的

  • 知识点: 指针(地址)的指针

  • 地址强转为指定类型(int) -> 加*读取对象(指针)的指针 -> 打印目标值

image.png

快速读取: p *((int *)0x7ffeefbff5b0)

image.png

我们通过首地址偏移,果然:

image

完美👍

但前提是我们必须知道偏移值是多少,也就是存储的是什么类型

  • 上面我们使用的是地址偏移,所以必须加入偏移值
  • 我们也可以使用指针偏移。直接在属性层在进行操作。
image.png

至此。我们已经掌握了地址偏移指针偏移

  • 只有知道属性类型,占用空间大小,才能使用地址偏移指针偏移,读取类的所有信息。

现在,我们来分析类的结构

4. 分析类结构

objc4源码中,搜索objc_class

  • 剔除不会占用类空间的constvoidstatic函数
struct objc_class : objc_object {
    // Class ISA;  。                          // 指针 - 占用8字节
    Class superclass;                          // 指针 - 占用8字节
    cache_t cache;                             //  ?
    class_data_bits_t bits;                  
};

如果我们要获取bits的首地址位置。只有cache_t类型不知道空间大小。 点进去看看:

  • 剔除不会占用类空间的constvoidstatic函数
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

};
  • 我们进入explicit_atomic查看:
image.png
  • 发现返回的类型就是传入的泛型T。 所以explicit_atomic的类型只跟他传入的类型相关。
image.png
  • 我们进入bucket_t 结构体:
image.png

这里的_sel_imp 是不是很眼熟。 😃 后续我们会详细介绍

进入uintptr_t

image.png

  • 发现 uintptr_t实际就是unsigned long,64位操作操作系统下占用8个字节

所以我们_buckets实际大小就是8字节

接下来我们看mask_t

image.png

  • 点进去


    image.png

发现它就是uint32_t类型。占用4字节

汇总一下:

image.png
  • 现在系统都是64位系统。

  • 所以cache_t内存大小为: 12 + 2 + 2 = 16 字节

回头继续分析object_class的内存大小

struct objc_class : objc_object {
    // Class ISA;  。                          // 指针 - 占用8字节
    Class superclass;                          // 指针 - 占用8字节
    cache_t cache;                             //       占用16字节
    class_data_bits_t bits;                   
};

所以我们得出结论。

  • object_class内部的找到bits,需要偏移8 + 8 + 16 = 32

现在,我们来读取bits

5. 读取bits

  • x/4gx HTPerson.class打印内存地址
  • p/x 0x0000000100002390 + 32 获取bits地址
  • p (class_data_bits_t *)$12 强转为class_data_bits_t类型
image.png

objc_class结构中,我们发现bits.data()返回的是class_rw_t类型

image.png

我们打印bits->data()检验一下

image.png

成功拿到,打印HTPerson的class_rw_t信息:

image.png

  • 我们想要寻找这个类的方法属性,但是发现只有一个ro_or_rw_ext

我们进入class_rw_t内部, 折叠所有函数。

Xcode折叠所有函数:command + shift + ←

image.png

6. 读取methods、properties和protocols

main.m加入测试代码:

@interface HTPerson : NSObject {
    NSString * hobby;
}

@property(nonatomic, copy) NSString * name;
@property(nonatomic, copy) NSString * nickname;

+(void) ClassFunction;

-(void) objectFunction;

@end

@implementation HTPerson

+(void) ClassFunction {
    NSLog(@"类方法");
}

-(void) objectFunction{
    NSLog(@"对象方法");
}

@end

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson * person = [[HTPerson alloc]init];
        
        NSLog(@"%p", person);
    }
    return 0;
}

NSLog处加入断点运行代码。快速找到data()对象的位置:

  • p/x HTPerson.class
  • -> p/x (class_data_bits_t *)(0x0000000100002258 + 32)
  • -> p $1->data()
image.png
methods

利用$2对象打印p $2->methods()

image.png

我从图中看到了list。继续打印p *$3.list:

image.png

  • 看到count有6个值,我们使用get打印一下:
image.png
properties

利用$2对象打印p $2->properties()

接下来打印p * properties()属性方法。

image.png

protocols

利用$2对象打印p $2->protocols()

image.png
成员变量hobby去哪了?
image.png

打印ro

image.png

打印p *$23.ivars

image.png

打印每一个成员变量p $25.get(0)

image.png
  • 我们发现,除了hobby变量。所有property属性,都自动生成了带下划线的成员变量。
为什么案例中的ClassFunction类方法没出现在Methods中?

对象的isa指向自己的类,所以对象方法存放到了HTPerosn中,难道类方法存放在它的isa上一级?HTPerson元类中

实践出真知:

  • p HTPerson.class -> x/4gx $0 -> p/x (class_data_bits_t *)(0x0000000100002230 + 32) -> p $1->data() -> p $2->methods() -> p $3.list -> p *$4 -> p $5.get(0)
    image.png

果然。 对象的方法存在中,的方法存在元类

下一章

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