iOS分类Category的本质

前言

首先,这里有几个与Category相关的面试题,大家可以看一下
1、Category如何使用?
2、Category的原理是什么?
3、Category与类扩展的区别?
4、Category中load方法是什么时候调用的?load方法能被继承吗?
5、load和initialize的区别是什么?他们在category中的调用顺序是怎样的?出现继承的时候他们之间的调用过程是什么?
6、Category是否可以添加成员变量?如果可以,如何添加?

这几个面试题你能答出几个呢?如果有不会的地方,那咱们一起来学习下吧


Category

Category分类的作用:在不改变原有的类的前提下,可以为类单独添加一些方法、协议、属性。

首先,我们创建一个类YZPerson,其里面有一个对象方法-(void)run;然后分别新建两个分类:YZPerson+Eat、YZPerson+Drink。里面分别有四个方法:

- (void)eat1
{
    NSLog(@"YZPerson+Eat-eat1");
}

- (void)eat2
{
    NSLog(@"YZPerson+Eat-eat2");
}

+ (void)eat3
{
    NSLog(@"YZPerson+Eat-eat3");
}

+ (void)eat4
{
    NSLog(@"YZPerson+Eat-eat4");
}

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc YZPerson+Eat.m命令行指令,可以将YZPerson+Eat.m转化为C语言源码YZPerson+Eat.cpp
编译后的分类文件,全部转化为_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;	//属性列表
};

查找源码,可以看到其赋值方法
在这里插入图片描述其中,第3和第4的赋值是如下两个图
在这里插入图片描述
在这里插入图片描述从源码可以看出,分类在经历过编译后,将分类里面的内容:对象方法、类方法、协议、属性都转化为类型为_category_t的结构体变量。

对分类的源码分析:

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述找到remethodizeClass(cls)核心方法的实现:

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    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));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {		//取出某个分类,i--,先取的最后编译的那一个
        auto& entry = cats->list[i];
		//对方法列表的操作
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;//mcount++,对第一个取出的进行操作
            fromBundle |= entry.hi->isBundle();
        }
		//对属性列表的操作
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
		//对协议列表的操作
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //将所有分类的对象(类)方法列表附加到原来类的对象(类)方法列表里面
    rw->methods.attachLists(mlists, mcount);//mcount个数
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

其中,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;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            
            //array()->lists + addedCount = array()->lists
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
                    
			//addedLists分类数据
			//addedLists覆盖array()->lists数据
            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]));
        }
    }

通过查阅以上源码,可以得到:

  1. 在运行时,通过runtime机制,将多个分类里面的【方法列表(包括:对象方法列表和类方法列表)、协议列表和属性列表】分别集合成数组,然后将新的数组添加到【原来类对象里面的方法列表、元类里面的类方法列表、类对象里面的协议列表、属性列表】的最前面,也就是将分类里面的内容动态的添加到了类对象和元对象里面。
  2. 同时,由于是添加在最前面,所以当分类、原类、父类里面都有同一个方法时(例如:-(void)run;方法),优先执行分类里面的方法,如果没有再执行原类里面的方法,如果再没有才会去父类里面找该方法。 需要注意的是,是优先调用,并没有覆盖原类中的方法。
  3. 有多个分类同时有某一个方法的时候,由于遍历是i- -,然后做的mcount++操作,因此,最后编译的分类文件,第一个被查找
问:什么时候决定分类文件是最后被编译的呢?

在这里插入图片描述在下面的文件,最后一个被编译。

总结:Category的加载过程
  • 通过Runtime加载某个类的所有Category数据
  • 把所有Category的方法、属性、协议数据合并到一个大数组中(最后面参与编译的Category数据会在数组前面)
  • 将合并后的分类数据(包括方法、属性、协议),插入到原来数据的前面

以上就是Category被加载的过程,也是Categorey的原理。

分原子父
分类在前,原类在后(分类添加到原类的前面)
原类在前,父类在后(消息发送机制)

在这里插入图片描述


+(void)load;方法

下面介绍一下有关load相关的知识

  • 在程序启动的时候会加载所有的类和分类,并调用所有类和分类的+load方法。也就是不管程序在运行过程中是否调用过该类,在程序初始化的时候,都会调用+load方法且只会调用一次。
  • 先加载父类,再加载子类
  • 先加载原始类,再加载分类

初始化load调用顺序:父子原分

有一点需要说明的是,+(void)load;方法跟分类中自定义方法不一样。因为,如果是自定义方法,原类跟分类方法一样的话,只会调用分类的方法。而+(void)load;方法会把所有的原来、分类里面的+(void)load;都会调用一遍。同样是原类和分类里面一样的方法,为什么会出现不一样的结果呢?

我们继续查看源码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述

自定义方法的调用[YZPerson test];是消息传递机制,因此会通过isa指针在元类中查找类方法,如果有分类+test方法,则优先调用分类的+test方法。
而+load方法,是根据直接在内存中找到+load的内存地址,通过load_method方法调用的。

先调用原类的load方法,再调用分类的load方法;
先调用父类的load方法,再调用子类的load方法;
没有继承关系的多个原类,按编译顺序调用(先编译,先调用);
多个分类只按编译顺序调用(先编译,先调用);


+initialize方法

下面介绍一下有关initialize相关的知识
+initialize方法会在类第一次接收到消息时调用。

  • 在第一次使用某个类时(比如创建对象等),就会调用一次+initialize方法
  • 一个类只会调用一次+initialize方法
  • 调用顺序:先调用父类的,再调用子类的

初始化initialize调用顺序:父子分原
我们查看相关源码:
在这里插入图片描述在这里插入图片描述在这里插入图片描述
通过源码分析,不难看出上面的知识点。
由于是基于isa指针机制,+initialize方法有以下特点:

  • 如果分类实现了+initialize,就调用分类的+initialize,不会再调用类本身的+initialize调用(网上有说是覆盖原类中的+initialize方法,其实并不是真正的覆盖,而是没有调用原类中的+initialize方法)
父类
@implementation YZPerson
+ (void)initialize
{
    NSLog(@"YZPerson-initialize");
}
@end

父类的分类
@implementation YZPerson (Eat)
+ (void)initialize
{
    NSLog(@"YZPerson(Eat)-initialize");
}
@end

子类(原类)
@implementation YZStudent
//+(void)load
//{
//    NSLog(@"YZStudent-load");
//}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [YZStudent alloc];
    }
    return 0;
}

神奇的一幕出现了:

2020-02-26 17:02:11.224559+0800 Category[75206:2732274] YZPerson(Eat)-initialize
2020-02-26 17:02:11.224800+0800 Category[75206:2732274] YZPerson(Eat)-initialize

不是说好的initialize只调用一次吗?怎么调用了两次?为什么呢?

首先,打印出来的是分类,这个没有问题,因为分类方法在父类的方法前面,优先显示分类的。
[YZStudent alloc];会先去找父类的,父类YZPerson并没有实现initialize方法,因此,第一次打印是父类的initialize;
父类调用完毕后,并没有结束,而是去调用其本身的initialize方法,其本身没有initialize方法,由于继承关系,就去父类里面找initialize,最后调父类的initialize。
伪代码:

if (原类没有初始化)
{
    if (父类没有初始化)
    {
        objc_msgSend([YZPerson alloc], @selector(initialize));
    }
    objc_msgSend([YZStudent alloc], @selector(initialize));
}

因此,会出现调用两次。其实每个类的初始化还是只有一次。第一次是父类Person的初始化,第二次是子类Student的初始化,由于子类没有+initialize,所以调用父类的+initialize方法,也就是:如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能被调用多次)


问:分类可以添加属性吗?

分类只能添加方法,不能直接添加属性,但可以间接添加属性

直接在分类里面写成员变量,会报错❌
在这里插入图片描述

分类里面,可以写属性,但是不可以直接写成员变量

因为,category里面没有ivarList相关的东西去存储成员变量
也正是这个原因,不能添加成员变量

在普通类中,@property (assign, nonatomic) int age;
会做三件事:

  1. 生成age的成员变量
  2. 生成age的get、set方法的声明
  3. 生成age的get、set方法的实现

而在分类中,@property (assign, nonatomic) int weight;可以写,但是它的作用只有一个:
生成weight的get、set方法的声明

如何实现为分类间接添加属性呢?
方法一:创建全局变量
static int height_;
-(void)setHeight:(int)height
{
    height_ = height;
}
- (int)height
{
    return height_;
}

使用全局变量,保存数据
可以实现赋值、取值
但问题是,多个对象创建,使用的都是同一份height,也就是对象1的height=10,对象2的height=12,那么,对象1的height也变成12了。因此,不行

方法二:字典

使用字典,将self(地址)作为key,属性值作为value
可以解决上面说的多个对象同时修改属性的问题(因为self不一样)

但是,也有缺点,一个是内存释放问题,一个是线程安全问题
当然,你也可以加一些判断条件,做好线程安全的问题,比较麻烦

方法三:关联对象

我们可以通过runtime中的关联对象的方法(objc_setAssociatedObject)实现分类中属性的get、set方法的实现,具体实现如下:

必看:

关联对象介绍


问:原类中有一个属性age,子类也有一个属性age,会有什么问题吗?

在这里插入图片描述
会有一个警告⚠️

警告产生的原因是:
在写属性值的时候,使用@property,起到三个作用:
生成成员变量_age
声明get\set方法
实现get\set方法

在之前的OC中,写一个@property,还需要对应写一个@synthesize
@synthesize age = _age;
其作用是,将你写的age属性,和_age成员变量联系起来(@synthesize 合成访问器方法)

现在,Xcode不需要写@synthesize age = _age;,在写@property的时候,会自动给加上

问题在于,由于父类、子类有同名属性,造成子类里面的属性,@synthesize age = _age;用的是父类的_age

一般不会出问题,但是,在特殊情况下会出问题,比如:

父类是readonly
@property (nonatomic, assign, readonly) int age;

子类是readwrite
@property (nonatomic, assign, readwrite) int age;

子类赋值:
student.age = 10;
会造成崩溃

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 
'-[YZStudent setAge:]: unrecognized selector sent to instance 0x600002a38150'

原因是,子类调用age的时候,会调用到父类里面的值,而父类是readonly的

面试题解答:

调用顺序
categrory:分原子父
load:父子原分
initialize:父子分原

categrory方法,完全遵守消息发送机制,因此是分子父
load和initialize方法,都是代码中明确写到的:递归调用父类,因此是 父子
load方法代码中明确写的,先调用原类再调用分类,因此是 原分
initialize方法中,没有明确写原类、分类的调用关系,因此,遵循消息发送机制,因此是分原

1、Category如何使用
分类可以在不修改原来类模型的基础上拓充方法;
2、Category的原理是什么?
编译的时候,转化为category_t类型的结构体类型。
运行时将所有Category的方法、属性、协议数据合并到一个大数组中(最后面参与编译的Category数据会在数组前面),将合并后的分类数据(包括方法、属性、协议),插入到原来数据的前面;
3、Category与类扩展的区别?
分类可以在不修改原来类模型的基础上拓充方法
• 分类只能扩充方法、不能扩充成员变量;
• 继承可以扩充方法和成员变量,继承会产生新的类;
• 分类是有名称的,类扩展没有名称;
• 分类只能扩充方法、不能扩充成员变量;类扩展可以扩充方法和成员变量;
• 类扩展一般就写在.m文件中,用来扩充私有的方法和成员变量(属性);
• 分类是在运行时将数据合并在类信息中,类扩展是编译的时候它的数据就已经包含在类信息中;
4、Category中load方法是什么时候调用的?load方法能被继承吗?
在程序启动的时候会加载所有的类和分类,并调用所有类和分类的+load方法。也就是不管程序在运行过程中是否调用过该类,在程序初始化的时候,都会调用+load方法且只会调用一次。

@implementation YZPerson
+(void)load
{
    NSLog(@"YZPerson-load");
}
@end

@implementation YZStudent

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"----");
        [YZStudent load];
        NSLog(@"----");
    }
    return 0;
}

结果:
2020-02-26 15:23:20.747580+0800 Category[74061:2678272] YZPerson-load
2020-02-26 15:23:20.747839+0800 Category[74061:2678272] ----
2020-02-26 15:23:20.747862+0800 Category[74061:2678272] YZPerson-load
2020-02-26 15:23:20.747871+0800 Category[74061:2678272] ----

load方法可以被继承
但,[YZStudent load];这种调用方法相当于消息发送机制,走的是isa指针那一套,并不是原有系统调用load方法。

5、load和initialize的区别是什么?他们在category中的调用顺序是怎样的?出现继承的时候他们之间的调用过程是什么?

1.调用方式的不同:
load是通过找到函数地址直接调用的;
initialize是通过消息机制objc_msgSend调用的;

2.调用时刻的不同
load是程序运行的时候,通过runtime加载类、分类的时候调用(只会调用一次)
initialize是类第一次使用的时候调用的;(如果子类没有+initialize方法,父类可能会被调用多次)

load在分类中,按编译顺序调用
initialize在分类中,按编译顺序调用

load在继承中调用是按isa指针调用
initialize在继承中调用是按isa指针调用

6、Category是否可以添加成员变量?如果可以,如何添加?
分类不可以直接添加属性,可以间接通过runtime中的关联方式进行添加属性。

扩展知识点:

在这里插入图片描述

更多学习

iOS分类(category),类扩展(extension)—史上最全攻略
iOS底层原理总结 - Category的本质

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值