玩转iOS开发:iOS开发中的装逼技术 - RunTime(三)

文章分享至我的个人技术博客:https://cainluo.github.io/15034954202343.html


我们已经来到了装逼技术学习的第三部分, 如果没有看到前面部分的朋友, 可以去看玩转iOS开发:iOS开发中的装逼技术 - RunTime(二).

这次说的是啥呢? 下面我们就来看看

转载声明:如需要转载该文章, 请联系作者, 并且注明出处, 以及不能擅自修改本文.


自动归档

我们平常撸归档的时候, 是怎么弄的呢? 看看代码:

- (id)initWithCoder:(NSCoder *)decoder {
    
    self = [super init];
    
    if (self = [super init]) {
    
        NSObject *obj = [decoder decodeObjectForKey:@"keyName"];
    }
    
    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    
    NSObject *obj = [[NSObject alloc] init];

    [coder encodeObject:obj
                 forKey:@"keyName"];
}
复制代码

看上去好像没什么问题, 但如果我们有一百个归档怎么办, 那我们就要一个一个的去写, 各一百个, 写的手软.

既然讲到这里, 那肯定有更好的方法去解决了, Runtime也可以通过几个步骤之后, 进行归档:

  • 通过class_copuIvarList方法获取当前的Model的所有成员变量.
  • 通过ivar_getName方法来获取成员变量的名称.
  • 通过KVC来读取Model的属性值, 最后就给Model的属性赋值, 就完事.

举个栗子?,新建了一个名为CoderModelModel类,详细代码为:

#import "CoderModel.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation CoderModel

- (void)encodeWithCoder:(NSCoder *)coder{
    
    unsigned int outCount = 0;
    
    Ivar *vars = class_copyIvarList([self class], &outCount);
    
    for (int i = 0; i < outCount; i ++) {
        
        Ivar var = vars[i];
        
        const char *name = ivar_getName(var);
        
        NSString *key = [NSString stringWithUTF8String:name];
        
        id value = [self valueForKey:key];
        
        [coder encodeObject:value
                     forKey:key];
    }
    
    free(vars);
}

- (nullable instancetype)initWithCoder:(NSCoder *)decoder{
    
    if (self = [super init]) {
        
        unsigned int outCount = 0;
        
        Ivar *vars = class_copyIvarList([self class], &outCount);
        
        for (int i = 0; i < outCount; i ++) {
            
            Ivar var = vars[i];
            
            const char *name = ivar_getName(var);
            
            NSString *key = [NSString stringWithUTF8String:name];
            
            id value = [decoder decodeObjectForKey:key];
            
            [self setValue:value forKey:key];
        }
        
        free(vars);
    }
    
    return self;
}

@end
复制代码
  • 耍过KVC的老铁们都知道, KVC的特性, 若能找到key的属性setter方法, 就会调用setter方法.
  • 如果找不到setter, 就会查找成员变量key或者成员变量_key
  • 所以我们在这里不需要再另外处理前缀有_前缀的成员变量名

这里我们还需要写一个转模型的Category, 自己在工程里找吧, 最终实现:

- (void)coderModel {
    
    CoderModel *coderModel = [CoderModel objectWithKeyValues:self.dictionary];
    
    NSLog(@"%@, %ld, %ld", coderModel.name, coderModel.age, coderModel.phoneNumber);
    
    NSDictionary *dictionary = [coderModel keyValuesWithObject];
    
    NSLog(@"dictionary is %@", dictionary);
}
复制代码
2017-08-23 22:27:51.349 1.RunTime[36904:3052474] 小明, -5764607523034234590, -5764607302232026877

2017-08-23 22:27:51.349 1.RunTime[36904:3052474] dictionary is {
    age = 18;
    name = "\U5c0f\U660e";
    phoneNumber = 13800138000;
}
复制代码

字典与模型互转

刚刚讲完归档之后, 就顺便把字典与模型的互转给说完吧.

我们以前给模型赋值的时候是:

- (instancetype)initWithDictionary:(NSDictionary *)dictionary {

    if (self = [super init]) {
    
        self.age  = dictionary[@"age"];
        self.name = dictionary[@"name"];
    }
    
    return self;
}
复制代码

看起来貌似没啥问题, 但实际上是和归档遇到的问题一个样, 后来用过MJMJExtension, YYModel之后, 就发现, 原来模型解析还可以有别的方式.

我们来看看流程:

  • 字典转模型的时候
    • 根据字典的key生成setter方法
    • 使用objc_msgSend调用setter方法为Model的属性赋值或者用KVC
+ (id)objectWithKeyValues:(NSDictionary *)dictionary {
    
    id objc = [[self alloc] init];
    
    for (NSString *key in dictionary.allKeys) {
        
        id value = dictionary[key];
        
        // 判断当前属性是否属于Model
        objc_property_t property = class_getProperty(self, key.UTF8String);
        
        unsigned int outCount = 0;
        
        objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
        
        objc_property_attribute_t attribute = attributeList[0];
        
        NSString *typeString = [NSString stringWithUTF8String:attribute.value];
        
        if ([typeString isEqualToString:@"@\"CoderModel\""]) {
            
            value = [self objectWithKeyValues:value];
        }
        
        // 生成setter方法,并用objc_msgSend调用
        NSString *methodName = [NSString stringWithFormat:@"set%@%@:", [key substringToIndex:1].uppercaseString, [key substringFromIndex:1]];
        
        SEL setter = sel_registerName(methodName.UTF8String);
        
        if ([objc respondsToSelector:setter]) {
            
            ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
        }
        
        free(attributeList);
    }
    
    return objc;
}
复制代码
  • 模型转字典的时候
    • 调用class_copyPropertyList方法获取当前Model的所有属性
    • 调用 property_getName 获取属性名称
    • 根据属性名称生成getter方法
    • 使用objc_msgSend调用getter方法获取属性值或者用KVC
- (NSDictionary *)keyValuesWithObject {
    
    unsigned int outCount = 0;
    
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    
    NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionary];
    
    for (int i = 0; i < outCount; i ++) {
        
        objc_property_t property = propertyList[i];
        
        //生成getter方法,并用objc_msgSend调用
        const char *propertyName = property_getName(property);
        
        SEL getter = sel_registerName(propertyName);
        
        if ([self respondsToSelector:getter]) {
            
            id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
            
            // 判断当前属性是否属于Model
            if ([value isKindOfClass:[self class]] && value) {
                
                value = [value keyValuesWithObject];
            }
            
            if (value) {
                
                NSString *key = [NSString stringWithUTF8String:propertyName];
                
                [mutableDictionary setObject:value
                                      forKey:key];
            }
        }
    }
    
    free(propertyList);
    
    return mutableDictionary;
}
复制代码

动态方法解析

说到这里基本上就已经结束了, 最后就是对前面没讲的补充.

一般我们调用了对象里不存在里的方法就会直接崩掉, 然后给一个错误信息:

unrecognized selector sent to instance 0x0bcde09abc0
复制代码

但是在崩掉之前, Runtime会给一个机会我们去进行动态解析, 流程大致是:

  • 检测selector需不需要忽略的, 比如在macOS上的开发, 有了ARC就不会理会什么retain, release这些方法了.
  • 检测target是不是为nil, Objective-C是允许对一个nil对象随便执行一个方法都不会崩掉, 所以就会被忽略掉了, 但是在Swift就会出问题了.
  • 如果上面两个都过了, 那就会去查找这个类的IMP, 先从cache里面找, 找到了就跳到对应的函数去执行.
    • 如果cache找不到就找一下方法分发表
  • 如果在分发表里找不到, 那就会去超类的分发表里去查找, 一直找到NSObject.
  • 如果还找不到就会开始进入消息转发

转发的大致过程:

1

图片是在网上的某个博文里偷过来的, 懒得画了.

  • 进入了resolveInstanceMethod:方法后, 指定是否是动态添加方法.
    • 如果返回是NO, 就会进入下一步
    • 如果返回YES, 就会通过class_addMethod函数动态的添加方法, 消息就会得到处理, 这个过程就会结束了.
  • resolveInstanceMethod:方法返回NO时, 就会进入forwardingTargetForSelector方法, 这是Runtime给我们的第二次机会, 用于指定哪个对象响应这个selector.
    • 如果返回nil, 就会进入下一步, 返回某个对象, 则会调用该对象的方法.
  • forwardingTargetForSelector:返回是nil的话, 我们就要通过methodSignatureForSelector:来指定方法签名.
    • 如果methodSignatureForSelector:返回nil, 就表示不处理, 返回方法签名, 就会进入下一步.
  • methodSignatureForSelector:方法返回签名后, 就会调用forwardInvocation:方法, 我们可以通过anInvocation对象做处理, 比如修改实现方法, 修改响应对象等等.
  • 如果到最后这里, 消息都还是没有得到响应或处理, 那么就会崩掉了.

我们来看看代码:

#import "BicycleModel.h"
#import "SportsCarModel.h"

#import <objc/runtime.h>

@implementation BicycleModel

- (void)ridingSpeed {
    
    NSLog(@"Slow Ride");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
#if 0
    return NO;
#else
    class_addMethod(self, sel, class_getMethodImplementation(self, sel_registerName("ridingSpeed")), "v@:");
    
    return [super resolveInstanceMethod:sel];
#endif
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    
#if 0
    return nil;
#else
    return [[SportsCarModel alloc] init];
#endif
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    return [anInvocation invokeWithTarget:[[SportsCarModel alloc] init]];
}
复制代码
#import "SportsCarModel.h"

@implementation SportsCarModel

- (void)rapidAcceleration {
    
    NSLog(@"High speed");
}

@end
复制代码

执行方法:

- (void)dynamicAnalysis {
    
    BicycleModel *bicycle = [[BicycleModel alloc] init];
    
    ((void (*) (id, SEL)) objc_msgSend) (bicycle, sel_registerName("rapidAcceleration"));
}
复制代码

打印出来的结果:

2017-08-24 00:09:15.238 1.RunTime[37657:3122648] Slow Ride
复制代码

工程地址

项目地址: https://github.com/CainRun/iOS-Project-Example/tree/master/RunTime/Three


最后

码字很费脑, 看官赏点饭钱可好

微信

支付宝