1. Category usage scenarios
Category
Also 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 Category
extend 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 Category
to the NSString
class, and then only need to import Category
the 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];
}
Category
In 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, AppDelegate
this 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, AppDelegate
and 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 Category
to AppDelegate
split, first we need to AppDelegate
divide 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 Category
extend 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 Category
how 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 Category
the memory storage structure. The Category
bottom layer is actually a category_t
type of structure. We can see its definition in the objc4
source code objc-runtime-new.h
file:
// 定义在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 Category
not 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.mm
files of _objc_init
the 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 Student
class, 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 respectivelybbb->instanceMethods = @[@"study",@"studentBbbTest"]
. At this timeStudent
, the method list of the class object is stored in theclass_ro_t
structurebaseMethodList
. So在编译阶段各个方法列表都是分开存储的
. - Until the operating phase,
Student
the class object initializationclass_rw_t
structure, this structure also has a list of methodsmethods
, it is a two-dimensional array, which after the initialization ofclass_ro_t
thebaseMethodList
copied at this timemethods = @[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 timecategoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods]
. - Then add
categoryMethodList
the data tomethods
it.methods
The capacity size before adding is 1, it will firstcategoryMethodList
expand according to the number of methods in the list (that is, there are several categories, here are 2 categories),methods
the size after expansion is 3, it will first add themethods
original data (baseMethodList
) Move to the end, and thencategoryMethodList
insert the data in, so the final result ismethods = @[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 study
this method in this class and two categories . If we print the method list of this class, we will find that there are three study
methods 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 Student
define a property for a class @property (nonatomic , assign) NSInteger score;
, the compiler will automatically generate a _score
member 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 Category
in the same way to give extended attributes and class member variables? We can see from Category
the underlying structure category_t
that there are method lists, protocol lists, and attribute lists in this structure, but there is no member variable list, so we can Category
define attributes in it, but we can not define member variables. If you define member variables, the compiler Will report an error directly.
If we Student
define 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 selector
an exception because the setter method is not implemented.
How can we use name
this 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 _name
to 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 UIView
extension x
and y
these 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 UIView
original attributes frame
. Obviously, this method has great limitations and can only be used in the above special circumstances.x
y
3.2 Store through a custom global dictionary
For example to Student
add a classification Student (add)
, the classification of the two attributes are defined name
and age
, then we can classify .m
definition file global dictionary 2 nameDic
and ageDic
, nameDic
for all instances of objects stored in the name
property value, wherein the pointer instance of an object as the key , The name
attribute value is used as value. ageDic
Used to store the age
attribute 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 runtime
a 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 avoid *
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 namestrong, nonatomic
, then the corresponding policy here isOBJC_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 stu
example, name
assign 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 @selector
as the key, because SEL
the 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 membersuintptr_t _policy
andid _value
the two it is clear that we set the associated object parameters passedpolicy
andvalue
.ObjectAssociationMap
: This is aHashMap
passed time (stored in key-value pairs, can be understood as a dictionary), the associated object to setkey
the value asHashMap
a键
toObjcAssociation
object asHashMap
a值
. For example, a category adds 3 attributes, and that instance object assigns values to these 3 attributes, thenHashMap
there are 3 elements in this instance. If one attribute of this instance object is assigned a value of nil, then thisHashMap
will change this The key-value pair corresponding to the attribute is removed, and thenHashMap
there are 2 elements left.AssociationsHashMap
: This is also oneHashMap
, taking the parameters passed in when setting the associated propertiesobject
as键
(actually a value is calculated for the object through an algorithm键
). TakeObjectAssociationMap
as值
. So when a class (provided that there is an associated object set in the category of this class) instantiates an object, thisHashMap
will add an element, and when an instantiated object is released, its corresponding key-value pair will alsoHashMap
Removed by this . Note that during the entire program run,AssociationsHashMap
there will only be one, which means that all the associated object information of the class is stored in thisHashMap
.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 oneAssociationsHashMap
.
The relationship diagram of these 4 objects is shown in the following figure:
Principle of Associated Object Storage.png