【iOS】老生常谈category增加属性的几种操作

前言

日常开发中,为一个已有的类(比如说不想影响其文件结构)、第三方库提供的类增加几个property,已经是十分常见且需要的操作了,有人会单独起草一份category.m文件,也有人直接继承,像我一般会用category,一是能减少类文件的数量提高编译速度,二也是为了代码结构更加清晰。

这篇文章是用来写Category的进行属性扩展的行为的,所以我还是言归正传,首先,我要阐述一下目前比较主流的几个属性扩展形式,再往下进行分析:

利用 objc_setAssociatedObject函数进行对象的联合。

利用 class_addProperty 函数进行类属性的扩展

通过内部创建一个其他对象(比如字典),通过重写本对象set和get或者消息转发。

下面对这三种常用方法进行分析,其实常见的都是前面两种,第三种也是比较非主流。在分析这三种之前,我要谈一下为什么不能

class_addIvar 函数。

class_addIvar 函数

在苹果文档中,对 class_addIvar 函数有下面一段话:

Thisfunctionmay only be called after objc_allocateClassPair(_:_:_:) and before objc_registerClassPair(_:). Adding an instancevariable to an existingclassisnot supported.

Theclassmust not be a metaclass. Adding an instancevariable to a metaclassisnot supported.

这个功能只能在 objc_allocateClassPair(_:_:_:) 之后和 objc_registerClassPair(_:) 之前调用。不支持将实例变量添加到现有的类。

该类不能是元类。不支持将实例变量添加到元类。

文档是说不能将此函数用于已有的类,必须是动态创建的类,为了能够知道为何会这样,我们需要翻阅一下苹果开源的 runtime 源码。

1.首先看一下关于 objc_allocateClassPair 函数的代码实现:

去除干扰代码,我们寻找到下面的函数调用链条:

objc_allocateClassPair -> objc_initializeClassPair_internal

// 下面的代码已经被我大部分剔除,只留下我们分析所需要用到的代码

staticvoidobjc_initializeClassPair_internal(Class superclass,constchar *name, Class cls, Class meta)

{

// Set basic info

cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;

meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;

cls->data()->version =0;

meta->data()->version =7;

// RW_CONSTRUCTING 类已分配但还未注册

// RW_COPIED_RO class_rw_t->ro 来自 class_ro_t 结构的复制

// RW_REALIZED //  class_t->data 的结构为 class_rw_t

// RW_REALIZING // 类已开始分配,但并未完成

// 以上几个宏都是对新类的class_rw_t结构设置基本信息

}

2.下面是class_addIvar的与我分析所需要的实现代码

// 无关代码已经剔除

BOOL

class_addIvar(Class cls,constchar *name, size_t size,

uint8_t alignment,constchar *type)

{

if(!cls)returnNO;

if(!type) type ="";

if(name  &&0== strcmp(name,"")) name = nil;

rwlock_writer_t lock(runtimeLock);

assert(cls->isRealized());

// No class variables

if(cls->isMetaClass()) {

returnNO;

}

// Can only add ivars to in-construction classes.

if(!(cls->data()->flags & RW_CONSTRUCTING)) {

returnNO;

}

}

// 重点在这最后一句,前面我们已经看到 objc_allocateClassPair 函数所分配的新类的flags位信息,在此处 & RW_CONSTRUCTING,必定为真,取反后跳过大括号向下执行。

3.已经存在的类,经过测试,flag位为 RW_REALIZED|RW_REALIZING,设置函数如下:

staticClass realizeClass(Class cls)

{

runtimeLock.assertWriting();

constclass_ro_t *ro;

class_rw_t *rw;

Class supercls;

Class metacls;

bool isMeta;

if(!cls)returnnil;

if(cls->isRealized())returncls;

assert(cls == remapClass(cls));

// fixme verify class is not in an un-dlopened part of the shared cache?

ro = (constclass_ro_t *)cls->data();

if(ro->flags & RO_FUTURE) {

// This was a future class. rw data is already allocated.

rw = cls->data();

ro = cls->data()->ro;

cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);

}else{

// Normal class. Allocate writeable class data.

rw = (class_rw_t *)calloc(sizeof(class_rw_t),1);

rw->ro = ro;

rw->flags = RW_REALIZED|RW_REALIZING;

cls->setData(rw);

}

}

所以在经过条件 !((RW_REALIZED | RW_REALIZING) & RW_CONSTRUCTING) 时返回NO。

以上便是对已有类不能使用 class_addIvar 函数的分析

好了,回到真正的话题,对上面三种操作的分析:

objc_setAssociatedObject

我们都知道,在category中使用property,可以生成set和get的方法声明,原因在此不做分析,一般为了方便的调用,我们都会写上property,关键在于没有set和get的实现,于是就会有下面这样的代码:

staticvoid*key ="key";

@implementation Person (Extra)

// 此处不考虑读写锁的问题

- (void)setName:(NSString *)name{

objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);

}

- (NSString *)name{

returnobjc_getAssociatedObject(self, key);

}

@end

上面的 objc_setAssociatedObject 函数内部的调用链条如下:

objc_setAssociatedObject -> objc_setAssociatedObject_non_gc -> _object_set_associative_reference

// 其中主要操作都在 _object_set_associative_reference 函数中,内部实现类似一般属性的set实现(保留新值,释放旧值),在此我们不进行深究,具体可以参考业内大佬的博客文章。

这种操作很直观的表达了我们的需要,且API十分友好,仅仅是对于 weak 策略我们需要自己设计一个。

并且这种操作的好处是我们无需关系关联对象的声明周期,因为和普通的属性一样,会随着宿主对象的释放而释放,具体可以看以下代码:

dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose -> objc_destructInstance

// 大部分释放操作在 objc_destructInstance 函数中完成

// 下面是 objc_destructInstance 函数的实现代码

void*objc_destructInstance(id obj)

{

if(obj) {

// Read all of the flags at once for performance.

bool cxx = obj->hasCxxDtor();

bool assoc = !UseGC && obj->hasAssociatedObjects();

bool dealloc = !UseGC;

// This order is important.

// 内部通过C++的析构函数进行对象属性的释放,具体可看sunny大神的博文

if(cxx) object_cxxDestruct(obj);

// 此处会移除所有的关联对象,也就是objc_setAssociatedObject 函数所设置上去的对象

if(assoc) _object_remove_assocations(obj);

// 清空引用计数与weak表

if(dealloc) obj->clearDeallocating();

}

returnobj;

}

当然也有不足之处,利用 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 是一个指向结构体 objc_class 的指针,而此结构体的结构如下所示:

struct objc_class : objc_object {

// Class ISA;

Class superclass;// 指向父类

cache_t cache;// 缓存指针与vtable(没学过C++,没了解过虚函数这些),加速方法的调用

class_data_bits_t bits;// 真正保存对象的ivar,property与method等信息的地方

}

在源码中大部分时候表现为将类的大部分信息保存在 class_rw_t *rw指针中,不过内部也是返回bits中处理后的信息

class_rw_t *data() {

returnbits.data();

}

在class_rw_t的结构中,结构如下所示:

struct class_rw_t {

// Be warned that Symbolication knows the layout of this structure.

uint32_t flags;// 类的信息标记

uint32_t version;// 当前运行时版本

constclass_ro_t *ro;

method_array_t methods;

property_array_t properties;

protocol_array_t protocols;

}

可以看到在class_rw_t的结构中,包含了另一个十分相似的 const class_ro_t *ro 成员变量。

这个成员变量为一个不可修改内容的结构体指针,其中存储了类在编译时就已经确定好的ivar、 property、method、protocol等信息,在类的初始化时会通过 methodizeClass 函数将其大部分内容都拷贝到 class_rw_t *rw中,其中 ivar 不会被拷贝,这也是前面所说的不能在运行时给已有的类增加 ivar的原因。

像property、method、protocol都是可以在运行时动态添加的,且存储到 rw 的结构中去。

好像说的有点跑题了,咱们还是一起看看property到底存储了什么信息:

struct property_t {

constchar *name;

constchar *attributes;

};

可以看到,propperty中并没有存储很多信息,只有name和配置的属性,也没有实现函数的地址,所以前面我说property的作用其实和方法的声明是差不多的。

关于property的好处,也就是在使用网上json转model库时可以被遍历到了,但是如果你没有实现set和get,那依然会导致KVC的crash。

通过内部创建一个其他对象(比如字典),通过重写本对象set和get或者消息转发。

最后一种方法,也是比较少用的方式,说起来也比较简单,比如定义一个静态的字典变量,然后通过实现interface中声明的set和get的实现对这个字典变量做存取操作,或者通过消息转发中的 (id)forwardingTargetForSelector:(SEL)aSelector 方法返回这个字典变量,但是要注意本类中没有对转发做过什么事,不然这种方法也是不适用的。

对上文的总结

其实刚刚所描述的三种分类策略并不是很严谨,因为其中几种总是会搭配着使用,所以在此也要选择一个比较均衡的策略来实现Category属性的绑定。

建议的策略:

由于我们肯定会在interface 中提供生的property(由于没有合成实现与ivar,在此称为生的),所以这样对于在外部访问时和普通property相同。

由于缺乏的是实现以及可以存取的数据量,这里我们可以直接实现这些set与get。

set与get的实现可以通过 associatedObject 进行对对象的存取操作。

好处: 这种操作由于提供了生的property,所以在第三方的json转model库遍历property时可以直接遍历到,由于你手动实现了set与get,所以在遍历后的KVC赋值时也能起到作用,保证了和普通成员变量的操作一致性。

估计会有人看完结论后觉得:“ 我本来就是这么写的啊,你写这么多字到头来得出的结论和我平时写的也一样。”是的,我只能略表抱歉啦!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容