【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 编译后,nameYPTPerson_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,就意味着:

  • 所有已创建的实例都需要重新分配内存,但它们的指针已经散落在各处
  • 系统类(如 NSStringUIView)的对象早在 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();
    }
};

_mapStoragestatic,全局只有一份。

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 三层哈希表提供属性存储,数据统一由全局容器管理,生命周期与宿主对象一致
相关推荐
2601_955767422 小时前
观复盾护景贴:东方哲思与双护科技的深度实测
人工智能·科技·ios·iphone·圆偏振光·磁控溅射
会Tk矩阵群控的小木2 小时前
企业级iMessage群发系统实战:单主机管控多iPhone设备完整实现
运维·ios·开源软件·个人开发
kcuwu.3 小时前
BERT文本分类完整实战指南
人工智能·分类·bert
人月神话-Lee3 小时前
【图像处理】vImage/Accelerate——SIMD 让 CPU 也能飞
图像处理·深度学习·ios·cnn·ai编程·swift
JeJe同学3 小时前
目标检测的分类原则
人工智能·目标检测·分类
万能小林子4 小时前
如何将网页在线转APP?5种打包工具对比速成指南(含在线/手机/电脑方案)
android·ios·uni-app·web app·wap2app·app打包·app封装
m沐沐4 小时前
【机器学习】Python 实现垃圾邮件分类(随机森林 + 可视化 + 特征重要性)
人工智能·python·随机森林·机器学习·分类·pycharm·回归算法
yingjie1104 小时前
Scanpy 单细胞转录组分析完整流程(上):从原始数据到细胞聚类
机器学习·数据挖掘·聚类
2601_955767424 小时前
iPhone 17屏幕反光怎么解?磁控溅射AR膜实测反射率低至0.5%
ios·ar·iphone·#观复盾护景贴·scinique双护技术