runtime的理解(二)
主要内容
- 利用 runtime 交换方法
- 利用 runtime 动态添加方法
- 利用 runtime 动态添加属性
- 利用 runtime 字典转模型
交换方法
交换自定义方法 ditImangeNamed: 和系统方法 imageNamed: 的implementation。可以在不大量修改项目代码的情况下,添加所需的功能。注:ditImangeNamed: 的方法实现里不要再掉用 imageNamed: 方法,否则会引起循环引用。
@implementation UIImage (DITImage)
+ (nullable UIImage *)ditImangeNamed:(NSString *)name
{
UIImage *image = [UIImage ditImangeNamed:name];
if (!image)
{
NSLog(@"资源图片不存在");
}
return image;
}
+ (void)load
{
Method imageNamedMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method ditImageNamedMethod = class_getClassMethod([UIImage class], @selector(ditImangeNamed:));
method_exchangeImplementations(imageNamedMethod, ditImageNamedMethod);
}
@end
动态添加方法
即是 runtime的理解(一) 的消息转发,动态解析部分,提供新的方法实现来接收消息并处理。
- (void)testDynamicMethod
{
[self performSelector:@selector(justTest:) withObject:@"testStr"];
// [DITRuntimeViewController performSelector:@selector(justClassTest:) withObject:@"classTestStr"];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(justTest:))
{
class_addMethod([self class], sel, (IMP)justTestMethod, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void justTestMethod(id self, SEL _cmd, id str) {
NSLog(@"%@的%@方法动态实现了,参数为:%@", self, NSStringFromSelector(_cmd), str);
}
动态添加属性
objc_setAssociatedObject()
方法全称: objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
给要添加的属性添加关联,参数分别表示如下:
- id object : 给哪个对象添加属性。
- void * key : 属性名,作为被关联属性对象的标示。
- id value : 被关联的属性对象。
- objc_AssociationPolicy policy : 策略,属性以什么形式保存。有以下几种:
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 // 指定相关的对象被复制,原子性
};
objc_getAssociatedObject()
方法全称: objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
,用来通过 key 值获取关联对象,参数分别表示如下:
- id object : 获取哪个对象里面的关联的属性。
- void * id key : 属性名,被关联对象的标示。
代码示例
以给 UITextView 的分类添加 placeholder 属性为例,代码如下:
/* UITextView+DITPlaceholder.h */
@interface UITextView (DITPlaceholder)
@property (nonatomic, copy) NSString *placeholder;
@property (nonatomic, weak) UILabel *placeholderLabel;
@end
/* UITextView+DITPlaceholder.m */
- (UILabel *)placeholderLabel
{
UILabel *tempLabel = objc_getAssociatedObject(self, @"placeholderLabel");
if (!tempLabel)
{
tempLabel = [[UILabel alloc] init];
tempLabel.font = self.font;
tempLabel.textColor = [UIColor grayColor];
tempLabel.textAlignment = NSTextAlignmentLeft;
tempLabel.numberOfLines = 0;
[self addSubview:tempLabel];
objc_setAssociatedObject(self, @"placeholderLabel", tempLabel, OBJC_ASSOCIATION_RETAIN);
}
return tempLabel;
}
字典转模型
class_copyIvarList()
方法 class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount)
参数释义:
- __unsafe_unretained Class cls: 获取哪个类的成员属性列表。
- unsigned int * outCount : 无符号int型指针,传入一个无符号整型变量的地址,传出来的值为成员属性总数。
- (返回值:Ivar * ) : 返回的是一个Ivar类型的指针 。指针默认指向的是数组的第0个元素,指针+1会向高地址移动一个Ivar单位的字节,也就是指向第一个元素。Ivar表示成员属性。
ivar_getName()
方法 ivar_getName(Ivar _Nonnull v)
通过上个方法获得的成员属性 Ivar ,传入此方法,获得成员属性名的C语言字符串。
代码示例
带二级转换的简单的字典转模型。
/* DITRuntimeViewController.m */
#pragma mark - 字典转模型
- (void)testModelWithDict
{
DITForwardingTest *forwardingTestModel = [DITForwardingTest modelWithKeyValues:@{
@"testName": @"testName123",
@"subModel": @{@"testSubName": @"testSubName123"}
}];
NSLog(@"%@", forwardingTestModel.mj_keyValues);
}
/* NSObject+DITKeyValues.m */
+ (instancetype)modelWithKeyValues:(NSDictionary *)dict
{
id obj = [[self alloc] init];
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++)
{
Ivar ivar = ivarList[i];
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
NSString *key = [propertyName substringFromIndex:1];
NSString *value = dict[key];
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; // @"@\"NSString\""
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"])
{
// 二级字典转模型
propertyType = [propertyType substringWithRange:NSMakeRange(2, propertyType.length-3)];
Class modelClass = NSClassFromString(propertyType);
value = [modelClass modelWithKeyValues:(NSDictionary *)value];
}
if (value)
{
[obj setValue:value forKey:key];
}
}
return obj;
}
/* DITSubModel.h */
@interface DITSubModel : NSObject
@property (nonatomic, copy) NSString *testSubName;
@end
/* DITForwardingTest.h */
@interface DITForwardingTest : NSObject
@property (nonatomic, copy) NSString *testName;
@property (nonatomic, strong) DITSubModel *subModel;
@end
KVO实现
KVO 的实现用到了 runtime 的方法IMP替换,当观察对象 Person 时,KVO机制动态创建一个名为:NSKVONotifying_ Person 的新类,该类继承自对象 Person 的本类,且 KVO 为 NSKVONotifying_ Person 重写要观察属性的 setter 方法,setter 方法会负责在实现 setter 之前和之后,通知所有观察对象属性值的更改情况。
KVO 的键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangeValueForKey: ,在存取数值的前后分别调用这 2 个方法:被观察属性发生改变之前, willChangeValueForKey: 被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context:也会被调用。代码如下:
- (void)setName:(NSString *)newName
{
[self willChangeValueForKey:@"name"]; // KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; // 调用父类的存取方法
[self didChangeValueForKey:@"name"]; // KVO 在调用存取方法之后总调用
}