一、前言
日常开发中,用runtime的在分类中添加属性可以说是最常见的操作了。但是往往越常用的东西,你约容易忽视一些细节。
关于category关联对象的技术干货网上比比皆是,我这里也给大家推荐一篇:iOS底层原理总结 - 关联对象实现原理
这篇文章只是把相关的知识点列出来,或者说是容易疏忽的地方,给大家分享一下。
二、正文
1 protocol 和 category 中使用@property的区别?
1.1 protocol中添加属性
平时我们常用的是在协议中添加方法进行反向传指等灯,但是很少见到在协议中添加属性。
首先明确一点,协议中是可以添加属性的,网上很多错误的资料可能会误导大家说“协议中不能定义属性和成员变量,只能添加方法”,这是不对的。
在protocol中使用@property只会生成setter/getter的方法声明,在实现协议的类中要加入协议中property的实现:
1 自动实现:@synthesize name; 手动实现:-(void)setName: 和 -(NSString *)name
但是在协议中定义属性时,涉及到另外一个知识点:autosynthesis(自动合成)
例:声明一个协议,并在协议中定义一个属性name
自动实现:在ViewController的@implementation中添加@synthesize name;即可。
#import "ViewController.h"
@protocol testDelegate <NSObject>
@property (nonatomic, strong) NSString *name;
@end
@interface ViewController () <testDelegate>
@end
@implementation ViewController
@synthesize name; // 自动实现
@end
2 手动实现:这里涉及到自动合成成员变量,首先明确一下什么情况下系统不会autosynthesis(自动合成)
1. 同时重写了 setter 和 getter 时
2. 重写了只读属性的 getter 时
3. 使用了 @dynamic 时
4. 在 @protocol 中定义的所有属性
5. 在 category 中定义的所有属性
6. 重载的属性
在这里因为手动实现时要同时重写setter和getter方法,所以我们要自己声明成员变量@synthesize name
#import "ViewController.h"
@protocol testDelegate <NSObject>
@property (nonatomic, strong) NSString *name;
@end
@interface ViewController () <testDelegate>
@end
@implementation ViewController
@synthesize name = _name;
- (void)setName:(NSString *)name {
_name = name;
}
- (NSString *)name {
return _name;
}
@end
否则系统会报错:Use of undeclared identifier '_name'
1.2 category中添加属性
在分类中添加属性同样也有两种方法:1 使用静态全局变量 2 动态关联对象
例:给Baby类扩展一个字符串属性idz
#import "Baby.h"
@interface Baby (Extension)
@property (nonatomic, copy) NSString *idz;
@end
方法1:使用静态全局变量
#import <objc/runtime.h>
static NSString *_idz;
@implementation Baby (Extension)
- (NSString *)idz {
return _idz;
}
-(void)setIdz:(NSString *)idz {
_idz = [idz copy];
}
这样_idz静态全局变量与类并没有关联,无论对象创建与销毁,只要程序在运行_idz变量就存在,并不是真正意义上的属性。
方法2:runtime动态关联对象
#import <objc/runtime.h>
@implementation Baby (Extension)
- (NSString *)idz {
NSString *idz = objc_getAssociatedObject(self, @selector(idz));
return idz;
}
-(void)setIdz:(NSString *)idz {
objc_setAssociatedObject(self, @selector(idz), idz, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
2 key关键字的写法
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
id *key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
id value : 关联的值,也就是set方法传入的值给属性去保存。
objc_AssociationPolicy policy : 策略,属性以什么形式保存。
其他三个参数并没有太多需要介绍的,除了策略需要了解一下以外,我们往往会忽视key值的写法。
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};
其实在关联对象时,key关键字的写法有很多种:
1 static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
2 static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
3 使用属性名作为key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
4 使用get方法的@selecor作为key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
我个人是推荐使用最后一种(getter名)的方式,因为这样你就不用多声明一个变量名,且相对安全。这里给大家例举多种写法是为了在阅读别人代码时不至于看不明白。
3 关联对象的移除
这本不是什么难点,只是大家往往会忽视的一个知识点,关联对象的移除有两种:
// 1 移除所有关联对象
objc_removeAssociatedObjects(self);
// 2 移除某个已有的关联对象
objc_setAssociatedObject 传入 nil 则可以移除已有的关联对象
我们一般不会采用第一种方法去移除所有的关联对象,这样做会造成一些潜在的隐患,尤其是你在维护别人的代码的时候,除非你非常的自信,否则并不建议这样做。
所以一般的做法是通过给 objc_setAssociatedObject 函数传入 nil
来移除某个已有的关联对象。
4 关联对象的原理
这个知识点,大多数人都是只会使用,但是从来没有了解过其中的原理,因为runtime的源码是开源的,所以感兴趣的同学可以去翻看一下源码,了解一下对象和key、policy和value之间到底是怎么联系在一起的?在分类中添加的属性又存放在什么地方?通过runtime获取类中属性的时候是否可以获取到?获取类中方法的时候时候可以找到属性的setter和getter方法?
上面列举的这类问题,都可以提升你对关联对象这个知识点的理解。
这里给大家分享两张图片,如果你去学习了关联对象的原理后,这两张图片会帮助你理解和记忆