分类、类扩展、继承的总结
因为最近在学习runtime,学习到关联对象的时候用到分类,所以顺便把分类复习了一下。我平时用继承多于分类,然后就很困惑的是,分类做的事情继承也能做,为什么要用分类呢?所以继续复习了一下继承。答案在后面,开始吧!
一.分类
详解:深入浅出理解分类(category)和类扩展(extension)
https://www.jianshu.com/p/9e827a1708c6
1.分类的特点
- 运行时决议;
- 可以为系统类添加分类;
2.分类中可以添加什么内容?
(面试考点)
分类的结构体
- 实例方法
- 类方法
- 协议
- 属性(@proterty修饰的)(未生成get、set方法,但是可以通过runtime添加get、set方法)。
注意
:分类不可以添加成员变量;
分类可以访问原有类中.h的属性
- 每个分类在编译的时候都是生成一个名叫catrgort_t的结构体;
3. Category的实现原理
- 每一个分类编译之后生成一个 struct category_t。里面存储着分类的对象方法、类方法、属性、协议信息;
- 把所有Category的方法、属性、协议数据,合并到一个大数组中;后面参与编译的Category数据,会在数组的前面。
- 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
或者说:
- Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
4. 分类的对象方法添加到类对象过程如下:
因为对象方法存储在类对象里面,所以分类的对象方法要添加到类对象里面。
- 有NSString+other2、NSString+other、NSString+other1三个分类,添加顺序如下图;
- 合并的时候:A数组里面放着NSString+other2、NSString+other、NSString+other1的对象方法。
-
NSString的类对象里面,存放对象方法的list里面,先把NSString的对象方法挪到最后一个位置。再把A数组里面的方法,从后往前添加到对象方法列表里面。所以相同的方法名,最后一个分类里面的方法先被调用。
添加分类
4.分类执行顺序
-
如果方法名和原有类里面的方法相同的话,会覆盖系统的方法;执行顺序:分类>本类>父类。所以尽量不要和原有类的方法重名。
-
如果不同的分类里面有相同的方法,调用的方法由编译器:targets->Build Phases->Complie Source里面添加的文件顺序决定,从上到下编译。调用这个方法时会执行最后一个参与编译的分类里面的方法;
如下:有NSString的三个分类,NSString+Other、NSString+Other1、NSString+Other2,假如三个分类里面都有方法A,调用方法A的时候。从上到下进行编译,会调用最后参与编译的NSString+Other1里面的方法A。
添加分类- 名字相同的分类会引起报错
5.分类的使用
1.扩展现有类/添加非正式协议
2.分解体积庞大的类文件
分类可以将类的实现分散到不同文件或多个不同的框架当中。
1)减少单个文件的体积
2)把相同的功能放到同一个category中
3)按需加载想要的category
4)多个开发者可以一起完成一个类
3.声明私有方法/创建对私有方法的前向引用
定义在类名.h文件中的方法/属性一定是公开的;
而在类名.m中的类延展(Extension)中定义的方法/属性是私有的;
不在任何地方申明,只在类.m中写实现代码的方法都是私有的。
但是通过分类声明私有方法,就可以实现对私有方法的调用。
4.实现多继承
5.把Framework的私有方法公开
5.1扩展现有类
为现有类添加分类很普遍,不再展示;
补充的是非正式协议:
凡是NSObject或其子类的分类,都是非正式协议。
5.2分解体积庞大的类文件
分解体积庞大的类文件,就是把类的.h文件声明方法,类的实现放到分类里面。这样我们就可以按照需求把不同的实现放到不同分类里面。
实例如下:
- Teacher类的.h进行方法声明:read、teach方法
Teacher.h:
@interface Teacher : NSObject
@property(nonatomic,strong)NSString * teahcerName;
-(void)read;
-(void)teach:(NSString *)teachClass;
@end
Teacher.m:
@implementation Teacher
@end
- Teacher+Read实现Teacher的read方法:
Teacher+Read.h:
@interface Teacher (Read)
@end
Teacher+Read.m:
@implementation Teacher (Read)
//实现Teacher类的read方法
-(void)read{
NSLog(@"read");
}
@end
- Teacher+Teach实现Teacher的teach方法:
Teacher+Teach.h:
@interface Teacher (Teach)
@end
Teacher+Teach.m:
@implementation Teacher (Teach)
//实现Teacher类的teach方法;
-(void)teach:(NSString *)teachClass{
NSLog(@"%@ teach:%@",self.teahcerName,teachClass);
}
@end
4.ViewController调用Teacher的read、teach方法。成功调用
#import "Teacher.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//分解体积较大的类文件
Teacher * teacher = [[Teacher alloc] init];
teacher.teahcerName = @"xp";
[teacher read];
[teacher teach:@"English"];
}
@end
5.3 声明私有方法/创建对私有方法的前向引用
类的.m文件进行私有方法实现,分类.h进行私有声明。实现外部对私有方法的使用。这也叫做对私有方法向前引用。
实例:假如Student类中有私有方法study,怎么在ViewController中调用study方法呢?
Student.h:
#import <Foundation/Foundation.h>
@interface Student : NSObject
@end
Student.m:
@implementation Student
-(void)study{
NSLog(@"study");
}
@end
方法1:
使用performSelector在Viewcontroller中调用:
Student * stu = [[Student alloc] init];
[stu performSelector:@selector(study) withObject:nil];
方法2(私有方法前向引用):
添加Student的分类。如下,可以把分类的方法声明直接添加到Studnt的.h文件中,分类不需要实现方法。
注意
分类可以添加到Student.h中,也可以新建分类文件。但是把分类添加到Student.m中仍旧调用不到Student的私有方法。
Student.h文件:
#import <Foundation/Foundation.h>
@interface Student : NSObject
@end
//添加分类
@interface Student(Study)
//声明私有方法
-(void)study;
@end
ViewController调用:
//分类的前向引用
Student * stu = [[Student alloc] init];
[stu study];
总结:要运用类的私有方法,可以运用分类进行私有方法声明既可。
相当于类.m进行方法实现。分类.h进行方法声明。
5.4 实现多继承
在oc里面是没有多继承的。我们现在要间接实现多继承的功能。
继承,就是可以使用父类的内容;
多继承,就是可以使用多个父类的内容,比如调用多个类的方法。
现在,我们需要让ClassA调用ClassB的方法,也可以调用ClassC的方法。我们需要使用分类和消息转发。
如下:
- 有ClassA类进行消息转发,接收到方法后,ClassB和ClassC谁能执行对应的方法就让谁执行:
ClassA.h:
@interface ClassA : NSObject
@end
ClassA.m:
@implementation ClassA
//消息转发,ClassB和ClassC谁能执行对应的方法就返回谁
-(id)forwardingTargetForSelector:(SEL)aSelector{
ClassB * b = [ClassB new];
ClassC * c = [ClassC new];
if ([b respondsToSelector:aSelector]) {
return b;
}else if ([c respondsToSelector:aSelector]){
return c;
}
return nil;
}
@end
2.有ClassB类声明并实现methodB方法:
ClassB.h:
@interface ClassB : NSObject
-(void)methodB;
@end
ClassB.m:
@implementation ClassB
-(void)methodB{
NSLog(@"methodB");
}
@end
3.有ClassC类声明并实现methodC方法:
ClassC.h:
@interface ClassC : NSObject
-(void)methodC;
@end
ClassC.m:
@implementation ClassC
-(void)methodC{
NSLog(@"methodC");
}
@end
4.在Viewcontroller里面,让ClassA调用ClassB、ClassC的方法:
方法1:使用performSelector调用
ClassA * a = [ClassA new];
[a performSelector:@selector(methodB)];
方法2:把ClassA的对象强转为ClassB的
ClassA * a = [ClassA new];
[(ClassB *)a methodB];
方法3: ClassA.h中增加ClassA的分类,声明ClassB和ClassC中的方法
ClassA.h文件:
//增加ClassA的分类;声明ClassB和ClassC中的方法
@interface ClassA(SuperForOtherClass)
-(void)methodB;
-(void)methodC;
@end
ViewController里面:
ClassA * a = [ClassA new];
[a methodB];
[a methodC];
总结:运用消息转发确定执行的对象、分类引用方法。从而达到ClassA调用其他对象的方法。
5.5 把Framework的私有方法公开
6. load、initialize
6.1 load方法:
load的调用时机
- +load方法会在runtime加载类、分类时调用
- 每个类、分类的+load,在程序运行过程中只调用一次
load的调用顺序
-
先调用类的+load
按照编译先后顺序调用(先编译,先调用)
调用子类的+load之前会先调用父类的+load -
再调用分类的+load
按照编译先后顺序调用(先编译,先调用)
如果分类写了和原本类相同的方法,调用的时候只调用分类的方法。但是load方法不是。
load方法的调用,直接是去找这个方法的地址,进行调用;不是消息机制调用(消息机制调用:通过isa指针,找到类对象的对象方法、元类对象的类方法),所以不是只调用分类的方法。
如下:Student:Person。有分类Person+Test1、Person+Test2、Student+Test1、Student+Test2。加载的时候:
1)先调用类的load方法、类里面先调用父类的load方法:所以先调用Person的load方法,再调用Student的load方法。
2)再调用分类的load方法。先编译先调用。
打印
Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
- 有load方法
- load方法在runtime加载类、分类的时候调用
- load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
6.2 initialize方法:
initialize调用时机
+initialize方法会在类第一次接收到消息时调用
(通过消息机制调用,+initialize放在元类对象里面)
调用顺序
- 先调用父类的+initialize,再调用子类的+initialize
(先初始化父类,再初始化子类,每个类只会初始化1次)
+load和+initialize的区别:
1.调用方式:
- +load是根据函数地址直接调用;
- +initialize是通过消息机制调用;
2.调用时刻:
- +load是在加载类、分类的时候调用,之后加载一次
- +initialize是类第一次接收到消息的时候调用,每一个类之后+ initialize一次。但是父类的+ initialize会被调用多次。
3.调用顺序:
-
load
1>load是先调用类的load
先编译的先调用;
如果有父类,先调用父类的load;
2>再调用分类的load
先编译的先调用; -
initaialize:
1>先初始化父类
2>再初始化子类(可能最终调用的是父类的initaialize方法)
3> 如果分类实现了+initialize,就覆盖类本身的+initialize调用
+initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点
如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
如果分类实现了+initialize,就覆盖类本身的+initialize调用
二.类扩展
类扩展的形式(其实平时我们在.m文件里面经常写):
@interface XXX ()
//私有属性
//私有方法(如果不实现,编译时会报警,Method definition for 'XXX' not found)
@end
总结:
- 可以添加属性、成员变量,都是私有的。(只能自身类里面使用,子类或者其他方法不可用)
- 可以添加方法,但是如果方法不实现的话会报警告;
- 分类里面如果有方法未实现的话不会报警告。因为类扩展是在编译时期添加到类里面,编译时期发现未实现就可以警告了。分类的方法是在运行的时候添加到类里面的;
- 类扩展没有自己的实现部分(@implementation),必须依靠对应类的实现部分来实现;
- 定义在.m里面的类扩展的方法是私有的,定义在.h文件中的类扩展的方法为共有的。
分类 | 扩展 |
---|---|
运行时决议 | 编译时决议 |
可以有声明、实现 | 只以声明的形式存在,多数情况下寄生于宿主类的.m中 |
可以为系统类添加分类 | 不可以为系统类添加扩展 |
是在运行时,才会将数据合并到类信息中 |
在编译的时候,它的数据就已经包含在类信息中 |
三.继承
详解:https://casatwy.com/tiao-chu-mian-xiang-dui-xiang-si-xiang-yi-ji-cheng.html
使用继承应该满足以下3点:
- 父类只是提供了服务。父类和子类没有业务关系;(Object提供了基础服务,比如内存计数功能)
- 层级关系明显,子类和父类各做各的。如果一件事情在父类做了,子类又做了,那他们就是并列关系了。
- 父类进行了修改子类要有所体现,这时候父类和子类是耦合了的,要有耦合需求才行。(ApiManager里面判断了网络状态,所有的派生类都是需要的。此时,牵一发而动全身,是适合继承的)。
回答开头的问题:如果继承就可以实现的,为什么要用分类呢?
因为继承容易造成代码耦合,再抽取出某一个功能的时候难。特别是第三层继承的时候就已经属于继承滥用了。
继承可以实现代码复用,但是要满足了以上三条才可使用。
四.继承和分类的区别
继承 | 分类 |
---|---|
继承可以添加属性,并且自动生成属性的get、set方法 | 分类添加属性后,需要使用runtime的关联对象生成get、set方法 |
继承的方法名如果和父类的一样,可以把父类的实现也添加上(super) | 分类的方法名和系统的一样,会把系统原的方法覆盖,无法添加上系统的实现; |
- 继承要满足三大要求后使用,否则会发生高耦合;
- 如果只是增加某一个方法,可以使用分类。(eg:针对系统的一些类进行扩展(例如,NSString, NSArray, NSNumber))
- 大型而复杂的类,可以把利用分类,把相关的方法放到多个单独的文件中,提高维护下和可读性;