分类类扩展

195 阅读7分钟

一 Category 分类

Category 是表示一个指向分类的结构体的指针,其定义如下:

struct objc_category {
  char *category_name                          OBJC2_UNAVAILABLE; // 分类名
  char *class_name                             OBJC2_UNAVAILABLE; // 分类所属的类名
  struct objc_method_list *instance_methods    OBJC2_UNAVAILABLE; // 实例方法列表
  struct objc_method_list *class_methods       OBJC2_UNAVAILABLE; // 类方法列表
  struct objc_protocol_list *protocols         OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}

从结构体可以看出,分类能

  • 给类添加实例方法 (instanceMethod)
  • 给类添加类方法 (classMethod)
  • 实现协议 (protocol)

这个结构体里面根本没有属性列表,所以不能添加实例变量,即无法自动生成实例变量的setter和getter方法。当然,我们可以通过关联对象来实现分类对实例变量的添加.

分类不能添加属性的实质原因

我们知道在一个类中用@property声明属性,编译器会自动帮我们生成成员变量和setter/getter,但分类的指针结构体中,根本没有属性列表。所以在分类中用@property声明属性,既无法生成成员变量也无法生成setter/getter。

因此结论是:我们可以用@property声明属性,编译和运行都会通过,只要不使用程序也不会崩溃。但如果调用了_成员变量和setter/getter方法,报错就在所难免了。

Category编译之后的底层结构是 struct category_t(结构体),里面存储着分类的对象方法、类方法、属性、协议信息

    const char *name; //类的名字(name)
    
    classref_t cls; //类(cls)

    structmethod_list_t *instanceMethods;//category中所有给类添加的实例方法的列表(instanceMethods)

    struct method_list_t *classMethods; //category中所有添加的类方法的列表(classMethods)

    struct protocol_list_t *protocols; //category实现的所有协议的列表(protocols)

    struct property_list_t *instanceProperties; //category中添加的所有属性(instanceProperties)
};

从category_t的结构体也可以看出category可以添加实例方法,类方法,遵守协议,添加属性;但无法添加实例变量。 在category中可以有属性(property),但是该属性只是生成了getter和setter方法的声明,并没有产生对应的实现,更不会添加对应的实例变量。如果想为实例对象添加实例变量,可以尝试使用关联引用技术。

2. 关联对象(Objective-C Associated Objects)给分类增加属性

分类是不能直接添加属性,但是可以通过关联对象实现给分类添加属性。

① API介绍

关联对象Runtime提供了下面几个接口:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除一个对象的所有关联对象。
void objc_removeAssociatedObjects(id object)

key值必须保证唯一性,有以下三种推荐的key 值

  • 声明static char kAssociatedObjectKey;使用&kAssociatedObjectKey作为key值;
  • 声明static void *kAssociatedObjectKey = &kAssociatedObjectKey;使用kAssociatedObjectKey作为key值;
  • 用 selector,使用getter方法的名称作为key值。

objc_AssociationPolicy的枚举值和说明

    OBJC_ASSOCIATION_ASSIGN = 0,            // 指定一个弱引用相关联的对象。相当于@property(assign)/@property(unsafe_unretained)
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,  // 指定相关对象的强引用,非原子性。@property(nonatomic,strong)
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,    // 指定相关的对象被复制,非原子性。@property(nonatomic,copy)
    OBJC_ASSOCIATION_RETAIN = 01401,        // 指定相关对象的强引用,原子性。@property(atomic,strong)
    OBJC_ASSOCIATION_COPY = 01403           // 指定相关的对象被复制,原子性。@property(atomic,copy)   
};

在绝大多数情况下,我们都会使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的关联策略,这可以保证我们持有关联对象。

objc_removeAssociatedObjects函数我们一般是用不上的,因为这个函数会移除一个对象的所有关联对象,将该对象恢复成“原始”状态。这样做就很有可能把别人添加的关联对象也一并移除,这并不是我们所希望的。所以一般的做法是通过给objc_setAssociatedObject函数传入 nil 来移除某个已有的关联对象。

② 使用示例

为系统类添加属性:OC类型name和简单类型age

@property (assign, nonatomic) int age;

- (void)setName:(NSString *)name {
    /**
     *  为某个类关联某个对象
     *
     *  @param object#> 要关联的对象 description#>
     *  @param key#>    要关联的属性key description#>
     *  @param value#>  你要关联的属性 description#>
     *  @param policy#> 添加的成员变量的修饰符 description#>
     */
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    /**
     *  获取到某个类的某个关联对象
     *
     *  @param object#> 关联的对象 description#>
     *  @param key#>    属性的key值 description#>
     */
    return objc_getAssociatedObject(self, @selector(name));
}

NSString * const recognizerAge = @"kAge";

- (void)setAge:(int)age{
    
    objc_setAssociatedObject(self, (__bridge const void *)(kAge), @(age), OBJC_ASSOCIATION_ASSIGN);
}
- (int)age{
  
    return [objc_getAssociatedObject(self, (__bridge const void *)(kAge)) intValue];
}

//或者如下
-(void)setAge:(NSInteger)age {
    NSString *string = [NSString stringWithFormat:@"%ld",(long)age];
    objc_setAssociatedObject(self, @selector(age), string, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSInteger)age {
    return [objc_getAssociatedObject(self, @selector(age)) integerValue];
}

3. Category格式

@end

@implementation 待扩展的名称(分类的名称)
@end

4. Category的方法会“覆盖”原来类的同名方法吗?

  • Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么Category附加完成之后,类的方法列表里会有两个methodA
  • Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,很开心的返回了,不会在理会后面的同名方法。
  • 同名方法的调用,是根据编译顺序决定的,对于“覆盖”掉的方法,会先找到最后一个编译的category里的对应方法。可查看项目的 Build Phases -> Compile Sources,位置越往后,越晚编译。

5. Category 作用

  • 减少单个文件的体积
  • 把不同的功能分配到不同的分类里,便于管理
  • 可以按需加载想要的分类
  • 把Framework私有方法公开
  • 模拟多继承(另外可以模拟多继承的还有protocol)

二 Extension扩展

Extension是Category的一个特例。类扩展与分类相比只少了分类的名称,所以称之为“匿名分类”。类扩展听上去很复杂,但其实我们很早就认识它了.就是我们平时在.m文件里使用的

//私有属性
//私有方法
@end

1. Extension的作用

  • 声明私有属性
  • 声明私有方法
  • 声明私有成员变量

2. Extension的特点

  • 编译时决议
  • 只以声明的形式存在,多数情况下寄生在宿主类的.m中
  • 一般的私有属性写到.m文件中的类扩展中
  • 不能为系统类添加扩展

3. Extension的使用格式

//私有属性
//私有方法(如果不实现,编译时会报警,Method definition for 'XXX' not found)
@end


三 Category和Extension的区别

  • 分类是运行时决议;扩展是编译时决议;(所以扩展中声明的方法没有被实现,编译器会报警,但是分类种的方法没有被实现编译器是不会有任何警告的)
  • 分类原则上只能增加方法,并且是公开的(无法直接添加属性,可以通过runtime添加属性,原因通过runtime可以解决无setter/getter的问题);扩展能添加方法,实例变量,默认是@private类型的,且只能作用于自身类,而不是子类或者其他地方;
  • 分类有自己的实现部分;扩展无自己的实现部分,只能依托对应类的实现部分来实现;
  • 分类可以为系统类添加分类;扩展不可以为系统类添加扩展(必须有一个类的源码才能添加一个类的Extension);
  • 定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。