Le principe de mise en œuvre sous-jacent de Category dans iOS

1. Scénarios d'utilisation des catégories

CategoryAussi 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 NSStringclasse, puis seul le Categoryfichier 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];
}

CategoryEn 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 AppDelegatecette 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, AppDelegateet il y aura de plus en plus de codes dedans, et certains ont même des milliers de lignes.

À ce stade, nous pouvons utiliser Categorypour AppDelegatefractionner, nous devons d'abord AppDelegatediviser 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, Categorycomment 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 Categoryla structure de stockage de la mémoire. La Categorycouche inférieure est en fait un category_ttype de structure. Nous pouvons voir sa définition dans le fichier de objc4code 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 Categorynon 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.mmfichiers de _objc_initla 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 Studentclasse, 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"]et bbb->instanceMethods = @[@"study",@"studentBbbTest"]. À ce stade Student, la liste des méthodes de l'objet de classe est stockée dans la class_ro_tstructure baseMethodList. Alors 在编译阶段各个方法列表都是分开存储的.
  • Jusqu'à la phase d'exploitation, Studentla class_rw_tstructure d' initialisation de l'objet de classe , cette structure a également une liste de méthodes methods, il s'agit d'un tableau à deux dimensions, qui après l'initialisation de class_ro_tla baseMethodListcopie à ce moment methods = @[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 fois categoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods].
  • Ajoutez-y ensuite categoryMethodListles données methods. methodsLa taille de la capacité avant l'ajout est de 1, elle categoryMethodLists'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), methodsla taille après l'expansion est de 3, elle ajoutera d' abord les methodsdonnées d'origine ( baseMethodList) Allez à la fin, puis categoryMethodListinsérez les données, de sorte que le résultat final soit methods = @[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 studycette 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 studymé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 Studentdéfinissons une propriété pour une classe @property (nonatomic , assign) NSInteger score;, le compilateur générera automatiquement une _scorevariable 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 Categoryde la même manière pour donner des attributs étendus et des variables de membre de classe? Nous pouvons voir à partir de Categoryla structure sous-jacente category_tqu'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 Categorydé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 Studentdé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 selectorune exception car la méthode setter n'est pas implémentée.

Comment pouvons-nous utiliser namecet 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 _namepour 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' UIViewextension xet yces 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 UIViewattributs 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.xy

3.2 Stocker via un dictionnaire global personnalisé

Par exemple pour Studentajouter une classification Student (add), la classification des deux attributs est définie nameet age, ensuite nous pouvons classer le .mfichier de définition dictionnaire global 2 nameDicet ageDic, nameDicpour toutes les instances d'objets stockés dans la namevaleur de propriété, où l'instance de pointeur d'un objet comme clé , La namevaleur d'attribut est utilisée comme valeur. ageDicUtilisé pour stocker les agevaleurs 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 runtimeun 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 un void *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 nom strong, nonatomic, alors la politique correspondante est ici OBJC_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 stuexemple, nameattribuez 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é @selectorcomme clé, car SELl'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 membres uintptr_t _policyet id _valueles deux il est clair que nous définissons les paramètres d'objet associés passés policyet value.
  • ObjectAssociationMap: Ceci est un HashMaptemps écoulé (stockés dans des paires clé-valeur, peut être comprise comme un dictionnaire), l'objet associé pour définir keyla valeur comme HashMapun pour ObjcAssociationobjet comme HashMapun . Par exemple, une catégorie ajoute 3 attributs et cet objet d'instance attribue des valeurs à ces 3 attributs, alors HashMapil y a 3 éléments dans cette instance. Si un attribut de cet objet d'instance reçoit la valeur nil, cela HashMapchangera la situation La paire clé-valeur correspondant à l'attribut est supprimée, puis HashMapil reste 2 éléments.
  • AssociationsHashMap: C'est également celui HashMapqui prend les paramètres passés lors de la définition des attributs associés objectcomme (en fait une valeur est calculée pour l'objet via un algorithme ). Prends ObjectAssociationMapcomme . Ainsi, lorsqu'une classe (à condition qu'il y ait un objet associé dans la catégorie de cette classe) instancie un objet, cela HashMapajoutera un élément, et lorsqu'un objet instancié est libéré, sa paire clé-valeur correspondante sera également HashMapSupprimé par ceci . Notez que pendant toute l'exécution du programme, AssociationsHashMapil n'y en aura qu'un, c'est-à-dire que les informations d'objet associées de toutes les classes y sont stockées HashMap.
  • 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'un AssociationsHashMap.

Le diagramme des relations de ces 4 objets est illustré dans la figure suivante:

 

 

Principe du stockage d'objets associés.png

Je suppose que tu aimes

Origine blog.csdn.net/wangletiancsdn/article/details/105248713
conseillé
Classement