OC动态语言特性(一): 类别(Category)、关联对象(AssociatedObject)在运行时的实现原理

前言:

本文所有runtime部分, 均来自objc4苹果官方源码

一 分类 Category

1 用处

1.1 分类可以为添加类方法, 协议, 属性(只有get和set方法的声明, 没有对其实现)
1.2 分解体积庞大的类文件
1.3 可以重载系统方法

2 特点

2.1 运行时决议
分类文件在编译后, 并没有立即把其添加的内容添加到原类中, 而是在运行时, 动态的把方法,协议等内容添加到原类中
2.2 可以为系统类添加方法

3 底层原理

3.1 底层结构
分类底层是个C++的结构体, 内部存储了分类的各种信息

struct category_t {
    const char *name;						// 分类名
    classref_t cls;							// 原类对象
    struct method_list_t *instanceMethods;	// 实例方法列表
    struct method_list_t *classMethods;		// 类方法列表
    struct protocol_list_t *protocols;		// 协议方法列表
    struct property_list_t *instanceProperties;	// property属性列表
    
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta) {
        if (isMeta) return nil; // classProperties;
        else return instanceProperties;
    }
};

3.2 分类的加载调用栈
当程序启动之后, 系统会在运行时通过一系列的函数调用进行初始化、加载镜像文件、读取可执行文件等操作

  • objc_init
  • map_2_images
  • map_images_nolock

之后调用了remethodizeClass函数来处理分类逻辑

static void remethodizeClass(Class cls) {
    category_list *cats;
    bool isMeta;
    runtimeLock.assertWriting();
    // 判断当前类是否为元类对象
    // 如果添加的是类方法,则为YES, 实例方法为NO
    isMeta = cls->isMetaClass();
    // Re-methodizing: check for more categories
    // 这里获取了未拼接的所有分类
    // 注意: 这里是运行时执行, 即分类的运行时决议特点, 在运行时添加
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        // 把分类拼接到原类上
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

把分类的内容动态拼接

static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    // 创建局部集合变量存储分类方法, 属性, 协议等
    method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));
        // Count backwards through cats to get newest categories first    
    int mcount = 0;
    int i = cats->count; // 原类的分类总数

3.3 敲黑板, 重点来了, 下面一段代码, 解释了分类内容什么时候被拼接到原类对象上, 以及如果多个分类有同名方法, 系统会调用哪个方法, 即哪个方法最终会生效

// 倒序遍历所有分类, 最先访问最后编译的分类
// 换句话说, 分类编译的越早, 被遍历到的越晚
while (i--) {
        auto& entry = cats->list[i];
        // 获取该分类的方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
        // 最后编译的分类, 最先被添加到分类数组中
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
    }
    // 读取原类数据,包含方法列表等信息
    auto rw = cls->data();
    // 对要添加的分类内容作一些内存方面的处理, 为后续拼接做准备
   	prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
   	// 注意:
   	// 这里, 将包括mcount个元素的mlists拼接到原类的methods方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
}

3.4 最后的组装, 需要注意的是, 其他分类的添加的内容, 如方法, 并非是被覆盖了, 而是因为方法的位置并非在首位, 所以该方法没有机会调用, 而造成了被覆盖的假象.
同理, '原类中方法被分类中方法<覆盖实现>'的原因也是这样.

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
        if (hasArray()) {
            // many lists -> many lists
            // 获取原类中元素总数
            uint32_t oldCount = array()->count;
            // 拼接之后的元素总数
            uint32_t newCount = oldCount + addedCount;
            // 根据拼接后的内容, 添加分类元素, 重新分配内存
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            // 设置总数
            array()->count = newCount;
            //// 重点来了
            //// 先把内存移动好, 给分类元素腾出空间 
            /*
            [[],[],[原有第一个元素],[原有第二个元素]....]
            */
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            //// 把分类元素添加到原类元素列表中 
            /*
            [
	            [addedLists中的第一个元素],
	            [addedLists中的第二个元素]...
	            [原有第一个元素],
	            [原有第二个元素]....
            ]
            */
            //// 到此, 因为addedLists中的元素添加规则是最后编译的最先被添加
            //// 所以, 以方法为例,最后编译的分类的方法最终插入到原类方法列表的第0个位置
            //// 也就是, 该方法在运行时调用时, 最终被执行
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }

二 关联对象 Associated Object

1 是什么

通俗来讲, 就是把一个对象关联到另一个对象上, 如给分类添加关联的成员变量. 但是, 只是关联, 并非被关联的对象属于关联对象.

2 怎么用

取关联值

// Returns the value associated with a given object for a given key.
// 根据指定的key获取对应的关联对象值, 并返回
id objc_getAssociatedObject(id object, const void *key)

设定关联值

// Sets an associated value for a given object using a given key and association policy.
// 设定值,并通过给定的关联策略建立key和value的关联关系关联到被关联对象object上(有点绕)
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

移除关联值

// Removes all associations for a given object.
// 移除该对象的所有关联对象
void objc_removeAssociatedObjects(id object)

3 为分类添加关联对象的本质

3.1 关联策略
相当于属性关键字, 有以下几种类型, 看到没, assign, retain, copy… 是不是很熟悉

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied.  *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.*   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied. *   The association is made atomically. */
};

3.2 关联本质
系统维护了一个全局的AssociationsManager单例管理类, 该类的生命周期和程序同属一个生命周期, AssociationsManager管理者维护了一个全局的AssociationsHashMap哈希表存储容器, 所有对象的关联内容在存储在这个容易中.所以说, 假如给分类添加了一个关联对象, 那么该关联内容既不需要分类管理, 也不是由原类管理.

class AssociationsManager {
    static spinlock_t _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()   { _lock.lock(); }
    ~AssociationsManager()  { _lock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

AssociationsHashMap内部维护了一个ObjectAssociationMap哈希表

class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };

ObjectAssociationMap内部维护了一个ObjcAssociation哈希表

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };

他们的关系如下图
在这里插入图片描述
一个简单理解图, 具体的代码实现不再贴出来, 有兴趣的同学可以去官方的源代码中寻找,这里苹果官方源码
在这里插入图片描述

三 扩展 Extension

和分类比有什么区别?

扩展可以为类添加私有属性, 私有方法, 声明私有变量.

  • 扩展是在编译时决议的, 类扩展中的方法属性, 在编译阶段就会被添加到类中, 因此扩展中的方法没有实现, 编译器会报警告;
  • 类扩展没有独立的实现(@implementation), 即类扩展所声明的方法必须依托对应类的实现部分来实现;
  • 类扩展即可以声明属性, 又可以声明私有成员变量;

我是个写代码的小学生, 如有不足, 万望不吝指教

参考资料:
苹果开发者网站
官方源码

发布了4 篇原创文章 · 获赞 1 · 访问量 139

猜你喜欢

转载自blog.csdn.net/weixin_43837354/article/details/104652741