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

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

什么是 Category ?

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


什么是 Extension ?

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


CategoryExtension 有什么区别 ?

让咱们从多个方面来回答这个问题。数组

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

  • Extension 是一个 .h . (固然,也能够在一个类的 .m 中伴生, 写法就是 @interface *** /.../ @end 就很少说了.) 数据结构

    那么一样, 建立时, 选择对应的类型便可.

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

举个🌰 app

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

如上使用, 发生崩溃.ide

*** 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 方法的探索. 欢迎关注交流.

简书地址

相关文章
相关标签/搜索