iOS底层原理总结 - Category的本质

iOS底层原理总结 - Category的本质

面试题

  1. Category的实现原理,以及Category为何只能加方法不能加属性。
  2. Category中有load方法吗?load方法是何时调用的?load 方法能继承吗?
  3. load、initialize的区别,以及它们在category重写的时候的调用的次序。

Category的本质

首先咱们写一段简单的代码,以后的分析都基于这段代码。c++

Presen类 
// Presen.h
#import <Foundation/Foundation.h>
@interface Preson : NSObject
{
    int _age;
}
- (void)run;
@end

// Presen.m
#import "Preson.h"
@implementation Preson
- (void)run
{
    NSLog(@"Person - run");
}
@end

Presen扩展1
// Presen+Test.h
#import "Preson.h"
@interface Preson (Test) <NSCopying>
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end

// Presen+Test.m
#import "Preson+Test.h"
@implementation Preson (Test)
- (void)test
{
}

+ (void)abc
{
}
- (void)setAge:(int)age
{
}
- (int)age
{
    return 10;
}
@end

Presen分类2
// Preson+Test2.h
#import "Preson.h"
@interface Preson (Test2)
@end

// Preson+Test2.m
#import "Preson+Test2.h"
@implementation Preson (Test2)
- (void)run
{
    NSLog(@"Person (Test2) - run");
}
@end
复制代码

咱们以前讲到过实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,当p调用run方法时,类对象的isa指针找到类对象的isa指针,而后在类对象中查找对象方法,若是没有找到,就经过类对象的superclass指针找到父类对象,接着去寻找run方法。面试

那么当调用分类的方法时,步骤是否和调用对象方法同样呢? 分类中的对象方法依然是存储在类对象中的,同对象方法在同一个地方,那么调用步骤也同调用对象方法同样。若是是类方法的话,也一样是存储在元类对象中。 那么分类方法是如何存储在类对象中的,咱们来经过源码看一下分类的底层结构。数组

分类的底层结构

如何验证上述问题?经过查看分类的源码咱们能够找到category_t 结构体。bash

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的方式,对象方法,类方法,协议,和属性均可以找到对应的存储方式。而且咱们发现分类结构体中是不存在成员变量的,所以分类中是不容许添加成员变量的。分类中添加的属性并不会帮助咱们自动生成成员变量,只会生成get set方法的声明,须要咱们本身去实现。markdown

经过源码咱们发现,分类的方法,协议,属性等好像确实是存放在categroy结构体里面的,那么他又是如何存储在类对象中的呢? 咱们来看一下底层的内部方法探寻其中的原理。 首先咱们经过命令行将Preson+Test.m文件转化为c++文件,查看其中的编译过程。app

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Preson+Test.m
复制代码

在分类转化为c++文件中能够看出_category_t结构体中,存放着类名,对象方法列表,类方法列表,协议列表,以及属性列表。iphone

c++文件中category_t结构体

紧接着,咱们能够看到_method_list_t类型的结构体,以下图所示函数

对象方法列表结构体

上图中咱们发现这个结构体_OBJC_$_CATEGORY_INSTANCE_METHODS_Preson_$_Test从名称能够看出是INSTANCE_METHODS对象方法,而且一一对应为上面结构体内赋值。咱们能够看到结构体中存储了方法占用的内存,方法数量,以及方法列表。而且从上图中找到分类中咱们实现对应的对象方法,test , setAge, age三个方法学习

接下来咱们发现一样的_method_list_t类型的类方法结构体,以下图所示this

类对象方法列表

同上面对象方法列表同样,这个咱们能够看出是类方法列表结构体 _OBJC_$_CATEGORY_CLASS_METHODS_Preson_$_Test,同对象方法结构体相同,一样能够看到咱们实现的类方法,abc。

接下来是协议方法列表

协议方法列表

经过上述源码能够看到先将协议方法经过_method_list_t结构体存储,以后经过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_$_Preson_$_Test中同_protocol_list_t结构体一一对应,分别为protocol_count 协议数量以及存储了协议方法的_protocol_t结构体。

最后咱们能够看到属性列表

属性列表结构体
属性列表结构体 _OBJC_$_PROP_LIST_Preson_$_Test同_prop_list_t结构体对应,存储属性的占用空间,属性属性数量,以及属性列表,从上图中能够看到咱们本身写的age属性。

最后咱们能够看到定义了_OBJC_$_CATEGORY_Preson_$_Test结构体,而且将咱们上面着重分析的结构体一一赋值,咱们经过两张图片对照一下。

_category_t

_OBJC_$_CATEGORY_Preson_$_Test

上下两张图一一对应,而且咱们看到定义_class_t类型的OBJC_CLASS_$_Preson结构体,最后将_OBJC_$_CATEGORY_Preson_$_Testcls指针指向OBJC_CLASS_$_Preson结构体地址。咱们这里能够看出,cls指针指向的应该是分类的主类类对象的地址。

经过以上分析咱们发现。分类源码中确实是将咱们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。接下来咱们在回到runtime源码查看catagory_t存储的方法,属性,协议等是如何存储在类对象中的。

首先来到runtime初始化函数

runtime初始化函数

接着咱们来到 &map_images读取模块(images这里表明模块),来到map_images_nolock函数中找到_read_images函数,在_read_images函数中咱们找到分类相关代码

Discover categories代码

从上述代码中咱们能够知道这段代码是用来查找有没有分类的。经过_getObjc2CategoryList函数获取到分类列表以后,进行遍历,获取其中的方法,协议,属性等。能够看到最终都调用了remethodizeClass(cls);函数。咱们来到remethodizeClass(cls);函数内部查看。

remethodizeClass函数内部
经过上述代码咱们发现attachCategories函数接收了类对象cls和分类数组cats,如咱们一开始写的代码所示,一个类能够有多个分类。以前咱们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。

咱们来到attachCategories函数内部。

attachCategories函数内部实现

上述源码中能够看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法须要多少内存来分配相应的内存地址。以后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,这三个数组放着全部分类的方法,属性和协议。 以后经过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中咱们介绍过,class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体经过类对象的data方法获取,因此rw里面存放这类对象里面的数据。 以后分别经过rw调用方法列表、属性列表、协议列表的attachList函数,将全部的分类的方法、属性、协议列表数组传进去,咱们大体能够猜测到在attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并。

咱们来看一下attachLists函数内部。

attachLists函数内部实现

上述源代码中有两个重要的数组 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);
复制代码

下面咱们图示通过memmove和memcpy方法事后的内存变化。

未通过内存移动和拷贝时

通过memmove方法以后,内存变化为

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

memmove方法以后内存变化

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

memcpy方法以后,内存变化

// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
复制代码

memmove方法以后,内存变化

咱们发现原来指针并无改变,至始至终指向开头的位置。而且通过memmove和memcpy方法以后,分类的方法,属性,协议列表被放在了类对象中本来存储的方法,属性,协议列表前面。

那么为何要将分类方法的列表追加到原本的对象方法前面呢,这样作的目的是为了保证分类方法优先调用,咱们知道当分类重写本类的方法时,会覆盖本类的方法。 其实通过上面的分析咱们知道本质上并非覆盖,而是优先调用。本类的方法依然在内存中的。咱们能够经过打印全部类的全部方法名来查看

- (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);
}
- (void)viewDidLoad {
    [super viewDidLoad];    
    Preson *p = [[Preson alloc] init];
    [p run];
    [self printMethodNamesOfClass:[Preson class]];
}
复制代码

经过下图中打印内容能够发现,调用的是Test2中的run方法,而且Person类中存储着两个run方法。

打印全部方法

总结:

问: Category的实现原理,以及Category为何只能加方法不能加属性?

答:分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,而后将结构体内的方法列表拷贝到类对象的方法列表中。 Category能够添加属性,可是并不会自动生成成员变量及set/get方法。由于category_t结构体中并不存在成员变量。经过以前对对象的分析咱们知道成员变量是存放在实例对象中的,而且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么咱们就没法再程序运行时将分类的成员变量中添加到实例对象的结构体中。所以分类中不能够添加成员变量。

load 和 initialize

load方法会在程序启动就会调用,当装载类信息的时候就会调用。 调用顺序看一下源代码。

load方法调用顺序
经过源码咱们发现是优先调用类的load方法,以后调用分类的load方法。

咱们经过代码验证一下: 咱们添加Student继承Presen类,并添加Student+Test分类,分别重写只+load方法,其余什么都不作经过打印发现

load方法打印
确实是优先调用类的load方法以后调用分类的load方法,不过调用类的load方法以前会保证其父类已经调用过load方法。

以后咱们为Preson、Student 、Student+Test 添加initialize方法。 咱们知道当类第一次接收到消息时,就会调用initialize,至关于第一次使用类的时候就会调用initialize方法。调用子类的initialize以前,会先保证调用父类的initialize方法。若是以前已经调用过initialize,就不会再调用initialize方法了。当分类重写initialize方法时会先调用分类的方法。可是load方法并不会被覆盖,首先咱们来看一下initialize的源码。

initialize调用源码

上图中咱们发现,initialize是经过消息发送机制调用的,消息发送机制经过isa指针找到对应的方法与实现,所以先找到分类方法中的实现,会优先调用分类方法中的实现。

咱们再来看一下load方法的调用源码

load方法的调用源码
咱们看到load方法中直接拿到load方法的内存地址直接调用方法,不在是经过消息发送机制调用。

分类load方法的调用源码
咱们能够看到分类中也是经过直接拿到load方法的地址进行调用。所以正如咱们以前试验的同样,分类中重写load方法,并不会优先调用分类的load方法,而不调用本类中的load方法了。

总结

问:Category中有load方法吗?load方法是何时调用的?load 方法能继承吗?

答:Category中有load方法,load方法在程序启动装载类信息的时候就会调用。load方法能够继承。调用子类的load方法以前,会先调用父类的load方法

问:load、initialize的区别,以及它们在category重写的时候的调用的次序。

答:区别在于调用方式和调用时刻 调用方式:load是根据函数地址直接调用,initialize是经过objc_msgSend调用 调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每个类只会initialize一次(父类的initialize方法可能会被调用屡次)

调用顺序:先调用类的load方法,先编译那个类,就先调用load。在调用load以前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,以后再初始化子类。若是子类没有实现+initialize,会调用父类的+initialize(因此父类的+initialize可能会被调用屡次),若是分类实现了+initialize,就覆盖类自己的+initialize调用。


本文是对底层原理学习的总结,若是有不对的地方请指正,欢迎你们一块儿交流学习 xx_cc 。

相关文章
相关标签/搜索