runtime的理解(二)

主要内容

  • 利用 runtime 交换方法
  • 利用 runtime 动态添加方法
  • 利用 runtime 动态添加属性
  • 利用 runtime 字典转模型
  • KVO 的实现
  • JSPatch 的实现
  • 实现 NSCoding 的自动归档和自动解档

交换方法

*  *交换自定义方法 ditImangeNamed: 和系统方法 imageNamed: 的implementation。可以在不大量修改项目代码的情况下,添加所需的功能。注:ditImangeNamed: 的方法实现里不要再掉用 imageNamed: 方法,否则会引起循环引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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的理解(一) 的消息转发,动态解析部分,提供新的方法实现来接收消息并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (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 : 策略,属性以什么形式保存。有以下几种:
    1
    2
    3
    4
    5
    6
    7
    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 属性为例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 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语言字符串。

代码示例

*  *带二级转换的简单的字典转模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* 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:也会被调用。示例代码如下:

1
2
3
4
5
6
- (void)setName:(NSString *)newName
{
[self willChangeValueForKey:@"name"]; // KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; // 调用父类的存取方法
[self didChangeValueForKey:@"name"]; // KVO 在调用存取方法之后总调用
}

JSPatch的实现

JSPatch 是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。

实现NSCoding的自动归档和自动解档

*  *原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。核心方法:在Model的基类中重写方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init])
{
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++)
{
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++)
{
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}