Files
knowledge-kit/Chapter1 - iOS/1.48.md
2020-02-25 17:46:51 +08:00

216 lines
8.1 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 类别Category、拓展Extension
> 很多人都知道类别、分类的用法但是对于一些细节就不是很清楚了本文主要梳理下这3个概念的细节
## 类别Category
### 文件特征
- 类别文件有2个分别为 .h 和 .m
- 命名为: “类名+类别名.h”和“类名+类别名.m”
### 文件内容格式
.h 文件格式
```
#import "类名.h"
@interface 类名 (类别名)
// 在此处声明方法
@end
```
.m 文件格式
```
#import "类名+类别名.h"
@implementation 类名 (类别名)
// 在此处实现声明的方法
@end
```
### 类别的作用
拓展当前类,为类添加方法
### 类别的局限性
- 无法向现有的类添加实例变量编译器报“instance variables may not be placed in categories”。Category 一般只为类提供方法的拓展,不提供属性的拓展。但是利用 Runtime 可以在 Category 中添加属性
- 方法名称冲突的情况下,如果 Category 中的方法与当前类的方法名称重名Category 具有更高的优先级,类别中的方法将完全取代现有类中的方法(调用方法的时候不会去调用现有类里面的方法实现)。
- 当现有类具有多个 Category 的时候,如果每个 Category 都有同名的方法,那么在调用方法的时候肯定不会调用现有类的方法实现。系统根据编译顺序决定调用哪个 Category 下的方法实现。(可以在 Targets -> Build phases -> Compile Sources 下给多个 Category 更换顺序看看到底在执行哪个方法)
### Category 的使用和注意
1. Category 中的方法如果和现有类方法一致,工程中任何调用当前类的方法的时候都会去调用 Category 里面的方法比如UIViewCtroller、UITableView这些的方法时要慎重。因为用Category重写类中的方法会对子类造成很大的影响。比如用Category 重写了 UIViewCtroller 的方法 A那么如果你在工程中用到的所有继承自 UIViewCtroller 的子类,去调用方法 A 时,执行的都是 Category 中重写的方法 A,如果不幸的是,你写的方法 A 有 Bug那么会造成整个工程中调用该方法的所有 UIViewCtroller 子类的不正常。除非你在子类中重写了父类的方法 A这样子类调用方法 A 时是调用的自己重写的方法 A消除了父类 Category 中重写方法对自己的影响
2. Category拓展方法按照有没有重写当前类中的方法分为未重写的拓展方法和重写拓展方法。且类引用自己的 Category 时,只能在 .m 文件中引用(.h 文件引用自己的类别会报错)。子类引用父类的 Category 在 .h 或 .m 都可以。如果类调用 Category 中重写的方法,不用引入 Category 头文件,系统会自动调用 Category 中的重写方法
3. Category 中如果重写了 A 类从父类继承来的某方法,不会影响与 A 同层级的 B 类
4. 子类会不会继承父类的 Category Category 中重写的方法会对子类造成影响,但是子类不会继承非重写的方法(现有类中没有的方法)。但是在子类中引入父类 Category 的声明文件后,子类就会继承 Category 的非重写方法。继承的表现是:当子类的方法和父类 Category 中的方法名完全相同,那么子类里的方法会覆盖掉父类 Category相当于子类重写了继承自父类的方法
5. Category 的作用是向下有效的。即只会影响到该类的所有子类。比如 A 类和 B 类是继承自 Super 类的2个子类当给 A 类添加一个 Category sayHello 方法仅有A 类的子类才可以使用 sayHello 方法
## 拓展Extension
### 文件特征
- 只存在一个文件
- 命名方式“类名_拓展名.h”
```
#import "类名.h"
@interface 类名 ()
// 在此添加私有成员变量、属性、声明方法
@end
```
### 拓展的作用
1. 为类增加额外的属性、成员变量、方法声明
2. 一般将类拓展直接写到当前类的 .m 文件中。不单独创建
3. 一般私有的属性和方法写到类拓展中
4. 和 Category 类似,但是小括号里面没有拓展的名字
### 拓展的局限性
1. Extension 中添加的属性、成员变量、方法属于私有(只可以在本类的 .m 文件中访问、调用。在其他类里面是无法访问的,同时子类也是无法继承的)。假如我们有这样一个需求,一个属性对外是只读的,对内是可以读写的,那么我们可以通过 Extension 实现。
2. 通常 Extension 都写在 .m 文件中,不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方,否则编译报错
3. 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话,也需要将类实现文件中包含进类扩展文件,否则会找不到属性的 setter 和 getter 方法。
```
//Web.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Web : Person
@end
NS_ASSUME_NONNULL_END
//Web.m
#import "Web.h"
#import "Web+H5.h"
@interface Web ()
@property (nonatomic, strong) NSString *skillStacks;
@end
@implementation Web
- (void)test {
self.skills = @"iOS && Web && Node && Hybrid";
self.skillStacks = @"iOS && Web && Node && Hybrid";
}
- (void)show {
NSLog(@"%@",self.skillStacks);
}
@end
```
## 总结
1. Category 只能拓充方法,不能拓展属性和成员变量(包含成员变量会报错。属性虽然不可以直接拓展,利用 Runtime 可以实现)
2. 如果 Category 中声明了1个属性那么 Category 只会生成 setter 和 getter 的声明,不会有实现
3. Extension 也被成为匿名的 Category
4. 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。
##「小插曲」:为 Category 实现属性的 Setter 和 Getter
```
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Student)
/**< 学号*/
@property (nonatomic, strong) NSString *studyNumber;
@end
NS_ASSUME_NONNULL_END
#import "Person+Student.h"
#import <objc/message.h>
@implementation Person (Student)
- (void)sayHi {
NSLog(@"大家好,我叫%@,我今年%zd岁了",self.name,self.age);
}
/*
* 传统的做法是在 setter 里面这样写
_studyNumber = studyNumber;
ARC 自动管理内存
MRC
[_studyNumber release];
[studyNumber retain];
_studyNumber = studyNumber;
但是在 Category里面不会生成对应的实例变量因此我们可以利用 Runtime 为我们的 category 关联属性的值
setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
getter:objc_getAssociatedObject(self, @selector(firstView));
}
*/
- (void)setStudyNumber:(NSString *)studyNumber {
objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
, OBJC_ASSOCIATION_RETAIN);
}
//@selector(studyNumber)
- (NSString *)studyNumber {
return objc_getAssociatedObject(self, @selector(studyNumber));
}
@end
```
说明: `objc_setAssociatedObject` 的第二个参数是`const void * _Nonnull key` 所以可以用 "studyNumber" 或者利用 `@selector()` 的特性返回的数据类型也满足,所以示例代码选用第二种方式
给分类添加属性的时候,为了避免多人开发对于属性添加造成的覆盖,我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程,那么我们可以为属性命名的时候在前面添加当前模块的前缀。
比如我们在 Login-Register-Module 模块为 NSURL 的 Category 添加一个 title 的属性的时候,可以这样命名 LR_Title。请查看下面的代码
```Objective-c
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (Title)
@property (nonatomic, copy) NSString *LR_title;
@end
NS_ASSUME_NONNULL_END
#import "NSURL+Title.h"
#import <objc/runtime.h>
@implementation NSURL (Title)
- (void)setLR_title:(NSString *)LR_title
{
objc_setAssociatedObject(self, @selector(LR_title), LR_title
, OBJC_ASSOCIATION_RETAIN);
}
- (NSString *)LR_title
{
return objc_getAssociatedObject(self, @selector(LR_title));
}
@end
```