博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
iOS问题整理03----Category
阅读量:7232 次
发布时间:2019-06-29

本文共 9192 字,大约阅读时间需要 30 分钟。

本篇文章解决的问题

  1. Category 的实现原理
  2. Category 和 Extension 的区别是什么?
  3. Category 中有 load 方法吗?load 方法时什么时候调用的?load 方法能继承吗?
  4. load、initialize 方法的区别是什么?他们在 category 中的调用的顺序?以及出现继承时他们之间的调用过程?
  5. Category为什么只能加方法不能加属性?
  6. Category 能否添加成员变量?如果可以,如何给 Category 添加成员变量?

1. Category 的实现原理

答案:

分类在编译之后的结构是一个 _category_t 类型的结构体,它里面存储着分类的类名、对象方法、类方法、属性、协议的相关信息。在运行过程中,runtime 会将这些信息合并到类对象与元类对象中去。合并之后分类的信息会插入到原来信息的前边。

分析:

假设我们现在有如下的一个类和分类

@interface Person@end@implmentation- (void)run;@end**************************************************@interface Person (test)@end@implementation Person (test)- (void)testInstanceMethod;+ (void)testClassMethod;@end复制代码

我们回想一下,当我们的一个 Person 对象调用 run 方法时:

Person *person = [[Person alloc] init];    [person run];复制代码

实际上是编译器将代码 [person run]转成 objc_msgSend(person, @selector(run)),系统给 person 发送消息,找到 person 的 isa,通过 isa 找到 Person class 对象,在 Person class 对象的方法列表中去找到 run 的实现并进行调用。

现在使用 person 调用分类的方法:

[person test];复制代码

实际上的流程也还是和上面是一样的,系统会先找到 person 的 isa,通过 isa 找到 Person class 对象,在 Person class 的方法列表中找到 test 的实现并进行调用。在程序运行的过程中,系统通过 runtime 动态地将分类中的对象方法添加进了 Person class 对象的方法列表中。

如果使用 Person 调用分类的类方法,情况也是类似的:

[Person test1];复制代码

在程序运行的过程中,系统通过 runtime 动态地将分类的类方法添加到了 Person 的 meta-class 对象的方法列表中去。调用方法的时候,系统先找到 Person class 的 isa,然后找到 Person meta-class 对象,在其方法列表中找到 test1 的实现并进行调用。

我们来看一下分类的底层结构。

先创建一个工程,创建一个Person类,在创建一个 Person+test 的分类,在里面添加好我们上面说到的方法。

进入 Person+test.m 所在的目录,执行命令:

$ xcrun -sdk iphoneos clang -arch i386 -rewrite-objc Person+test.m
Person+test.m 的代码转成 c/c++ 代码,然后打开它。

在这个文件中,我们可以找到这个结构体。在
编译阶段,编译器会将分类先转成这种结构体类型,将分类相关的信息存储在这个结构体中。从名称我们可以猜出来:name 存储的是类名,instance_methods 用于存储分类中的对象方法,class_method 用于存储分类中的类方法,protocols 用于存储分类的协议相关信息,properties 存储的是分类中属性相关的信息。

我们还可以找到这样一段代码

这就是 Person+test 编译后的的结构体,我们可以看到它传递了两个有效的成员变量,一个是类名
Person,另一个是对象方法列表
&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test
我们来看一下
&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test 这个东西是什么:

我们可以在 .cpp 文件中找到这段代码,我们在创建分类的时候有一个对象方法
- (void)test;这段代码中我们也看见了有"test"的相关描述,所以我们可以判断出
&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test 就是描述分类的对象方法信息的一个东西。

综上,我们可以判断出分类在编译之后确实是编译成了 _category_t 这个类型。

我们现在得出的结论是,添加分类后:在编译时,编译器将分类编译成了 _category_t 类型的结构体,它里面存储的有分类中的类名、对象方法、类方法、属性信息、协议信息。在运行的过程中,Runtime 将所有分类对应的结构体中的对象方法列表合并到类对象的方法列表中,将类方法合并到元类对象的方法列表中。

如果阅读源码的话,还可以发现:合并之后,Category 的数据,会插入到原来数据的前面。 再联想到对象调用方法的流程,现在我们就可以解释为什么如果类和分类中都有某个方法时,总是会先调用分类中的方法了。如果多个分类中都有这个方法,调用哪个分类中的方法,和编译顺序有关。

2. Category 和 Extension 的区别是什么?

答案:

Extension 中的数据,在编译的时候就已经合并到类里边去了。Category 中在编译的时候会编译成一个结构体,在运行时才会合并到类里边去。(Extension 的作用是可以把一些需要私有化的属性、方法写在 .m 文件中去。)

3. Category 中有 load 方法吗?load 方法时什么时候调用的?load 方法能继承吗?

答案:

  • Category 中有 load 方法。
  • runtime 加载类、分类的时候调用,每个类、分类的 load 方法,在程序运行的过程中只调用一次。
  • load 方法可以继承。但是一般情况不会主动去调用 load 方法,都是让系统自动调用。

分析:

新创建一个工程。

创建一个类 Person,再创建其分类 Person+test
PersonPerson+test 的实现中都加上如下代码:

+ (void)load{    NSLog(@"Person +load");// NSLog(@"Person (test) +load");}复制代码

然后点击运行。

我们发现了以下这些现象:

  • 我们还没有在任何地方调用 + (void)load; 方法,甚至没有在任何地方导入过类或者分类的头文件。
  • + (void)load;main函数之前执行,因为 main函数 里面的 hello world 先打印。
  • 之前我们学习的:如果分类和类中都有某个方法,那么只会执行分类中的方法,这里却都执行了+ (void)load; 方法。

解释这个现象需要看一下源码:

objc-os.mm 文件中,可以找到这段源码,可以看出
_objc_init 是在程序启动之初被调用的。
最后一行有一个
load_images,点进去看一下,看到这段代码:

可以看见这段代码就是系统在做
load 方法的相关处理,最后还调用了
call_load_methods()。因为这段代码是在程序启动时调用的,所以
load 方法是在程序启动时调用的,用官方的原话来说是:
load 方法在 runtime 加载类、分类的时候调用。这也就解释了前两个现象。

继续来看 call_load_methods 做了什么:

它先通过函数的指针直接找到了类中的
load 方法,然后调用了类中的
load 方法:
call_class_loads();,然后又通过同样的方法直接调用了分类的
load 方法:
call_categry_loads();。它不像
objc_msgSend() 方法那样会有找 isa 、找元类对象等操作,系统会自己调用
load 方法。这解释了第三个现象。

再深入看源码的话可以发现,如果有多种类和分类,调用顺序是这样的: 先按照编译顺序调用所有类的 load 方法,再按照调用所有子类的 load 方法,最后再按照编译顺序调用所有分类的 load 方法。

4. load、initialize 方法的区别是什么?他们在 category 中的调用的顺序?以及出现继承时他们之间的调用过程?

答:

  • load 方法是在 runtime 加载类和分类的时候,通过函数指针找到每个类和分类的 load 方法,直接进行调用;initialize 是类在第一次接收消息的时候,会使用消息机制 objc_msgSend(类, @selector(initialize))去给类发送消息进行调用。
  • 当有分类时:类和分类的 load 方法都会被调用,因为 load 的原理是找到每个类和分类的函数指针直接调用,先调用父类的 load 方法,然后在按照编译顺序先后调用所有分类的 load 方法;而 initialize 它通过消息机制 objc_msgSend() 进行调用的,所以它会通过类对象的 isa 找到元类对象的方法列表,在列表中去找 initialize 的实现并进行调用,如果分类中有 initialize 方法,那么先找到的是分类的 initialize 方法,就会先调用它。
  • 出现继承时:系统会先调用所有类的 load 方法,然后再调用所有子类的 load 方法,最后再调用所有分类的 load 方法。当第一次给子类对象发送消息时,会先给父类发送消息objc_msgSend(父类,@selector(initialize)),让父类去调用initialize,然后给子类发送消息objc_msgSend(子类, @selector(initialize)),让子类去调用initialize

分析:

initialize 的原理是在类第一次接受到消息的时候进行调用。比如说在第一次执行代码 [NSObject alloc][Class alloc]的时候,都向类发送了消息,这个时候都会调用。也就是说,如果我们使用了这个类,那么 initialize 就会被调用,如果我们从来都没有使用到这个类,那么 initialize 就永远都不会被调用。可以创建一个类,在类的.m文件中实现 + (void)initialize 去进行验证。

我们在在类、它的分类或者多个分类中都实现 + (void)initialize 方时,我们发现第一次个类发送消息时,只会调用一个 initialize 方法,调用的是分类中的方法,而 load 方法是:在 runtime 加载类的时候会调用所有的 load 方法。我们可以猜想 initialize 的调用是使用的消息机制 objc_msgSend() 来进行调用的,通过 Class 的 isa 找到元类对象,在元类对象的方法列表中找 initialize 方法,由于分类中的 initialize 方法已经被合并到了元类对象方法列表的前面,所以调用的其实是分类中的 initialize 方法。

再写出一个子类进行测试,可以发现:在调用子类的 initialize 之前,会先调用父类的 initialize (如果父类的已经调用过了,就不调用了)。 我们可以猜测,在编译后肯定是调用了这样的代码:

objc_msgSend(父类, @selector(initialize));objc_msgSend(子类, @selector(initialize));复制代码

我们已经知道,当类接收到这消息的时候,它会通过类对象的 isa 找到元类对象,找到元类对象的方法列表,然后在里面查找方法。

我们可以在源码中找到这一段:

这其实就是上述过程中查找方法用到的函数。 我们按照
lookUpImpOrNil ->
lookUpImpOrForward ->
_class_initialize 的顺序点进去。然后我们主要注意这个函数的两个部分:

我们先来看一下 callInitialize() 点进去:

可以就把
callInitialize(cls) 简单的理解成
objc_msgSend(cls, @selector(initialize))

接下来把刚才找到的这一大段代码再结合前一段lookUpImpOrForward中的大吗用伪代码做一简化,只留下我们关注的部分,得到如下的样子:

if (cls 没有被初始化) {        if (有父类 && 父类没有初始化) {        objc_msgSend(父类,@selector(initialize));        初始化父类;    }        objc_msgSend(cls, @selector(initialize));    初始化 cls;}复制代码

这段伪代码就完全说明白了 + (void)initialize 方法的根本实现。

所以上边测试子类的现象就可以理解了。

父类的 initialize 方法可能会被调用很多次,我们来看一个例子。

@interface Person : NSObject@end@implementation Person+ (void)initialize{    NSLog(@"Person - initialize");}@end**************************************************@interface Person (test)@end@implementation Person (test)+ (void)initialize{    NSLog(@"Person test - initialize");}@end**************************************************@interface Student : Person@end@implementation Student@end**************************************************@interface Teacher : Student@end@implementation Teacher@end复制代码

然后调用下列方法:

[Student alloc]; [Teacher alloc];复制代码

你可以试着猜测一下打印出来的语句是什么。

5. Category为什么只能加方法不能加属性?

答:

分类能添加方法是因为分类编译之后的结构体 _category_t 中有可以存储方法列表的成员变量。

分类其实也是可以添加属性的,因为 _category_t 中也有可以存储属性的成员变量。

分类不能直接添加成员变量,因为 _category_t 不能存储成员变量。

分析:

再看一下分类编译之后的结构体:

因为它有
instance_methods
class_methods ,所以分类是可以添加方法的。

分类其实是可以添加属性的,因为 _category_t 中有 properties,它是用来存储属性的

我们可以看到在
Person (test) 分类中添加属性
@property (nonatomic, copy) NSString *name; 之后,还是可以编译成功的。

一般我们在类中添加 @property 之后,系统会自动生成成员变量,在 @interface 中添加 settergetter 方法声明,在 @implementation 中实现 settergetter

在分类中添加 @property,系统只会在 @interface 中添加settergetter的方法声明。

6. Category 能否添加成员变量?如果可以,如何给 Category 添加成员变量?

答:

不能直接添加,可以间接使用关联对象实现 `Category` 有成员变量的效果。

分析:

Category 中是不能直接添加成员变量

可以看到,如果在分类中直接添加成员变量 Xcode 会报错。

关联对象提供了一下 API [6. 你使用过 runtime 中的哪些方法?]:

  • 添加关联对象
/*    参数 object 表示的是要给哪个对象进行关联,一般传实例对象;    第二个参数 key ,它的类型是 void *,指针,也就是一个地址;    第三个参数 value ,表示要关联的是哪个值,把 value 和 object 通过 key 关联起来;    第四个参数 policy ,关联策略,设置它就类似于设置 @property 后边的修饰符,它有以下选项:        OBJC_ASSOCIATION_ASSIGN = 0,        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,        OBJC_ASSOCIATION_RETAIN = 01401,        OBJC_ASSOCIATION_COPY = 01403*/void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)复制代码
  • 获得关联对象
/*    取出与 object 通过 key 关联的值 value*/id objc_getAssociatedObject(id object, const void * key)复制代码
  • 移除所有的关联对象
    id objc_removeAssociatedObjects(id object)

参数 key 的三种简便的设置方式:

第一种方式

Person+test.h@interface Person (test)@property (nonatomic, assign) int weight;@endPerson+test.mconst int WeightKey;@implementation Person (test) - (void)setWeight:(int)weight{    objc_setAssociatedObject(self, &WeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);       //因为 value 是 id 类型,所以把它转成 NSNumber}- (int)weight{    return [objc_getAssociatedObject(self, &WeightKey) intValue];}@end复制代码

通过这种方式确实可以实现想要的效果。但是存在两个问题:

a. 我们定义的全局变量可以被外部的文件访问。可以在前面用 static 修饰,限制它的作用域。

static const int WeightKey;复制代码

b. 它只是作为一个 key,如果用 int 类型,需要占用 4 个字节。可以把它改成 char 类型的,就只用占 1 个字节了。

static const char WeightKey;复制代码

第二种方式,在 .m 中这样设置 key :

- (void)setWeight:(int)weight{    objc_setAssociatedObject(self, @"weight", @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (int)weight{    return objc_getAssociatedObject(self, @"weight");}复制代码

key 的类型是指针,将 @"weight" 传进去实际上是将这个字符串的地址传进去。

那么,这里产生了一个疑问,这里用了两个 @"weight",他们的地址是一样的吗?

是一样的。在 iOS 中我们这种直接写字符串的方式,字符串是存储在常量区的。 存储在常量区中的数据一经初始化就不能再修改,程序结束后由系统释放。所以不论我们写多少遍 @"weight",它其实都是存放在常量区的一个 @"weight"。

这里可以延伸到另个一面试题:判断两个字符串字面量是否相同,为什么要用 isEqualToString , 而不能用 == 来判断?在以后的问题里会有讲到。

第三种比较好的方式,使用 @selector()

- (void)setWeight:(int)weight{    objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (int)weight{    return [objc_getAssociatedObject(self, @selectory(weight)) intValue];   }复制代码

测试一下,可以发现 @selector(weight) 的地址也都是一样的:

NSLog(@"%p %p %p", @selector(weight), @selector(weight), @selector(weight));复制代码

上面介绍了可以采用关联对象的方式间接实现分类中添加成员变量的效果和参数 key 的三种常用的方式。

关于 AssociatedObjects 的实现原理等,可以参考和文章。

转载地址:http://aepfm.baihongyu.com/

你可能感兴趣的文章
虚拟磁盘工具vmkfstools的使用
查看>>
职场思想分享005 | 别让背后抱怨说别人坏话成为聊天习惯
查看>>
oracle11gR2 DataGuard switchover切换的两个错误状态解决
查看>>
不登陆数据库执行mysql命令小结
查看>>
SQL Server 2014 许可证(一)版本区别
查看>>
话里话外:成功CEO的用人之道——按需激励
查看>>
使用Visual Studio迁移远程网站到Micorosft Azure
查看>>
Dr.Elephant mysql connection error
查看>>
Tomcat网络输出数据流图
查看>>
Cloudera CDH 离线安装与使用
查看>>
安装 SQL Server 客户端驱动程序
查看>>
<北京青年>--思考
查看>>
Linux Bash Shell高级重定向操作--深入了解标准错误输出和标准输出
查看>>
HP LaserJet Pro P1106网络打印机64位驱动安装
查看>>
JDK和JAXB的对应
查看>>
Numpy快速入门
查看>>
Nginx查看 并发连接数
查看>>
Hyper-V虚拟机快照占用磁盘空间过多,导致虚拟机不能启动怎么办
查看>>
LAMP下http跳转到 https
查看>>
RHEL6入门系列之一,Linux的来龙去脉
查看>>