Objective-C之Category实现分类示例详解

目录
  • 引言
  • 编译时
  • 运行时

引言

在写 Objective-C 代码的时候,如果想给没法获得源码的类增加一些方法,Category 即分类是一种很好的方法,本文将带你了解分类是如何实现为类添加方法的。

先说结论,分类中的方法会在编译时变成 category_t 结构体的变量,在运行时合并进主类,分类中的方法会放在主类中方法的前面,主类中原有的方法不会被覆盖。同时,同名的分类方法,后编译的分类方法会“覆盖”先编译的分类方法。

编译时

在编译时,所有我们写的分类,都会转化为 category_t 结构体的变量,category_t 的源码如下:

struct category_t {
    const char *name; // 分类名
    classref_t cls; // 主类
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods; // 实例方法
    WrappedPtr<method_list_t, PtrauthStrip> classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; // 类属性
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

这个结构体主要是用来存储分类中可表现的信息,同时也从侧面说明了分类是不能创建实例变量的。

运行时

map_images_nolock 是运行时的开始,同时也决定了编译顺序对分类方法之间优先级的影响,后编译的分类方法会放在先编译的前面:

void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    ...
    {
        uint32_t i = mhCount;
        while (i--) { // 读取 header_info 的顺序,决定了后编译的分类方法会放在先编译的前面
            const headerType *mhdr = (const headerType *)mhdrs[i];
            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
    ...

在运行时,加载分类的起始方法是 loadAllCategories,可以看到,该方法从 FirstHeader 开始,遍历所有的 header_info,并依次调用 load_categories_nolock 方法,实现如下:

static void loadAllCategories() {
    mutex_locker_t lock(runtimeLock);
    for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
        load_categories_nolock(hi);
    }
}

load_categories_nolock 方法中,会判断类是不是 stubClass 切是否初始化完成,来决定分类到底附着在哪里,其实现如下:

static void load_categories_nolock(header_info *hi) {
    // 是否具有类属性
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    size_t count;
    auto processCatlist = [&](category_t * const *catlist) { // 获取需要处理的分类列表
        for (unsigned i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls); // 获取分类对应的主类
            locstamped_category_t lc{cat, hi};
            if (!cls) { // 获取不到主类(可能因为弱链接),跳过本次循环
                // Category's target class is missing (probably weak-linked).
                // Ignore the category.
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }
            // Process this category.
            if (cls->isStubClass()) { // 如果时 stubClass,当时无法确定元类对象是哪个,所以先附着在 stubClass 本身上
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) { // 表示类对象已经初始化完毕,会进入合并方法。
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) { // 表示元类对象已经初始化完毕,会进入合并方法。
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };
    processCatlist(hi->catlist(&count));
    processCatlist(hi->catlist2(&count));
}

合并分类的方法是通过 attachCategories 方法进行的,对方法、属性和协议分别进行附着。需要注意的是,在新版的运行时方法中不是将方法放到 rw 中,而是新创建了一个叫做 rwe 的属性,目的是为了节约内存,方法的实现如下:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }
    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];
    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS); // 是否是元类对象
    auto rwe = cls->data()->extAllocIfNeeded(); // 为 rwe 生成分配存储空间
    for (uint32_t i = 0; i < cats_count; i++) { // 遍历分类列表
        auto& entry = cats_list[i];
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta); // 获取实例方法或类方法列表
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) { // 达到容器的容量上限时
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__); // 准备方法列表
                rwe->methods.attachLists(mlists, mcount); // 附着方法到主类中
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist; // 将分类的方法列表放入准备好的容器中
            fromBundle |= entry.hi->isBundle();
        }
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi); // 获取对象属性或类属性列表
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) { // 达到容器的容量上限时进行附着
                rwe->properties.attachLists(proplists, propcount); // 附着属性到类或元类中
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }
        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta); // 获取协议列表
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) { // 达到容器的容量上限时进行附着
                rwe->protocols.attachLists(protolists, protocount); // 附着遵守的协议到类或元类中
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }
    // 将剩余的方法、属性和协议进行附着
    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

而真正进行方法附着的 attachLists 方法,其作用是将分类的方法放置到类对象或元类对象中,且放在类和元类对象原有方法的前面,这也是为什么分类和类中如果出现同名的方法,会优先调用分类的,也从侧面说明了,原有的类中的方法其实并没有被覆盖:

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return; // 数量为 0 直接返回
        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count; // 原有的方法列表的个数
            uint32_t newCount = oldCount + addedCount; // 合并后的方法列表的个数
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount)); // 创建新的数组
            newArray->count = newCount;
            array()->count = newCount;
            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i]; // 将原有的方法,放到新创建的数组的最后面
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i]; // 将分类中的方法,放到数组的前面
            free(array()); // 释放原有数组的内存空间
            setArray(newArray); // 将合并后的数组作为新的方法数组
            validate();
        }
        else if (!list  &&  addedCount == 1) { // 如果原本不存在方法列表,直接替换
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        }
        else { // 如果原来只有一个列表,变为多个,走这个逻辑
            // 1 list -> many lists
            Ptr<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; // 将原有的方法,放到新创建的数组的最后面
            for (unsigned i = 0; i < addedCount; i++) // 将分类中的方法,放到数组的前面
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

以上就是Objective-C实现分类示例详解的详细内容,更多关于Objective-C分类的资料请关注我们其它相关文章!

(0)

相关推荐

  • 在Swift中使用KVO的细节以及内部实现解析(推荐)

    KVO是什么? KVO 是 Objective-C 对观察者设计模式的一种实现.[另外一种是:通知机制(notification),详情参考:iOS 趣谈设计模式--通知]: KVO提供一种机制,指定一个被观察对象(例如A类),当对象某个属性(例如A中的字符串name)发生更改时,对象会获得通知,并作出相应处理:[且不需要给被观察的对象添加任何额外代码,就能使用KVO机制] 在MVC设计架构下的项目,KVO机制很适合实现mode模型和view视图之间的通讯. 例如:代码中,在模型类A创建属性数据

  • iOS自动移除KVO观察者的实现方法

    问题 KVO即:Key-Value Observing, 直译为:基于键值的观察者. 它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知. 简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了. KVO的优点:当有属性改变,KVO会提供自动的消息通知. 这样开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知. 这是KVO机制提供的最大的优点. 因为这个方案已经被明确定义,获得框架级支持,可以方便地采用. 开发人员不需要添加任何代码,不需要

  • Objective-C const常量的优雅使用方法

    目录 正文 Objective-C 的常量声明方式 在 Objective-C 中使用 let 来声明常量 正文 在编写代码时经常要使用常量,来替代 magic number.比较简单的做法是通过预处理指令 #define 来实现. #define ANIMATION_DURATION 0.3 上述预处理指令会在编译时的预处理阶段会将代码中 ANIMATION_DURATION 字符串替换为 0.3.这种定义常量的方式比较简便,但是存在两个问题: 丢失了类型信息. 若该预处理指令声明在头文件中,

  • iOS 监听回调机制KVO实例

    监听某个对象,如果这个对象的数据发生变化,会发送给监听者从而触发回调函数 [self.bean addObserver:self forKeyPath:@"data" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL]; 这个就是注册监听,这个@"data"作为标识符方便回调函数辨认 -(void)observeValueForKeyPath:(NSStrin

  • Objective-C优雅使用KVO观察属性值变化

    目录 引言 KVOController YYCategories 引言 KVO 是苹果为我们提供的一套强大的机制,用于观察属性值的变化,但是大家在日常开发中想必多少也感受到了使用上的一些不便利,比如: 添加观察者和移除观察者的次数需要一一对应,否则会 Crash. 添加观察者和接受到属性变更通知的位置是分开的,不利于判断上下文. 多次对同一个属性值进行观察,会触发多次回调,影响业务逻辑. 为了解决上述三个问题,业界提出了一些方便开发者的开源方案,我们一起来看一下. KVOController K

  • Objective-C之Category实现分类示例详解

    目录 引言 编译时 运行时 引言 在写 Objective-C 代码的时候,如果想给没法获得源码的类增加一些方法,Category 即分类是一种很好的方法,本文将带你了解分类是如何实现为类添加方法的. 先说结论,分类中的方法会在编译时变成 category_t 结构体的变量,在运行时合并进主类,分类中的方法会放在主类中方法的前面,主类中原有的方法不会被覆盖.同时,同名的分类方法,后编译的分类方法会“覆盖”先编译的分类方法. 编译时 在编译时,所有我们写的分类,都会转化为 category_t 结

  • java内部类的定义与分类示例详解

    内部类 基本介绍 一个类的内部又完整的嵌套了另一个类结构.被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类(outer class).是我们类的第五大成员,内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系 基本语法: class Outer{ class Inner{ } } 内部类的分类: 1.定义在外部类局部位置上(比如方法内): 1).局部内那类(有类名) 2).匿名内部类(没有类名,重点); 定义在外部类的成员位置上: 1)成员内部类(没

  • Python垃圾邮件的逻辑回归分类示例详解

     加载垃圾邮件数据集spambase.csv(数据集基本信息:样本数: 4601,特征数量: 57, 类别: 1 为垃圾邮件,0 为非垃圾邮件),阅读并理解数据. 按以下要求处理数据集 (1)分离出仅含特征列的部分作为 X 和仅含目标列的部分作为 Y. (2)将数据集拆分成训练集和测试集(70%和 30%). 建立逻辑回归模型 分别用 LogisticRegression 建模. 结果比对 (1)输出测试集前 5 个样本的预测结果. (2)计算模型在测试集上的分类准确率(=正确分类样本数/测试集

  • C语言函数基础教程分类自定义参数及调用示例详解

    目录 1.  函数是什么? 2.  C语言中函数的分类 2.1 库函数 2.1.1 为什么要有库函数 2.1.2 什么是库函数 2.1.3 主函数只能是main()吗 2.1.4常见的库函数 2.2 自定义函数 2.2.1自定义函数是什么 2.2.2为什么要有自定义函数 2.2.3函数的组成 2.2.4 举例展示 3. 函数的参数 3.1 实际参数(实参) 3.2  形式参数(形参) 4. 函数的调用 4.1 传值调用 4.2  传址调用 4.3 练习 4.3.1. 写一个函数判断一年是不是闰年

  • SpringBoot框架集成ElasticSearch实现过程示例详解

    目录 依赖 与SpringBoot集成 配置类 实体类 测试例子 RestHighLevelClient直接操作 索引操作 文档操作 检索操作 依赖 SpringBoot版本:2.4.2 <dependencies> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <opti

  • Oracle数据库创建存储过程的示例详解

    1.1,Oracle存储过程简介: 存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合,调用存储过程可以简化应用开发人员的很多工作, 减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的. 优点: 允许模块化程序设计,就是说只需要创建一次过程,以后在程序中就可以调用该过程任意次. 允许更快执行,如果某操作需要执行大量SQL语句或重复执行,存储过程比SQL语句执行的要快. 减少网络流量,例如一个需要数百行的SQL代码的操作有一条执行语句完成,不需要在网络中发送数百行代

  • 关于Python可视化Dash工具之plotly基本图形示例详解

    Plotly Express是对 Plotly.py 的高级封装,内置了大量实用.现代的绘图模板,用户只需调用简单的API函数,即可快速生成漂亮的互动图表,可满足90%以上的应用场景. 本文借助Plotly Express提供的几个样例库进行散点图.折线图.饼图.柱状图.气泡图.桑基图.玫瑰环图.堆积图.二维面积图.甘特图等基本图形的实现. 代码示例 import plotly.express as px df = px.data.iris() #Index(['sepal_length', '

  • Python线性点运算数字图像处理示例详解

    目录 点运算 定义 分类 线性点运算 分段线性点运算 非线性点运算 对数变换 幂次变换 点运算 定义 分类 线性点运算 例子: 分段线性点运算 非线性点运算 对数变换 幂次变换 1. 点运算是否会改变图像内像素点之间的空间位置关系? 点运算是一种像素的逐点运算,它与相邻的像素之间没有运算关系,点运算不会改变图像内像素点之间的空间位置关系. 2. 对图像灰度的拉伸,非线性拉伸与分段线性拉伸的区别? 非线性拉伸不是通过在不同灰度值区间选择不同的线性方程来实现对不同灰度值区间的扩展与压缩,而是在整个灰

  • Python数学建模PuLP库线性规划入门示例详解

    目录 1.什么是线性规划 2.PuLP 库求解线性规划 -(0)导入 PuLP库函数 -(1)定义一个规划问题 -(2)定义决策变量 -(3)添加目标函数 -(4)添加约束条件 -(5)求解 3.Python程序和运行结果 1.什么是线性规划 线性规划(Linear programming),在线性等式或不等式约束条件下求解线性目标函数的极值问题,常用于解决资源分配.生产调度和混合问题.例如: max fx = 2*x1 + 3*x2 - 5*x3 s.t. x1 + 3*x2 + x3 <=

  • TensorFlow卷积神经网络AlexNet实现示例详解

    2012年,Hinton的学生Alex Krizhevsky提出了深度卷积神经网络模型AlexNet,它可以算是LeNet的一种更深更宽的版本.AlexNet以显著的优势赢得了竞争激烈的ILSVRC 2012比赛,top-5的错误率降低至了16.4%,远远领先第二名的26.2%的成绩.AlexNet的出现意义非常重大,它证明了CNN在复杂模型下的有效性,而且使用GPU使得训练在可接受的时间范围内得到结果,让CNN和GPU都大火了一把.AlexNet可以说是神经网络在低谷期后的第一次发声,确立了深

随机推荐