Customizing Existing Classes

对象定义了明确的任务,比如model化指定信息、展示可视化内容、控制流程。一个类的interface定义了和其他类的交互方式以便以完成任务。
有时候你会发现,你希望拓展现有类。Objective-C提供两种途径来拓展现有类:Categories(类别)和Class Extensions(类拓展)。

Category

如果需要给已有类添加Method,最简单的方法就是使用Category。

声明

声明类别的语法是通过@interface关键字,类似声明Class,但是没有继承关系,而是在圆括号中声明类别的名字。例如

@interface ClassName (CategoryName)
 
@end

1、可以给任何类声明类别,即使你没有源码,比如Cocoa Touch的类。
2、可以像子类一样访问所有的实例变量
3、在runtime,category和原类实现的方法没有区别
4、category通常在独立的header file 和 implemented file中,所以使用时需要导入头文件,否则编译器报错
比如所XYZPerson有很多属性,其中包括lastName和firstName,现在需要直接返回完整的姓名,那么可以通过category添加方法

#import "XYZPerson.h"
 
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end


#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
 
@implementation XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
    return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end

然后在导入XYZPerson+XYZPersonNameDisplayAdditions.h的任何类中使用

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation SomeObject
- (void)someMethod {
    XYZPerson *person = [[XYZPerson alloc] initWithFirstName:@"John"
                                                    lastName:@"Doe"];
    XYZShoutingPerson *shoutingPerson =
                        [[XYZShoutingPerson alloc] initWithFirstName:@"Monica"
                                                            lastName:@"Robinson"];
 
    NSLog(@"The two people are %@ and %@",
         [person lastNameFirstNameString], [shoutingPerson lastNameFirstNameString]);
}
@end

声明Method

Category可以声明实例方法和类方法;可以使用@property语法声明property,但是不能声明实例变量,这就意味着编译器不能自动生成实例变量,不能为property自动生成setter和getter方法。

//例如在类别中@property一个name,收到如下警告
Property 'name' requires method 'name' to be defined - use @dynamic or provide a method implementation in this category

Property 'name' requires method 'setName:' to be defined - use @dynamic or provide a method implementation in this category

尽管可以自己实现set和get方法,但是不能自己声明实例变量来保持数据(除非使用原类的实例变量)。
既然category添加的方法在runtime无差别,可以使用runtime接口来验证一下
给Data类声明一个类别,类别中添加property和Method

#import "Data.h"

@interface Data (DataCategory)

@property (nonatomic,copy)NSString *name;

+(void)classMethod;
-(void)ivarMethod;
@end


#import "Data+DataCategory.h"

@implementation Data (DataCategory)

+(void)classMethod
{
    NSLog(@"调用classMethod");
    
}

-(void)ivarMethod
{
    NSLog(@"调用ivarMethod");
}
@end

然后运行一下代码查看结果

/* 获取实例变量列表 */
unsigned int count = 0;
Ivar *list =  class_copyIvarList([Data class], &count);
for (int i = 0; i < count ; i ++)
{
   Ivar var = list[i];
   const char *name_var = ivar_getName(var);
   const char *type_var = ivar_getTypeEncoding(var);//有对照表
   ptrdiff_t offset_var = ivar_getOffset(var);//获取变量内存偏移量

   NSLog(@"Ivar Name:%@ TypeEncoding:%@ Offset:%td",[NSString stringWithUTF8String:name_var],[NSString stringWithUTF8String:type_var],offset_var);
}
free(list);
    
    
/* property列表 */
unsigned int property_count = 0;
objc_property_t *property_list =  class_copyPropertyList([Data class], &property_count);

for (int i = 0; i < property_count ; i ++)
{
   objc_property_t property = property_list[i];
   const char *name_property = property_getName(property);//名称
   const char *name_attributes = property_getAttributes(property);//属性字符串
   NSLog(@"Property:%@  attributes:%@",[NSString stringWithUTF8String:name_property],[NSString stringWithUTF8String:name_attributes]);

}
free(property_list);//必须free
    
/* 获取实例方法列表 */
unsigned int count_method = 0;
Method *method_list = class_copyMethodList([Data class], &count_method);
for (int i = 0; i < count_method; i ++)
{
   Method method = method_list[i];
   SEL sel_Method = method_getName(method);
   NSLog(@"实例方法列表:%@",NSStringFromSelector(sel_Method));

}
    
/* 获取类方法列表 */
unsigned int count_method2 = 0;
Method *method_list2 = class_copyMethodList(object_getClass([Data class]), &count_method2);
for (int i = 0; i < count_method2; i ++)
{
   Method method = method_list2[i];
   SEL sel_Method = method_getName(method);
   NSLog(@"类方法列表:%@",NSStringFromSelector(sel_Method));
}
free(method_list2);

//输出结果
Property:name  attributes:T@"NSString",C,N
实例方法列表:ivarMethod
类方法列表:classMethod

从输出结果可以验证:
1、@property语法不能生成实例变量和访问方法
2、可以正常声明实例和类方法

注意方法名称冲突

由于category中声明的方法被增加到原类的方法列表中,名称一定不能冲突。比如说,你的Method名称和原类同名,或者Category A 和Category B有相同的Method,这将导致其中一个不能正常使用,编译器同时发出警告。解决方案是像new class一样添加前缀:小写前缀+下划线_+方法名称。例如

@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end

功能

1、拓展现有类Method
2、可以把复杂类分开实现。例如NSString的UIStringDrawing类别

Class Extensions

声明

类扩展扩展类的内部实现。类扩展和Category类似,但是只能添加到开源类。类扩展中声明的方法,都在原类的@implementation代码块中实现。所以不能给SDK中的不开源类添加类拓展,即时Xcode中可以添加但是没地方实现这些方法。
声明类扩展的语法和Category类似

@interface ClassName ()
 
@end

由于圆括号中没有名字,所以被称为匿名Category。和类别不同的是,类扩展可以添加实例变量和Property

@interface XYZPerson ()
{
    id _someCustomInstanceVariable;
}
@property NSObject *extraProperty;
@end

1、编译器自动生成实例变量和访问器方法
2、类扩展方法必须在原类的implementation部分实现

隐藏私有信息

类的interface用来定义公共接口。类扩展一般用来定义私有接口和property。比如说XYZPerson有一个公共的property,但是不希望被其他对象之间修改值,所以声明为只读readonly,然后提供一个方法来进行修改值。在类内部希望能够之间修改值,所以使用类扩展然后把该property声明为可读写readwrite。例如

@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end


@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
 
@implementation XYZPerson
...
@end

提示:
1、property默认readwrite,可以不写,但是可以起到对比作用,增加可读性
2、readonly的界限可以通过dynamic runtime features来打破。比如NSObject的performSelector:系列方法,或者runtime的method_invoke()函数
解读:@property本质通过set和get方法实现。readonly即在header文件中只声明getter方法,类扩展重新声明为可读性,即在.m文件中补上了setter方法。

抽象类 和 Delegate

尽管类别和类扩展可以很方便的扩展现有类,但是有时候不是最好的选择。面向对象编程的目标之一就是写出可以重复利用的代码。比如说写一个view来展示可视化信息,
1、与其努力的关注如何布局和展示内容,不如利用继承把需要做决定的部分留给子类进行重写。此类情况尽管父类可以服用,但是每次都要创建子类才可以使用--抽象类,例如CAAnimation。
2、把需要做决定的部分交给delegate对象,其它部分代理可以服用。经典例子如UITableView。

Runtime扩展类

Objective-C通过Runtime系统展现动态性。方法调用的决定时间不是在编译器决定,实在运行时决定。可以通过Associative References和runtime直接交互达到扩展现有类的目的,与类扩展不同,他不会影响原类的声明和实现就可以给对象链接另一个对象,所以可以扩展任何类。

/** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object 目标对象
 * @param key The key for the association.
 * @param value 添加对象。 Pass nil to clear an existing association.
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
 */
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
 
/** 
 * 返回关联对象
 * 
 * @param object 目标对象
 * @param key The key for the association.
 * 
 * @return 返回对象
 */   
id objc_getAssociatedObject(id object, const void *key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

/** 
 * 移除所有关联对象
 * 
 * @param object 目标对象
 * 
 * @note 此函数作用用来还原到初始状态。若果要要移除某个关联请使用:objc_setAssociatedObject 传nil值
 * 
 */
void objc_removeAssociatedObjects(id object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

给Viewcontroller类关联一个Data的data对象,代码如下

//随意一个类
Data *data = [[Data alloc] init];
    
//关联
objc_setAssociatedObject(self, "data", data, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
id objc_data1 = objc_getAssociatedObject(self, "data");
NSLog(@"objc_data1:%@",objc_data1);
    
//移除单个
objc_setAssociatedObject(self, "data", nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
id objc_data2 = objc_getAssociatedObject(self, "data");
NSLog(@"objc_data2:%@",objc_data2);
    
//移除所有
objc_setAssociatedObject(self, "data", data, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_removeAssociatedObjects(self);
id objc_data3 = objc_getAssociatedObject(self, "data");
NSLog(@"objc_data3:%@",objc_data3);

//输出结果
objc_data1:<Data: 0x6080000047b0>
objc_data2:(null)
objc_data3:(null)

参考文献:Customizing Existing Classes

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

推荐阅读更多精彩内容