iOS探索 类的结构分析

2,753 阅读7分钟

欢迎阅读iOS探索系列(按序阅读食用效果更加)

写在前面

iOS探索系列前面几篇讲到了对象的初始化、对象的内存分布、对象的isa初始化以及指向分析,本文就来讲讲实例出实例对象的类对象——类

一、类的本质

1.类的本质

objc源码下准备代码

#import <objc/runtime.h>

@interface FXPerson : NSObject
@end
@implementation FXPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *p = [FXPerson alloc];
        Class cls = object_getClass(p);
    }
    return 0;
}

利用clang将OC文件输出cpp文件(仿佛打开了新世界的大门)

发现类在底层用Class接收

然后开始大海捞针(开天眼模式)找到了Class的定义

typedef struct objc_class *Class;

想要找到objc_class就搜不到了,但是总觉得它似曾相似,或许能在objc源码找到灵感

在源码中搜索代码的经验 "objc_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() { 
        return bits.data();
    }
    ...
}

objc_class继承于objc_object

struct objc_object {
private:
    isa_t isa;
    
public:
    ...
}

结论:类的本质是objc_class类型的结构体,objc_class继承于objc_object,所以满足万物皆对象

以后不要再在面试的时候回答一句 万物皆对象 就完事了

2.objc_object和NSObject的关系

等等,为什么继承objc_object就满足万物皆对象了???

看过NSObject的定义就知道了

OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

仔细比较的话就能看出NSObjectobjc_object有着说不清道不明的关系

  • 其实NSObjectobjc_object的仿写,和objc_object的定义是一样的,在底层会编译成objc_object
  • 同理NSObject类是OC版本的objc_class

3.Class isa??

isa明明是isa_t类型的,为什么注释了一句Class ISA

  • 万物皆对象,用继承于objc_object的Class接收是没问题的
  • 强转,方便isa走位时返回类的类型

二、类的结构

objc_class的定义可以得出,类有4个属性:isa、superclass、cache、bits

1.Class ISA

不但实例对象中有isa指针类对象中也有isa指针关联着元类

Class本身就是一个指针,占用8字节

2.Class superclass

顾名思义就是类的父类(一般为NSObject)superclass是Class类型,所以占用8字节

3.cache_t cache

虽然对这个属性比较陌生(详见iOS探索 cache_t分析),但是cache在英文中的意思是缓存

cache_t是一个结构体,内存长度有所有元素决定:_buckets是一个指针,占用8字节;mask_t是个int类型,_mask占用4字节;_occupied占用4字节

=>cache_t占用16字节

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;

public:
    ...
};

4.class_data_bits_t bits

又是一个陌生的属性,但是苹果工程师还是蛮友好的,这一看就是存数据的地方

struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits;
private:
    bool getBit(uintptr_t bit)
    {
        return bits & bit;
    }
    ...
public:

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    ...
};

那么问题来了,类的属性方法都去哪儿了?是在cache还是在bits? 其实前文中有提到一丢丢——objc_class中有个class_rw_t *data()方法

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    ...
}

三、类的属性方法

为FxPerson添加nickname成员属性、nationality属性变量、walk类方法、fly实例方法

@interface FXPerson : NSObject {
    NSString *nickname;
}
@property (nonatomic, copy) NSString *nationality;
+ (void)walk;
- (void)fly;
@end

@implementation FXPerson
+ (void)walk {}
- (void)fly {}
@end

1.类的属性

x/4gx cls打印当前类结构

bits刚好是类的内存首地址+isa、superclass、cache的内存长度

=> 0x100001560+32字节 = 0x100001580

po打印不出来,那就类型强转打印输出bits的内存地址

根据class_rw_t *data() { return bits.data(); }打印bits.data()

再往里面探索进去(这比西天取经还难)

天呐!怎么只有一个nationality了?难道我FXPerson类只配拥有“国籍”不配拥有“姓名”?让我静静... 既然此路不通,那么换条路走走(内心很不情愿)

在一顿猛如虎的操作(开天眼)之后,发现了class_rw_t有个属性class_ro_t

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

清空控制台输出ro,跟class_ro_t的结构类型一摸一样

打印robaseProperties

天呐?这个成员变量nickname是迷路了吗?等等!可能是自己迷路了!

尝试了一下打印ivars找到了nickname,但是为什么count = 2呢?

ivar_list_t继承entsize_list_tt,而后者又有个get(uint32_t i)方法

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

struct entsize_list_tt {
    ...
    Element& get(uint32_t i) const { 
        assert(i < count);
        return getOrEnd(i);
    }
    ...
}

如愿拿到了nicknamenationality,但是这个nationality长得有点不一样

仔细想想这不就是编译器会在底层自动将属性变量生成一个成员变量_nationality(_前缀+属性变量)

2.类的方法

打印robaseMethodList

系统在底层添加了一个c++的.cxx_destruct方法,同时编译器还在底层帮属性变量生成了一个setter方法getter方法

但是FXPerson的walk类方法又被吃了...

无奈之下只能开大招(开天眼)在元类的datarobaseMethodList找到了

其实也很好理解:类方法可以理解成元类对象实例方法,因此存在元类

3.结论

  • 成员变量存放在ivar
  • 属性存放在property,同时也会存一份在ivar,并生成settergetter方法
  • 对象方法存放在里面
  • 类方法存放在元类里面

4.API验证

利用底层开放的API可以验证以上结论

void testObjc_copyIvar_copyProperies(Class cls) {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(cls, &count);
    for (unsigned int i=0; i < count; i++) {
        Ivar const ivar = ivars[i];
        //获取实例变量名
        const char*cName = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:cName];
        NSLog(@"class_copyIvarList:%@",ivarName);
    }
    free(ivars);

    unsigned int pCount = 0;
    objc_property_t *properties = class_copyPropertyList(cls, &pCount);
    for (unsigned int i=0; i < pCount; i++) {
        objc_property_t const property = properties[i];
        //获取属性名
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
        //获取属性值
        NSLog(@"class_copyProperiesList:%@",propertyName);
    }
    free(properties);
}

void testObjc_copyMethodList(Class cls) {
    unsigned int count = 0;
    Method *methods = class_copyMethodList(cls, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        
        NSLog(@"Method, name: %@", key);
    }
    free(methods);
}

void testInstanceMethod_classToMetaclass(Class cls) {
    const char *className = class_getName(cls);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(cls, @selector(walk));
    Method method2 = class_getInstanceMethod(metaClass, @selector(fly));

    Method method3 = class_getInstanceMethod(cls, @selector(walk));
    Method method4 = class_getInstanceMethod(metaClass, @selector(fly));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

void testClassMethod_classToMetaclass(Class cls) {
    const char *className = class_getName(cls);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(cls, @selector(walk));
    Method method2 = class_getClassMethod(metaClass, @selector(fly));

    Method method3 = class_getClassMethod(cls, @selector(walk));
    Method method4 = class_getClassMethod(metaClass, @selector(fly));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

void testIMP_classToMetaclass(Class cls) {
    const char *className = class_getName(cls);
    Class metaClass = objc_getMetaClass(className);

    IMP imp1 = class_getMethodImplementation(cls, @selector(walk));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(fly));

    IMP imp3 = class_getMethodImplementation(cls, @selector(walk));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(fly));

    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
    NSLog(@"%s",__func__);
}

5.clang编译验证

其实直接用clang编译也能看出蹊跷

关于v@:这个玩意,在苹果开发者文档上也有介绍

四、写在后面

虽然一直碰壁,但是本文最后还是找到了想要的答案,有的时候你离成功只差一步之遥,也有的时候是你选错了研究方向。学习是如此,人生亦是如此,道阻且长,且行且珍惜。幸运的是有前人替我们踩坑,我们可以吸取前人的教训少走弯路