文章目录
- 分类(Category)
- 扩展(Extension)
-
- [分类 vs 扩展](#分类 vs 扩展)
- 分类的加载过程
-
- [分类在 _read_images 中的处理](#分类在 _read_images 中的处理)
- [rw / ro / rwe](#rw / ro / rwe)
- [attachCategories 批处理](#attachCategories 批处理)
- 分类不能加成员变量的深层原因
- 懒加载与非懒加载
- [+load 的调用顺序](#+load 的调用顺序)
- [关联对象(Associated Objects)](#关联对象(Associated Objects))
- 总结
分类(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在分类还是有作用的,比如:
- 调用点语法
- 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]));
}
原来方法列表往后挪,分类的插到头部。方法查找从前往后,插前面就先被找到。不是覆盖,是插队
分类的应用场景
- 为系统类扩展功能
- 分解体积庞大的类文件
- 声明 Framework 的私有方法
- 向对象添加非正式协议
扩展(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
- ro (
class_ro_t):Clean Memory,编译时数据,存在 Mach-O,不可变 - rw (
class_rw_t) :Dirty Memory,运行时数据,指向 ro + 运行时状态 - 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 层面:
_category_t结构体没有 ivar 列表(结构层面)- 成员变量存在 ro 里,ro 是只读的,编译时就写死了(内存层面)
- 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 -
如果已经有过关联,直接拿到已有的
ObjectAssociationMaprefs.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三层哈希表提供属性存储,数据统一由全局容器管理,生命周期与宿主对象一致