1. Escenarios de uso de categorías
Category
Tambié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 Category
extender 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 Category
a la NSString
clase, y luego solo Category
se 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];
}
Category
Ademá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, AppDelegate
esta 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, AppDelegate
y habrá cada vez más códigos en ella, y algunos incluso tendrán miles de líneas.
En este momento, podemos usar Category
para AppDelegate
dividir, primero tenemos que AppDelegate
dividir 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 Category
extendemos 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, ¿ Category
có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 Category
estructura de almacenamiento de memoria. La Category
capa inferior es en realidad un category_t
tipo de estructura. Podemos ver su definición en el archivo de objc4
có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 Category
no 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.mm
archivos de _objc_init
la 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 Student
clase 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 respectivamentebbb->instanceMethods = @[@"study",@"studentBbbTest"]
. En este momentoStudent
, la lista de métodos del objeto de clase se almacena en laclass_ro_t
estructurabaseMethodList
. Entonces在编译阶段各个方法列表都是分开存储的
. - Hasta la fase de operación,
Student
laclass_rw_t
estructura de inicialización del objeto de clase , esta estructura también tiene una lista de métodosmethods
, es una matriz bidimensional, que luego de la inicialización declass_ro_t
labaseMethodList
copia en este momentomethods = @[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 vezcategoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods]
. - Luego agréguele
categoryMethodList
los datosmethods
.methods
El tamaño de la capacidad antes de agregar es 1, primero secategoryMethodList
expandirá de acuerdo con la cantidad de métodos en la lista (es decir, hay varias categorías, aquí hay 2 categorías),methods
el tamaño después de la expansión es 3, primero agregará losmethods
datos originales (baseMethodList
) Vaya al final y luegocategoryMethodList
inserte los datos, de modo que el resultado final seamethods = @[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 study
este método en esta clase y dos categorías.Si imprimimos la lista de métodos de esta clase, encontraremos que hay tres study
mé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 Student
definimos una propiedad para una clase @property (nonatomic , assign) NSInteger score;
, el compilador generará automáticamente una _score
variable 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 Category
de la misma manera para dar atributos extendidos y variables de miembro de clase? Podemos ver en Category
la estructura subyacente category_t
que 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 Category
definir atributos en ella, pero no podemos definir variables miembro. Si define variables miembro, el compilador Informará un error directamente.
Si Student
definimos 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 selector
una excepción porque el método setter no está implementado.
¿Cómo podemos utilizar name
este 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 _name
para 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 UIView
extensión x
y y
estos 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 UIView
atributos originales Obviamente, este método tiene grandes limitaciones y solo se puede usar en las circunstancias especiales anteriores.frame
x
y
3.2 Almacenar a través de un diccionario global personalizado
Por ejemplo, para Student
agregar una clasificación Student (add)
, se define la clasificación de los dos atributos name
y age
, luego, podemos clasificar .m
el diccionario global 2 del archivo de definición nameDic
y ageDic
, nameDic
para todas las instancias de objetos almacenados en el name
valor de la propiedad, donde la instancia de puntero de un objeto como clave , El name
valor del atributo se utiliza como valor. ageDic
Se utiliza para almacenar los age
valores 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 runtime
un 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 unvoid *
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 nombrestrong, nonatomic
, entonces la política correspondiente aquí esOBJC_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 stu
ejemplo, name
asigne 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 @selector
como clave, porque SEL
la 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 miembrosuintptr_t _policy
yid _value
los dos está claro que configuramos los parámetros del objeto asociado que pasamospolicy
yvalue
.ObjectAssociationMap
: Este es unHashMap
tiempo transcurrido (almacenado en pares de valores clave, se puede entender como un diccionario), el objeto asociado para establecerkey
el valor comoHashMap
un键
aObjcAssociation
objeto comoHashMap
una值
. Por ejemplo, una categoría agrega 3 atributos, y ese objeto de instancia asigna valores a estos 3 atributos, entoncesHashMap
hay 3 elementos en esta instancia. Si a un atributo de este objeto de instancia se le asigna un valor de nil, estoHashMap
cambiará este El par clave-valor correspondiente al atributo se elimina y luegoHashMap
quedan 2 elementos.AssociationsHashMap
: Este es también elHashMap
que toma los parámetros pasados al configurar los atributos asociadosobject
como键
(en realidad, se calcula un valor para el objeto a través de un algoritmo键
). TomaObjectAssociationMap
como值
. Entonces, cuando una clase (siempre que haya un objeto asociado en la categoría de esta clase) instancia un objeto, estoHashMap
agregará un elemento, y cuando se libera un objeto instanciado, su par clave-valor correspondiente tambiénHashMap
Eliminado por esto . Tenga en cuenta que durante toda la ejecución del programa,AssociationsHashMap
solo habrá uno, lo que significa que toda la información del objeto asociado de la clase se almacena en esteHashMap
.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 unoAssociationsHashMap
.
El diagrama de relaciones de estos 4 objetos se muestra en la siguiente figura:
Principio de almacenamiento de objetos asociados.png