iOS Runtime学习笔记 (二) - 实战应用
iOS runtime实战应用
iOS runtime 进行添加属性,并支持KVO监听
iOS 中category和runtime的AssociatedObject是两大非常重要的工具:
- category可以给既有类直接添加方法
- associateObject可以给既有类添加属性(类似成员变量)
结合这两个工具, 那么通过category添加property方法.然后结合associateObject增加关联对象,完成属性存取.
@interface UIViewController (Extension)
@property (nonatomic, copy) NSString * categoryString;
@end
@implementation UIViewController (Extension)
-(NSString *)categoryString{
return objc_getAssociatedObject(self, @selector(categoryString));
}
-(void)setCategoryString:(NSString *)categoryString{
objc_setAssociatedObject(self, @selector(categoryString), categoryString, OBJC_ASSOCIATION_COPY);
}
@end
并且这种方法也支持KVO的监听:
-(void)test{
self.categoryString = @"Runtime生成的属性";
[self addObserver:self forKeyPath:@"categoryString" options:NSKeyValueObservingOptionNew context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"<接收到通知: object:%@ keyPath:%@ change:%@>", object, keyPath,change);
}
iOS 方法交换Method Swizzling
这里是哟个呢 method swizzling劫持 UIViewController 的viewWillAppear:
方法, 周全起见,有两种情况要考虑一下(需要明确一下,它的目的是为了使用一个重写的方法替换掉原来的方法。但重写的方法可能是在父类中重写的,也可能是在子类中重写的):
- 复写的方法(overridden)并没有在目标类中实现(notimplemented),而是在其父类中实现了。
- 这个方法已经存在于目标类中(does existing the class itself)。这两种情况要区别对待。
对于第一种情况,应当先在目标类增加一个新的实现方法(override),然后将复写的方法替换为原先(的实现(original one)。 对于第二情况(在目标类重写的方法)。这时可以通过method_exchangeImplementations来完成交换.
+(void)load{
NSString *className = NSStringFromClass(self.class);
NSLog(@"classname %@", className);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//要特别注意你替换的方法到底是哪个性质的方法
// When swizzling a Instance method, use the following:
// Class class = [self class];
// When swizzling a class method, use the following:
Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
/*
我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。
*/
- (void)xxx_viewWillAppear:(BOOL)animated {
// 由于 系统在调用viewWillAppear时候 会调用到这里. 然后.调用这个以后. 继续调用xxx_viewWillAppear方法, 实际是调用系统的 viewWillAppear方法(因为两个交换过)
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
使用Runtime进行 json/dict -> model
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *job;
- (instancetype)initWithNSDictionary:(NSDictionary *)dict;
@end
@implementation Person
-(instancetype)initWithNSDictionary:(NSDictionary *)dict{
self = [super init];
if (self) {
[self processDict:dict];
}
return self;
}
-(void)processDict:(NSDictionary *)dict{
NSMutableArray *keys = [[NSMutableArray alloc] init];
unsigned int count = 0;
objc_property_t *props = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
objc_property_t prop = props[i];
const char *propCStr = property_getName(prop);
NSString *propName = [NSString stringWithCString:propCStr encoding:NSUTF8StringEncoding];
[keys addObject:propName];
}
free(props);
for (NSString *key in keys) {
if ([dict valueForKey:key]) {
[self setValue:[dict valueForKey:key] forKey:key];
}
}
}
@end
使用runtime进行简单的json -> model, 分成以下步骤:
- json string -> NSDictionary/NSArray<Dict>
- NSDictionary -> model property
主要在第二步中, 使用runtime遍历model中的所有property, 然后根据Dict中的key去设置model中的property对应的值.
还有一个比较常用的是 model -> dict, 在实际中需要自己根据实际项目情况进行调整. 这里只是最简单的model -> dict的方法,很多情况没有考虑到.
-(NSDictionary *)toDictionary{
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
Class cls = [self class];
while (cls != [NSObject class]) {
unsigned int count = 0;
objc_property_t *props = class_copyPropertyList(cls, &count);
for (int i = 0; i < count; i++) {
objc_property_t prop = props[i];
NSString *propName = [NSString stringWithUTF8String:property_getName(prop)];
if ([propName length] == 0) {
continue;
}
id value = [self valueForKey:propName];
if (value) {
[parameters setValue:value forKey:propName];
if (![NSJSONSerialization isValidJSONObject:parameters]) {
[parameters removeObjectForKey:propName];
}
}
}
if (props) {
free(props);
}
cls = class_getSuperclass(cls);
}
return [parameters copy];
}
runtime进行model <-> json/dict 有许多非常棒的框架进行了很多优化, 例如 MJExtension, YYModel. 可以参考一下他们的源码. 当然这种比较简单的json/dict <-> model方式, 我们可以自己写.
iOS Runtime 对对象进行序列化与反序列化
序列化和反序列化的目的是将对象存储到文件的方法.
- 序列化: 将数据结构/对象转化成二进制数据
- 反序列化: 将二进制数据恢复成数据结构/对象
需要实现上面的功能, 需要具有序列化能力的类必须实现NSCoding协议的两个函数:
-(void) encodeWithCoder:(NSCoder *)encoder;
-(id) initWithCoder:(NSCoder *)decoder;
下面有一个实例:
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
NSLog(@"%s",__func__);
Class cls = [self class];
while (cls != [NSObject class]) {
/*判断是自身类还是父类*/
BOOL bIsSelfClass = (cls == [self class]);
unsigned int iVarCount = 0;
unsigned int propVarCount = 0;
unsigned int sharedVarCount = 0;
Ivar *ivarList = bIsSelfClass ? class_copyIvarList([cls class], &iVarCount) : NULL;/*变量列表,含属性以及私有变量*/
objc_property_t *propList = bIsSelfClass ? NULL : class_copyPropertyList(cls, &propVarCount);/*属性列表*/
sharedVarCount = bIsSelfClass ? iVarCount : propVarCount;
for (int i = 0; i < sharedVarCount; i++) {
const char *varName = bIsSelfClass ? ivar_getName(*(ivarList + i)) : property_getName(*(propList + i));
NSString *key = [NSString stringWithUTF8String:varName];
id varValue = [aDecoder decodeObjectForKey:key];
NSArray *filters = @[@"superclass", @"description", @"debugDescription", @"hash"];
if (varValue && [filters containsObject:key] == NO) {
[self setValue:varValue forKey:key];
}
}
free(ivarList);
free(propList);
cls = class_getSuperclass(cls);
}
return self;
}
/*
这里需要特别注意的是:
编解码的范围不能仅仅是自身类的变量,还应当把除NSObject类外的所有层级父类的属性变量也进行编解码!
由此可见,这几乎是个纯体力活。而runtime在遍历变量这件事情上能为我们提供什么帮助呢?
我们可以通过runtime在运行时获取自身类的所有变量进行编解码;然后对父类进行递归,获取除NSObject外每个层级父类的属性(非私有变量),进行编解码。
*/
- (void)encodeWithCoder:(NSCoder *)coder
{
NSLog(@"%s",__func__);
Class cls = [self class];
while (cls != [NSObject class]) {
/*判断是自身类还是父类*/
BOOL bIsSelfClass = (cls == [self class]);
unsigned int iVarCount = 0;
unsigned int propVarCount = 0;
unsigned int sharedVarCount = 0;
Ivar *ivarList = bIsSelfClass ? class_copyIvarList([cls class], &iVarCount) : NULL;/*变量列表,含属性以及私有变量*/
objc_property_t *propList = bIsSelfClass ? NULL : class_copyPropertyList(cls, &propVarCount);/*属性列表*/
sharedVarCount = bIsSelfClass ? iVarCount : propVarCount;
for (int i = 0; i < sharedVarCount; i++) {
const char *varName = bIsSelfClass ? ivar_getName(*(ivarList + i)) : property_getName(*(propList + i));
NSString *key = [NSString stringWithUTF8String:varName];
/*valueForKey只能获取本类所有变量以及所有层级父类的属性,不包含任何父类的私有变量(会崩溃)*/
id varValue = [self valueForKey:key];
NSArray *filters = @[@"superclass", @"description", @"debugDescription", @"hash"];
if (varValue && [filters containsObject:key] == NO) {
[coder encodeObject:varValue forKey:key];
}
}
free(ivarList);
free(propList);
cls = class_getSuperclass(cls);
}
}
相关Demo: https://github.com/brownfeng/RuntimeDemo.git
参考资料
http://www.cocoachina.com/ios/20170424/19102.html
https://www.jianshu.com/p/07b6c4a40a90
https://www.jianshu.com/p/fed1dcb1ac9f
https://www.jianshu.com/p/0497afdad36d