iOS 分类(Category)

一、分类的使用场景

  • 可以减少单个文件的体积
  • 可以按照功能分组,放到不同的分类里,使类结构更清晰
  • 降低耦合性,同一个类可以有多个开发人员进行开发
  • 模拟多继承
  • 把静态库的私有方法公开

二、特点

  • 运行时决议
  • 给系统类添加分类

三、分类的底层结构

runtime文件objc-runtime-new.h中,找到分类category_t的结构体:

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

    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

从上面的category_t结构体中可以看出,我们可以在分类中添加实例方法类方法协议属性

并且我们发现分类结构体中不存在成员变量的,因此分类是不允许添加成员变量的。分类中添加的属性并不会帮组我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。

下面写一个简单的分类,然后将其转换为c/c++的 .cpp文件。

/** Person.h */
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, copy) NSString *age;

- (void)run;

@end

/** Person+Test.h */
@interface Person (Test)
@property (nonatomic, copy) NSString *categoryName;
- (void)printClassName;
+ (void)classMethods;
@end

/** Person+Test.m */
@implementation Person (Test)
- (void)setCategoryName:(NSString *)categoryName {

}
- (NSString *)categoryName {
    return @"categoryName";
}
- (void)printClassName {
    NSLog(@"printClassName");
}
+ (void)classMethods {
    NSLog(@"classMethods");
}
#pragma mark -- NSCopying
- (id)copyWithZone:(nullable NSZone *)zone {
    return self;
}
@end

使用clang命令生成.cpp文件

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

1、_category_t

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

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

2、_method_list_t

然后是_method_list_t类型结构体

对象方法

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    3,
    {{(struct objc_selector *)"setCategoryName:", "v24@0:8@16", (void *)_I_Person_Test_setCategoryName_},
    {(struct objc_selector *)"categoryName", "@16@0:8", (void *)_I_Person_Test_categoryName},
    {(struct objc_selector *)"printClassName", "v16@0:8", (void *)_I_Person_Test_printClassName}}
};

上图中我们发现这个结构体_OBJC_$_CATEGORY_INSTANCE_METHODS_Preson_$_Test从名称可以看出是INSTANCE_METHODS对象方法,并且一一对应为上面结构体内赋值。我们可以看到结构体中存储了方法占用的内存方法数量,以及方法列表。并且从上图中找到分类中我们实现对应的对象方法,setCategoryName , categoryName, printClassName三个方法。

类方法

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"classMethods", "v16@0:8", (void *)_C_Person_Test_classMethods}}
};

同上面对象方法列表一样,这个我们可以看出是类方法列表结构体 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,同对象方法结构体相同,同样可以看到我们实现的类方法,classMethods

3、_protocol_list_t

协议列表

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};

struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
    0,
    "NSCopying",
    0,
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
    0,
    0,
    0,
    0,
    sizeof(_protocol_t),
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;

static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1,
    &_OBJC_PROTOCOL_NSCopying
};

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

4、_prop_list_t

属性列表

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"categoryName","T@\"NSString\",C,N"}}
};

属性列表结构体_OBJC_$_PROP_LIST_Person_$_Test_prop_list_t结构体对应,存储属性的占用空间,属性属性数量,以及属性列表,从上图中可以看到我们自己写的categoryName属性。

5、赋值

最后我们可以看到定义了_OBJC_$_CATEGORY_Preson_$_Test结构体,并且将我们上面着重分析的结构体一一赋值,我们通过两段代码对照一下。

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Test,
};
static void OBJC_CATEGORY_SETUP_$_Person_$_Test(void ) {
    _OBJC_$_CATEGORY_Person_$_Test.cls = &OBJC_CLASS_$_Person;
}

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

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

四、catagory_t的方法,属性,协议等的存储类对象中

objc4-781.tar.gz

1、runtime初始化函数

在这里插入图片描述

2、map_images

接着我们来到 &map_images读取模块(images这里代表模块或镜像)


在这里插入图片描述

来到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到load_categories_nolock函数


在这里插入图片描述

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

3、attachCategories();

在这里插入图片描述

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

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

4、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方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。

那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。

其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。

你可能感兴趣的:(iOS 分类(Category))