iOS面试资料iOS进阶生活和工作

iOS底层原理探索—Category的本质(一)

2019-07-21  本文已影响61人  劳模007_Mars

探索底层原理,积累从点滴做起。大家好,我是Mars。

往期回顾

iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质

今天带领大家探索iOS之Category的本质。

Category

首先我们声明一个Person

//Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
{
    int _age;
}
- (void)run;
@end
//Person.m
#import "Person.h"
@implementation Person
- (void)run
{
    NSLog(@"Person:run");
}
@end

我们之前在iOS底层原理探索—OC对象的本质中讲到:实例对象的isa指针指向类对象,类对象的isa指针指向元类对象。我们创建一个Person对象p,当p调用run方法时,通过实例对象的isa指针找到类对象,然后在类对象中查找对象方法,如果没有找到,就通过类对象的superclass指针找到父类对象,接着去寻找run方法。

那么当我们调用分类的方法时,是否跟上面的调用顺序一样呢?下面我们创建分类来验证一下:

创建Person的分类:

New FileiOS文件下选择Objective-C File

创建分类1.png

File Type选择CategoryClass父类选择Person

创建分类2.png
//Person+test.h
#import "Person.h"
@interface Person (Test)
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end
//Person+test.m
#import "Person+Test.h"
@implementation Person (Test)
- (void)test
{
    
}
+ (void)abc
{
    
}
- (void)setAge:(int)age
{
    
}
- (int)age
{
    return 18;
}

- (void)run
{
    NSLog(@"Person+test:run");
}
@end

以上我们就完成创建了PersonTest分类。

在此先告诉大家结论:分类中的对象方法是存储在类对象中的,和类对象方法在同一个地方,调用步骤也和调用对象方法一样。如果是类方法的话,同样也是存储在元类对象中

这一点大致可以从分类的底层结构中看出来:

分类的底层结构

struct category_t {
    const char *name;
    classref_t cls;
    //对象方法
    struct method_list_t *instanceMethods;
    // 类方法
    struct method_list_t *classMethods;
    // 协议
    struct protocol_list_t *protocols; 
    // 属性
    struct property_list_t *instanceProperties; 
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

从分类的源码中可以看出Categroy在底层是以categroy _t的结构存在,里面包括对象方法类方法协议,和属性。注意分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成setget方法的声明,需要我们自己去实现。

至此我们可以得出结论:

  • 分类的实现原理是将分类中的方法,属性,协议信息放在 category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。

  • 分类中可以添加属性,但是并不会自动生成成员变量setget方法。因为底层的category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的,那么我们就无法在程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量

由于上述结论的验证是依据底层源码,过程比较枯燥,也不能保证大家阅读一次就能弄清楚整个流程,所以将结论提前告知。不愿意阅读源码的读者也可以忽略以下内容,掌握上面的结论即可。

首先把Person+Test.m文件通过命令行转化为c++文件,查看底层编译过程。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m

然后将生成的.cpp文件拖拽至Xcode中查看。在.cpp文件中搜索category_t,通过搜索结果我们可以看到,_category_t结构体中,存放着类名对象方法列表类方法列表协议列表,以及属性列表

category_t结构体.png

.cpp文件中继续往下看,我们可以看到_method_list_t *instance_methods结构体的内容:

method_list_t对象方法结构体.png

通过结构体名称_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test可以看出是INSTANCE_METHODS--对象方法。我们可以看到结构体中存储了方法占用的内存,方法数量以及方法列表。并且从上图中可以看到在分类中我们实现的test,setAge, agerun四个方法。

同样,我们继续往下阅读,查看看到_method_list_t *class_methods结构体的内容:

method_list_t类方法结构体.png

同样通过结构体名称 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test可以看出是CLASS_METHODS--类方法。同样可以看到我们实现的abc类方法。

继续往下查看时,我们可以看到属性列表结构体_prop_list_t

prop_list_t属性列表结构体.png

属性列表结构体_OBJC_$_PROP_LIST_Person_$_Test_prop_list_t结构体,里面存储了属性的占用空间属性数量以及属性列表,从上图中可以看到我们声明的age属性。

同时我们发现,.cpp文件中没有protocol_list_t *protocols协议信息列表结构体的相关信息。这是由于我们创建分类是并没有遵守任何协议,自认分类里面也就没有任何协议相关的信息。我们返回分类Person+Test,使其遵守NSCopying协议,再通过命令行将分类的.m文件编译成.cpp文件后查看:

protocol_list_t *protocols协议列表结构体.png

通过上图可以看到分类底层先将协议方法通过_method_list_t结构体存储,之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test中,分别为protocol_count--协议数量以及存储协议方法_protocol_t结构体。

.cpp文件末尾处,我们看到系统定义了_category_t类型的_OBJC_$_CATEGORY_Person_$_Test结构体:

_category_t _OBJC_$_CATEGORY_Person_$_Test.png

_OBJC_$_CATEGORY_Person_$_Test结构体跟上文提到的catrgory_t结构体对照:

category_t结构体.png

不难看出,上下两图中两个结构体内容一一对应,并且我们在红框标注的方法中看到,定义的_class_t类型的OBJC_CLASS_$_Person结构体,最后将_OBJC_$_CATEGORY_Person_$_Testcls指针指向OBJC_CLASS_$_Person结构体地址。我们可以得出结论,cls指针指向的应该是分类的主类类对象的地址。

通过以上分析我们发现,分类确实是将我们定义的对象方法类方法属性等都存放在catagory_t结构体中。那么catagory_t结构体又如何让将这些信息存储到类对象中呢?我们通过分析runtime的源码来进一步了解。

runtime源码

我们通过opensource网站下载最新的源码来进一步分析。

首先来到runtime初始化函数

runtime初始化函数.png

接着我们来到&map_images读取模块,来到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到分类相关代码:

Discover categories代码.png

从上述代码中for循环中的判断我们可以知道这段代码是用来检查有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls)函数。我们来到remethodizeClass(cls)函数内部查看:

remethodizeClass函数.png

通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,当然一个类可以有多个分类,分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。

我们来到attachCategories函数内部:

attachCategories函数.png

上述源码中可以看出,首先根据方法列表属性列表协议列表通过malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。之后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法属性以及协议放入对应mlistproplistsprotolosts数组中,这三个数组放着所有分类的方法属性协议

之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中我们介绍过,class_rw_t中存放着类对象的方法属性协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。

之后分别通过rw调用方法列表属性列表协议列表attachList函数,将所有的分类的方法属性协议列表数组传进去,我们大致可以猜想到在attachList方法内部将分类和本类相应的对象方法属性协议进行了合并。

我们来看一下attachLists函数内部查看:


attachLists函数.png

上述源代码中有两个重要的数组
array()->lists: 类对象原来的方法列表,属性列表,协议列表。
addedLists:传入所有分类的方法列表,属性列表,协议列表。

attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝。我们先来分别看一下这两个函数

// memmove :内存移动。
/*  __dst : 移动内存的目的地
*   __src : 被移动的内存首地址
*   __len : 被移动的内存长度
*   将__src的内存移动__len块内存到__dst中
*/
void    *memmove(void *__dst, const void *__src, size_t __len);

// memcpy :内存拷贝。
/*  __dst : 拷贝内存的拷贝目的地
*   __src : 被拷贝的内存首地址
*   __n : 被移动的内存长度
*   将__src的内存移动__n块内存到__dst中
*/
void    *memcpy(void *__dst, const void *__src, size_t __n);

下面我们图示经过memmovememcpy方法过后的内存变化:

首先未经过内存移动和拷贝时:


未经过内存移动和拷贝时.png

经过memmove方法之后,内存变化为:

// array()->lists 原来方法、属性、协议列表数组
// addedCount 分类数组长度
// oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
memmove(array()->lists + addedCount, array()->lists, 
                  oldCount * sizeof(array()->lists[0]));

如图所示:


memmove方法之后内存变化.png

经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。

memcpy方法之后,内存变化为:

// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
memcpy方法之后,内存变化.png

我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmovememcpy方法之后,分类的方法属性协议列表被放在了类对象中原本存储的方法属性协议列表前面。

那么为什么要将分类方法的列表追加到本来的对象方法前面呢?

其实这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。

但是经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的,这一点可以通过打印所有类的所有方法名来查看,我们自己实现一个方法,打印所有类的所有方法名:

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    // 释放
    free(methodList);
    // 打印方法名
    NSLog(@"%@ - %@", cls, methodNames);
}

我们在控制器中引入Person类,在控制器的viewDidLoad方法中创建Person对象,并且调用run方法和上面的打印所有类的所有方法名的方法:

- (void)viewDidLoad {
    [super viewDidLoad];    
    Preson *p = [[Preson alloc] init];
    [p run];
    [self printMethodNamesOfClass:[Preson class]];
}

通过打印台打印内容可以发现,调用的是分类中的run方法,并且Person类中存储着两个run方法。

打印内容.png

关于Category的底层原理探索我们告一段落,如有疑问,欢迎在评论区留言。

更多技术知识请关注公众号
iOS进阶


iOS进阶.jpg
上一篇 下一篇

猜你喜欢

热点阅读