阅读 1499

iOS Category底层原理详细研究流程

本文探索源码 objc4 ( git开源地址 )

什么是 Category ?

CategoryObject-C 2.0 之后添加的语言特性, Category 的主要作用是为已经存在的类添加方法.


什么是 Extension ?

extension 通常我们称之为扩展、延展、匿名分类。extension 看起来很像一个匿名的 category ,但是 extensioncategory 几乎完全是两个东西。和 category 不同的是 extension 不但可以声明方法,还可以声明属性、成员变量。extension 一般用于声明私有方法,私有属性,私有成员变量。


CategoryExtension 有什么区别 ?

让我们从多个方面来回答这个问题。

  1. 表现形式
  • Category 是一个 .h 和一个 .m.

  • Extension 是一个 .h . (当然,也可以在一个类的 .m 中伴生, 写法就是 @interface *** /.../ @end 就不多说了.)

    那么同样, 创建时, 选择对应的类型即可.

  1. 功能机制
1️⃣ : Extension
  • Extension 是一个类的一部分, 它在 编译期 和头文件中 @interface , 实现文件中的 @implement 一起形成一个完整类 , Extension 伴随类的产生而产生 , 亦随之一起消亡.
  • Extension 可以添加实例变量.
  • Extension 一般用来隐藏类的私有信息 , 它无法直接为系统类提供扩展 , 但可以县创建系统类的子类 , 然后添加扩展.

举个🌰

p.objExtension = 28;
NSLog(@"%d",p.objExtension);
复制代码

如上使用, 发生崩溃.

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[LBPerson setObjExtension:]: unrecognized selector sent to instance 0x6000008ed4e0'
复制代码

NSObject 修改为 LBPerson , 结果正常.

2️⃣ : Category
  • Category 是在运行时期决议. 这个时期对象的内存布局已经确定了 , 如果再添加实例变量会破坏类的内部布局 , 这在编译型语言是灾难性的.

  • Category 可以给系统类添加分类.

  • Category 可以添加属性 , 但是并不会生成成员变量和对应的 getter 以及 setter 方法 .

同样 , 我们实验一下 : 🌰

//  LBPerson+Category.h

#import "LBPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface LBPerson (Category)
@property (nonatomic,assign) int ageCategory;
@end
复制代码

使用:

LBPerson * p = [[LBPerson alloc] init];
p.ageCategory = 25;   //分类添加属性错误
NSLog(@"%d",p.ageCategory);
复制代码

打印结果: 闪退.

-[LBPerson setAgeCategory:]: unrecognized selector sent to instance 0x6000021a72a0

当然, 我们可以通过 runtime 设置关联对象 来解决这个问题 , 下面会仔细阐述.


Category 有什么用 ?

  • 减少单个文件的体积 . 抽取 , 分离
  • 把不同的功能组织到不同的 Category
  • 可以随意按需加载
  • 声明私有方法
  • framework 的私有方法公开 ( 在子类中通过引用 , 声明父类类别后 , 即可调用其未公开的私有方法)

Tips:

请不要乱来:苹果官方会拒绝使用系统私有API的应用上架,因此即使学会了如何调用私有方法,在遇到调用其它类的私有方法时,要谨慎处理,尽量用其它方法替代。


Category 底层原理解析

1. 编译时

说了这么多 , 终于要开始看看它到底是个啥了. 打开终端/iterm2 , 编译转换成 c++.

clang -rewrite-objc LBPerson+Category.m
复制代码

找到编译后的文件, 打开, 搜索 _category_t , 找到结构体定义.

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; // 分类属性列表
};
复制代码

继续搜索 , 即可找到编译时 , 这个结构体存放的内容

static struct _category_t _OBJC_$_CATEGORY_LBPerson_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"LBPerson",
	0, // &OBJC_CLASS_$_LBPerson,
	0,
	0,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LBPerson_$_Category,
};
复制代码

搜索一下 _PROP_LIST_LBPerson_ 会发现我们定义的属性. 当然, 概括一下: 定义的方法, 属性, 等都会在编译时存放在对应的字段中 , 编译后通过 section 段区分标识存放到生成的 Mach-O 可执行文件中.

同时 , 再往下看

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_LBPerson_$_Category,
};
复制代码

也就是说当在编译时 , 工程中所有的分类会被存储到 __DATA 数据段中的 __objc_catlist 这个 section 段中.

以上就是编译时分类所做的事情 , 分类结构体, 分类的方法 , 以及声明的属性 , 和存放 已经完成.

2. 运行时

想要了解分类在运行时是如何加载和处理的. 我们需要先知道一个概念.

2.1 dyld

什么是 dyld?

  • dyld 是苹果的动态加载器 , 用来加载 image ( 注意: 这里的 image 不是指图片 , 而是 Mach-O 格式的二进制文件 )

  • 当程序启动时 , 系统内核会首先加载 dyld , 而 dyld 会将我们的 APP 所依赖的各种库加载到内存中 , 其中就包括 libobjc ( OC 和 runtime ) , 这些工作是在 APP 的 main 函数执行之前完成的.

  • _objc_initObject-C - runtime 库的入口函数 , 在这里主要是读取 Mach-O 文件 OC 对应的 Segment section , 并根据其中的数据代码信息 , 完成为 OC 的内存布局 , 以及初始化 runtime 相关的数据结构.

那么也就是说 , 我们要去 _objc_init 中一探究竟, 看看分类到底是怎么加载 , 如何读取 , 又是如何释放的呢 ?

2.2 查看源码

步骤 1️⃣: 直接打开 objc4 源码. 找到 _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(&map_images, load_images, unmap_image);
复制代码
  • map_images : dyldimage 加载进内存时 , 会触发该函数.
  • load_images : dyld 初始化 image 会触发该方法. ( 我们所熟知的 load 方法也是在此处调用 )
  • unmap_image : dyldimage 移除时 , 会触发该函数 .

那么我们就去研究研究 map_images 他加载时 , 我们的分类到底处理了什么 .

步骤 2️⃣ : 点击进去 , 中间过渡方法我就省略了. 直接来到 _read_images 的方法实现.

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t i;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0;
    static bool doneOnce;
    TimeLogger ts(PrintImageTimes);

    runtimeLock.assertLocked();

    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }
}
复制代码

由于方法过长 , 我就把一些读取其他的数据删除掉了, 只保留了读取分类部分 , 感兴趣的同学可以去研究下其他数据的读取逻辑.

点击进去 _getObjc2ClassList

GETSECT(_getObjc2ClassList,           classref_t,      "__objc_classlist");
复制代码

这个就是我们刚刚提到的 , 在编译时 , 分类被加载到这个 section 段中 , 我们看到读取是这么读的 . 也顺便验证了我们编译时的流程 .

那么也就是说 遍历所有的分类 , 然后一一添加设置 . 接下来 , 注意到在遍历中有一个方法

addUnattachedCategoryForClass(cat, cls->ISA(), hi);
复制代码

看名字大概也猜得到, 往原类中添加 ( 注意是原类, 不是元类. 也就是原先的类 ).

步骤 3️⃣: 直接进去查看这个方法.

static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    runtimeLock.assertLocked();

    // DO NOT use cat->cls! cls may be cat->cls->isa instead
    NXMapTable *cats = unattachedCategories();
    category_list *list;

    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    NXMapInsert(cats, cls, list);
}
复制代码

这里主要是 NXMapInsert(cats, cls, list); 这个方法, 其实简单说一下也就是把当前分类 , 和原类 建立起一个绑定关系 ( 其实就是通过数据结构映射起来 ). 为下面的事情做准备.

步骤 4️⃣ : 回到遍历方法中 , 建立起了绑定关系之后 , 下面还有一个方法 , 这个方法是遍历中最后的方法了 , 那么它必然是要将分类中的方法添加到原类中的 . 当然这是我们的猜想 , 我们带着这个目的去分析代码 .

remethodizeClass(cls->ISA());
复制代码

点击进去 , 前面开辟空间 , 读取分类数据和其他操作就不细说了 , 图上我都写进去了 . 其实简单归纳一下就是 开辟了一个空间 , 然后在下面的 prepareMethodLists 中 , 把分类等要添加要元类的数据放进去.

步骤 5️⃣ : 重点 attachCategories 方法前半段我们已经简单概述了一下 , 那么接下来来到

rw->methods.attachLists(mlists, mcount);
复制代码

点击进去 :

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;
        /**
         拷贝的操作
         void    *memmove(void *__dst, const void *__src, size_t __len);
         */
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
    } 
    else {
        // 1 list -> many lists
        List* oldList = list;
        uint32_t oldCount = oldList ? 1 : 0;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        array()->count = newCount;
        if (oldList) array()->lists[addedCount] = oldList;
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
    }
}
复制代码

以上方法就是分类添加方法的核心逻辑 . 简单来说就是通过 memmovememcpy 这两个函数 , 以及原本类的方法列表和新分类的方法列表合并 , 添加到原方法的方法列表中.

而且要注意的是:

分类的方法是添加在方法列表数组前面的位置的 .

运行时读取分类设置总结 :

  • ① : 在运行时, 读取 Mach-O 可执行文件 , 加载其中的 image 资源时 ( 也就是 map_images 方法中的 _read_images ) , 去读取编译时所存储到 __objc_catlistsection 段中的数据结构 , 并存储到 category_t 类型的一个临时变量中.
  • ② : 遍历这个临时变量数组, 依次读取
  • ③ : 将 catlist 和原先类 cls 进行映射
  • ④ : 调用 remethodizeClass 修改 method_list 结构 , 将分类的内容添加到原本类中.

至于属性和协议, 跟方法的流程是一样的, 只是存放在不同的 section 段中. 这里就不多赘述了 , 参考以下 :

疑问:

最后有一个疑问: ❓

首先我们知道 OC 查找方法流程中 , 当查找类方法的方法列表时 , 是采用了一个二分查找的方式的 . 那么我们类方法的扩展方法是添加到了原类的方法列表中前面位置的 . 那么它如何保证外部调用方法时 , 是一定会调用到类方法中的呢 ?

答:

看如下图:

当方法遍历二分查找时 , 后面的方法查找到 , 同样会往前查找一遍看看有没有同名 ( 方法编号 ) 方法 , 如果有 , 则返回的是前面的方法 . 以此来保证了其优先级顺序 , 也就是说 方法列表中前面的方法会有高优先级执行权限 .

从而也就保证了分类实现的目的.


Category 关联属性

众所周知 , Category 中声明属性 , 但并不会在 method_list 中生成对应的 settergetter 方法以及对应的实例变量 , 编译时会有警告.

那么解决方法大家也都知道 , 就是手动设置关联属性 , 可以理解成手动补上 settergetter 方法 . 具体写法如下:

#import "LBPerson+Category.h"
#import <objc/runtime.h>

static NSString * ageCategoryKey = @"ageCategoryKey";
@implementation LBPerson (Category)
- (NSString *)ageCategory {
    return objc_getAssociatedObject(self, &ageCategoryKey);
}

- (void)setAgeCategory:(NSString *)age {
    objc_setAssociatedObject(self, &ageCategoryKey, age, OBJC_ASSOCIATION_COPY);
}
复制代码

为什么分类并不会为其属性自动生成对应方法?

看了上面分类结构体的源码之后 , 其实我们就很清楚了 . 因为我们并没有看到像类的结构体中的实例变量列表 , 也就是我们所说的 ivar_list , 因此也就不会有编译器像类中自动帮我们做 @synthesize 生成实例变量 ivar 和自动生成settergetter 方法了

那么我们就来深入探讨一下 , 关联属性到底是如何实现属性以及到底是如何存储 , 又是如何销毁的呢 ?

注意 : 同样是刚刚的代码 objc4

步骤 1. 直接点击进入 objc_setAssociatedObject 方法. 过渡方法跳过.

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    //进行内存管理!!!
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        //初始化 HashMap
        AssociationsHashMap &associations(manager.associations());
        //当前对象的地址按位取反(key)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            //<fisrt : disguised_object , second : ObjectAssociationMap>
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                 //<fisrt : 标识(自定义的) , second : ObjcAssociation>
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    //ObjcAssociation
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
复制代码

具体总结一下这个方法: ( 其实我们也能大概猜出来这个方法要做什么 , 无非是和自动生成的 setter 方法类似的操作 )

  • 先根据内存管理语义做对应的引用计数等其他的操作. ( acquireValue(value, policy) 方法 )

  • 创建了一个管理者 AssociationsManager , 如何创建了一个 AssociationsHashMap , 给这个 map 对象赋值 : ( 将当前对象的地址 按位取反作为 key , 创建 ObjectAssociationMap 对象作为 value)

  • ObjectAssociationMap 赋值 : ( 将用户指定 , 传进来的 key 作为 key , 创建 ObjcAssociation 对象作为 value)

  • ObjcAssociation 对象中存放了用户指定的值 以及内存管理策略语义 . ( ObjcAssociation(policy, new_value) )

  • 给当前原类的 isa 添加标识 , 以便销毁时识别是否需要释放关联对象.( object->setHasAssociatedObjects() )

步骤 2. 搜索 delloc , 点击依次进入, 找到:

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
复制代码

那我们看到析构函数中根据 isa 判断了当有关联对象或者其他标识时 , 会调用 object_dispose 方法. 再点击 , 依次进入

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
复制代码

依次释放关联属性等等. 那么也就是说, 我们的关联属性的生命周期 是跟随原对象走的.

至此 , 关联对象的原理我们已经解析完毕 , 总结一下:

Category 关联属性总结:

  • 关联属性通过自己定义一个新的数据结构 ObjcAssociation 容器来保存用户设置的内容 以及读取用户设置的内容 . 以此达到属性那种通过方法访问实例变量的效果.

  • 分类关联属性的生命周期同原先类 . 通过在 isa 中标识是否有关联对象来在 dealloc 中实现销毁操作.


下篇会继续 load 方法的探索. 欢迎关注交流.

简书地址