08-OC底层原理之Category实现原理

简介: 08-OC底层原理之Category实现原理

Category原理探索

Category的作用主要是在不改变原有类的前提下,动态地给这个类添加一些方法,同时可以将类的实现分散到多个不同文件或多个不同框架中,方便代码管理;接下来分析一下它的底层是如何实现的;

测试代码(为Person类添加分类)

Person+Cate.h:

@interface Person (Cate)<NSCoding>
@property(strong, nonatomic) NSString *name;
@property(assign, nonatomic) int age;
- (void)cate_method;
+ (void)cate_class_method;
@end
Person+Cate.m:
@implementation Person (Cate)
- (void)cate_method { }
+ (void)cate_class_method {}
- (void)encodeWithCoder:(NSCoder *)coder { }
- (instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super init]) { }
    return self;
}
@end

通过以下命令将Person+Cate.m文件转换为c++文件,查看一下编译过程:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Cate.m

通过Person+Cate.cpp文件我们可以看出_category_t结构体中,存放着类名,对象方法列表,类方法列表,协议列表,以及属性列表,并不包含成员变量信息(分类里面可以添加属性,但是默认并没有getter的和setter的实现)

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;
};

1. 通过 _OBJC_$_CATEGORY_Person_$_Cate可以看出,是在_category_t各个成员赋值

static struct _category_t _OBJC_$_CATEGORY_Person_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
  "Person",
  0, // &OBJC_CLASS_$_Person,
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate,
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Cate,
  (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Cate,
  (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Cate,
};

首先看方法列表_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate和_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Cate,可以看到它们别赋值给了 instance_methods和class_methods类型的结构体中;

2. _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate方法实现:

static struct /*_method_list_t*/ {
  unsigned int entsize; // sizeof(struct _objc_method)
  unsigned int method_count;
  struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  sizeof(_objc_method),
  3,
  {{(struct objc_selector *)"cate_method", "v16@0:8", (void *)_I_Person_Cate_cate_method},
  {(struct objc_selector *)"encodeWithCoder:", "v24@0:8@16", (void *)_I_Person_Cate_encodeWithCoder_},
  {(struct objc_selector *)"initWithCoder:", "@24@0:8@16", (void *)_I_Person_Cate_initWithCoder_}}
};

从_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate中可以看到,里面为实例方法列表,也可以看到这个里面的方法与我们声明的方法一致,(_CATEGORY_CLASS_METHODS_Person_$_Cate类方法列表同理,这里不再列出);3. 接下来看协议列表3.1 可到遵守了一个协议NSCoding:

static struct /*_protocol_list_t*/ {
  long protocol_count; // Note, this is 32/64 bit
  struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  1,
  &_OBJC_PROTOCOL_NSCoding
};

3.2 接下来看参数_OBJC_PROTOCOL_NSCoding干了什么?

struct _protocol_t _OBJC_PROTOCOL_NSCoding __attribute__ ((used)) = {
  0,
  "NSCoding",
  0,
  (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding,
  0,
  0,
  0,
  0,
  sizeof(_protocol_t),
  0,
  (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCoding
};

继续向下看调用的_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding函数;

3.3 _OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding函数:

static struct /*_method_list_t*/ {
  unsigned int entsize; // sizeof(struct _objc_method)
  unsigned int method_count;
  struct _objc_method method_list[2];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  sizeof(_objc_method),
  2,
  {{(struct objc_selector *)"encodeWithCoder:", "v24@0:8@16", 0},
  {(struct objc_selector *)"initWithCoder:", "@24@0:8@16", 0}}
};

可以看到里面为NSCoding需要实现的方法;

4. 接下来看属性列表_OBJC_$_PROP_LIST_Person_$_Cate

static struct /*_prop_list_t*/ {
  unsigned int entsize; // sizeof(struct _prop_t)
  unsigned int count_of_properties;
  struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  sizeof(_prop_t),
  2,
  {{"name","T@\"NSString\",&,N"},
  {"age","Ti,N"}}
};

里面有name和age两个不同类型的属性;通过上述我们可以发现:分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中,接下来通过源码探究一下分类是如何和类对象以及元类对象进行合并的;

Category源码探索

通过上文分析我们已经知道,类是如何进行加载的,在这里我们在一些关键方法的地方加上一些调试方法,直接查看“glt新增方法”

找到readClass函数增加以下测试方法:

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized) {
    const char *mangledName = cls->nonlazyMangledName();
    auto ro = (const class_ro_t *)cls->data();
    // glt新增方法
    if (strcmp(mangledName, "Person") == 0) {
        method_list_t *list = ro->baseMethods();
        if (list != NULL) {
            for (uint32_t i = 0; i < list->count; i++) {
                printf("Category - Person - ro中的方法 - readClass:%s\n", (char *)list->get(i).big().name);
            }
        }
    }
    ...
}

找到realizeClassWithoutSwift函数增加以下测试代码:

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    // 我加的调试代码
    const char *mangledName = cls->mangledName();
    const char *myPersonName = "Person";
    //从Mach- O里面读取到cls->data(),强制转换成class_ro_t结构
    auto ro = (const class_ro_t *)cls->data();
    ...
    //Attach分类
    methodizeClass(cls, previously);
    //glt新增方法
    if (strcmp(mangledName, myPersonName) == 0) {
        method_list_t *list = ro->baseMethods();
        if (list != NULL) {
            for (uint32_t i = 0; i < list->count; i++) {
                printf("Category - Person中的方法 realizeClassWithoutSwift:%s\n", (char *)list->get(i).big().name);
            }
        }
    }
    return cls;
}

找到attachCategories函数增加以下函数:

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags) {
    // glt新增方法
    if (strcmp(cls->mangledName(), "Person") == 0) {
        printf("Category - attachCategories\n");
    }
    bool fromBundle = NO;
    ...
}

调试方法:

通过不同的文件是否添加load方法进行调试,可以分为以下几个,因篇幅较长,这里不再一一贴出运行结果;

  1. 主类有load + 分类有load
  2. 主类有load + 分类没有load
  3. 主类没有load + 1个分类1个load
  4. 主类没有load + 1个分类没有load
  5. 主类没有load + 2个分类(1个load、1个没有load)
  6. 主类没有load + 2个分类(2个load)
  7. 主类没有load + 3个分类(当有2个或者2个以上的load的时候)

调试源码执行流程:
通过源码调试结论,读取分类信息的地方有两处,调用顺序分别为:

  1. load_images -> prepare_load_methods -> realizeClassWithoutSwift -> methodizeClass -> attachCategories
  2. _objc_init -> map_images -> map_images_nolock -> _read_images -> readClass

调试得出的结果:

  • 主类有load + 分类有load(不区分有几个):调用attachCategories
  • 主类有load + 分类没有load(不区分有几个):从ro读取,也就是readClass函数刚开始的ro里面已经包含了分类的信息了;
  • 主类没有load + 1个分类1个load:从ro读取
  • 主类没有load + 1个分类没有load:从ro读取
  • 主类没有load + 2个分类(1个load、1个没有load):从ro读取
  • 主类没有load + 2个分类(2个load):调用attachCategories
  • 主类没有load + 3个分类(当有2个或者2个以上的load的时候):调用attachCategories

调试结果总结:

  • 当主类有load的时候,无论分类是否有load,均会调用attachCategories
  • 当主类没有load的时候,至少有1个以上load才会调用attachCategories
  • 其他情况均从ro中读取

接下来具体看一下attachCategories函数里面都做了哪些事?

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags) {
    // glt新增方法
    if (strcmp(cls->mangledName(), "Person") == 0) {
        printf("Category - attachCategories\n");
    }
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();
    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);
}


在该方法中有mlists[ATTACH_BUFSIZ - ++mcount] = mlist;,首先mlist是一个数组,而又将这个数组赋值给mlists最后一个元素。那么mlists相当于一个二维数组,mlists中存储着当前分类中所有的方法列表,接下来调用attachLists函数:

rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);

主要看一下attachLists里面做了哪些事

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;
            //数组进行扩容
            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;
            //把旧数组当做一个元素放到lists最后一位
            if (oldList) array()->lists[addedCount] = oldList;
            //把新数组从头依次放入
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

结合注释可以得知新的list总是放到旧的list的前面,也就是说将来调用方法的时候由于后编译的类的方法总会插入到list的最前面,所以后编译的方法会优先调用,执行的并不是方法覆盖,而是找到以后便不再查找了;

总结

Category的实现原理?

Category编译之后的底层结构是category_t,里面存储着分类的对象方法、类方法、属性、协议列表等信息,程序运行的时候,通过runtime加载某个类的所有Category数据(区分load方法是否实现),把所有Category的方法、属性、协议列表数据合并到一个大数组中,后面参与编译的Category数据,会在数组的前面,最后将合并后的分类数据,再插入到原类的前面;

类和分类中有同名方法的调用顺序?

1. 类和分类中同名方法会优先调用分类方法(合并后的分类数据,插入到了原来的类的前面)

2. 多个分类的同名方法(load除外)调用顺序后编译的会优先调用(后编译的会插入到方法列表的前面)

Category和Class Extension扩展的区别?

Class Extension在编译的时候,它的数据已经包含在类信息中;Category是在运行时才会将数据合并到类信息中;本文部分内容来可能来源于网络,发布的内容如果侵犯了您的权益,请联系我们尽快删除!

相关文章
|
7月前
|
存储 编解码 资源调度
鸿蒙相机开发实战:从设备适配到性能调优 —— 我的 ArkTS 录像功能落地手记(API 15)
本文分享鸿蒙相机开发经验,从环境准备到核心逻辑实现,涵盖权限声明、模块导入、Surface关联与分辨率匹配,再到录制控制及设备适配法则。通过实战案例解析,如旋转补偿、动态帧率调节和编解码优化,帮助开发者掌握功能实现、设备适配与体验设计三大要点,减少开发坑点。适合鸿蒙新手及希望深化硬件交互能力的工程师参考收藏。
245 2
|
安全 数据安全/隐私保护 网络虚拟化
亲测有效:注册谷歌邮箱账号gmail的最新教程
谷歌邮箱,也被称为 Gmail,是由谷歌公司开发的一项电子邮件服务。自 2004 年首次推出以来,Gmail 迅速成为全球最受欢迎的电子邮件服务之一。截至 2023 年,Gmail 拥有超过 18 亿活跃用户。Gmail 以其简洁易用的界面、强大的功能和高可靠性著称,成为个人、企业和组织广泛使用的电子邮件平台。
2286 1
|
小程序 前端开发 中间件
ThinkPHP 配置跨域请求,使用TP的内置跨域类配置,小程序和web网页跨域请求的区别及格式说明
本文介绍了如何在ThinkPHP框架中配置跨域请求,使用了TP内置的跨域类`\think\middleware\AllowCrossDomain::class`。文章还讨论了小程序和web网页在跨域请求格式上的区别,并提供了解决方案,包括修改跨域中间件源码以支持`Origin`和`token`。此外,还介绍了微信小程序跨域请求的示例和web网页前端发送Axios跨域请求的请求拦截器配置。
ThinkPHP 配置跨域请求,使用TP的内置跨域类配置,小程序和web网页跨域请求的区别及格式说明
|
负载均衡 算法 网络虚拟化
ensp中链路聚合配置命令
链路聚合(Link Aggregation)是结合多条物理链路形成逻辑链路的技术,提升网络带宽、增强冗余性和优化负载均衡。在高带宽、高可靠性及负载均衡需求的场景如服务器集群、数据中心等中广泛应用。配置包括手动和自动模式,手动模式下,如LSW1和LSW2,通过`int eth-trunk`、`trunkport`等命令配置接口和成员链路。自动模式下,如SW3和LSW4,使用LACP协议动态聚合,通过`mode lacp-static`和`load-balance dst-mac`命令设置。配置后,使用`dis eth-trunk`检查聚合状态。
1913 1
ensp中链路聚合配置命令
|
开发工具
如何在 Vim / Vi 中撤消和重做?
【4月更文挑战第19天】
788 0
如何在 Vim / Vi 中撤消和重做?
|
传感器 存储 监控
毕业设计 基于51单片机WIFI智能家居系统设计
毕业设计 基于51单片机WIFI智能家居系统设计
275 0
|
网络协议 网络安全 数据安全/隐私保护
内网穿透实现公网SSH远程连接树莓派
内网穿透实现公网SSH远程连接树莓派
|
机器学习/深度学习 人工智能 物联网
快速玩转 Llama2!机器学习 PAI 最佳实践(二)—全参数微调训练
本实践将采用阿里云机器学习平台PAI-DSW模块针对 Llama-2-7B-Chat 进行全参数微调。PAI-DSW是交互式建模平台,该实践适合需要定制化微调模型,并追求模型调优效果的开发者。
2518 1