Objective-C中的分类与扩展

分类(category)

分类是苹果为引入的一个新的概念,通过使用分类,可以将一个类中的不同方法分散到多个的文件或模块中,或者在添加新的需求时,可以做到不修改类的源文件,通过分类将新的方法写到新的文件中。

首先看分类的声明和实现,分类的类名必须是已经存在的类。

分类的声明

@interface 类名 (分类名)
方法的声明;
...
@end

分类的实现

@implementation 类名 (分类名)
方法的实现;
...
@end

分类的头文件和实现文件的命名规则为“类名+分类名.h”和“类名+分类名.m”,使用分类的方法时必须引用该分类的头文件

为现有类追加分类

无论是自己定义的类还是系统的类,都可以通过分类为其追加新方法,追加的新方法可以访问类中已有的实例变量和方法。

如想给NSString类增加新方法,若使用继承的方式,在Foundation中众多类的参数和返回值都是NSString,若将这些NSString都替换为子类则不是一个好方法。这种情况下,便可通过分类实现。

通过分类为现有类追加方法后,该类和子类不需要做任何修改就可以使用新的方法。

注意:使用分类虽然很方便,但不宜滥用。

NSString类中有一个方法stringByAppendingPathComponent:,为一个目录字符串追加一个"/"和名称,如路径名是@"/Users/wilsonhan",调用该方法,传入@"Documents",返回的结果为@"/Users/wilsonhan/Documents"。

现在通过分类实现一个新的方法stringByAppendingPathComponents,接收多个字符串参数,为一个NSString同时追加多个参数。

//NSString+PathComp.h
@interface NSString (PathComp)

- (NSString *)stringByAppendingPathComponents:(NSString *)str, ...//可变参数
  NS_REQUIRES_NIL_TERMINATION;//宏变量,编译器在编译的时候发现结尾没有nil,就会提示警告

@end

//NSString+PathComp.m
@implementation NSString (PathComp)

- (NSString *)stringByAppendingPathComponents:(NSString *)str, ...{
    va_list varglist;
    NSString *work, *comp;
    
    if(str == nil){
        return self;
    }
    work = [self stringByAppendingPathComponent:str];//首先将可变参数的第一个str加入到地址后
    va_start(varglist, str);//初始化varglist列表
    while((comp = va_arg(varglist, NSString *)) != nil){//通过va_arg获取下一个参数,传入需要获得的类型为NSString *
        work = [work stringByAppendingPathComponent:comp];//将获取的字符串通过NSString自带的方法连接到地址后
    }
    va_end(varglist);//结束
    return work;
}

@end

以上代码分别在NSString+PathComp.h和NSString+PathComp.m文件中,为NSString增加了新的分类PathComp。

下面来使用一下这个新的分类

#import <Foundation/Foundation.h>
#import "NSString+PathComp.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *path = @"/Users";
        NSString *s;
        
        s = [path stringByAppendingPathComponents:@"wilsonhan", @"Documents", @"学习资料", nil];
        NSLog(@"%@", s);
    }
    return 0;
}

打印结果

/Users/wilsonhan/Documents/学习资料

覆盖已有方法

在分类中可以重新实现已有的方法,新实现的方法会覆盖旧的方法。如在上面的例子中,在实现文件中增加一个函数,覆盖stringByAppendingPathComponent方法。

- (NSString *)stringByAppendingPathComponent:(NSString *)str{
    NSLog(@"%@已被我拦截", str);
    return str;
}

再次运行刚才的代码,因为在stringByAppendingPathComponents方法中多次调用了stringByAppendingPathComponent方法,所以出现了以下结果:

wilsonhan已被我拦截
Documents已被我拦截
学习资料已被我拦截
学习资料

扩展(Extension)

扩展是分类的一个特例,作用是为类添加原来没有的变量,方法和属性,如果将类扩展写到.m文件中则是私有,写到.h文件则是共有的。

扩展中声明的方法必须在类的实现文件中实现,扩展中声明的方法没有实现,编译器会报警,相反分类中没有实现的编译器不会警告。原因是扩展是在编译阶段被加入到类中,而分类是在运行时添加到类中。

扩展的声明方式

扩展的声明方式和分类很类似,只是圆括号之间没有名称

@interface MyClass (){
    BOOL flag;//可以添加实例变量
}
@property (strong, nonatomic) NSString *myClassName;//声明属性
- (void)extensionMethod;//声明方法
@end

分类与扩展中属性的声明

分类

分类中,不能定义实例变量,但可以声明属性,且分类的实现部分不能包含@synthesize,需要手动定义存取器。

这里经过测试,分类中的属性声明,编译器不会生成getter、setter方法,同时也不会生成实例变量,只能通过关联引用的方式获取,这个在下一个小节会有介绍。

扩展

扩展中可以包含属性声明,通过在类实现部分添加@synthesize或者属性方法来实现。扩展中可以声明实例变量的属性。

关联引用(associative references)

分类可以为一个类追加方法,但不能追加实例变量,通过OC语言的动态性,借助运行时(runtime)的功能,为已存在的实例对象增加实例变量,这个功能就叫做关联引用。

添加和检索关联

在objc/runtime.h文件中定义了以下两个方法

//这个方法是为对象object添加以key指定的地址作为关键字、以value为值得关联引用,
//第四个参数policy指定关联引用的存储策略,通过将value指定为nil,就可以删除key的关联
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)

//返回object以key为关键字关联的对象。如果没有关联到任何对象,则返回nil
id objc_getAssociatedObject(id object, void *key)

key的选取
必须使用确定的、不再改变的地址作为key,如使用实现文件中的静态局部变量的地址作为key。

关联策略

关联策略表明了关联的对象是通过何种方式进行关联的,以及这种关联是原子的还是非原子的。

  • OBJC_ASSOCIATION_ASSIGN - 仅仅通过赋值进行关联,不给关联对象发送retain消息,可能会导致悬垂指针,以弱引用的方式保存关联对象。
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC - 发送retain消息,若key已经关联到其他对象,则会给其他对象发送release消息,以强引用的形式保存关联对象。
  • OBJC_ASSOCIATION_RETAIN - 与上一个相同,区别是多线程安全。
  • OBJC_ASSOCIATION_COPY_NONATOMIC - 复制一份原对象。
  • OBJC_ASSOCIATION_COPY - 与上一个相同,区别是多线程安全。
使用关联引用的例子

在前一节介绍的分类中属性声明中,用一般的方法无法在分类的实现文件中实现属性的getter和setter方法,这里通过关联引用,实现分类中属性的getter、setter方法。

//Weapon+Display.h
@interface Weapon (Display) <Swing>

@property(strong, nonatomic) NSString* categoryDisplay;

@end

//Weapon+Display.m
@implementation Weapon (Display)

static void *key;

- (NSString *)categoryDisplay{
    return objc_getAssociatedObject(self, key);//返回key的地址关联的引用对象
}

- (void)setCategoryDisplay:(NSString *)categoryDisplay{
    objc_setAssociatedObject(self, key, categoryDisplay, OBJC_ASSOCIATION_RETAIN);//使用key的地址,为当前实例对象添加categoryDiaplay的关联引用
}

@end

通过上面的代码,我们便可以使用存取方法操作categoryDisplay属性

这里实际上删除@property(strong, nonatomic) NSString* categoryDisplay;这句代码,程序也是可以正常运行的,因为实际运行过程中,传入的变量存储的地方是与key的地址关联的。通过关联引用,模拟了categoryDisplay这个属性的getter和setter方法。

总结

本文主要从使用角度介绍了分类和扩展,理解了分类和扩展的特点,初探了runtime中关联引用的使用方式,在后面学习runtime的过程中,可以通过runtime的源码对这一部分进行更深的理解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。