分类、类扩展区别
- 分类-运行时决议,类扩展-编译时决议,类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
- 分类中原则上只能增加方法(能添加属性的的原因只是通过runtime的关联对象解决无setter/getter的问题而已);
- 类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的(用范围只能在自身类,而不是子类或其他地方);
- 类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
- 定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。
分类(categroy)
- 运行时创建
- 作用:扩展已有类的功能。
因为OC是单继承的,子类可以拥有父类的方法和属性。在大型项目,企业级开发中多人同时维护同一个类,此时程序员A因为某项需求只想给当前类增加方法或属性,使用继承会导致子类也拥有父类的方法和属性。为了解决的这个问题,就引入了分类(Category)的概念。
继承:
- OC是单继承:一个类只能继承一个直接父类;
- OC是多层继承:B类继承A类,C类可以继承B类
- 当A类继承B类,A类就拥有B类中所有成员变量(属性)和方法。这也是继承的主要目的。
- 如果重写了父类的方法,但还想使用父类的功能。则使用super。用来调用父类的方法。可以认为,super就是指父类。
- 在继承体系中方法调用的顺序:1)在自己类中找;2)如果没有就去父类中找;3)如果父类中没有,就去父类的父类中找……直到找到基类。
继承的注意点:
OC中,类方法也是可以继承的(通过子类的类名调用父类的类方法);类方法也是可以重写的;类方法可以和对象方法重名(+表示类方法,-表示对象方法);子类中不能定义与父类中同名的成员变量。
继承的好处:
代码重用;
继承的缺点:
父类的改变影响所有的子类。子类与父类耦合度很高。当子类中需要有自己独特的行为,不想使用父类的方法,可以把父类的方法覆盖掉:直接在子类中用一样的名字写个方法。不用在.h中写,因为父类已经声明过了,直接在.m中重写。
Category 是表示一个指向分类的结构体的指针,其定义如下:
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
通过上面我们可以发现,这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods 列表是 objc_class 中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
但这个结构体里面,没有属性列表。
分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。所以< 原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性> ;
总结:
1.分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。所以< 原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性> ;
2.分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量(编译时会报警告);
3.可以在分类中访问原有类中.h中的属性;
4.如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会覆盖忽略原有类的方法。所以同名方法调用的优先级为 子类分类 > 子类本类 > 父类分类 > 父类本类
因此在开发中尽量不要覆盖原有类;
5.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;多个category的调用顺序按照:Build Phases ->Complie Source 中的编译顺序。
6.即使我们不主动引入 Category 的头文件,Category 中的方法也会被添加进主类中。我们可以通过 - performSelector: 等方式对 Category 方法进行调用。
a)将 Category 和它的主类(或元类)注册到哈希表中;
b)如果主类(或元类)已实现,那么重建它的方法列表。
2.在这里分了两种情况进行处理:
a)Category 中的实例方法和属性被整合到主类中;
b)类方法则被整合到元类中
c)对协议的处理比较特殊,Category 中的协议被同时整合到了主类和元类中。
7.最终都是通过调用 staticvoid remethodizeClass(Class cls) 函数来重新整理类的数据的。
分类的语法
.h文件
@interface 待扩展的类 (分类的名称)
@end
.m文件
@interface 待扩展的类(分类的名称)
@end
@implementation 待扩展的名称(分类的名称)
@end
1.基本用法,给原有类添加方法,以UIControl为例
.h文件
@interface UIControl (YuAdd)
@end
.m文件
#import "UIControl+YuAdd.h"
@interface UIControl ()
- (NSString *)strTodealWithString:(NSString *)str;
@end
@implementation UIControl (YuAdd)
- (NSString *)strTodealWithString:(NSString *)str {
return [NSString stringWithFormat:@"分类处理完的str --- %@",str];
}
@end
使用时导入#import "UIControl+YuAdd.h"
UIButton * btn = [[UIButton alloc]init];
NSLog(@"%@",[btn strTodealWithString:@"原始数据"]);
2.进阶用法,通过runtime为分类增加属性,关联对象,以UIControl为例:
.h文件
@interface UIControl (YuAdd)
@property (nonatomic, copy) NSString * nameControl;
@end
.m文件
static NSString *nameControlWithSetterGetterKey = @"nameControlWithSetterGetterKey";
#import "UIControl+YuAdd.h"
@interface UIControl ()
@end
@implementation UIControl (YuAdd)
//运行时实现setter方法
- (void)setNameControl:(NSString *) nameControl {
objc_setAssociatedObject(self, & nameControlWithSetterGetterKey, nameControl, OBJC_ASSOCIATION_COPY);
}
//运行时实现getter方法
- (NSString *) nameControl {
return objc_getAssociatedObject(self, & nameControlWithSetterGetterKey);
}
@end
使用时导入#import "UIControl+YuAdd.h"
UIButton * btn = [[UIButton alloc]init];
btn.nameControl = @"runtime为分类增加属性,手动实现setter/getter";
NSLog(@"%@",btn.nameControl);
3.高阶用法,通过runtime交换方法,为类增加自定义逻辑,以UIControl为例:
.h文件
@interface UIControl (YuAdd)
/// 响应区域需要改变的大小,负值表示往外扩大,正值表示往内缩小
///btn2.yu_touchExtendInset = UIEdgeInsetsMake(-30, -30, -30, -30);
@property (nonatomic, assign) UIEdgeInsets yu_touchExtendInset;
@end
.m文件
#import "UIControl+YuAdd.h"
@interface UIControl ()
@end
@implementation UIControl (YuAdd)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ReplaceMethod([self class], @selector(pointInside:withEvent:), @selector(yu_pointInside:withEvent:));
});
}
/// 捕获 `pointInside` 方法
- (BOOL)yu_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (UIEdgeInsetsEqualToEdgeInsets(self.yu_touchExtendInset, UIEdgeInsetsZero) || self.isHidden ||
([self isKindOfClass:UIControl.class] && !((UIControl *)self).isEnabled)) {
return [self yu_pointInside:point withEvent:event];
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.yu_touchExtendInset);
hitFrame.size.width = MAX(hitFrame.size.width, 0);
hitFrame.size.height = MAX(hitFrame.size.height, 0);
return CGRectContainsPoint(hitFrame, point);
}
/** 交换方法 */
CG_INLINE void
ReplaceMethod(Class _class, SEL _originSelector, SEL _newSelector) {
Method oriMethod = class_getInstanceMethod(_class, _originSelector);
Method newMethod = class_getInstanceMethod(_class, _newSelector);
BOOL isAddedMethod = class_addMethod(_class, _originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
if (isAddedMethod) {
class_replaceMethod(_class, _newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, newMethod);
}
}
// runtime实现属性set,get方法
static char * kAssociated_touchExtendInset = "kAssociatedObject_touchExtendInset";
- (void)setYu_touchExtendInset:(UIEdgeInsets)yu_touchExtendInset {
objc_setAssociatedObject(self, &kAssociated_touchExtendInset, [NSValue valueWithUIEdgeInsets:yu_touchExtendInset],
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (UIEdgeInsets)yu_touchExtendInset {
return [objc_getAssociatedObject(self, &kAssociated_touchExtendInset) UIEdgeInsetsValue];
}
@end
使用时导入#import "UIControl+YuAdd.h"
UIButton * btn = [[UIButton alloc]init];
btn.yu_touchExtendInset = UIEdgeInsetsMake(-15, -15, -15, -15);
例:
面试题
1.categroy为什么不能添加属性?怎么实现添加?与Extension的区别?category覆盖原类方法?多个category调用顺序
- Runtime初始化时categroy的内存布局已经确定,没有ivar,所以默认不能添加属性。
- 使用runtime的关联对象,并重写setter和getter方法。
- Extenstion编译期创建,可以添加成员变量ivar,一般用作隐藏类的信息。必须要有类的源码才可以添加,如NSString就不能创建Extension。
- category方法会在runtime初始化的时候copy到原来前面,调用分类方法的时候直接返回,不再调用原类。如何保持原类也调用(https://www.jianshu.com/p/40e28c9f9da5)。
- 多个category的调用顺序按照:(不分子分类,父类分类)通一按照Build Phases ->Complie Source 中的编译顺序。
2.我们知道,在类和category中都可以有+load方法,那么有两个问题:在类的+load方法调用的时候,我们可以调用category中声明的方法么?这么些个+load方法,调用顺序是咋样的呢
- 可以调用,因为附加category到类的工作会先于+load方法的执行
- +load的执行顺序是先本类,后category,本类与category的+load执行顺序都是是根据编译顺序决定的。 【Build Phases ->Complie Source 中的编译顺序。先所有本类按顺序执行,然后所有category按顺序执行】
类扩展(Extension)
- 编译时决议
Extension是Category的一个特例。类扩展与分类相比只少了分类的名称,所以称之为“匿名分类”。
类扩展的语法
类扩展没有.h文件
.m文件
@interface 待扩展的类()
@end
- 在.m文件中为类增加私有的成员变量(属性)和方法。这样使得这些控件成员是私有的,不会被外界干扰到。
- 与分类不一样的是:类扩展的原类名称后面的括号中没有东西,也没有.h文件。
注意:类扩展得到的属性和方法,都是私有的!在外界中无法直接使用,【可通过kvc或runtime进行访问私有属性和方法】即无法直接get后者set到这些成员。但是可以在.h里提供方法接口来改变这些私有属性的情况。
类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
kvc访问
#import "baseViewController.h"
@interface baseViewController ()
@property (nonatomic, copy) NSString *nameString; // 通过KVC在外界访问此私有属性
@end
@implementation baseViewController
@end
导入头文件
///消息发送头文件
#import <objc/message.h>
///运行时头文件
#import <objc/runtime.h>
///kvc访问私有属性
baseViewController *vc = [[baseViewController alloc] init];
[vc setValue:@"婉儿"forKey:@"nameString"];
NSLog(@"%@", [vc valueForKey:@"nameString"]);
runtime访问
///runtime访问私有属性
///获取私有属性「这里必须用_nameString」
Ivar ivarName = class_getInstanceVariable([vc class], "_nameString");
NSLog(@"Ivar对应的属性名 -- %s", ivar_getName(ivarName));
///赋值私有属性
object_setIvar(vc, ivarName, @"麻子");
///获取私有属性值
NSLog(@"%@", object_getIvar(vc, ivarName));
参考:
https://blog.csdn.net/chenxi506343891/article/details/79013709
https://blog.csdn.net/lyxleft/article/details/80590957
https://tech.meituan.com/2015/03/03/diveintocategory.html
https://www.jianshu.com/p/01911be8ce83