浅谈系列-分类的结构和加载时机

1,325 阅读3分钟

分类(category)

导语:通过之前对类的结构分析,我们知道类本质是一个结构体。今天我们来了解一下分类的结构和加载流程。

分类结构

  • 简单来说,分类也是结构体,独立于类,并不是直接把扩展的内容添加到类,而是创建了一个分类结构体把能扩展的都放到里面,在runtime运行期动态添加到类中。

创建一个类的分类,在终端cd到.m的目录,通过以下命令,可以导出一个c++的分类代码文件,搜索"category_t {",可以知道分类的结构。

xcrun -sdk iphoneos -arch arm64 clang -rewrite-objc 分类名称.m

除此之外,可以直接查看runtime源码,在objc-runtime-new中可以查找到分类的结构,如下

  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;
   }
}; 
  • 分类结构
category_t备注
name类名
instanceMethods对象方法列表
classMethods类方法列表
protocols协议列表
instanceProperties属性列表
...

通过分类的结构图可以知道分类里面的实例方法,类方法,协议,成员变量都是通过一个结构体category_t保存的,在编译阶段跟主类是相对独立的。

注意:在分类结构体(category_t)里,并没有发现成员列表(ivars),可以看出分类默认是不允许添加成员变量的。

Category是如何加载到主类的

分类数据加载过程
1 . 通过runtime加载主类的所有Category的数据
2 . 把所有的Category的对象方法,类方法,属性,协议数据合并到一个大数组中
3 . 将大数组中的分类数据(方法,属性,协议),插入到主类当中

越是后面编译的Category的数据,放在数组的前面,分类数据在主类数据的前面。

实际加载数据时,如果分类有与主类一样的方法,会调用分类的方法。

如果不同分类有相同的方法,会调用后面参加编译分类的方法。

整个加载流程都是通过runtime完成的。

runtime的加载影响程序的启动,因此需要合理运用分类,如果过度使用,会增加程序启动时间,影响用户体验。

关联对象

  • 分类是不可以直接定义成员变量的

  • @property 在分类中定义属性,只会简单生成setter,getter方法的声明,并不会生成"_属性名"的成员变量

  • 可以通过runtime中的关联属性,间接完成属性通过setter赋值,getter取值

举个例子:

#import "FRPerson.h"

@interface FRPerson (Eat)

@property (nonatomic, strong) NSArray *foots;

-(void)eat;

@end
#import "FRPerson+Eat.h"
#import < objc/runtime.h >
 
const static char *associateID = "foots";
@implementation FRPerson (Eat)

 -(void)eat{
    NSLog(@"吃东西,%@", self.foots[0]);
 }
 
#pragma mark - 关联对象
-(NSArray *)foots{
    return  objc_getAssociatedObject(self, associateID);
}

-(void)setFoots:(NSArray *)foots{
    objc_setAssociatedObject(self, associateID, foots, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 }
 
@end

在分类里不能添加成员变量,所以使用了关联对象间接完成了属性的功能,却只能使用“.”语法赋值取值。

关联对象中的key,其实可以直接使用setter或者setter方法地址,直接@selector(foots)就可以,这样做更节省系统资源。

关于关联对象的原理,我们暂不讨论。之后有时间专门研究一下关联对象的原理。相信网络上也有很多相关资料,读者有兴趣也可以搜索来看一下。

浅谈系列-OC对象创建出来到底是怎么样的呢

浅谈系列-OC方法调用到底是个什么流程

浅谈系列 - KVO&KVC到底是怎么样实现的