likes
comments
collection
share

分类(Category) VS 扩展(Extension)

作者站长头像
站长
· 阅读数 40

一、Category 分类

1. Category底层实现

我们知道在一个类中用@property声明属性,编译器会自动帮我们生成成员变量(下划线开头的变量)和setter/getter。但是在分类中添加属性,系统不会生成对应的成员变量以及set和get方法实现,只会生成set和get方法的声明,更不会添加对应的实例变量。所以如果在分类中用@property声明属性,编译和运行都会通过,只要不使用程序也不会崩溃。但如果调用了成员变量和setter/getter方法,报错就在所难免了。下面我们来探索下分类不能添加实例变量的原因。

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

struct _category_t {
    const char *name;  // 分类名
    struct _class_t *cls; // 分类所属的类名
    const struct _method_list_t *instance_methods;// 实例方法列表
    const struct _method_list_t *class_methods; // 类方法列表
    const struct _protocol_list_t *protocols; // 分类所实现的协议列表
    const struct _prop_list_t *properties;//属性列表
};

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

  • 给类添加实例方法 (instanceMethod)
  • 给类添加类方法 (classMethod)
  • 实现协议 (protocol)
  • 添加属性 (property) (一般通过关联对象的方式)

通过 分类(Category) VS 扩展(Extension)

可以看出方法列表,属性列表,协议列表都是可读可写的,但是成员变量列表是只读的。这也说明一个类生成之后,编译时就已经把成员列表信息放在class_ro_t中,不允许再动态的修改。以上都证明不能在分类中添加成员变量

面试题:Category中能不能添成员变量?为什么? 答:Category中不能添加成员变量。因为objc_class结构体中ivars成员变量信息是放在只读的class_ro_t结构体中,类一旦生成,就不能动态的添加成员变量。(这也说明了一个类生成之后,编译时就已经把成员列表信息放在class_ro_t中,不允许再动态的修改。) Category本身的底层结构category_t中也只保存了方法、属性和协议等信息,并没有保存成员变量信息。综合来说是Category中不能添加成员变量。

2. 关联对象给分类增加属性

分类可以通过关联对象(Objective-C Associated Objects)的方式添加属性。

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

//关联对象,传入nil则可以移除已有的关联对象
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的枚举值和说明

typedef OBJC_ENUM(uintptr_t, 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 (strong, nonatomic) NSString *name;
@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];
}

3. Category的代码格式

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

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

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

  • Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么Category附加完成之后,类的方法列表里会有两个methodA

  • Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,很开心的返回了,不会在理会后面的同名方法。

  • 同名方法的调用,是根据编译顺序决定的,对于“覆盖”掉的方法,会先找到最后一个编译的category里的对应方法。可查看项目的 Build Phases -> Compile Sources,位置越往后,越晚编译。

5. Category 作用

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

ibireme大神开源的YYCategories,针对系统的类使用分类拓展的小功能,很实用。

空白页框架DZNEmptyDataSet,通过对UIScrollView使用分类功能,完美的处理了空白页面的展示

6. Objective-C Associated Objects 的实现原理

关联对象的方法是

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

实现关联对象技术的核心对象有四个:

  • AssociationsManager:内部有一个AssociationsHashMap哈希表
  • AssociationsHashMap:以key-value的形式存储着disguised_objectObjectAssociationMap。(第一个参数object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object)
  • ObjectAssociationMap:以key-value的形式存储着设置关联对象传入的keyObjcAssociation
  • ObjcAssociation:存储着设置关联对象传入的值(value)和内存管理的策略(policy)

关系链:AssociationsManager-->AssociationsHashMap-->ObjectAssociationMap-->ObjcAssociation

void objc_setAsso ciatedObject(id object, const void * key,id value, objc_AssociationPolicy policy)为例,首先会通过AssociationsManager获取AssociationsHashMap,然后以object的内存地址为key,从AssociationsHashMap中取出ObjectAssociationMap,若没有,则新创建一个ObjectAssociationMap,然后通过key获取旧值,以及通过key和policy生成新值ObjcAssociation(policy, new_value),把新值存放到ObjectAssociationMap中,若新值不为nil,并且内存管理策略为retain,则会对新值进行一次retain,若新值为nil,则会删除旧值,若旧值不为空并且内存管理的策略是retain,则对旧值进行一次release

// 简化后的伪代码:
class AssociationsManager {
    static AssociationsHashMap *_mapStore;
}

// DenseMap是个map,存放key,value
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
}

原理图如下 分类(Category) VS 扩展(Extension)

二、Extension扩展

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

@interface ViewController ()
//私有属性
//私有方法
@end

1. Extension的作用

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

2. Extension的特点

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

3. Extension的代码格式

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

三、Category和Extension的区别

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