美文网首页
iOS 类的结构分析(上)

iOS 类的结构分析(上)

作者: 远方竹叶 | 来源:发表于2020-09-14 15:40 被阅读0次

类的初探

在我们平常的 iOS 开发中,类和对象是出现很高频的名词,在之前的isa 底层结构分析 中介绍了对象,那么类到底是什么呢?它的内部结构如何?怎么探索呢?今天我们就来揭开它神秘的面纱

准备工作

首先我们定义两个类,LCPerson,LCTeacher,且 LCTeacher 继承于 LCPerson

  • LCPerson 类,继承自 NSObject
@interface LCPerson : NSObject {
    NSString *name;
}

@property (nonatomic, copy) NSString *nickname;

- (void)sayHello;
+ (void)say666;

@end

@implementation LCPerson

- (void)sayHello {
}

+ (void)say666 {
}

@end
  • LCTeacher 类,继承自 LCPerson
@interface LCTeacher : LCPerson
@end

@implementation LCTeacher
@end
  • 在 main 函数中添加如下代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LCPerson *person = [LCPerson alloc];
        LCTeacher *teacher = [LCTeacher alloc];
        NSLog(@"%@ - %@", person, teacher);
    }
    return 0;
}

LLDB 调试

  • 首先我们打印 person 的信息以及内存地址
  • 根据 isa 底层结构分析 我们知道对象的第一个属性是 isa ,里面存储的类信息(类的指针地址),我们可以通过它的 16 进制地址 与上 ISA_MASK 就可以得到 person 的类LCPerson,并打印类的指针地址
  • 我们再来看下 LCPerson 的内存情况,并打印 LCPerson 内存中 isa 指针指向的内容
  • 这个打印的依然是 LCPerson ,继续之前的操作,如下

在以上的过程中,为什么会打印两次 LCPersonNSObject,再之后就一直打印 NSObject

  • 0x001d800100002195personisa 指针地址,其 & 上后得到的是 person 的类 LCPerson

  • 0x0000000100002168LCPerson 类的isa的指针地址,指向的是 LCPerson 的元类

  • 0x00000001003330f0LCPerson 的元类 的 isa 指针地址,指向的是 根元类

  • 根元类的 isa 指向的是它自己

所以,上述打印两个 LCPerson 的根本原因就是因为元类导致的

元类

  • 对象的 isa 是指向类,类其实也是一个对象,可以称为类对象,它的 isa 指向苹果定义的 元类

  • 元类 是系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于 元类

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

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

类存在几份

从上面的我们可以看到,最后的根元类打印了很多份,那么他们是同一份吗?与我们开发中的 NSObject 是同一份吗?

验证方式

通过三种不同的方式获取类,看他们打印的地址是否相同

Class class1 = [LCPerson class];
Class class2 = [LCPerson alloc].class;
Class class3 = object_getClass([LCPerson alloc]);
NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);

运行代码,打印结果如下

从结果中可以看出,打印的地址都是同一个,所以 NSObject 只有一份,即 NSObject(根元类)在内存中永远只存在一份

总结:类的信息在内存中永远只存在一份,所以 类对象 只有一份

isa 的走位

根据上面的探索以及各种验证,对象元类根元类 的关系如下图所示

⚠️实例对象之间没有继承关系,类之间才有继承关系

objc_object vs objc_class

为什么 对象 都有 isa 属性呢?首先我们去源码查看他们的底层结构

  • NSObject 的底层编译是 NSObject_IMPL 结构体

    • Classisa 指针的类型,是由 objc_class 定义的类型
    • objc_class 是一个结构体指针。在 iOS 中,所有的 (Class) 都是以 objc_class 为模板创建的
  • Class 的源码

typedef struct objc_class *Class;
  • 源码中搜索 objc_class 的定义
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

    class_rw_t *data() const {
        return bits.data();
    }
    ...//很长,省略
}

⚠️objc_class 的定义有两个版本,早期的版本已经被废弃,使用最新本的定义是在 objc-runtime-new.h 中,后面的分析也是基于最新版的

此时,我们可一得知,objc_class 是一个继承于 objc_object 的结构体,在源码中搜索 objc_object 的定义

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
  • objc_object 也是一个结构体,而且它的成员只有一个 isa,那么 objc_class 通过继承 objc_object 也会获取到唯一的 isa 属性

objc_object 与 对象 的关系

  • 所有的 对象 都是以 objc_object 为模板继承过来的
  • 所有的 对象 是 来自 NSObject(OC) ,但是真正到底层的 是一个 objc_object(C/C++)的结构体类型

总结

  • 所有的 对象 + + 元类 都有 isa 属性
  • 所有的 对象 都是由 objc_object 继承来的
  • 在结构层面可以通俗的理解为 上层OC底层 的对接:
    • 下层 是通过 结构体 定义的 模板,例如 objc_classobjc_object
    • 上层 是通过底层的 模板创建 的 一些类型,例如 LCPerson

类的内存分布

在探索之前我们并不知道类的结构是什么样子的,进入源码

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 有 三个属性:superclasscachebits,因为继承自 objc_object,所以它会有一个默认的属性 isa,如果我们想要查看属性 bits 的内部结构,此时我们要怎么操作呢?

我们知道,获取一个类,就是获取它的首地址(即 isa),然后根据首地址平移前面属性占用的内存和来获取其他的属性(即属性 bits 的地址=首地址 + isa 的内存大小 + superclass 的内存大小 + cache 的内存大小)

拓展-内存偏移

1.普通变量
int a = 10; 
int b = 10;
NSLog(@"%d -- %p", a, &a);
NSLog(@"%d -- %p", b, &b);

打印结果如下

  • a、b 都指向10,但是 a、b 的地址不一样,这是一种值拷贝,也称为浅拷贝
  • a、b 的地址之间相差 4 个字节,这取决于 a、b 的类型
2.对象指针
LCPerson *p1 = [LCPerson alloc]; 
LCPerson *p2 = [LCPerson alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &p2);

打印结果如下

  • p1、p2 是指针,p1 是指向 [LCPerson alloc] 创建的空间地址,即内存地址,p2 同理

  • &p1、&p2 是 指向 p1、p2 对象指针的地址,这个指针是 二级指针

3.数组指针
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);

打印结果如下

  • &c 和 &c[0] 都是取首地址,即数组名等于首地址
  • &c 与 &c[1] 相差4个字节,地址之间相差的字节数,主要取决于存储的数据类型
  • 可以通过 首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于 偏移量 * 数据类型字节数

获取 bits

计算前面三个属性的内存大小

  • isa 占 8 字节
  • superclass 也是 Class 类型,所以占 8 字节
  • cache 占 16 字节
    • static 类型 和 函数方法 不占用内存大小,所以最终占用内存的只有下面 这四个成员
    • _buckets 的真正类型是 struct 指针,占用 8 字节
    • _mask 真正的类型是 uint32_t 占用 4 字节
    • _flags 真正的类型是 unsigned short 类型占 2 字节
    • _occupied 真正的类型是 unsigned short 类型占 2 字节

cache_t 的源码定义 ,有如下属性

explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
uint16_t _flags;
uint16_t _occupied;

想要获取 bits 的信息,需要将类的地址平移 32 字节才可以获取到

  • 获取类对象的首地址
  • 打印首地址中 isa 指针的内存信息
  • 首地址 0x1000021f0 平移 32 字节 就是 0x100002210,就是 bits 存储地址,打印 bits 数据
class_rw_t *data() const {
    return bits.data();
}

bits 中存储的信息,其类型是 class_rw_t,也是一个结构体类型。里面存储类的 属性列表方法列表协议列表

    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }
属性列表

在上述基础上,通过 class_rw_t 的源码定义,探索 bits 中的 属性列表

  • 获取属性列表数据,根据结果取出 list 数据并打印出来
  • 获取每个属性,并打印出来

获取 LCPerson 中的成员变量 name, 发现会报错,提示数组越界了,说明 property_list 中只有 一个属性 nickname。

方法列表
  • 获取方法列表数据,根据结果取出 list 数据并打印出来
  • 获取每个方法信息,并打印出来

在获取第五个方法,也会报错,提示数组越界

相关文章

  • iOS 类的结构分析(下)

    在上一篇 iOS 类的结构分析(上) 分析了类的结构、isa 的走位以及类的内存分布(属性列表&实例方法列表),这...

  • iOS 类的结构分析(上)

    类的初探 在我们平常的 iOS 开发中,类和对象是出现很高频的名词,在之前的isa 底层结构分析 中介绍了对象,那...

  • iOS类结构:cache_t分析

    一、cache_t 内部结构分析 1.1 在iOS类的结构分析中,我们已经分析过类(Class)的本质是一个结构体...

  • iOS-底层分析之类的结构分析

    类的结构分析 本文主要分析iOS中的类以及类的结构,下面我们通过一个例子来探索类的结构 我们定义一个WPerson...

  • iOS底层之cache_t探究

    我们在iOS底层之类的结构分析分析了类的内部结构,而类的C/C++底层实际是objc_class结构体,其中包含了...

  • [iOS] 类 & 类结构分析

    1. 类的分析 1.1 元类的引入 我们可能之前已经知道类其实也是一个对象,类的 isa 指针指向的是它的元类,...

  • iOS 类结构分析

    前言 通过本篇文章可以了解1.isa的走位2.类结构的分析3.什么是元类4.supclass的走位5.objc_c...

  • iOS类结构分析

    本文主要来探索一下iOS中类的结构,作为一个iOS开发者,我们有必要去了解关于类的底层知识。下面开始我们的探索。 ...

  • iOS - 类结构分析

    我们都知道,一个类可以创建多个不同实例对象,类自己也是对象(类对象),那么类在内存中会存在几份呢?看下面结果 得出...

  • iOS 类的结构分析

    在谈及面向对象编程的时候,总是离不开 对象 与 类 。对象 是对客观事物的抽象,类 是对 对象 的抽象。它们的关系...

网友评论

      本文标题:iOS 类的结构分析(上)

      本文链接:https://www.haomeiwen.com/subject/yijwektx.html