OC归纳总结 -- (5)OC对象的Category总结

262 阅读4分钟

Category在OC中地位很重要, 本文梳理关于Category的常见知识点!

1. Category在开发中的作用

  1. 给已知的类增加方法和属性
    1. 例如给系统的NSString添加-md5方法等
  2. 把分类的实现在不同的文件中
    1. 可以减少单个文件的大小
    2. 不同功能分开组织
  3. 声明一些私有方法

2. Category与Extension的区别

Extension比较像匿名Category, 但是他们完全不同:

  1. Extension是在编译时就完全确定, 它就是类的一个部分!!!
  2. 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可以看到, 分类可以添加:

  1. 实例方法
  2. 类方法
  3. 协议方法
  4. 属性的getter方法

Category无法添加实例变量

4. Category的加载与+load/+initialize

OC的运行依赖runtime, 而程序的启动依赖dyld,

  1. 在编译以后, 编译器会在Mach-O的DATA段中DATA段下的objc_catlist section保存Category信息, 用于在运行时加载

  2. 在运行时runtime加载的时候, 依赖dyld

这里简单介绍一下dyldruntime关系:

  1. dyld 是苹果的 the dynamic link editor, 就是动态连接器, 动态加载镜像image文件的
  2. 程序启动时, 内核首先加载dyld, dyld动态链接器会将程序依赖的各种动态库加载到内存中, 这部分内容都是在main函数之前调用的! 其中就有libobjc, 它是OC和runtime相关的库
  3. _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监听回调函数:

  1. map_images : 当dyldimage加载到内存时触发
  2. load_images: 当dyld初始化image时触发
  3. unmap_image: 当dyld移除image时触发

在Category第一次被加载到内存时, 会调用call_load_methods()方法, 该方法中展示:

  1. 一定会先调用类的+load方法,之后调用分类的+load方法.
  2. 如果类有继承关系, 先调用父类的+load, 再调用子类的+load.

所以load方法的调用, 不是走的消息发送的流程 -- objc_msgSend(). 而是底层通过c层代码直接调用的

当Category的Mach-O被完全加载到内存以后, 就会进行Category方法和类方法列表的融合, 调用流程如下:

// 方法调用栈: map_images -> map_images_nolock -> _read_images

具体过程的简单总结就是:

  1. 将Mach-O中的Category编译好的 instanceMethods, Protocols, Propertys 添加到 类对象!!! 类方法和协议添加到meta-class
  2. Category的方法列表会插入到原来类的方法的列表中前侧(方法链式一个二维链表),

小结:

  1. 分类方法并没有替换原来类已经有的方法, 在后续用objc_msgSend消息发送时, 在方法列表中查找时, 优先找到了Category的方法!
  2. 不同的Category方法之间的顺序与编译Category的顺序有关
  3. 如果我们要调用原来类的方法(被Category的方法覆盖了的方法), 可以使用runtime 遍历method_list, 然后找到最后一个方法就是类本身的方法

附加内容+initialize触发时机:

  1. +initialize是类第一次接收到消息(objc_msgSend)的时候调用(即是类调用alloc时), 每一个类只会initialize一次(如果子类没有实现+initialize, 会调用父类的+initialize, 这样父类的initialize方法可能会被调用多次)
  2. 由于+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; // 关联值
	...
}

关联对象的小结:

  1. 关联对象底层由AssociationsManager维护一个全局唯一 AssociationsHashMap
  2. 通过对象指针 DisguisedPtr<objc_object>AssociationsHashMap 取得 ObjectAssociationMap
  3. 用关联对象中的的参数 key(const void *key) 从 ObjectAssociationMap 取得 ObjcAssociation
  4. ObjcAssociation对象成员变量是关联策略 _policy 和关联值 _value