The underlying implementation principle of Category in iOS

1. Category usage scenarios

CategoryAlso called 分类or 类别, is a way to extend classes provided by OC. Regardless of whether it is a custom class or a system class, we can Categoryextend methods to the original class (both instance methods and class methods), and the extension method is exactly the same as the original method. For example, in my project, I often need to count the number of letters in a string, but the system does not provide this method, then we can extend a method Categoryto the NSStringclass, and then only need to import Categorythe header file to call the system method. Extension methods.

// 给NSString类添加一个Category,并扩展一个实例方法
@interface NSString (QJAdd)

- (NSInteger)letterCount;

@end
// 在需要使用这个扩展方法的地方引入头文件 #import "NSString+QJAdd.h",然后就可以调用这个扩展方法了
- (void)test{
    NSString *testStr = @"sdfjshdfjk.,d.889";
    NSInteger letterCount = [testStr letterCount];
}

CategoryIn addition to being used to extend classes, there is also a more advanced usage, which is to 拆分模块split a large module into multiple smaller modules to facilitate maintenance and management. What does that mean? I will cite a problem that many developers will have, that is, AppDelegatethis category. This class is automatically generated when the project is just created and is used to manage the program life cycle. When the project was first created, there was not much code in this class, but as the project progressed, more and more codes would be placed in this class. For example, when integrating various third-party frameworks such as Jiguang Push, Youmeng, Baidu Maps, WeChat SDK, etc., the initialization work of these third-party frameworks, and even related business logic codes, will be placed in this category, which leads to the application The function of is becoming more and more complex, AppDelegateand there will be more and more codes in it, and some even have thousands of lines, which makes people scalp numb to look at.

At this time, we can use Categoryto AppDelegatesplit, first we need to AppDelegatedivide the code in, extract the code of the same function and put it in a category. For example, I can create a new category of Jiguang Push, and then extract all the codes related to Jiguang Push into this category, extract all the codes related to WeChat and put them into the WeChat category, and then there will be new functions. To add, I just need to create a new category. Just find the corresponding category directly for the code of any function to be changed during maintenance.

// 把所有和极光推送有关的代码都抽出来放入这个分类
#import "AppDelegate.h"

@interface AppDelegate (JPush)

@end

 

2. The underlying implementation of Category

Before explaining this issue, we need to have a certain understanding of the underlying mechanism of OC method calls and the memory storage structure of class objects. If you are unfamiliar with this one, you can first go to see the essence of my other blog OC object .

Let us first think about such a problem. When we Categoryextend an instance method to a class , when we call this instance method, it also finds the class object through the isa pointer of the instance, and then finds this method in the method list of the class object . So Categoryhow are the extended methods added to the method list of the class object? Is it added at compile time or at runtime?

First, let's look at Categorythe memory storage structure. The Categorybottom layer is actually a category_ttype of structure. We can see its definition in the objc4source code objc-runtime-new.hfile:

// 定义在objc-runtime-new.h文件中
struct category_t {
    const char *name; // 比如给Student添加分类,name就是Student的类名
    classref_t cls;
    struct method_list_t *instanceMethods; // 分类的实例方法列表
    struct method_list_t *classMethods; // 分类的类方法列表
    struct protocol_list_t *protocols; // 分类的协议列表
    struct property_list_t *instanceProperties; // 分类的实例属性列表
    struct property_list_t *_classProperties; // 分类的类属性列表
};

It can be seen from this structure that Categorynot only the method list, but also the protocol list and the attribute list are stored in it.

Every time we create a category, such a structure will be generated at compile time and the classification method list and other information will be stored in this structure. The relevant information classified in the compilation stage and the relevant information of this category are separated. At the runtime, all Category data of a certain class will be loaded through runtime, all the methods, attributes, and protocol data of all Category will be merged into an array respectively, and then the merged data will be inserted in front of the data of this class.

If you want to understand the detailed process, you can go to the source code . Because there are too many source codes, I won't post it here. Here is the function call process of the whole process when interpreting the source code:

From the objc-os.mmfiles of _objc_initthe function Start -> map_images-> map_images_nolock-> _read_images-> remethodizeClass-> attachCategories-> attachLists-> realloc、memmove、 memcpy.

Based on my understanding, let me give an example to describe the entire process. I will only explain the merging process of the instance method list. The merging process of the class method list, attribute list, protocol list and other information is the same.

First, we declare a Studentclass, and then create two categories: Student (aaa)and Student (bbb), originally and in the category have two methods, as shown in the following code:

// Student.m文件
#import "Student.h"
@implementation Student

- (void)study{
    NSLog(@"%s",__func__);
}

- (void)studentTest{
    NSLog(@"%s",__func__);
}
@end

// Student+aaa.m文件
#import "Student+aaa.h"
@implementation Student (aaa)

- (void)study{
    NSLog(@"%s",__func__);
}

- (void)studentAaaTest{
    NSLog(@"%s",__func__);
}
@end

// Student+bbb.m文件
#import "Student+bbb.h"
@implementation Student (bbb)

- (void)study{
    NSLog(@"%s",__func__);
}

- (void)studentBbbTest{
    NSLog(@"%s",__func__);
}
@end

 

 

 

  • When the compilation is complete, the information of the two categories aaa and bbb are stored in their corresponding structures, and the instance method lists of the structures are aaa->instanceMethods = @[@"study",@"studentAaaTest"]and respectively bbb->instanceMethods = @[@"study",@"studentBbbTest"]. At this time Student, the method list of the class object is stored in the class_ro_tstructure baseMethodList. So 在编译阶段各个方法列表都是分开存储的.
  • Until the operating phase, Studentthe class object initialization class_rw_tstructure, this structure also has a list of methods methods, it is a two-dimensional array, which after the initialization of class_ro_tthe baseMethodListcopied at this time methods = @[baseMethodList].
  • Then load the data of the two categories of aaa and bbb through runtime and merge their method lists. Their order in the merged array (I’ll give it a name here categoryMethodList) is related to the order in which they participate in the compilation. If aaa is compiled first, and then bbb is compiled, then in the merged array, bbb’s method list In the front, the method list of aaa is in the back, so this time categoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods].
  • Then add categoryMethodListthe data to methodsit. methodsThe capacity size before adding is 1, it will first categoryMethodListexpand according to the number of methods in the list (that is, there are several categories, here are 2 categories), methodsthe size after expansion is 3, it will first add the methodsoriginal data ( baseMethodList) Move to the end, and then categoryMethodListinsert the data in, so the final result is methods = @[bbb->instanceMethods,aaa->instanceMethods,baseMethodList].

This completes the merging of the classification method list and this class method list. So after the merger 分类的方法在前面(the last method involved the classification list compiled at the top), 本类的方法列表在最后面. So when there is a method with the same name as this class in the classification, the method in the classification is executed by calling this method. From this phenomenon, it seems that the method of this class is covered by the method of the same name in the classification. In fact, it is not covered, but the classification method is found first when the method is called, so the classification method is executed. For example, in the above example, there are studythis method in this class and two categories . If we print the method list of this class, we will find that there are three studymethods called .

3. How does Category extend properties to classes

Let's first look at the definition of a property for a normal class. For example, when we Studentdefine a property for a class @property (nonatomic , assign) NSInteger score;, the compiler will automatically generate a _scoremember variable for us, and will automatically implement the setter/getter method of this property:

@implementation Student
{
    NSInteger _score;
}

- (void)setScore:(NSInteger)score{
    _score = score;
}

- (NSInteger)score{
    return _score;
}
@end

Can we use it Categoryin the same way to give extended attributes and class member variables? We can see from Categorythe underlying structure category_tthat there are method lists, protocol lists, and attribute lists in this structure, but there is no member variable list, so we can Categorydefine attributes in it, but we can not define member variables. If you define member variables, the compiler Will report an error directly.

If we Studentdefine an attribute in the category, @property (nonatomic , strong) NSString *name;what will the compiler do for us? The compiler will only help us declare - (void)setName:(NSString *)name;and - (NSString *)name;these two methods, but will not implement these two methods, nor will it define member variables. So if we set the name attribute value to an instance object outside student.name = @"Jack", the compiler will not report an error, because the setter method is declared, but once the program runs, it will throw unrecognized selectoran exception because the setter method is not implemented.

How can we use namethis attribute normally ? We can manually implement the setter/getter methods. The key to implementing these two methods is how to save the attribute value. In ordinary classes, we define a member variable _nameto save the attribute value, but in classification we cannot Define member variables, so you need to think of other ways to save. We can achieve this requirement in the following ways.

3.1 Use the existing attributes in this class to store

What does it mean to store the existing attributes in this class? Let's give an example directly. For example, if I want to give the UIViewextension xand ythese two attributes, then we can add a category to achieve:

// .h文件
@interface UIView (Add)

@property (nonatomic , assign) CGFloat x;
@property (nonatomic , assign) CGFloat y;

@end


// .m文件
#import "UIView+Add.h"
@implementation UIView (Add)

- (void)setX:(CGFloat)x{
    CGRect origionRect = self.frame;
    CGRect newRect = CGRectMake(x, origionRect.origin.y, origionRect.size.width, origionRect.size.height);
    self.frame = newRect;
}

- (CGFloat)x{
    return self.frame.origin.x;
}

- (void)setY:(CGFloat)y{
    CGRect origionRect = self.frame;
    CGRect newRect = CGRectMake(origionRect.origin.x, y, origionRect.size.width, origionRect.size.height);
    self.frame = newRect;
}

- (CGFloat)y{
    return self.frame.origin.y;
}
@end

This method is to access the newly added attributes and values through the UIVieworiginal attributes frame. Obviously, this method has great limitations and can only be used in the above special circumstances.xy

3.2 Store through a custom global dictionary

For example to Studentadd a classification Student (add), the classification of the two attributes are defined nameand age, then we can classify .mdefinition file global dictionary 2 nameDicand ageDic, nameDicfor all instances of objects stored in the nameproperty value, wherein the pointer instance of an object as the key , The nameattribute value is used as value. ageDicUsed to store the ageattribute values ​​of all instance objects . The code is as follows:

#import "Student+add.h"

// 以实例对象的指针作为key
#define QJKey [NSString stringWithFormat:@"%p",self]

@implementation Student (add)

// 定义2个全局字典用来存储2个新增的属性的值
NSMutableDictionary *nameDic;
NSMutableDictionary *ageDic;

+ (void)load{
    nameDic = [NSMutableDictionary dictionary];
    ageDic = [NSMutableDictionary dictionary];
}

//
- (void)setName:(NSString *)name{
    nameDic[QJKey] = name;
}

- (NSString *)name{
    return nameDic[QJKey];
}

- (void)setAge:(NSInteger)age{
    ageDic[QJKey] = @(age);
}

- (NSInteger)age{
    return [ageDic[QJKey] integerValue];
}

@end

Although this method can meet our needs, there is a problem. Whenever we instantiate an object, we will add an element to the two global dictionaries, and when the instantiated object is destroyed, the global dictionary corresponds to it. The elements are not removed, which will cause these two dictionaries to occupy more and more memory, and there is a risk of memory overflow.

3.3 Store through associated objects

3.3.1 Description of associated object API

Associated objects are runtimea set of APIs provided, so header files need to be introduced #import <objc/runtime.h>.


Add associated object API:

void objc_setAssociatedObject(id object, 
                              const void * key,
                              id value, 
                              objc_AssociationPolicy policy);

For example, a name attribute is added to the Student category. I want to assign the name attribute of the stu of an instance object to Jack:

  • The first parameter ( object): the associated object, which is the abovestu
  • The second parameter ( key): Here you pass in a void *type of pointer as the key, this key is set by yourself, and the associated object is obtained later based on this key. Later, I will list several common ways to set the key.
  • The third parameter ( value): the attribute value to be set, which is Jack above
  • The fourth parameter ( policy): The modification type of the attribute to be set, such as the modification type of name strong, nonatomic, then the corresponding policy here is OBJC_ASSOCIATION_RETAIN_NONATOMIC. The specific correspondence is as follows (note that there is no policy corresponding to weak):
objc_AssociationPolicy Corresponding modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

Get associated objects:

id objc_getAssociatedObject(id object, const void * key);

Remove all associated objects:

void objc_removeAssociatedObjects(id object);

Common ways to set the key:
key is a void *type of pointer. In principle, you can set a pointer at will, as long as you ensure that the key when setting the associated object is the same as when obtaining the associated object. But in order to improve the readability of the code, we can set the key in the following ways:

For stuexample, nameassign values to the properties of instance objects Jack.

method one:

A static global void *variable is declared for each attribute , and the value stored in this variable is its own address, so that the uniqueness of the variable value can be guaranteed.

// 声明全局静态变量
static void *_name = &_name;

// 设置关联对象
objc_setAssociatedObject(stu, _name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

// 获取关联对象
NSString *temName = objc_getAssociatedObject(stu, _name);

Method 2:
This method is similar to the above method, except that no value is assigned after the global variable is declared, and the address value of the variable is directly set as the key:

static void *_name;

objc_setAssociatedObject(stu, &_name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

NSString *temName = objc_getAssociatedObject(stu, &_name);

Way three:

Use the address of the attribute name string as the key. Note that key = @"name"this is to assign the address of the string to the key instead of assigning the string itself to the key. In iOS, no matter how many pointer variables are defined to point to @"name"this string, these pointers actually point to the same memory space, so the uniqueness of the key can be guaranteed.

objc_setAssociatedObject(stu, @"name", @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

NSString *temName = objc_getAssociatedObject(stu, @"name");

Way four:

Use the getter method of the property @selectoras the key, because SELthe address corresponding to the same method name in a class is always the same. It is recommended to use this method, because there will be prompts when writing code.

objc_setAssociatedObject(stu, @selector(name), @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

NSString *temName = objc_getAssociatedObject(stu, @selector(name));

3.3.2 Save property values ​​through associated objects

// .h文件
#import "Student.h"
@interface Student (Add)

@property (nonatomic , strong) NSString *name;
@property (nonatomic , assign) NSInteger age;
@end


// .m文件
#import "Student+Add.h"
#import <objc/runtime.h>
@implementation Student (Add)

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, @selector(name));
}

- (void)setAge:(NSInteger)age{
    objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
}

- (NSInteger)age{
    return [objc_getAssociatedObject(self, @selector(age)) integerValue];
}
@end

3.3.3 Principle of Associated Object Storage

Some people may have questions. How is the underlying implementation of the associated object implemented? Does it generate member variables through attributes and then merge them into the member attribute list of the class object? In fact, it is not. The associated objects are stored separately. There are 4 core objects that implement the associated object technology at the bottom:

  • ObjcAssociation: This object there are two members uintptr_t _policyand id _valuethe two it is clear that we set the associated object parameters passed policyand value.
  • ObjectAssociationMap: This is a HashMappassed time (stored in key-value pairs, can be understood as a dictionary), the associated object to set keythe value as HashMapa to ObjcAssociationobject as HashMapa . For example, a category adds 3 attributes, and that instance object assigns values ​​to these 3 attributes, then HashMapthere are 3 elements in this instance. If one attribute of this instance object is assigned a value of nil, then this HashMapwill change this The key-value pair corresponding to the attribute is removed, and then HashMapthere are 2 elements left.
  • AssociationsHashMap: This is also one HashMap, taking the parameters passed in when setting the associated properties objectas (actually a value is calculated for the object through an algorithm ). Take ObjectAssociationMapas . So when a class (provided that there is an associated object set in the category of this class) instantiates an object, this HashMapwill add an element, and when an instantiated object is released, its corresponding key-value pair will also HashMapRemoved by this . Note that during the entire program run, AssociationsHashMapthere will only be one, which means that all the associated object information of the class is stored in this HashMap.
  • AssociationsManager: It can be seen from the name that it is a manager. Note that there is only one during the entire program operation, and it only contains one AssociationsHashMap.

The relationship diagram of these 4 objects is shown in the following figure:

 

 

Principle of Associated Object Storage.png

Guess you like

Origin blog.csdn.net/wangletiancsdn/article/details/105248713