El principio de implementación subyacente de Category en iOS

1. Escenarios de uso de categorías

CategoryTambién llamado 分类o 类别, es una forma de extender las clases proporcionadas por OC. Independientemente de si se trata de una clase personalizada o una clase del sistema, podemos Categoryextender los métodos a la clase original (tanto métodos de instancia como métodos de clase), y el método de extensión es exactamente el mismo que el método original. Por ejemplo, en mi proyecto, a menudo necesito contar la cantidad de letras en una cadena, pero el sistema no proporciona este método, luego podemos extender un método Categorya la NSStringclase, y luego solo Categoryse puede llamar al archivo de encabezado importado como método del sistema. Métodos de extensión.

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

- (NSInteger)letterCount;

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

CategoryAdemás de usarse para extender clases, también hay un uso más avanzado, que consiste en 拆分模块dividir un módulo grande en varios módulos más pequeños para facilitar el mantenimiento y la administración. Qué significa eso? Citaré un problema que tendrán muchos desarrolladores, es decir, AppDelegateesta categoría. Esta clase se genera automáticamente cuando se acaba de crear el proyecto y se utiliza para administrar el ciclo de vida del programa. Cuando se creó el proyecto por primera vez, no había mucho código en esta clase, pero a medida que el proyecto avanzaba, se colocarían más y más códigos en esta clase. Por ejemplo, al integrar varios marcos de terceros como Jiguang Push, Youmeng, Baidu Maps, WeChat SDK, etc., el trabajo de inicialización de estos marcos de terceros, e incluso los códigos de lógica empresarial relacionados, se colocarán en esta categoría, lo que conduce a la aplicación. La función de es cada vez más compleja, AppDelegatey habrá cada vez más códigos en ella, y algunos incluso tendrán miles de líneas.

En este momento, podemos usar Categorypara AppDelegatedividir, primero tenemos que AppDelegatedividir el código, extraer el código de la misma función y ponerlo en una categoría. Por ejemplo, puedo crear una nueva categoría de Jiguang Push y luego extraer todos los códigos relacionados con Jiguang Push en esta categoría, y extraer todos los códigos relacionados con WeChat y ponerlos en la categoría WeChat. Habrá nuevas funciones más adelante. Para agregar, solo necesito crear una nueva categoría. Simplemente busque la categoría correspondiente directamente para el código de cualquier función que se cambiará durante el mantenimiento.

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

@interface AppDelegate (JPush)

@end

 

2. La implementación subyacente de Categoría

Antes de explicar este problema, necesitamos tener una cierta comprensión del mecanismo subyacente de las llamadas al método OC y la estructura de almacenamiento de memoria de los objetos de clase. Si no está familiarizado con este, primero puede ir a ver la esencia de mi otro objeto OC de blog .

Pensemos primero en un problema de este tipo. Cuando Categoryextendemos un método de instancia a una clase , cuando llamamos a este método de instancia, también encuentra el objeto de clase a través del puntero isa de la instancia y luego encuentra este método en la lista de métodos del objeto de clase. . Entonces, ¿ Categorycómo se agregan los métodos extendidos a la lista de métodos del objeto de clase? ¿Se agrega en tiempo de compilación o en tiempo de ejecución?

Primero, veamos la Categoryestructura de almacenamiento de memoria. La Categorycapa inferior es en realidad un category_ttipo de estructura. Podemos ver su definición en el archivo de objc4código fuente 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; // 分类的类属性列表
};

Se puede ver en esta estructura que Categoryno solo se almacena la lista de métodos, sino también la lista de protocolos y la lista de atributos.

Cada vez que creamos una categoría, dicha estructura se generará en el momento de la compilación y la lista de métodos de clasificación y otra información se almacenará en esta estructura. Se separa la información relevante clasificada en la etapa de compilación y la información relevante de esta categoría. En el tiempo de ejecución, todos los datos de categoría de una determinada clase se cargarán a través del tiempo de ejecución, todos los métodos, atributos y datos de protocolo de todas las categorías se fusionarán en una matriz, respectivamente, y luego los datos fusionados se insertarán delante de los datos de esta clase.

Si desea comprender el proceso detallado, puede ir al código fuente . Debido a que hay demasiados códigos fuente, no lo publicaré aquí. Aquí está el proceso de llamada de función de todo el proceso al interpretar el código fuente:

Desde los objc-os.mmarchivos de _objc_initla función Inicio -> map_images-> map_images_nolock-> _read_images-> remethodizeClass-> attachCategories-> attachLists-> realloc、memmove、 memcpy.

Según mi entendimiento, déjeme dar un ejemplo para describir el proceso completo. Solo explicaré el proceso de fusión de la lista de métodos de instancia. El proceso de fusión de la lista de métodos de clase, la lista de atributos, la lista de protocolos y otra información es el mismo.

Primero, declaramos una Studentclase y luego creamos dos categorías: Student (aaa)y Student (bbb), originalmente y en la categoría tenemos dos métodos, como se muestra en el siguiente código:

// 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

 

 

 

  • Cuando se completa la compilación, la información de las dos categorías aaa y bbb se almacena en sus estructuras correspondientes, y las listas de métodos de instancia de las estructuras son aaa->instanceMethods = @[@"study",@"studentAaaTest"]y respectivamente bbb->instanceMethods = @[@"study",@"studentBbbTest"]. En este momento Student, la lista de métodos del objeto de clase se almacena en la class_ro_testructura baseMethodList. Entonces 在编译阶段各个方法列表都是分开存储的.
  • Hasta la fase de operación, Studentla class_rw_testructura de inicialización del objeto de clase , esta estructura también tiene una lista de métodos methods, es una matriz bidimensional, que luego de la inicialización de class_ro_tla baseMethodListcopia en este momento methods = @[baseMethodList].
  • Luego cargue los datos de las dos categorías de aaa y bbb a través del tiempo de ejecución y combine sus listas de métodos. Su orden en la matriz fusionada (le daré un nombre aquí categoryMethodList) está relacionado con el orden en el que participan en la compilación. Si aaa se compila primero, y luego se compila bbb, luego en la matriz fusionada, la lista de métodos de bbb En el frente, la lista de métodos de aaa está en la parte posterior, así que esta vez categoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods].
  • Luego agréguele categoryMethodListlos datos methods. methodsEl tamaño de la capacidad antes de agregar es 1, primero se categoryMethodListexpandirá de acuerdo con la cantidad de métodos en la lista (es decir, hay varias categorías, aquí hay 2 categorías), methodsel tamaño después de la expansión es 3, primero agregará los methodsdatos originales ( baseMethodList) Vaya al final y luego categoryMethodListinserte los datos, de modo que el resultado final sea methods = @[bbb->instanceMethods,aaa->instanceMethods,baseMethodList].

Esto completa la fusión de la lista de métodos de clasificación y esta lista de métodos de clase. Así que después de la fusión 分类的方法在前面(el último método consiste en la lista de clasificación compilado en la parte superior), 本类的方法列表在最后面. Entonces, cuando hay un método con el mismo nombre que esta clase en la clasificación, el método en la clasificación se ejecuta llamando a este método. A partir de este fenómeno, parece que el método de esta clase está cubierto por el método del mismo nombre en la clasificación. De hecho, no se cubre, pero el método de clasificación se encuentra primero cuando se llama al método, por lo que se ejecuta el método de clasificación. Por ejemplo, en el ejemplo anterior, hay studyeste método en esta clase y dos categorías.Si imprimimos la lista de métodos de esta clase, encontraremos que hay tres studymétodos llamados .

3. ¿Cómo extiende la categoría las propiedades a las clases?

Echemos un vistazo a la definición de una propiedad para una clase normal. Por ejemplo, cuando Studentdefinimos una propiedad para una clase @property (nonatomic , assign) NSInteger score;, el compilador generará automáticamente una _scorevariable miembro para nosotros e implementará automáticamente el método setter / getter de esta propiedad:

@implementation Student
{
    NSInteger _score;
}

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

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

¿Podemos usarlo Categoryde la misma manera para dar atributos extendidos y variables de miembro de clase? Podemos ver en Categoryla estructura subyacente category_tque hay listas de métodos, listas de protocolos y listas de atributos en esta estructura, pero no hay una lista de variables miembro, por lo que podemos Categorydefinir atributos en ella, pero no podemos definir variables miembro. Si define variables miembro, el compilador Informará un error directamente.

Si Studentdefinimos un atributo en la categoría, @property (nonatomic , strong) NSString *name;¿qué hará el compilador por nosotros? El compilador sólo nos ayudará a declaramos - (void)setName:(NSString *)name;y - (NSString *)name;estos dos métodos, pero no va a poner en práctica estos dos métodos, ni va a definir las variables miembro. Entonces, si establecemos el valor del atributo de nombre en un objeto de instancia externo student.name = @"Jack", el compilador no reportará un error, porque el método setter está declarado, pero una vez que el programa se ejecuta, lanzará unrecognized selectoruna excepción porque el método setter no está implementado.

¿Cómo podemos utilizar nameeste atributo normalmente ? Podemos implementar manualmente los métodos setter / getter. La clave para implementar estos dos métodos es cómo guardar el valor del atributo. En las clases ordinarias, definimos una variable miembro _namepara guardar el valor del atributo, pero en la clasificación no podemos Defina variables de miembro, por lo que debe pensar en otras formas de ahorrar. Podemos lograr este requisito de las siguientes formas.

3.1 Utilice los atributos existentes en esta clase para almacenar

¿Qué significa almacenar los atributos existentes en esta clase? Pongamos un ejemplo directamente. Por ejemplo, si quiero dar la UIViewextensión xy yestos dos atributos, entonces podemos agregar una categoría para lograr:

// .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

Este método consiste en acceder a los atributos y valores recién agregados a través de los UIViewatributos originales Obviamente, este método tiene grandes limitaciones y solo se puede usar en las circunstancias especiales anteriores.framexy

3.2 Almacenar a través de un diccionario global personalizado

Por ejemplo, para Studentagregar una clasificación Student (add), se define la clasificación de los dos atributos namey age, luego, podemos clasificar .mel diccionario global 2 del archivo de definición nameDicy ageDic, nameDicpara todas las instancias de objetos almacenados en el namevalor de la propiedad, donde la instancia de puntero de un objeto como clave , El namevalor del atributo se utiliza como valor. ageDicSe utiliza para almacenar los agevalores de atributo de todos los objetos de instancia . El código es el siguiente:

#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

Aunque este método puede satisfacer nuestras necesidades, existe un problema: siempre que instanciamos un objeto, agregaremos un elemento a los dos diccionarios globales, y cuando se destruya el objeto instanciado, el diccionario global le corresponde. El elemento no se ha eliminado, lo que hará que estos dos diccionarios ocupen cada vez más memoria, y existe el riesgo de desbordamiento de memoria.

3.3 Almacenar a través de objetos asociados

3.3.1 Descripción de la API del objeto asociado

Los objetos asociados son runtimeun conjunto de API que se proporcionan, por lo que es necesario introducir archivos de encabezado #import <objc/runtime.h>.


Agregar API de objeto asociado:

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

Por ejemplo, se agrega un atributo de nombre a la categoría Estudiante. Quiero asignar el atributo de nombre del objeto de un objeto de instancia a Jack:

  • El primer parámetro ( object): el objeto asociado, que es el anteriorstu
  • El segundo parámetro ( key): aquí se pasa un void *tipo de puntero como clave, esta clave la configura usted mismo y el objeto asociado se obtiene más tarde basándose en esta clave. Más adelante, enumeraré varias formas comunes de configurar la clave.
  • El tercer parámetro ( value): el valor del atributo que se establecerá, que es Jack arriba
  • El cuarto parámetro ( policy): El tipo de modificación del atributo que se va a establecer, como el tipo de modificación del nombre strong, nonatomic, entonces la política correspondiente aquí es OBJC_ASSOCIATION_RETAIN_NONATOMIC. La correspondencia específica es la siguiente (tenga en cuenta que no hay una política correspondiente a débil):
objc_AssociationPolicy Modificador correspondiente
OBJC_ASSOCIATION_ASSIGN asignar
OBJC_ASSOCIATION_RETAIN_NONATOMIC fuerte, no atómico
OBJC_ASSOCIATION_COPY_NONATOMIC copia, no atómico
OBJC_ASSOCIATION_RETAIN fuerte, atómico
OBJC_ASSOCIATION_COPY copia, atómico

Obtener objetos asociados:

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

Eliminar todos los objetos asociados:

void objc_removeAssociatedObjects(id object);

Formas comunes de configurar la clave:
clave es un void *tipo de puntero. En principio, puede configurar un puntero a voluntad, siempre que se asegure de que la clave al configurar el objeto asociado sea la misma que al obtener el objeto asociado. Pero para mejorar la legibilidad del código, podemos configurar la clave de las siguientes maneras:

Por stuejemplo, nameasigne valores a las propiedades de los objetos de instancia Jack.

método uno:

Se declara una void *variable global estática para cada atributo y el valor almacenado en esta variable es su propia dirección, por lo que se puede garantizar la unicidad del valor de la variable.

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

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

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

Método 2:
este método es similar al método anterior, excepto que no se asigna ningún valor después de que se declara la variable global, y el valor de la dirección de la variable se establece directamente como la clave:

static void *_name;

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

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

Camino tres:

Utilice la dirección de la cadena del nombre del atributo como clave. Tenga en cuenta que key = @"name"esto es para asignar la dirección de la cadena a la clave en lugar de asignar la cadena en sí a la clave. En iOS, no importa cuántas variables de puntero se definan para apuntar a @"name"esta cadena, estos punteros en realidad apuntan al mismo espacio de memoria, por lo que se puede garantizar la unicidad de la clave.

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

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

Camino cuatro:

Utilice el método getter de la propiedad @selectorcomo clave, porque SELla dirección correspondiente al mismo nombre de método en una clase es siempre la misma. Se recomienda utilizar este método, ya que habrá avisos al escribir código.

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

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

3.3.2 Guardar valores de propiedad a través de objetos asociados

// .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 Principio de almacenamiento de objetos asociados

Algunas personas pueden tener preguntas. ¿Cómo se implementa la implementación subyacente del objeto asociado? ¿Genera variables miembro a través de atributos y luego las fusiona en la lista de atributos miembros del objeto de clase? De hecho, no lo es. Los objetos asociados se almacenan por separado. Hay 4 objetos principales que implementan la tecnología de objetos asociados en la parte inferior:

  • ObjcAssociation: Este objeto tiene dos miembros uintptr_t _policyy id _valuelos dos está claro que configuramos los parámetros del objeto asociado que pasamos policyy value.
  • ObjectAssociationMap: Este es un HashMaptiempo transcurrido (almacenado en pares de valores clave, se puede entender como un diccionario), el objeto asociado para establecer keyel valor como HashMapun a ObjcAssociationobjeto como HashMapuna . Por ejemplo, una categoría agrega 3 atributos, y ese objeto de instancia asigna valores a estos 3 atributos, entonces HashMaphay 3 elementos en esta instancia. Si a un atributo de este objeto de instancia se le asigna un valor de nil, esto HashMapcambiará este El par clave-valor correspondiente al atributo se elimina y luego HashMapquedan 2 elementos.
  • AssociationsHashMap: Este es también el HashMapque toma los parámetros pasados ​​al configurar los atributos asociados objectcomo (en realidad, se calcula un valor para el objeto a través de un algoritmo ). Toma ObjectAssociationMapcomo . Entonces, cuando una clase (siempre que haya un objeto asociado en la categoría de esta clase) instancia un objeto, esto HashMapagregará un elemento, y cuando se libera un objeto instanciado, su par clave-valor correspondiente también HashMapEliminado por esto . Tenga en cuenta que durante toda la ejecución del programa, AssociationsHashMapsolo habrá uno, lo que significa que toda la información del objeto asociado de la clase se almacena en este HashMap.
  • AssociationsManager: Se puede ver por el nombre que es un administrador Tenga en cuenta que solo hay uno durante toda la operación del programa, y ​​solo contiene uno AssociationsHashMap.

El diagrama de relaciones de estos 4 objetos se muestra en la siguiente figura:

 

 

Principio de almacenamiento de objetos asociados.png

Supongo que te gusta

Origin blog.csdn.net/wangletiancsdn/article/details/105248713
Recomendado
Clasificación