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

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://philm.gitbook.io/philm-ios-wiki/mei-zhou-yue-du/lao-sheng-chang-tan-category-zeng-jia-shu-xing-de-ji-zhong-cao-zuo.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
