iOS底层原理_04:类的原理分析(上)

第四节课 类的原理分析(上)

isa分析到元类

p/x p  //拿到当前指针地址

04-拿地址.png

我们通过x p指令可以验证下,看到打印出的地址与我们拿到的地址一模一样。

接下来我们
x/4gx 0x000000010292f480

04-isa.png

0x001d800100008365 就是我们的对象的isa

p/x 0x001d800100008365 & 0x00007ffffffffff8 po 0x0000000100008360 //就是我们当前的类

04-当前的类.png

其实最后还是这么个地址,有那么一丢丢好奇,再次x/4g会得到什么,干就完了~

04-x:4g类.png

可以看到,打印出来的结果里跟上面的指针对象拥有同样的内存结构,那第一个地址是啥?还是isa嘛?
04-po.png

又一次得到LGPerson,0x0000000100008360LGPerson0x0000000100008338还是LGPerson,小朋友,你是否有很多问号??
猜想:类会和我们的对象无限开辟,内存不止有一个类
接下来就来验证下我们的猜想

    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);
    
    <----输出结果---->
0x100008360 
0x100008360 
0x100008360 
0x100008360

我们可以看到最终输出的类都是0x100008360,那就证明之前的0x0000000100008338它不是类,那么它是个什么?接下来我们就需要使用烂苹果来分析了

04-烂苹果.png

在这里我们看到了熟悉的东西,但是并没有看到0x0000000100008338这个东西

当我们看到符号表的时候看到了一个奇奇怪怪的东西


04-METACLASS.png

这个METACLASS实际上并不是我们通过代码创建的,这个是系统或者编译器帮我们生成好的,这个东西就叫做元类
我们得到了一个大概的流程:对象isa -> 类isa -> 元类

isa走位图和继承链

isa走位图

上面我们通过isa分析出了一个元类,那我们就想,元类之后会不会还有东西?
继续用上面元类的isa

04-根元类.png

po打印输出的是NSObject,&上我们的掩码,又是他本身,这个东西就是我们的根元类
对象isa -> 类isa -> 元类 -> 根元类
我们看到了NSObject有点奇怪,那我们就跟直接打印的对比下
04-NSObject.png

我们发现,通过NSObject.class获取到的地址与刚才po的地址并不相同,但是我们继续往下x/4g的时候发现isa的位置又与po的地址相同了。就相当于是NSObject.class的isa -> NSObject的元类,只有两层
根类 isa -> 根元类 isa
04-isa流程图.png

根据上面我们分析的过程可以看到isa的链路如上图虚线部分。同时我们也可以使用代码来进行验证一下

    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);
    
    // LGPerson元类
    Class pMetaClass = object_getClass(LGPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"%@ - %p",psuperClass,psuperClass);
    
    
<----输出结果---->
0x100547790 实例对象
0x7fff9103a118 类
0x7fff9103a0f0 元类
0x7fff9103a0f0 根元类
0x7fff9103a0f0 根根元类

NSObject - 0x7fff9103a0f0

继承链

根据上面的打印结果,我们发现任何对象,元类的父类都是根元类,有点烧脑了吧?
接下来我们写了一个LGTeacher继承自LGPerson,再次通过上面代码进行打印

    // LGTeacher -> LGPerson -> NSObject
    // 元类也有一条继承链
    Class tMetaClass = object_getClass(LGTeacher.class);
    Class tsuperClass = class_getSuperclass(tMetaClass);
    NSLog(@"%@ - %p",tsuperClass,tsuperClass);
    <----输出结果---->
    LGPerson - 0x100008338

元类的继承链如下图实线部分
[图片上传失败...(image-743b09-1624451069459)]

我们继续分析NSObject特殊情况

    // NSObject 根类特殊情况
    Class nsuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"%@ - %p",nsuperClass,nsuperClass);
    // 根元类 -> NSObject
    Class rnsuperClass = class_getSuperclass(metaClass);
    NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
    <----输出结果---->
(null) - 0x0
 NSObject - 0x7fff9103a118

根据上述结果,我们推出了图中RootClass(class) -> nil的这一步
以及RootClass(meta) -> RootClass(class)这一步

小结:

  1. NSOject对象的元类根元类同一个
  2. 元类间存在着继承的关系,跟类是一样的
  3. 根元类的父类指向了NSObject
  4. NSObject的父类是(null),地址为0x0,即NSObject没有父类

指针和内存平移

如果要想获取对象内存中的变量,底层实现方式是对象的首地址+偏移值。下面探究下内存偏移
我们平时遇到的指针分为三种:普通指针、对象指针、数组指针

普通指针

int a = 10; 
int b = 10; 
NSLog(@"%d -- %p",a,&a);
NSLog(@"%d -- %p",b,&b);
<----输出结果---->
10 -- 0x7ffeefbff41c
10 -- 0x7ffeefbff418
  • 我们发现,值是一样的,但是指针地址却是不同的,这就是我们所说的copy:值拷贝
  • a的地址是0x7ffeefbff41cb的地址是0x7ffeefbff418,相差4字节,主要取决于a的类型

对象指针

HZMPerson *p1 = [HZMPerson alloc];
HZMPerson *p2 = [HZMPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);
<----输出结果---->
<HZMPerson: 0x1007049f0> -- 0x7ffeefbff410
<HZMPerson: 0x100704650> -- 0x7ffeefbff408

我们发现,对象的地址不同,地址指向的空间也不同

数组指针

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);
<----输出结果---->
0x7ffeefbff430 - 0x7ffeefbff430 - 0x7ffeefbff434
0x7ffeefbff430 - 0x7ffeefbff434 - 0x7ffeefbff438

  • 数组的地址就是数组元素中的首地址,即&c&c[0]都是首地址
  • 数组中每个元素之间的地址间隔,由当前元素的数据类型决定
  • 数组的元素地址可以通过首地址+n*类型大小方式,这种方式是数组中的元素类型必须相同
  • 数组元素不相同用首地址+偏移量方式,根据当前变量的偏移值(需要前面类型大小相加)

小结:

  • 内存地址就是内存元素的首地址
  • 内存偏移可以根据首地址+ 偏移值方法获取相对应变量的地址

源码分析类的结构

在之前的探索时我们发现isaClass类型的。Class类型是objc_class *objc_class是一个结构体。 所有的Class底层实现都是objc_class。又来到了我们的源码分析部分,在 objc4-818.2 中全局搜索objc_class 代码如下

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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

    Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
#   if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
        if (superclass == Nil)
            return Nil;
            
//省略部分源码
}

objc_class这个结构体内部存储了几个成员变量,怎么去探究它们呢?类的地址是知道的,那么就根据上面探究过的首地址+偏移值来获取里面的成员变量的地址,然后获取值。但是偏移值需要知道当前变量之前的所有成员变量的大小

  • Class ISA://结构体指针占8字节 继承自objc_object
  • Class superclass:结构体指针占8字节
  • cache_t cache:??母鸡
  • class_data_bits_t bits;

计算 cache 类的内存大小

接下来我们就先看cache的内存大小,查看cache_t

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;   // 4
#if __LP64__
            uint16_t                   _flags;       // 2
#endif
            uint16_t                   _occupied;    // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; //8
    };
    
    //下面是一些方法省略
};

cache_t结构体类型,有两个成员变量_bucketsAndMaybeMask和一个联合体

_bucketsAndMaybeMaskuintptr_t无符长整型占8字节

联合体里面有两个成员变量结构体_originalPreoptCache,联合体的内存大小由成员变量中的最大变量类型决定
_originalPreoptCache 是结构体指针占8字节
结构体中有_maybeMask_flags_occupied_maybeMaskuint32_t4字节_flags_occupieduint16_t 各占2字节结构体大小是8字节

cache_t的内存大小是 8+8 或者是8+4+2+2都是16字节,所以cache就是16字节

*isa的内存地址是首地址,前面已经探究了

  • superclass的内存地址是首地址+0x8
  • cache的内存地址是首地址+0x10
  • bits的内存地址是首地址+0x20

具体cache里面的内容我们后续再进行讲解,我们先看看bits这个东西

获取bits

所以有上述计算可知,想要获取bits的中的内容,只需通过首地址平移32字节即可

以下是通过lldb命令调试的过程


04-bits.png

ps:获取类的首地址有两种方式

  1. 通过p/x LGPerson.class直接获取首地址
  2. 通过x/4gx LGPerson.class,打印内存信息获取

$2指针的打印结果中可以看出bits中存储的信息,其类型是class_rw_t,也是一个结构体类型。但我们还是没有看到属性列表、方法列表等,需要继续往下探索

探索 属性列表,即 property_list

通过查看class_rw_t定义的源码发现,结构体中有提供相应的方法去获取 属性列表、方法列表等,如下所示

04-class_rw_t.png

在获取bits并打印bits信息的基础上,通过class_rw_t提供的方法,继续探索 bits中的属性列表,以下是lldb探索的过程图示

04-获取属性列表2.png

  • p $3.properties()命令中的propertoes方法是由class_rw_t提供的,方法中返回的实际类型为property_array_t

  • 由于list的类型是property_list_t,是一个指针,所以通过 p *$6获取内存中的信息,同时也证明bits中存储了 property_list,即属性列表

  • p $7.get(2),想要获取LGPerson中的下一个成员变量,发现提示数组越界了,说明 property_list 中只有两个属性

目前我们的LGPerson中只有两个属性,那么我们添加一些其他的东西后,property_list会有什么变化呢?

04-新增成员变量.png

我们新增了一个成员变量,重复上面步骤结果发现,还是只能取出两个属性,由此可得出property_list 中只有属性,没有成员变量与方法。那成员变量哪去了?

通过查看objc_classbits属性中存储数据的类class_rw_t的定义发现,除了methods、properties、protocols方法,还有一个ro方法,其返回类型是class_ro_t,通过查看其定义,发现其中有一个ivars属性,我们大胆猜测:是否成员变量就存储在这个ivar_list_t类型的ivars属性中呢?下图是lldb命令的调试流程

04-成员变量.png

通过{}定义的成员变量,会存储在类的bits属性中,通过bits -> data() ->ro() -> ivars获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量
通过@property定义的属性,也会存储在bits属性中,通过bits -> data() -> properties() -> list获取属性列表,其中只包含属性

探索methods_list

上面我们新增了成员变量,现在我们添加两个方法来看看


04-新增方法.png

继续上面的lldb调试


04-methodslist.png

但是我们发现直到我们取到越界,打印出来的方法都是空的方法,这是为啥?
这个其实是因为
property_t的底层是存在两个成员变量的

struct property_t {
    const char *name;
    const char *attributes;
};

而我们的method_t则没有

struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    }; 

但是我们可以看见 有一个结构体big,里面则存在着成员变量name,我们取一下试试

04-big.png

可以取出来了,但是发现并没有类方法。在之前,我们曾提及了元类,类对象isa指向就是元类元类是用来存储类的相关信息的,所以我们大胆的猜一下:是否类方法存储在元类的bits中呢?可以通过lldb命令来验证我们的猜测。下图是lldb命令的调试流程

04-类方法.png

通过打印,我们看到了之前的类方法,证明了我们的猜想成立

实例方法存储在类的bits属性中类方法存储在元类的bits属性中。通过bits/元类bits -> methods() -> list获取实例方法列表

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容