小码哥iOS学习笔记第五天: Category

Category一般用来给已有类添加新的功能, 或者给自定义类分模块

一、Category的使用

  • 定义Person类, 继承自NSObject

  • 定义Person类的Category, Test

  • 定义Person类的Category, Eat

  • Person, TestEat中, 都有一个实例方法和一个类方法, 其中TestEat都是对Person的扩展
  • 我们可以再main.m中调用这三个文件中的方法

  • 已知, 实例方法存在于类对象中, 类方法存在于元类对象
  • 对象在调用方法的时候, 会有以下顺序:
    • 调用实例方法: 实例通过isa找到类对象, 然后查看是否有方法, 有就调用
    • 调用类方法: 类对象通过isa找到元类对象, 然后查看是否有方法, 有就调用

问: Category中的方法, 存储在什么地方呢?

二、查看Category的底层实现

  • 首先使用命令行, 获取Person+Test.m在底层的C++实现
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m
复制代码
  • 然后使用Xcode打开生成的Person+Test.cpp文件

  • 查看文件, 可以发现有如下结构体

  • 在这个结构体中, 定义了类名, 实例方法列表, 类方法列表, 协议列表属性列表
  • 接着还可以看到如下的代码

  • 这是一个_category_t结构体类型的实例_OBJC_$_CATEGORY_Person_$_Test

  • 在里面可以看到:

    • "Person"
    • 0
    • (const struct _method_list_t)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test
    • (const struct _method_list_t)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test
    • 0
    • 0
  • 查找_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test:

  • 查找_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test:

  • 可以看到, 结构体每一个有值的部分, 都是Person+Test中定义的方法

  • 这说明, Category中的对象方法类方法在编译后, 在底层是以结构体的形式存在, 而不是并入到Person的类对象和元类对象中

  • 也可以在Person+Test添加属性, 并遵守协议, 此时在编译文件中, 就可以看到结构体里有属性和协议的值

三、当Category中的方法名与类定义的方法名相同时, 会有怎样的效果

  • Person, Person+Eat, Person+Test中分别添加相同的方法, 如下图

  • 添加了相同的-(void)say+(void)say1方法, 此时调用这些方法, 结果如下

  • 根据结果, 可以知道Person-(void)say+(void)say1方法已经被Person+Eat-(void)say+(void)say1方法覆盖了

疑问: Person中的-(void)say+(void)say1方法真的被覆盖了吗? 为什么调用的是Person+Eat中的方法, 而不是Person+Test中的方法

四、查看加载分类方法的源码

  • 源码下载地址
  • 找到版本最新的源码, 并下载

  • 下载后, 打开程序, 找到objc-os.mm文件中的void _objc_init(void)函数

  • 进入map_images函数

  • 进入map_images_nolock函数

  • map_images_nolock函数中, 可以找到_read_images函数的调用

  • 进入_read_images函数

  • _read_images函数中, 可以找到加载categorys的代码

  • 在这些代码里, 可以找到重组类方法的代码remethodizeClass(cls)函数调用

  • 进入static void remethodizeClass(Class cls)函数

  • 进入static void attachCategories(Class cls, category_list *cats, bool flush_caches)函数

  • attachCategories函数中, 进行的就是对categorys进行加载的代码
// 将Category中的方法, 属性, 协议等加入到类对象和元类对象中
// cls: 类对象或元类对象
// cats: 类所有的category组成的数组
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    // 如果没有分类, 直接返回, 不进行处理
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    // 判断是否是 元类对象
    bool isMeta = cls->isMetaClass();

    // 创建二维数组, 存放 分类中 对象方法(对象包括: 类对象和元类对象)
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    // 创建二维数组, 存放 分类中 属性(对象包括: 类对象和元类对象)
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    // 创建二维数组, 存放 分类中 协议(对象包括: 类对象和元类对象)
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // 记录category中方法的数量索引
    int mcount = 0;
    // 记录category中属性的数量索引
    int propcount = 0;
    // 记录category中协议的数量索引
    int protocount = 0;
    // 获取Category的数量
    int i = cats->count;
    bool fromBundle = NO;
    // 从后向前遍历
    while (i--) {
        // 取出传入类对象的 category
        auto& entry = cats->list[i];

        // 找出category中的方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            // 将category中的方法列表, 加入到容器mlists中
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 找出category中的属性列表
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            // 将category中的属性列表, 加入到容器proplists中
            proplists[propcount++] = proplist;
        }
        
        // 找出category中的协议列表
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            // 将category中的属性列表, 加入到容器protolists中
            protolists[protocount++] = protolist;
        }
    }

    // 取出类对象中的数据(属性, 协议, 方法等)
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    // 将所有的Category中的方法, 合并到类对象和元类对象中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    // 将所有的Category中的属性, 合并到类对象和元类对象中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    
    // 将所有的Category中的协议, 合并到类对象和元类对象中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}
复制代码
  • 在上述的代码, 最后面, 调用了attachLists函数, 将Category中的数据合并到类对象和元类对象中

  • 进入void attachLists(List* const * addedLists, uint32_t addedCount)函数, 可以找到合并的代码

  • 首先重新分配内存, 可以足够放下类和Category中所有的方法数据
  • 接着使用void *memmove(void *__dst, const void *__src, size_t __len);函数, 将类中原有方法移动到方法列表的最后边
  • 最后将Category中的方法copy到方法列表的最前边
  • 属性和协议也是同样的方式

此时, Category的方法就放在了方法列表中的前面, 而类中的原有方法则存在于方法列表的最后边

  • 此时, 如果在调用类的方法, 就会从方法列表中从前往后查询, 而如果Category中有相同的方法, 那么就会直接使用Category中的方法
  • 这就是上面Person类调用saysay1两个方法时, 会调用分类Person+Eat中的方法

问: 为什么调用Personsaysay1方法时, 调用的是Person+Eat中方法, 而不是Person+Test中方法呢?可以控制调用哪个Category中的方法吗?

  • 我们继续看static void attachCategories(Class cls, category_list *cats, bool flush_caches)函数
  • 这个函数传入的类的Category列表, 但是在遍历的时候却是从后向前遍历

  • 这就说明, 在运行时, 后加载的Category会合并在先加载的Category的面前, 我们也可以在项目中找到这一事实

  • 由上图可知, 程序运行时先加载的Person+Test, 后加载的Person+Eat, 所以在合并后的Person的方法列表里, Person+Eat中的方法排在了Person+Test中方法的前面

  • 我们可以在Target -> Build Phases -> Comple Sources中看到代码在运行时加载的顺序

  • 代码是根据上图中的顺序, 从上到下的顺序加载的。
  • 我们可以手动修改文件的加载顺序, 从而达到调用某个Category中方法的目的

  • Person+Eat上移, 接着重新运行程序, 此时就是调用的Person+Test中的方法

五、面试题

  • Category的实现原理

    • Category编译之后的底层结构是struct category_t, 里面存储着分类的对象方法、类方法、属性、协议信息
    • 在程序运行的时候, runtime会将Category的数据, 合并到类信息中(类对象、元类对象中)
  • CategoryClass Extension的区别是什么?

    • Class Extension在编译的时候, 他的数据就已经包含在类信息中
    • Category是在运行时, 才会将数据合并到类信息中

你可能感兴趣的:(小码哥iOS学习笔记第五天: Category)