# 老生常谈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 objc*allocateClassPair(*:*:*:) and before objc*registerClassPair(*:). 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.

这个功能只能在 objc*allocateClassPair(*:*:*:) 之后和 objc*registerClassPair(*:) 之前调用。不支持将实例变量添加到现有的类。 该类不能是元类。不支持将实例变量添加到元类。

```
文档是说不能将此函数用于已有的类，必须是动态创建的类，为了能够知道为何会这样，我们需要翻阅一下苹果开源的 runtime 源码。
- 首先看一下关于 objc_allocateClassPair 函数的代码实现:
```

去除干扰代码，我们寻找到下面的函数调用链条: objc\_allocateClassPair -> objc\_initializeClassPair\_internal

// 下面的代码已经被我大部分剔除，只留下我们分析所需要用到的代码 static void objc\_initializeClassPair\_internal(Class superclass, const char \*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结构设置基本信息
```

}

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

// 无关代码已经剔除 BOOL class\_addIvar(Class cls, const char *name, size\_t size, uint8\_t alignment, const char* type) { if (!cls) return NO;

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

rwlock_writer_t lock(runtimeLock);

assert(cls->isRealized());

// No class variables
if (cls->isMetaClass()) {
    return NO;
}

// Can only add ivars to in-construction classes.
if (!(cls->data()->flags & RW_CONSTRUCTING)) {
    return NO;
}
```

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

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

static Class realizeClass(Class cls) { runtimeLock.assertWriting();

```
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;

if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));

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

ro = (const class_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的实现，于是就会有下面这样的代码:
```

static void \*key = "key"; @implementation Person (Extra)

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

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

  &#x20; objc\_setAssociatedObject(self, key, name, OBJC\_ASSOCIATION\_COPY\_NONATOMIC);

  }
* (NSString \*)name{

  &#x20; return objc\_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();
    }

    return obj;
}
```

当然也有不足之处，利用 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() { 
        return bits.data();
    }

    在class_rw_t的结构中，结构如下所示:
    struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;   // 类的信息标记
    uint32_t version; // 当前运行时版本

    const class_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 {
    const char *name;
    const char *attributes;
};
```

可以看到，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赋值时也能起到作用，保证了和普通成员变量的操作一致性。

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