老生常谈category增加属性的几种操作
原文地址: https://juejin.im/post/5a2963876fb9a0452207681d?utm_source=gold_browser_extension
前言
日常开发中,为一个已有的类(比如说不想影响其文件结构)、第三方库提供的类增加几个property,已经是十分常见且需要的操作了,有人会单独起草一份category.m文件,也有人直接继承,像我一般会用category,一是能减少类文件的数量提高编译速度,二也是为了代码结构更加清晰。 这篇文章是用来写Category的进行属性扩展的行为的,所以我还是言归正传,首先,我要阐述一下目前比较主流的几个属性扩展形式,再往下进行分析:
利用 objc_setAssociatedObject函数进行对象的联合。 利用 class_addProperty 函数进行类属性的扩展 通过内部创建一个其他对象(比如字典),通过重写本对象set和get或者消息转发。
下面对这三种常用方法进行分析,其实常见的都是前面两种,第三种也是比较非主流。在分析这三种之前,我要谈一下为什么不能用 class_addIvar 函数。
class_addIvar 函数
在苹果文档中,对 class_addIvar 函数有下面一段话:
``` This function may only be called after objcallocateClassPair(:::) and before objcregisterClassPair(:). Adding an instance variable to an existing class is not supported. The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
这个功能只能在 objcallocateClassPair(:::) 之后和 objcregisterClassPair(:) 之前调用。不支持将实例变量添加到现有的类。 该类不能是元类。不支持将实例变量添加到元类。
去除干扰代码,我们寻找到下面的函数调用链条: objc_allocateClassPair -> objc_initializeClassPair_internal
// 下面的代码已经被我大部分剔除,只留下我们分析所需要用到的代码 static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta) { // Set basic info
}
// 无关代码已经剔除 BOOL class_addIvar(Class cls, const char name, size_t size, uint8_t alignment, const char type) { if (!cls) return NO;
} // 重点在这最后一句,前面我们已经看到 objc_allocateClassPair 函数所分配的新类的flags位信息,在此处 & RW_CONSTRUCTING,必定为真,取反后跳过大括号向下执行。
static Class realizeClass(Class cls) { runtimeLock.assertWriting();
}
所以在经过条件 !((RW_REALIZED | RW_REALIZING) & RW_CONSTRUCTING) 时返回NO。
static void *key = "key"; @implementation Person (Extra)
// 此处不考虑读写锁的问题
(void)setName:(NSString *)name{
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
(NSString *)name{
return objc_getAssociatedObject(self, key);
}
@end
```
上面的 objc_setAssociatedObject 函数内部的调用链条如下:
这种操作很直观的表达了我们的需要,且API十分友好,仅仅是对于 weak 策略我们需要自己设计一个。 并且这种操作的好处是我们无需关系关联对象的声明周期,因为和普通的属性一样,会随着宿主对象的释放而释放,具体可以看以下代码:
当然也有不足之处,利用 objc_setAssociatedObject 生成的关联对象无法直接利用目前主流的Json转Model库(原因是无法在ivar及property中遍历出来)。
利用 class_addProperty 函数进行类属性的扩展
class_addProperty 函数可以为我们生成类的property,@property是编译器的标识符,在普通类中可生成property、ivar、setMethod与getMethod,在我看来property的真实作用类似于方法的声明,后面我会再谈为什么。 在分类中使用class_addProperty和普通类一样, 只能生成set和get方法的声明,无论有没有被实现,我们都可以用 class_copyMethodList 函数得到property的list,如果这时候你想存储属性值,你依然必须手动或动态实现那些set和get方法,并且真实数据的存储也必须由你自己提供实现,比如可以使用前面所说的objc_setAssociatedObject 函数。 现在说说为啥property只是一个类似声明的作用呢,我们可以从苹果开源的代码中找到蛛丝马迹:
可以看到在class_rw_t的结构中,包含了另一个十分相似的 const class_ro_t ro 成员变量。 这个成员变量为一个不可修改内容的结构体指针,其中存储了类在编译时就已经确定好的ivar、 property、method、protocol等信息,在类的初始化时会通过 methodizeClass 函数将其大部分内容都拷贝到 class_rw_t rw中,其中 ivar 不会被拷贝,这也是前面所说的不能在运行时给已有的类增加 ivar的原因。 像property、method、protocol都是可以在运行时动态添加的,且存储到 rw 的结构中去。 好像说的有点跑题了,咱们还是一起看看property到底存储了什么信息:
可以看到,propperty中并没有存储很多信息,只有name和配置的属性,也没有实现函数的地址,所以前面我说property的作用其实和方法的声明是差不多的。 关于property的好处,也就是在使用网上json转model库时可以被遍历到了,但是如果你没有实现set和get,那依然会导致KVC的crash。
通过内部创建一个其他对象(比如字典),通过重写本对象set和get或者消息转发。
最后一种方法,也是比较少用的方式,说起来也比较简单,比如定义一个静态的字典变量,然后通过实现interface中声明的set和get的实现对这个字典变量做存取操作,或者通过消息转发中的 (id)forwardingTargetForSelector:(SEL)aSelector 方法返回这个字典变量,但是要注意本类中没有对转发做过什么事,不然这种方法也是不适用的。
对上文的总结
其实刚刚所描述的三种分类策略并不是很严谨,因为其中几种总是会搭配着使用,所以在此也要选择一个比较均衡的策略来实现Category属性的绑定。
建议的策略:
1、由于我们肯定会在interface 中提供生的property(由于没有合成实现与ivar,在此称为生的),所以这样对于在外部访问时和普通property相同。 1、由于缺乏的是实现以及可以存取的数据量,这里我们可以直接实现这些set与get。 1、set与get的实现可以通过 associatedObject 进行对对象的存取操作。
好处: 这种操作由于提供了生的property,所以在第三方的json转model库遍历property时可以直接遍历到,由于你手动实现了set与get,所以在遍历后的KVC赋值时也能起到作用,保证了和普通成员变量的操作一致性。
估计会有人看完结论后觉得:“ 我本来就是这么写的啊,你写这么多字到头来得出的结论和我平时写的也一样。”是的,我只能略表抱歉啦😏!
Last updated