1. Scénarios d'utilisation des catégories
Category
Aussi appelé 分类
ou 类别
, est un moyen d'étendre les classes fournies par OC. Qu'il s'agisse d'une classe personnalisée ou d'une classe système, nous pouvons Category
étendre les méthodes à la classe d'origine (à la fois les méthodes d'instance et les méthodes de classe), et la méthode d'extension est exactement la même que la méthode d'origine. Par exemple, dans mon projet, j'ai souvent besoin de compter le nombre de lettres dans une chaîne, mais le système ne fournit pas cette méthode, nous pouvons alors étendre une méthode Category
à la NSString
classe, puis seul le Category
fichier d'en-tête importé peut être appelé comme méthode système. Méthodes d'extension.
// 给NSString类添加一个Category,并扩展一个实例方法
@interface NSString (QJAdd)
- (NSInteger)letterCount;
@end
// 在需要使用这个扩展方法的地方引入头文件 #import "NSString+QJAdd.h",然后就可以调用这个扩展方法了
- (void)test{
NSString *testStr = @"sdfjshdfjk.,d.889";
NSInteger letterCount = [testStr letterCount];
}
Category
En plus d'être utilisé pour étendre les classes, il existe également une utilisation plus avancée, qui consiste à 拆分模块
diviser un grand module en plusieurs modules plus petits pour faciliter la maintenance et la gestion. Qu'est-ce que ça veut dire? Je vais citer un problème que de nombreux développeurs auront, c'est-à-dire AppDelegate
cette catégorie. Cette classe est générée automatiquement lorsque le projet vient d'être créé et est utilisée pour gérer le cycle de vie du programme. Lorsque le projet a été créé pour la première fois, il n'y avait pas beaucoup de code dans cette classe, mais à mesure que le projet progressait, de plus en plus de codes étaient placés dans cette classe. Par exemple, lors de l'intégration de divers frameworks tiers tels que Jiguang Push, Youmeng, Baidu Maps, WeChat SDK, etc., le travail d'initialisation de ces frameworks tiers, et même les codes de logique métier associés, seront placés dans cette catégorie, ce qui conduit à l'application La fonction de devient de plus en plus complexe, AppDelegate
et il y aura de plus en plus de codes dedans, et certains ont même des milliers de lignes.
À ce stade, nous pouvons utiliser Category
pour AppDelegate
fractionner, nous devons d'abord AppDelegate
diviser le code, extraire le code de la même fonction et le mettre dans une catégorie. Par exemple, je peux créer une nouvelle catégorie de Jiguang Push, puis extraire tous les codes liés à Jiguang Push dans cette catégorie, et extraire tous les codes liés à WeChat et les mettre dans la catégorie WeChat. Il y aura de nouvelles fonctions plus tard. Pour ajouter, il me suffit de créer une nouvelle catégorie. Il suffit de trouver la catégorie correspondante directement pour le code de toute fonction à modifier pendant la maintenance.
// 把所有和极光推送有关的代码都抽出来放入这个分类
#import "AppDelegate.h"
@interface AppDelegate (JPush)
@end
2. La mise en œuvre sous-jacente de la catégorie
Avant d'expliquer ce problème, nous devons avoir une certaine compréhension du mécanisme sous-jacent des appels de méthode OC et de la structure de stockage de la mémoire des objets de classe. Si vous n'êtes pas familier avec celui-ci, vous pouvez d'abord aller voir l'essence de mon autre objet OC de blog .
Pensons d'abord à un tel problème. Lorsque nous Category
étendons une méthode d'instance à une classe , lorsque nous appelons cette méthode d'instance, elle trouve également l'objet de classe via le pointeur isa de l'instance, puis trouve cette méthode dans la liste des méthodes de l'objet de classe . Alors, Category
comment les méthodes étendues sont-elles ajoutées à la liste des méthodes de l'objet de classe? Est-il ajouté au moment de la compilation ou à l'exécution?
Examinons d'abord Category
la structure de stockage de la mémoire. La Category
couche inférieure est en fait un category_t
type de structure. Nous pouvons voir sa définition dans le fichier de objc4
code source objc-runtime-new.h
:
// 定义在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; // 分类的类属性列表
};
On peut voir à partir de cette structure que Category
non seulement la liste des méthodes, mais aussi la liste des protocoles et la liste des attributs y sont stockées.
Chaque fois que nous créons une catégorie, une telle structure sera générée au moment de la compilation et la liste des méthodes de classification et d'autres informations seront stockées dans cette structure. Les informations pertinentes classées au stade de la compilation et les informations pertinentes de cette catégorie sont séparées. Au moment de l'exécution, toutes les données de catégorie d'une certaine classe seront chargées via l'exécution, toutes les méthodes, attributs et données de protocole de toutes les catégories seront fusionnés dans un tableau respectivement, puis les données fusionnées seront insérées devant les données de cette classe.
Si vous voulez comprendre le processus détaillé, vous pouvez accéder au code source . Comme il y a trop de codes source, je ne le publierai pas ici. Voici le processus d'appel de fonction de l'ensemble du processus lors de l'interprétation du code source:
À partir des objc-os.mm
fichiers de _objc_init
la fonction Démarrer -> map_images
-> map_images_nolock
-> _read_images
-> remethodizeClass
-> attachCategories
-> attachLists
-> realloc、memmove、 memcpy
.
D'après ce que j'ai compris, je donnerai un exemple pour décrire l'ensemble du processus. Je n'expliquerai que le processus de fusion de la liste des méthodes d'instance. Le processus de fusion de la liste des méthodes de classe, de la liste d'attributs, de la liste des protocoles et d'autres informations est le même.
Tout d'abord, nous déclarons une Student
classe, puis créons deux catégories: Student (aaa)
et Student (bbb)
, à l'origine et dans la catégorie, nous avons deux méthodes, comme indiqué dans le code suivant:
// 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
- Lorsque la compilation est terminée, les informations des deux catégories aaa et bbb sont stockées dans leurs structures correspondantes, et les listes de méthodes d'instance des structures sont respectivement
aaa->instanceMethods = @[@"study",@"studentAaaTest"]
etbbb->instanceMethods = @[@"study",@"studentBbbTest"]
. À ce stadeStudent
, la liste des méthodes de l'objet de classe est stockée dans laclass_ro_t
structurebaseMethodList
. Alors在编译阶段各个方法列表都是分开存储的
. - Jusqu'à la phase d'exploitation,
Student
laclass_rw_t
structure d' initialisation de l'objet de classe , cette structure a également une liste de méthodesmethods
, il s'agit d'un tableau à deux dimensions, qui après l'initialisation declass_ro_t
labaseMethodList
copie à ce momentmethods = @[baseMethodList]
. - Ensuite, chargez les données des deux catégories aaa et bbb via le runtime et fusionnez leurs listes de méthodes. Leur ordre dans le tableau fusionné (je vais lui donner un nom ici
categoryMethodList
) est lié à l’ordre dans lequel ils participent à la compilation. Si aaa est compilé en premier, puis bbb est compilé, puis dans le tableau fusionné, la liste des méthodes de bbb À l'avant, la liste des méthodes de aaa est à l'arrière, donc cette foiscategoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods]
. - Ajoutez-y ensuite
categoryMethodList
les donnéesmethods
.methods
La taille de la capacité avant l'ajout est de 1, ellecategoryMethodList
s'agrandira d' abord en fonction du nombre de méthodes dans la liste (c'est-à-dire qu'il y a plusieurs catégories, voici 2 catégories),methods
la taille après l'expansion est de 3, elle ajoutera d' abord lesmethods
données d'origine (baseMethodList
) Allez à la fin, puiscategoryMethodList
insérez les données, de sorte que le résultat final soitmethods = @[bbb->instanceMethods,aaa->instanceMethods,baseMethodList]
.
Ceci termine la fusion de la liste des méthodes de classification et de cette liste des méthodes de classe. Ainsi , après la fusion 分类的方法在前面
(la dernière méthode comprend la liste de classification établie en haut), 本类的方法列表在最后面
. Ainsi, quand il y a une méthode avec le même nom que cette classe dans la classification, la méthode dans la classification est exécutée en appelant cette méthode. De ce phénomène, il semble que la méthode de cette classe soit couverte par la méthode du même nom dans la classification. En fait, elle n'est pas couverte, mais la méthode de classification est trouvée en premier lorsque la méthode est appelée, donc la méthode de classification est exécutée. Par exemple, dans l'exemple ci-dessus, il y a study
cette méthode dans cette classe et deux catégories . Si nous imprimons la liste des méthodes de cette classe, nous verrons qu'il y a trois study
méthodes appelées .
3. Comment Category étend-elle les propriétés aux classes
Jetons un œil à la définition d'une propriété pour une classe normale. Par exemple, lorsque nous Student
définissons une propriété pour une classe @property (nonatomic , assign) NSInteger score;
, le compilateur générera automatiquement une _score
variable membre pour nous et implémentera automatiquement la méthode setter / getter de cette propriété:
@implementation Student
{
NSInteger _score;
}
- (void)setScore:(NSInteger)score{
_score = score;
}
- (NSInteger)score{
return _score;
}
@end
Pouvons-nous l'utiliser Category
de la même manière pour donner des attributs étendus et des variables de membre de classe? Nous pouvons voir à partir de Category
la structure sous-jacente category_t
qu'il existe des listes de méthodes, des listes de protocoles et des listes d'attributs dans cette structure, mais il n'y a pas de liste de variables membres, nous pouvons donc y Category
définir des attributs, mais nous ne pouvons pas définir de variables membres. Si vous définissez des variables membres, le compilateur Rapportera une erreur directement.
Si nous Student
définissons un attribut dans la catégorie, @property (nonatomic , strong) NSString *name;
que fera le compilateur pour nous? Le compilateur ne nous aider à déclarer - (void)setName:(NSString *)name;
et - (NSString *)name;
ces deux méthodes, mais ne sera pas mise en œuvre de ces deux méthodes, ne définira pas non variables membres. Donc, si nous définissons la valeur de l'attribut name sur un objet d'instance à l'extérieur student.name = @"Jack"
, le compilateur ne signalera pas d'erreur, car la méthode setter est déclarée, mais une fois que le programme s'exécute, il lèvera unrecognized selector
une exception car la méthode setter n'est pas implémentée.
Comment pouvons-nous utiliser name
cet attribut normalement ? Nous pouvons implémenter manuellement les méthodes setter / getter. La clé de l'implémentation de ces deux méthodes est de savoir comment enregistrer la valeur d'attribut. Dans les classes ordinaires, nous définissons une variable membre _name
pour enregistrer la valeur d'attribut, mais dans la classification, nous ne pouvons pas Définissez des variables de membre, vous devez donc penser à d'autres méthodes de sauvegarde. Nous pouvons répondre à cette exigence des manières suivantes.
3.1 Utilisez les attributs existants dans cette classe pour stocker
Que signifie stocker les attributs existants dans cette classe? Prenons un exemple directement. Par exemple, si je veux donner l' UIView
extension x
et y
ces deux attributs, alors nous pouvons ajouter une catégorie à réaliser:
// .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
Cette méthode permet d'accéder aux attributs et valeurs nouvellement ajoutés via les UIView
attributs d'origine frame
. De toute évidence, cette méthode a de grandes limitations et ne peut être utilisée que dans les circonstances spéciales ci-dessus.x
y
3.2 Stocker via un dictionnaire global personnalisé
Par exemple pour Student
ajouter une classification Student (add)
, la classification des deux attributs est définie name
et age
, ensuite nous pouvons classer le .m
fichier de définition dictionnaire global 2 nameDic
et ageDic
, nameDic
pour toutes les instances d'objets stockés dans la name
valeur de propriété, où l'instance de pointeur d'un objet comme clé , La name
valeur d'attribut est utilisée comme valeur. ageDic
Utilisé pour stocker les age
valeurs d'attribut de tous les objets d'instance . Le code est comme suit:
#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
Bien que cette méthode puisse répondre à nos besoins, il y a un problème: chaque fois que nous instancions un objet, nous ajouterons un élément aux deux dictionnaires globaux, et lorsque l'objet instancié est détruit, le dictionnaire global lui correspond. L'élément n'a pas été supprimé, ce qui entraînera une occupation de plus en plus importante de ces deux dictionnaires, et il y a un risque de débordement de mémoire.
3.3 Stocker via les objets associés
3.3.1 Description de l'API objet associé
Les objets associés sont runtime
un ensemble d'API fournis, les fichiers d'en-tête doivent donc être introduits #import <objc/runtime.h>
.
Ajouter l'API d'objet associé:
void objc_setAssociatedObject(id object,
const void * key,
id value,
objc_AssociationPolicy policy);
Par exemple, un attribut name est ajouté à la catégorie Student. Je souhaite attribuer l'attribut name du stu d'un objet instance à Jack:
- Le premier paramètre (
object
): l'objet associé, qui est le ci-dessusstu
- Le second paramètre (
key
): Ici, vous passez unvoid *
type de pointeur comme clé, cette clé est définie par vous-même, et l'objet associé est obtenu plus tard sur la base de cette clé. Plus tard, je vais énumérer plusieurs façons courantes de définir la clé. - Le troisième paramètre (
value
): la valeur d'attribut à définir, qui est Jack au-dessus - Le quatrième paramètre (
policy
): Le type de modification de l'attribut à définir, tel que le type de modification du nomstrong, nonatomic
, alors la politique correspondante est iciOBJC_ASSOCIATION_RETAIN_NONATOMIC
. La correspondance spécifique est la suivante (notez qu'il n'y a pas de politique correspondant à faible):
objc_AssociationPolicy | Modificateur correspondant |
---|---|
OBJC_ASSOCIATION_ASSIGN | attribuer |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | fort, non atomique |
OBJC_ASSOCIATION_COPY_NONATOMIC | copie, non atomique |
OBJC_ASSOCIATION_RETAIN | fort, atomique |
OBJC_ASSOCIATION_COPY | copie, atomique |
Obtenez les objets associés:
id objc_getAssociatedObject(id object, const void * key);
Supprimez tous les objets associés:
void objc_removeAssociatedObjects(id object);
Méthodes courantes pour définir la clé: la
clé est un void *
type de pointeur. En principe, vous pouvez définir n'importe quel pointeur, du moment que vous vous assurez que la clé lors de la définition de l'objet associé est la même que lors de l'obtention de l'objet associé. Mais afin d'améliorer la lisibilité du code, nous pouvons définir la clé de la manière suivante:
Par stu
exemple, name
attribuez des valeurs aux propriétés des objets d'instance Jack
.
première méthode:
Une void *
variable globale statique est déclarée pour chaque attribut et la valeur stockée dans cette variable est sa propre adresse, de sorte que l'unicité de la valeur de la variable puisse être garantie.
// 声明全局静态变量
static void *_name = &_name;
// 设置关联对象
objc_setAssociatedObject(stu, _name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 获取关联对象
NSString *temName = objc_getAssociatedObject(stu, _name);
Méthode 2:
Cette méthode est similaire à la méthode ci-dessus, sauf qu'aucune valeur n'est attribuée après la déclaration de la variable globale et que la valeur d'adresse de la variable est directement définie comme clé:
static void *_name;
objc_setAssociatedObject(stu, &_name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, &_name);
Troisième voie:
Utilisez l'adresse de la chaîne de nom d'attribut comme clé. Notez qu'il key = @"name"
s'agit d'attribuer l'adresse de la chaîne à la clé au lieu d'affecter la chaîne elle-même à la clé. Dans iOS, quel que soit le nombre de variables de pointeur définies pour pointer vers @"name"
cette chaîne, ces pointeurs pointent en fait vers le même espace mémoire, de sorte que l'unicité de la clé peut être garantie.
objc_setAssociatedObject(stu, @"name", @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, @"name");
Quatrième voie:
Utilisez la méthode getter de la propriété @selector
comme clé, car SEL
l'adresse correspondant au même nom de méthode dans une classe est toujours la même. Il est recommandé d'utiliser cette méthode, car il y aura des invites lors de l'écriture du code.
objc_setAssociatedObject(stu, @selector(name), @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, @selector(name));
3.3.2 Enregistrer les valeurs de propriété via les objets associés
// .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 Principe du stockage d'objets associés
Certaines personnes peuvent avoir des questions. Comment l'implémentation sous-jacente de l'objet associé est-elle implémentée? Génère-t-elle des variables membres via des attributs, puis les fusionne-t-elle dans la liste d'attributs membres de l'objet de classe? En fait, ce n'est pas le cas. Les objets associés sont stockés séparément. Il y a 4 objets principaux qui implémentent la technologie d'objet associée en bas:
ObjcAssociation
: Cet objet il y a deux membresuintptr_t _policy
etid _value
les deux il est clair que nous définissons les paramètres d'objet associés passéspolicy
etvalue
.ObjectAssociationMap
: Ceci est unHashMap
temps écoulé (stockés dans des paires clé-valeur, peut être comprise comme un dictionnaire), l'objet associé pour définirkey
la valeur commeHashMap
un键
pourObjcAssociation
objet commeHashMap
un值
. Par exemple, une catégorie ajoute 3 attributs et cet objet d'instance attribue des valeurs à ces 3 attributs, alorsHashMap
il y a 3 éléments dans cette instance. Si un attribut de cet objet d'instance reçoit la valeur nil, celaHashMap
changera la situation La paire clé-valeur correspondant à l'attribut est supprimée, puisHashMap
il reste 2 éléments.AssociationsHashMap
: C'est également celuiHashMap
qui prend les paramètres passés lors de la définition des attributs associésobject
comme键
(en fait une valeur est calculée pour l'objet via un algorithme键
). PrendsObjectAssociationMap
comme值
. Ainsi, lorsqu'une classe (à condition qu'il y ait un objet associé dans la catégorie de cette classe) instancie un objet, celaHashMap
ajoutera un élément, et lorsqu'un objet instancié est libéré, sa paire clé-valeur correspondante sera égalementHashMap
Supprimé par ceci . Notez que pendant toute l'exécution du programme,AssociationsHashMap
il n'y en aura qu'un, c'est-à-dire que les informations d'objet associées de toutes les classes y sont stockéesHashMap
.AssociationsManager
: On peut voir du nom qu'il s'agit d'un gestionnaire Notez qu'il n'y en a qu'un pendant toute l'opération du programme, et qu'il n'en contient qu'unAssociationsHashMap
.
Le diagramme des relations de ces 4 objets est illustré dans la figure suivante:
Principe du stockage d'objets associés.png