目录
一、分类里不能添加成员变量,但能添加属性
二、给分类里的属性绑定关联对象
本篇主要讲解runtime的实际应用场景:给分类里的属性绑定关联对象。是对成员变量和属性应用的一个例子。
一、分类里不能添加成员变量,但能添加属性
我们知道,分类一般是用来给一个类(无论是系统的类、三方的类还是自己写的类)扩展方法和扩展遵守协议用的,但是不能给类扩展属性。
其实准确的说,分类不能给类扩展成员变量,但可以给类扩展属性,只不过编译器没有为扩展的属性自动生成与之相关的成员变量,也没有自动生成相应setter
、getter
方法的实现,但是自动生成了setter
、getter
方法的声明。(和@dynamic
的功能比较像是吧)
我们举例来验证一下上面这个说法:
- 分类不能给类扩展成员变量
我们在Person
的Category
分类里添加一个成员变量_name
,结果编译器直接报错Instance variables may not be placed in categories
。

可以得出结论:分类里不能添加成员变量。
- 分类能给类扩展属性
然后我们在Person
的Category
分类里添加一个属性name
,发现编译器并没有报错,仅仅是给了一个警告Property 'name' requires method 'name' and 'setName:' to be defined - use @dynamic or provide a method implementation in this category
。


可以得出结论:分类里能添加属性,但是没有自动生成相应
setter
、getter
方法的实现,编译器提示我们要自己实现。
此时,我们打印一下Person
类的成员变量列表、属性列表、实例方法列表。如下:
- (void)viewDidLoad {
[super viewDidLoad];
// 获取Person类的成员变量列表
NSLog(@"===========Person类的成员变量列表===========");
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &ivarCount);
for (int i = 0; i < ivarCount; i ++) {
Ivar ivar = ivars[I];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
NSLog(@"%@", ivarName);
}
free(ivars);
// 获取Person类的属性列表
NSLog(@"===========Person类的属性列表===========");
unsigned int propertyCount = 0;
objc_property_t *properties = class_copyPropertyList([Person class], &propertyCount);
for (int i = 0; i < propertyCount; i ++) {
objc_property_t property = properties[I];
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
NSLog(@"%@", propertyName);
}
free(properties);
// 获取Person类的实例方法列表
NSLog(@"===========Person类的实例方法列表===========");
unsigned int methodCount = 0;
Method *methods = class_copyMethodList([Person class], &methodCount);
for (int i = 0; i < methodCount; i ++) {
Method method = methods[I];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"%@", methodName);
}
free(methods);
}
打印结果:
2018-10-09 14:52:26.883166+0800 Test[3029:468752] ===========Person类的成员变量列表===========
2018-10-09 14:52:26.883317+0800 Test[3029:468752] ===========Person类的属性列表===========
2018-10-09 14:52:26.883400+0800 Test[3029:468752] name
2018-10-09 14:52:26.883477+0800 Test[3029:468752] ===========Person类的实例方法列表===========
发现Person
类的成员变量列表为空,属性列表中有一个name
,实例方法列表也为空(一个类的实例方法只有实现了才会被放入它的methodLists
中,只声明是不会被放进去的)。
可以得出结论:分类里能添加属性,但是编译器没有为添加的属性自动生成与之相关的成员变量,也没有自动生成相应
setter
、getter
方法的实现。
接着,我们去Person+Category.m
文件中试图自己实现一下name
的setter
、getter
方法,发现方法名敲了一半它们竟然自动弹出来了,这就说明name
的setter
和getter
方法是被声明了的。


可以得出结论:分类里能添加属性,但是编译器没有为添加的属性自动生成与之相关的成员变量,也没有自动生成相应
setter
、getter
方法的实现,但是自动生成了setter
和getter
方法的声明。
- 那为什么分类里不能添加成员变量,但能添加属性?
我们看下runtime库对分类(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;
}
可见分类结构体内部并没有用来存储扩展的成员变量的成员变量,只有用来扩展方法和遵守的协议的成员变量,所以分类不能添加成员变量。但是分类内部也没看到什么成员变量能用来存储属性啊,那为什么分类里能添加属性呢?
下载runtime的源码,找到objc-runtime-new.h
文件,搜索category_t
,我们可以看到分类一个更底层的实现:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
// 分类扩展的属性列表
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
可以看到分类内部其实是有一个成员变量instanceProperties
用来存储扩展的属性。
二、给分类里的属性绑定关联对象
- 为什么要给分类里的属性动态绑定关联对象?
实际开发中,我们确实会遇到一些场景需要给分类添加属性,但是上面我们也知道了虽然分类里能添加属性,但是编译器没有为添加的属性自动生成与之相关的成员变量,也没有自动生成相应setter
、getter
方法的实现,但是自动生成了setter
和getter
方法的声明,所以如果我们给分类添加了属性就去访问的话,程序是会崩掉的,因为找不到setter
、getter
方法的实现。
例如:
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [Person new];
person.name = @"11";
NSLog(@"%@", person.name);
}
崩溃信息:
2018-10-10 13:40:06.250510+0800 Test[1190:228046] -[Person setName:]: unrecognized selector sent to instance 0x600001dbef20
于是我们按照上面提到的编译器报的警告Property 'name' requires method 'name' and 'setName:' to be defined - use @dynamic or provide a method implementation in this category。
考虑自己实现一下setter、getter方法:
#import "Person+Category.h"
@implementation Person (Category)
- (void)setName:(NSString *)name {
}
- (NSString *)name {
}
@end
但是当我们实现这两个方法的时候才发现根本无从实现啊,因为编译器根本没有为我们自动生成与属性相关的成员变量啊,方法的实现怎么写嘛。这个时候关联对象就派上用场了,我们可以用它来代替成员变量绑定到指定的属性上,借它来存取数据。
- 什么是关联对象?
关联对象(Associated Object)和成员变量其实没什么本质上的区别,都是一个对象,可以用来存取数据。但是成员变量是直接存在于类的ivars
中,而关联对象则存在于一个单独的哈希表中;成员变量在编译时就创建好,而关联对象则在运行时调用setter
、getter
方法的时候才创建的;它没那么神秘,因此我们可以用它来代替成员变量绑定到指定的属性上,借它来存取数据。
- 怎么给分类的属性动态绑定关联对象
很简单,就用到runtime的两个方法而已:
#import "Person+Category.h"
#import <objc/runtime.h>
@implementation Person (Category)
- (void)setName:(NSString *)name {
// object:想要把关联对象动态绑定到哪个对象的属性上,一般填self
// key:想要把关联对象动态绑定到哪个属性上,此处为@"name"
// value:关联对象,调用setter方法时才动态生成的一个对象,此处为name
// policy:关联对象的内存管理策略
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
// object,key:想要获取哪个对象的哪个属性动态绑定的关联对象
return objc_getAssociatedObject(self, @"name");
}
@end
好了,只要我们给分类的属性绑定了关联对象,就可以向平常的属性那样访问它们了。此外,如果我们给分类添加的属性不想让外界看到,也可以把属性添加在分类的延展中:
-----------Person+Category.m-----------
#import "Person+Category.h"
#import <objc/runtime.h>
@interface Person ()
@property (nonatomic, assign) NSInteger age;
@end
@implementation Person (Category)
@end
网友评论