【iOS】底层原理:分类、扩展和关联对象

文章目录

分类(Category)

底层结构

打开 objc 源码,找到分类的底层结构 _category_t:

c 复制代码
struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
    WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) const {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi) const;
    
    protocol_list_t *protocolsForMeta(bool isMeta) const {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

有方法列表、协议列表、属性列表------但没有 ivar 数组。这是分类不能添加成员变量的直接原因(结构体层面)

再往深层想------为什么苹果要把分类设计成这样?因为对象的内存布局在编译期就确定了 。一个类编译完成后,class_getInstanceSize 返回的大小就固定了,每个实例在堆上占据的内存是连续的一块。如果允许分类在运行时添加 ivar,就相当于要动态修改所有已存在对象的内存布局------已经创建的那些实例怎么办?它们的空间已经分配好了,没法扩容。更不用说系统类的对象在 App 启动前就可能已经被创建了。

所以分类不能加 ivar 的根本原因不是"runtime 做不到",而是从设计上就不允许打破编译期确定的内存布局,保证所有对象的兼容性。

用 clang 验证

objc 复制代码
@interface YPPerson (Text)
@property (nonatomic, copy) NSString *name;
@end

再通过clang处理这个分类文件

cpp 复制代码
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;
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
#pragma warning(disable:4273)

static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_YPTPerson_$_Text __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	1,
	{{"name","T@\"NSString\",C,N"}}
};

extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_YPTPerson;

static struct _category_t _OBJC_$_CATEGORY_YPTPerson_$_Text __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"YPTPerson",
	0, // &OBJC_CLASS_$_YPTPerson,
	0,
	0,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_YPTPerson_$_Text,
};

只有方法列表和属性列表,确实没有 ivar

分类的 @property

分类用 @property 只生成 setter/getter 的声明,不生成实现和 _ 成员变量。需要自己实现:

objc 复制代码
@implementation YPPerson (Text)
- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
    return objc_getAssociatedObject(self, @selector(name));
}
@end

但是@property在分类还是有作用的,比如:

  1. 调用点语法
  2. KVC兼容,KVC依赖 -setName: / -name 方法存在

方法优先级

本类和分类有同名方法时,分类优先。多个分类时,后编译的分类优先

复制代码
后编译分类方法 -> 先编译分类方法 -> 本类方法 -> 父类方法

从 attachLists 的源码看本质:

cpp 复制代码
void attachLists(List* const * lists, uint32_t mcount)
{
    memmove(array()->lists + mcount, array()->lists, oldCount * sizeof(array()->lists[0]));
    memcpy(array()->lists, lists, mcount * sizeof(array()->lists[0]));
}

原来方法列表往后挪,分类的插到头部。方法查找从前往后,插前面就先被找到。不是覆盖,是插队

分类的应用场景

  1. 为系统类扩展功能
  2. 分解体积庞大的类文件
  3. 声明 Framework 的私有方法
  4. 向对象添加非正式协议

扩展(Extension)

扩展有时叫"匿名分类":

objc 复制代码
@interface YPTPerson ()
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
@end
cpp 复制代码
// _name 成员变量直接出现在 YPTPerson_IMPL 中
struct YPTPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *__strong _name;
};
//_name 被注册到 ivar 列表
extern "C" unsigned long int OBJC_IVAR_$_YPTPerson$_name __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct YPTPerson, _name);

static struct /*_ivar_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count;
	struct _ivar_t ivar_list[1];
} _OBJC_$_INSTANCE_VARIABLES_YPTPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_ivar_t),
	1,
	{{(unsigned long int *)&OBJC_IVAR_$_YPTPerson$_name, "_name", "@\"NSString\"", 3, 8}}
};
//sayHello + name + setName: 全在类的实例方法列表里
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_YPTPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	5,
	{{(struct objc_selector *)"sayHello", "v16@0:8", (void *)_I_YPTPerson_sayHello},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_YPTPerson_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_YPTPerson_setName_},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_YPTPerson_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_YPTPerson_setName_}}
};
//整个文件搜索不到任何 _category_t 相关的东西。扩展的方法和 ivar 就是编译期直接写进 _class_ro_t:

clang 编译后,name 在 YPTPerson_IMPL 成员变量表里,sayHello 在 method list 中,编译期就合并进了本类

分类 vs 扩展

维度 分类 扩展
时机 运行时动态合并 编译时直接合并进 ro
成员变量 不能直接添加 可以添加(私有)
@property 只生成声明 正常生成 ivar + setter/getter
方法实现 分类自己的 .m 必须在主类的 @implementation 中
范围 公开,任意类 私有,仅自身类
文件 独立 .h/.m 不一定要独立 .m

扩展无法对系统类使用,比如UIKit、Foudation框架或者Core Data、Core Animation、AVFoundation 等框架的系统类都是不能使用的

分类的加载过程

完整的类加载流程在类的加载那篇博客已经详细分析过了,这里从分类视角来看

分类在 _read_images 中的处理

_read_images 的第 8 阶段会扫描镜像中的分类,通过 load_categories_nolock 做标记。但这步只是标记,实际附加是在 load_images 阶段

数据流动:

复制代码
Mach-O 中的 __objc_catlist 段
  ↓ _read_images
unattachedCategories 全局表(暂存)
  ↓ realizeClassWithoutSwift → methodizeClass
attachToClass → attachCategories → attachLists(真正合并)

分类的加载不是一步到位的,而是先暂存、等类准备好了、再 merge------这是为了处理类之间的依赖和加载顺序问题

rw / ro / rwe

  1. ro (class_ro_t):Clean Memory,编译时数据,存在 Mach-O,不可变
  2. rw (class_rw_t) :Dirty Memory,运行时数据,指向 ro + 运行时状态
  3. rwe (class_rw_ext_t):按需分配,有分类要附加时才创建

约 90% 的类没有分类,不需要 rwe。extAllocIfNeeded 只在第一次有分类附加时调用。

attachCategories 批处理

cpp 复制代码
static void attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count, int flags)
{
    if (cats_count == 0) return;

    // 按 64 个一组批处理
    bool init = (cats_count + ATTACH_BUFSIZ - 1) / ATTACH_BUFSIZ != 1;
    
    // 从后往前遍历,逆序处理------确保后编译的分类在前
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[cats_count - 1 - i];
        // 取出分类的方法列表、属性列表、协议列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            // 排序后加入 rwe
            prepareMethodLists(cls, &mlist, 1, YES, isBundleClass(cls));
            rwe->methods.attachLists(&mlist, 1);
        }
        // 同理处理属性和协议...
    }
}

两点关键:

  • 逆序遍历:后编译的分类在数组后面,逆序处理让它先被 attach,排在更前面
  • 64 个一批:批处理优化性能

分类不能加成员变量的深层原因

从结构体和 ro/rwe 层面:

  1. _category_t 结构体没有 ivar 列表(结构层面)
  2. 成员变量存在 ro 里,ro 是只读的,编译时就写死了(内存层面)
  3. rwe 没有 ivar 列表,只能加方法/属性/协议(设计层面)

从对象内存布局层面:

一个类在编译时它的 instanceSize(实例大小)就已经确定了。如果分类能在运行时添加 ivar,就意味着:

  • 所有已创建的实例都需要重新分配内存,但它们的指针已经散落在各处
  • 系统类(如 NSString、UIView)的对象早在 App 启动过程中就已经创建了
  • 无法兼容已有的所有类

所以苹果从设计上就不允许分类打破编译期确定的内存布局。**分类只允许添加行为(方法),不允许添加状态(成员变量)**如果确实需要存储状态,就用关联对象------它的数据存在全局哈希表里,不修改对象本身的内存布局

懒加载与非懒加载

  • 主类实现了 +load -> 启动时立即 realize,同时促使分类提前加载
  • 分类实现了 +load -> 反过来促使主类提前 realize
  • 都没有 +load -> 第一次发消息时才 realize

+load 的调用顺序

分类的 +load 在主类的 +load 之后调用:

复制代码
1. 先调所有类的 +load(父类 → 子类,按编译顺序)
2. 再调所有分类的 +load(按编译顺序)

+load 是通过函数指针直接调用的,不经过 objc_msgSend,所以不存在"覆盖"问题。而 +initialize 是通过消息发送走的,分类实现了就会覆盖主类的。

更详细的调用流程在前面类的加载那篇的 load_images 章节

关联对象(Associated Objects)

分类不能加成员变量,但又要存数据,所以Runtime 提供了关联对象机制

核心 API

objc 复制代码
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,关联的值存在字典里,不在对象本身。 所以它不改变类的结构,分类也能用。

最常见的实际写法(分类加@property的标配):

objc 复制代码
@implementation YPPerson (Text)
- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
    return objc_getAssociatedObject(self, @selector(name));
}
@end

key 用 @selector(name) 最方便------选择器本身是一个唯一指针,不用另外声明静态变量。

对象关联

注意没有 weak

Key 的几种用法

  • @selector 作为 key(最推荐)
objc 复制代码
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_getAssociatedObject(self, @selector(name));
  • 静态指针地址
objc 复制代码
static void *MyKey = &MyKey;
objc_setAssociatedObject(self, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  • 静态 char 变量
objc 复制代码
static char MyKey;
objc_setAssociatedObject(self, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  • 错误写法
objc 复制代码
static void *MyKey; // 不赋值 = NULL,多个属性共用会冲突

底层数据结构

关联对象的核心设计:所有关联数据不存对象本身,存在全局唯一的容器里。

复制代码
AssociationsManager
  └── AssociationsHashMap(全局唯一)
        ├── key: DisguisedPtr<objc_object>    → 被关联的对象
        └── value: ObjectAssociationMap
              ├── key: const void *           → 关联时传的 key
              └── value: ObjcAssociation
                      ├── _policy: 内存策略
                      └── _value: 关联的值

从源码看每一层

AssociationsManager

cpp 复制代码
class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;          // 全局唯一
public:
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }
};

_mapStorage 是 static,全局只有一份。

AssociationsHashMap

cpp 复制代码
using AssociationsHashMap = DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;

以 DisguisedPtr<objc_object> 为 key。DisguisedPtr 通过位运算包装指针,避免内存分析工具暴露关联关系。

ObjectAssociationMap

cpp 复制代码
using ObjectAssociationMap = DenseMap<const void *, ObjcAssociation>;

一个对象可以关联多个值,每个对应一个 key。

ObjcAssociation

cpp 复制代码
class ObjcAssociation {
    uintptr_t _policy;
    id _value;
public:
    ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}

    void acquireValue() {
        if (_policy & OBJC_ASSOCIATION_SETTER_RETAIN) {
            _value = objc_retain(_value);
        }
        if (_policy & OBJC_ASSOCIATION_SETTER_COPY) {
            _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
        }
    }

    void releaseHeldValue() {
        if (_policy == OBJC_ASSOCIATION_ASSIGN) return;
        objc_release(_value);
    }

    id retainReturnedValue() {
        if (_policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
            objc_retain(_value);
        }
        return _value;
    }

    id autoreleaseReturnedValue() {
        if (_policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE) {
            return objc_autorelease(_value);
        }
        return _value;
    }
};

总结:一个实例对象 → 一个 ObjectAssociationMap → 多个 key → 多个 ObjcAssociation

设值流程

调用链:

复制代码
objc_setAssociatedObject(object, key, value, policy)
  -> SetAssocHook.get()(object, key, value, policy)
  -> _base_objc_setAssociatedObject(object, key, value, policy)
  -> _object_set_associative_reference(object, key, value, policy)

跳到核心方法:

cpp 复制代码
void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) {
    // 1. 预处理
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};
    association.acquireValue();  // 根据策略 retain/copy 新值

    // 2. 获取全局哈希表
    AssociationsManager manager;
    AssociationsHashMap &associations(manager.get());

    if (value != nil) {
        // 3. value 非空 → 插入/更新
        auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
        if (refs_result.second) {
            // 首次关联 → 标记 has_assoc
            object->setHasAssociatedObjects();
        }
        auto &refs = refs_result.first->second;
        auto result = refs.try_emplace(key, std::move(association));
        if (!result.second) {
            // key 已存在 → swap 替换旧值
            result.first->second = std::move(association);
        }
    } else {
        // 4. value 为空 → 移除该 key
        auto i = associations.find(disguised);
        if (i != associations.end()) {
            auto &refs = i->second;
            auto j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;  // 保存旧值
                refs.erase(j);
                if (refs.empty()) {
                    associations.erase(i);
                }
            }
        }
    }

    // 5. 释放旧值(在锁外执行,避免死锁)
    association.releaseHeldValue();
}

分步拆解

第一步:预处理

把 object 包装成 DisguisedPtr,把 policy+value 包装成 ObjcAssociation,调用 acquireValue()。acquireValue 内部:

  • 策略带 RETAIN 标记 → objc_retain(value)
  • 策略带 COPY 标记 → [value copy]
  • 都带 → 先 retain 再 copy

第二步:获取全局哈希表

通过 AssociationsManager 拿到唯一的 AssociationsHashMap。内部有锁,多线程安全。

第三步:value 非空时的插入

复制代码
associations.try_emplace(disguised, ObjectAssociationMap{})

这步在第一层 map 中查找或创建:

  • 如果是该对象第一次 设置关联对象,try_emplace 返回 second = true,创建新的 ObjectAssociationMap

  • 同时调用 setHasAssociatedObjects(),把 isa 的 has_assoc 位置为 1

  • 如果已经有过关联,直接拿到已有的 ObjectAssociationMap

    refs.try_emplace(key, std::move(association))

在第二层 map 中:

  • key 不存在 → 直接插入
  • key 已存在 → try_emplace 返回 second = false,通过 = std::move 替换旧值

第四步:value为nil 时的移除

两层查找后删除对应条目,如果 ObjectAssociationMap 空了,也从第一层 map 移除。

第五步:释放旧值

在锁外调用 releaseHeldValue(),根据策略 release 旧值,避免死锁。

has_assoc 标志位

之前分析 isa 时看过 ISA_BITFIELD:

c 复制代码
// __arm64__
uintptr_t has_assoc         : 1;  // 是否有关联对象

第一次设置关联对象时,setHasAssociatedObjects() 将该位置 1。dealloc 时判断这个位,跳过无关联对象的清理,提升性能。

取值流程

cpp 复制代码
id _object_get_associative_reference(id object, const void *key) {
    ObjcAssociation association{};
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        // 第一层查找
        auto i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            // 第二层查找
            auto j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }
    return association.autoreleaseReturnedValue();
}

两步走:先按对象找 map,再按 key 找 value。

移除流程

objc 复制代码
// 移除所有关联对象
objc_removeAssociatedObjects(object);

// 移除单个 key
objc_setAssociatedObject(object, key, nil, policy);

_object_remove_associations 中有一个细节------SYSTEM_OBJECT 策略的关联对象不会被移除,只会移除用户设置的:

cpp 复制代码
void _object_remove_associations(id object, bool deallocing) {
    AssociationsManager manager;
    AssociationsHashMap &associations(manager.get());
    auto i = associations.find((objc_object *)object);
    if (i != associations.end()) {
        ObjectAssociationMap refs = std::move(i->second);
        associations.erase(i);

        for (auto &ref : refs) {
            if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
                // 系统级别的关联对象重新插入回去
                associations[disguised].try_emplace(ref.first, std::move(ref.second));
            } else {
                // 用户设置的关联对象释放
                ref.second.releaseHeldValue();
            }
        }
    }
}

对象 dealloc 时的清理

复制代码
1. objc_cxxDestruct          -> C++ 析构
2. _object_remove_associations -> 移除关联属性  <- 关联对象在这里清理
3. weak_clear_no_lock        -> 弱引用置 nil
4. 引用计数处理              -> 从 refcnts 擦除
5. free(obj)                -> 销毁对象

关联对象自动随宿主对象销毁而清理,不需要手动释放

总结

分类不能加成员变量 -> 想存数据怎么办?-> 关联对象。

三个 API 就够了:set(存)、get(取)、remove(清)。

底层是个全局大字典(AssociationsHashMap),数据挂在对象外面,不改变类结构。生命期跟对象走,对象释放自动清

最常见的用途是给系统类挂数据 (比如给 UIButton 绑一个回调 block)。自己写的类能直接写 @property 就别折腾关联对象

总结

特性 分类 扩展 关联对象
本质 Runtime 特性 编译器特性 Runtime 机制
加载时机 运行时合并到 rwe 编译时合并进 ro 运行时绑定到全局哈希表
成员变量 结构体无 ivar 列表 直接加进 ro 间接实现,不改类结构
添加方法 插到方法列表头部 私有 无
添加属性 声明通过不合成 自动合成 提供存储能力
使用范围 任意类(含系统类) 仅自身类 任意对象

三者的关系:

  • 扩展编译时合并进 ro,不参与运行时动态过程
  • 分类 运行时通过 attachLists 把方法列表插到 rwe 头部,由于 _category_t 无 ivar 列表 + ro 只读,无法添加成员变量
  • 关联对象 通过 AssociationsManager → AssociationsHashMap → ObjectAssociationMap → ObjcAssociation 三层哈希表提供属性存储,数据统一由全局容器管理,生命周期与宿主对象一致
相关推荐
计算机毕业设计杰瑞2 分钟前
【2027原创精品大数据】基于大数据的电商销售数据可视化分析系统,附源码_数据可视化_数据分析_数据挖掘_Hadoop_spark_文档指导_毕设指导
大数据·信息可视化·数据挖掘
weixin_403810133 分钟前
不用越狱也能批量控制 iPhone:EasyClick iOS 免越狱脚本实战
ios·iphone
计算机毕业设计杰瑞9 分钟前
【2027最新原创大数据】基于大数据的广西医疗机构及药店清单可视化分析,附源码_数据可视化_数据分析_数据挖掘_Hadoop_spark_文档指导_毕设指导
大数据·信息可视化·数据挖掘
计算机毕业设计杰瑞14 分钟前
【2027最新原创大数据】基于大数据的二手房成交信息分析与可视化,附源码_数据可视化_数据分析_数据挖掘_Hadoop_spark_文档指导_毕设指导
大数据·信息可视化·数据挖掘
计算机毕业设计杰瑞35 分钟前
【2027大数据毕设精品】基于大数据的社交媒体用户行为数据分析与可视化,附源码_数据可视化_数据分析_数据挖掘_Hadoop_spark_文档指导_毕设指导
大数据·信息可视化·数据挖掘·课程设计
言乐637 分钟前
逻辑回归与利弊
人工智能·算法·机器学习·数据挖掘·逻辑回归
weixin_403810131 小时前
iOS 自动化脚本怎么输入文字:代理、输入法与快捷指令三级方案
运维·ios·自动化
计算机毕业设计杰瑞1 小时前
【最新大数据项目】基于大数据的能见度探测数据分析与可视化,附源码_高质量项目_可视化_数据分析_毕设选题推荐_SPark_Hadoop_毕设指导
大数据·信息可视化·数据挖掘·课程设计
山顶夕景10 小时前
【Jev】Jev模型和Laya架构
分类·大模型·dev
2601_9627809118 小时前
需求预测校招能力栈|2026 届 JD 梳理、日常任务、面试题与备考方案
python·数据挖掘·数据分析