iOS 类的结构分析(上)

2020-09-14  本文已影响0人  远方竹叶

类的初探

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

准备工作

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

@interface LCPerson : NSObject {
    NSString *name;
}

@property (nonatomic, copy) NSString *nickname;

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

@end

@implementation LCPerson

- (void)sayHello {
}

+ (void)say666 {
}

@end
@interface LCTeacher : LCPerson
@end

@implementation LCTeacher
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LCPerson *person = [LCPerson alloc];
        LCTeacher *teacher = [LCTeacher alloc];
        NSLog(@"%@ - %@", person, teacher);
    }
    return 0;
}

LLDB 调试

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

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

元类

类存在几份

从上面的我们可以看到,最后的根元类打印了很多份,那么他们是同一份吗?与我们开发中的 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 属性呢?首先我们去源码查看他们的底层结构

typedef struct objc_class *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 与 对象 的关系

总结

类的内存分布

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

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);

打印结果如下

2.对象指针
LCPerson *p1 = [LCPerson alloc]; 
LCPerson *p2 = [LCPerson alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &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);

打印结果如下

获取 bits

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

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

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

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

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 中的 属性列表

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

方法列表

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

上一篇 下一篇

猜你喜欢

热点阅读