分类(categroy),类扩展(Extension)

分类、类扩展区别

  • 分类-运行时决议,类扩展-编译时决议,类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
  • 分类中原则上只能增加方法(能添加属性的的原因只是通过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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356

推荐阅读更多精彩内容