阅读 230

iOS底层 -- 类的本质分析

 欢迎阅读iOS底层系列(建议按顺序) 
iOS底层 - alloc和init探索
iOS底层 - 包罗万象的isa
iOS底层 - 类的本质分析
iOS底层 - cache_t流程分析
iOS底层 - 方法的本质和查找流程分析

1.本文概述

本文旨在通过 类&元类的创建时机类的结构及相关属性添加的类信息等分析类在内存中的实际存在,并分享一些关于类的经典面试题


2.类&元类的创建时机

上文说到,对象通过isa和类关联,同个类型的对象可以多次创建,所以对象可以有多个。那么类呢,根据开发经验,很容易得出类在内存中只有一个,那究竟要怎么实锤呢。提供验证方式:

  • command + b ,通过machoView查看

新建一个项目,并创建一个CJPerson类,在Products目录下.app结尾的文件还没有编译是报红的,

command + b下变黑,Show In Finder后显示包内容,把可执行文件拖入machoView

machoView中显示如下

可以看到,在DATA段的_objc_classrefs内已经加载了CJPerson类,并指定了内存地址,说明类的创建是在编译时期,并且只有一份。

元类也是在编译期,由系统创建的,在我的理解,元类的作用是:
1.承接isa走向
2.缓存类方法
总觉得苹果大费周章引入元类的概念,还有其他的作用,至于是什么,希望可以指导我下
复制代码

2.类的本质

main.m下初始化一个CJPerson对象

int main(int argc, const char * argv[]) {
    @autoreleasepool {        
        CJPerson * person = [[CJPerson alloc]init];        
        NSLog(@"%@",person.class);    
    }    
    return 0;
}复制代码

clang编译下main.m

clang -rewrite-objc main.m -o main.cpp复制代码

打开编译后的main.cpp文件,直接来到最后面,会看到初始化时候是调用objc_getClass

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
         CJPerson * person = ((CJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((CJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJPerson"), sel_registerName("alloc")), sel_registerName("init"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_3995175d2_v6g81lknk8jdwh0000gn_T_main_e6fa2f_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("class")));
    }    
    return 0;
}复制代码

在文件内搜索objc_getClass,看到

__OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);复制代码

再搜索objc_class,找到重定义

typedef struct objc_class *Class;复制代码

原来类是objc_class重定义的,感觉这个objc_class很熟悉,好像在objc源码里面有看到过,那就搜索试试

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_object的结构

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};复制代码
  • objc_class结构体内ISA是被注释的,不代表结构体内不包含它,因为objc_class继承objc_object,所以ISA是来自于父类
  • 同样使用clang编译下NSObject可以得到objc_object
  • isa的结构是isa_t,可是这里使用class接收是可以的,内部通过shift_class得到class
总结:类的真正类型为objc_class,继承于objc_object(也就是NSObject的底层结构),说明万物皆对象,类也是一个对象,CJPerson严格来说是类对象


3.类的数据结构

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结构体可以清楚看到,类有4个属性,分别是isasuperclasscachebits

1.Class isa

isa在(iOS底层-包罗万象的isa)中已经分析过了,占有8个字节,只是这里的isa是指向元类

2.Class superclass

superclass就是指向的父类

typedef struct objc_class *Class;
复制代码

superclass的结构同样是objc_class,因为它是结构体指针,占有8个字节

3.cache_t cache

cache是方法缓存,方法缓存涉及到方法查找流程缓存策略动态扩容等,下一章详细说明,先看下cache_tbucket_t结构

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}复制代码

struct bucket_t {
private:
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif
...
}复制代码
  • buckets是装方法的桶子,里面放着方式实现imp,根据方法编号sel生成的key

  • mask是扩容因子,决定扩容的时机

  • occupied是当前占用的容量

  • bucket_t 是结构体指针,占用8个字节,mask_t就是int,占用4个字节,所以maskoccupied各占4个字节

  • cache_t是个结构体,结构体大小是内部所有大小的和,所以cache_t占有16个字节


4.class_data_bits_t bits

bits是数据存放的地方,bits里面有datadata是从macho里面读取的class_rw_t

class_rw_t *data() {
    return bits.data();
}复制代码
struct class_rw_t {
...
    const class_ro_t *ro;
    method_array_t methods;//方法列表
    property_array_t properties;//属性列表
    protocol_array_t protocols;//协议列表
...
}复制代码

class_rw_t有方法列表,属性列表,协议列表等等

class_rw_t里面有个class_ro_t

struct class_ro_t {
    ..
    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;
    }
};复制代码

class_ro_t也有方法列表,属性列表,协议列表等,但是多了ivar_list_t实例变量列表。下面探索下两者等区别

4.类添加的属性方法

类中添加实例变量sex,属性age,对象方法work,类方法play

@interface CJPerson : NSObject{
    NSString * sex;
}
@property (nonatomic , copy , readonly) NSString * age;

- (void)work;
+ (void)play;复制代码

object_getClass输出下类对象

CJPerson * person = [CJPerson alloc];
Class cls = object_getClass(person);         
NSLog(@"%@",cls);复制代码

x/4gx打印cls的内存地址如下:


通过类的数据结构分析,bits位于ISA,superclass,cache之后,这三者分别是8,8,16字节,根据内存偏移,bitsisa地址后的32字节,

0x100001218 + 32字节 = 0x100001238复制代码

pop直接打印都不行,那么需要用class_data_bits_t类型强转打印


取出bits内的class_rw_t


class_rw_t的methods


看到methods中有3个方法,分别输出下

  • .cxx_destruct 系统添加c++的析构方法

  • work 对象方法

  • age 属性生成的age方法

  • method_t中的types,在苹果开发者文档中有详细说明

结论:有对象方法work,居然没有类方法play

class_rw_t的properties

如法炮制,输出下属性列表properties


结论:符合预期,只有一个添加的age属性

那继续看下class_ro_t


class_ro_t的ivar_list_t


结论:ivar_list_t中有添加的实例变量sex,还有一个_age,这也符合常规认知,属性在底层会生成带下划线的实例变量

class_ro_t的baseMethodList

结论:和class_rw_t中的methods一致

class_ro_t的baseProperties

结论:和class_rw_t中的properties一致

搜索疑点

目前添加的实例变量,属性,对象方法都在内存中寻找到了,唯独缺少类方法,不过根据经验可以知道,类方法是缓存在元类中,那尝试去元类搜索

通过isa_mask找到元类,然后一步步找到baseMethodList,果然在其中找到类方法play

结论

  • 成员变量存放在class_ro_t中的ivar_list_t
  • 属性在class_rw_t中的property_array_tclass_ro_t中的的property_list_t都存着一份,并且会生成实例变量,和对应的方法
  • 方法在class_rw_t中的method_array_tclass_ro_t中的的method_list_t都存着一份
  • 对象方法存放在里面
  • 类方法存放在元类里面

class_ro_t和class_rw_t内容大部分相同的原因:

class_ro_t存储了当前类在编译期就已经确定的属性方法以及遵循的协议,class_rw_t是在运行时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。class_rw_tclass_ro_t的超集。


5.类的面试题

这是一道比较经典的面试题,CJPerson继承NSObject

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //        
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //        
BOOL re3 = [(id)[CJPerson class] isKindOfClass:[CJPerson class]];       //        
BOOL re4 = [(id)[CJPerson class] isMemberOfClass:[CJPerson class]];     //        
NSLog(@"%hhd%hhd%hhd%hhd",re1,re2,re3,re4);      

  
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //        
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //        
BOOL re7 = [(id)[CJPerson alloc] isKindOfClass:[CJPerson class]];       //        
BOOL re8 = [(id)[CJPerson alloc] isMemberOfClass:[CJPerson class]];     //
NSLog(@"%hhd%hhd%hhd%hhd",re5,re6,re7,re8);复制代码

输出为1000,1111

re1是NSObject调用类方法isKindOfClass

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}复制代码

递归查找NSObject的元类是否存在,存在就继续找元类的父类,直到根元类指向NSObject的时候,返回YES复制代码

re2是NSObject调用类方法isMemberOfClass

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}复制代码

只对比一次,NSObject的元类是否等于NSObject,返回NO复制代码

re3是CJPerson调用类方法isKindOfClass

第一步就到CJPerson元类,然后一直递归下去,不会回到CJPerson类,返回NO复制代码

re4是CJPerson调用类方法isMemberOfClass

只对比一次CJPerson元类和CJPerson类,返回NO复制代码

re5是NSObject对象调用对象方法isKindOfClass

- (BOOL)isKindOfClass:(Class)cls { 
   for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}复制代码

递归查找对象的父类是否等于NSObject,总有一次等于NSObject,返回YES复制代码

re6是NSObject对象调用对象方法isMemberOfClass

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}复制代码

只对比一次,NSObject对象的类是否等于NSObject,刚好继承关系满足复制代码

re7是CJPerson对象调用对象方法isKindOfClass

递归查找CJPerson对象的类是否等于CJPerson,第一次就满足继承关系复制代码

re8是CJPerson对象调用对象方法isMemberOfClass

只对比一次,CJPerson对象的类是否等于CJPerson,刚好继承关系满足复制代码


写在最后

以上就是关于类的探索,下一章是cache_t流程分析引出消息发送流程,后续继续更新类的底层结构,block,锁,多线程等底层探索,还有应用程序加载,启动优化,内存优化等相关知识点,敬请关注。