Category在OC中地位很重要, 本文梳理关于Category的常见知识点!
1. Category在开发中的作用
- 给已知的类增加方法和属性
- 例如给系统的NSString添加
-md5方法等
- 例如给系统的NSString添加
- 把分类的实现在不同的文件中
- 可以减少单个文件的大小
- 不同功能分开组织
- 声明一些私有方法
2. Category与Extension的区别
Extension比较像匿名Category, 但是他们完全不同:
- Extension是在编译时就完全确定, 它就是类的一个部分!!!
- Category在编译时生成
struct Category_t的结构体, 直到运行时才完全加载!!
3. Category的底层实现
OC语言Category在编译以后会生成如下结构体:
typedef struct category_t {
const char *name; // 类的名字
classref_t cls; // 类cls
struct method_list_t *instanceMethods; // Category 实例方法列表
struct method_list_t *classMethods; // Category 类方法列表
struct protocol_list_t *protocols; // Category 的Protocol列表
struct property_list_t *instanceProperties; // Category的@property
} category_t;
因此Category可以看到, 分类可以添加:
- 实例方法
- 类方法
- 协议方法
- 属性的getter方法
Category无法添加实例变量
4. Category的加载与+load/+initialize
OC的运行依赖runtime, 而程序的启动依赖dyld,
-
在编译以后, 编译器会在Mach-O的DATA段中DATA段下的objc_catlist section保存Category信息, 用于在运行时加载
-
在运行时runtime加载的时候, 依赖
dyld
这里简单介绍一下dyld与runtime关系:
- dyld 是苹果的
the dynamic link editor, 就是动态连接器, 动态加载镜像image文件的 - 程序启动时, 内核首先加载dyld, dyld动态链接器会将程序依赖的各种动态库加载到内存中, 这部分内容都是在main函数之前调用的! 其中就有
libobjc, 它是OC和runtime相关的库 _objc_init是runtime库的入口函数!!! 主要工作是读取Mach-O文件信息中的OC的Segment-setion, 完成OC的内存布局和runtime相关初始化
其中 _objc_init初始化的函数如下:
void _objc_init(void) {
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
最关键的是_dyld_objc_notify_register, 会在dyld处理image的过程中, 注册几个observer监听回调函数:
map_images: 当dyld将image加载到内存时触发load_images: 当dyld初始化image时触发unmap_image: 当dyld移除image时触发
在Category第一次被加载到内存时, 会调用call_load_methods()方法, 该方法中展示:
- 一定会先调用类的+load方法,之后调用分类的+load方法.
- 如果类有继承关系, 先调用父类的+load, 再调用子类的+load.
所以load方法的调用, 不是走的消息发送的流程 -- objc_msgSend(). 而是底层通过c层代码直接调用的
当Category的Mach-O被完全加载到内存以后, 就会进行Category方法和类方法列表的融合, 调用流程如下:
// 方法调用栈: map_images -> map_images_nolock -> _read_images
具体过程的简单总结就是:
- 将Mach-O中的Category编译好的 instanceMethods, Protocols, Propertys 添加到
类对象!!! 类方法和协议添加到meta-class上 - Category的方法列表会插入到原来类的方法的列表中前侧(方法链式一个二维链表),
小结:
- 分类方法并没有替换原来类已经有的方法, 在后续用objc_msgSend消息发送时, 在方法列表中查找时, 优先找到了Category的方法!
- 不同的Category方法之间的顺序与编译Category的顺序有关
- 如果我们要调用原来类的方法(被Category的方法
覆盖了的方法), 可以使用runtime 遍历method_list, 然后找到最后一个方法就是类本身的方法
附加内容+initialize触发时机:
+initialize是类第一次接收到消息(objc_msgSend)的时候调用(即是类调用alloc时), 每一个类只会initialize一次(如果子类没有实现+initialize, 会调用父类的+initialize, 这样父类的initialize方法可能会被调用多次)- 由于
+initialize是通过消息发送机制调用的, 因此如果Category实现+initialize, 会覆盖类的实现
5. Category与关联对象
Category要给类添加成员变量, 需要借助AssociatieObject关联对象实现.
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)
关联对象底层依赖一个AssociationsManager, 内部维护一个静态的Storage管理着AssociationsHashMap, 它是一个HashTable
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
AssociationsManager::Storage AssociationsManager::_mapStorage;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
class ObjcAssociation {
uintptr_t _policy; // 关联策略
id _value; // 关联值
...
}
关联对象的小结:
- 关联对象底层由
AssociationsManager维护一个全局唯一AssociationsHashMap - 通过对象指针
DisguisedPtr<objc_object>从AssociationsHashMap取得ObjectAssociationMap - 用关联对象中的的参数
key(const void *key) 从ObjectAssociationMap取得ObjcAssociation ObjcAssociation对象成员变量是关联策略_policy和关联值_value