美文网首页iOS相关
runtime:一句代码实现对象NSCoding

runtime:一句代码实现对象NSCoding

作者: 上发条的树 | 来源:发表于2017-12-14 20:39 被阅读2次

    参考

    背景介绍

    我们知道,一个自定义对象要归档的时候,需要让对象实现NSCoding协议,对象内的每一个归档属性做一些encode和decode操作。归档的对象需要根据自身的属性写代码,耦合性太高。而且归档的属性一多,那写起来可就痛苦多了。
    实际上,MJExtension框架上已经有了很好的解决方式:使用运行时的方式,获取对象内的所有属性,遍历归档,也可以设置排除掉不需要归档的属性(设置归档属性白名单/黑名单)。还可以将其抽取成一句宏,使用的时候就可以一句代码搞定!方便又解耦

    常规的对象归档

    这里为了文章的完整性,提供一下常规的对象归档做法。可以看到,需要实现NSCoding协议的两个方法initWithCoderencodeWithCoder,在其中对属性逐个进行encodedecode操作。代码如下:

    WxxAccount.h

    #import <Foundation/Foundation.h>
    
    @interface WxxAccount : NSObject<NSCoding>
    
    @property(nonatomic,assign)NSInteger CAR_ID;    //车辆ID
    @property(nonatomic,assign) NSInteger UID;      //用户ID
    @property(nonatomic,assign)NSInteger is_driver; //是否是司机 0否 1是
    @property(nonatomic,copy)NSString *USER_NAME;   //用户手机号
    @property(nonatomic,copy)NSString *session_id;  //session
    @property(nonatomic,copy)NSString *nickName;    //昵称
    @property(nonatomic,copy)NSString *PICURL;      //头像
    
    @end
    

    WxxAccount.m

    -(instancetype)initWithCoder:(NSCoder *)aDecoder{
        if(self == [super init]){
            self.CAR_ID = [aDecoder decodeIntegerForKey:
                           @"CAR_ID"];
            self.UID = [aDecoder decodeIntegerForKey:@"UID"];
            self.is_driver = [aDecoder decodeIntegerForKey:@"is_driver"];
            self.USER_NAME = [aDecoder decodeObjectForKey:@"USER_NAME"];
            self.session_id = [aDecoder decodeObjectForKey:@"session_id"];
            self.nickName = [aDecoder decodeObjectForKey:@"nickName"];
            self.PICURL = [aDecoder decodeObjectForKey:@"PICURL"];
        }
        return self;
    }
    
    -(void) encodeWithCoder:(NSCoder *)aCoder{
        [aCoder encodeInteger: self.CAR_ID forKey:@"CAR_ID"];
        [aCoder encodeInteger: self.UID forKey:@"UID"];
        [aCoder encodeInteger: self.is_driver forKey:@"is_driver"];
        [aCoder encodeObject:self.USER_NAME forKey:@"USER_NAME"];
        [aCoder encodeObject:self.session_id forKey:@"session_id"];
        [aCoder encodeObject:self.nickName forKey:@"nickName"];
        [aCoder encodeObject:self.PICURL forKey:@"PICURL"];
    }
    

    获取对象的所有属性

    运行时真是一个神奇的存在,具体使用可以参考文章开头第一个链接。下面是获取WxxAccount对象内所有属性的方式:

    #import "WxxAccount.h"
    #import <objc/runtime.h>
    
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([WxxAccount class], &count);
        //遍历获取所有属性
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);
            const char *type = ivar_getTypeEncoding(ivar);
            NSLog(@"成员变量:%s ------- 成员变量类型:%s",name,type);
        }
        //释放内存
        free(ivars);
    

    运行结果:

    成员变量:_CAR_ID ------- 成员变量类型:q
    成员变量:_UID ------- 成员变量类型:q
    成员变量:_is_driver ------- 成员变量类型:q
    成员变量:_USER_NAME ------- 成员变量类型:@"NSString"
    成员变量:_session_id ------- 成员变量类型:@"NSString"
    成员变量:_nickName ------- 成员变量类型:@"NSString"
    成员变量:_PICURL ------- 成员变量类型:@"NSString"

    遍历对象归档

    创建NSObject的分类NSObject+WXXCodingdecodeencode换了方法名,才不会覆盖系统原来的方法,具体实现:
    NSObject+WXXCoding.h

    #import <Foundation/Foundation.h>
    
    @interface NSObject (WXXCoding)
    
    -(NSArray*)ignoredProperties;//不需要归档的属性,在具体的主类中实现
    -(void)wxx_decode:(NSCoder*)aDecoder;
    -(void)wxx_encode:(NSCoder*)aCoder;
    
    @end
    

    NSObject+WXXCoding.m

    #import "NSObject+WXXCoding.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (WXXCoding)
    
    -(void)wxx_decode:(NSCoder*)aDecoder{
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        //遍历获取所有属性
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            //有该方法实现,再排除无需归档的属性
            if ([self respondsToSelector:@selector(ignoredProperties)]) {
                //continue 是因为需要释放ivars
                if ([[self ignoredProperties]containsObject:key]) continue;
            }
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        //释放内存
        free(ivars);
    }
    
    -(void)wxx_encode:(NSCoder*)aCoder{
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            if ([self respondsToSelector:@selector(ignoredProperties)]) {
                if ([[self ignoredProperties]containsObject:key]) continue;
            }
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
    }
    
    @end
    

    此时,WxxAccount的写法就变成这样了:

    WXXAcount.h

    不变
    

    WXXAcount.m

    #import "WxxAccount.h"
    #import "NSObject+WXXCoding.h"
    
    @implementation WxxAccount
    
    //假设PICURL这个属性不需要归档,注意加上下划线,因为系统自动生成订单实例变量会默认加下划线的。
    -(NSArray*)ignoredProperties{
        return @[@"_PICURL"];
    }
    
    -(instancetype)initWithCoder:(NSCoder *)aDecoder{
        if(self == [super init]){
            [self wxx_decode:aDecoder];
        }
        return self;
    }
    
    -(void) encodeWithCoder:(NSCoder *)aCoder{
        [self wxx_encode:aCoder];
    }
    
    @end
    

    是不是简洁不少!可是每次还是需要实现NSCoding协议的两个方法,但是对于需要归档的对象来说,initWithCoderencodeWithCoder的代码是重复的,因此我们可以把这两个方法抽取成宏WXXCodingImplementation,放到NSObject+WXXCoding.h

    #import <Foundation/Foundation.h>
    
    @interface NSObject (WXXCoding)
    
    -(NSArray*)ignoredProperties;//不需要归档的属性,在具体的主类中实现
    -(void)wxx_decode:(NSCoder*)aDecoder;
    -(void)wxx_encode:(NSCoder*)aCoder;
    
    #define WXXCodingImplementation \
    -(instancetype)initWithCoder:(NSCoder *)aDecoder{\
    if(self == [super init]){\
    [self wxx_decode:aDecoder];\
    }\
    return self;\
    }\
    \
    -(void) encodeWithCoder:(NSCoder *)aCoder{\
    [self wxx_encode:aCoder];\
    }
    
    @end
    

    此时,WXXAcount中的写法就相当简介了:
    WXXAcount.h

    不变
    

    WXXAcount.m

    #import "WxxAccount.h"
    #import "NSObject+WXXCoding.h"
    
    @implementation WxxAccount
    
    WXXCodingImplementation
    
    //假设PICURL这个属性不需要归档,注意加上下划线,因为系统自动生成订单实例变量会默认加下划线的。
    -(NSArray*)ignoredProperties{
        return @[@"_PICURL"];
    }
    
    @end
    

    关于继承,父类属性也要归档

    当然,在我们的例子中,WXXAcount直接继承自NSObject,如果WXXAcount间接继承自NSObject,也就是XXX存放公共的属性,WXXAcount继承自XXXXXX继承自NSObject。那么我们的例子就不适用了,因为我们只针对当前类做属性遍历,而忽略了它的父类XXX,甚至更多层的继承。
    具体的做法,应该是在遍历的时候,加一个循环,如果是NSObject就终止循环。

    修改了NSObject+WXXCoding.m中的代码:

    #import "NSObject+WXXCoding.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (WXXCoding)
    
    -(void)wxx_decode:(NSCoder*)aDecoder{
        Class clazz = [self class];
        //循环遍历父类,知道父类为NSObject才停止,此处并不完美,具体看总结。
        while (clazz && clazz != [NSObject class]) {
            unsigned int count = 0;
            Ivar *ivars = class_copyIvarList(clazz, &count);
            //遍历获取所有属性
            for (int i = 0; i < count; i++) {
                Ivar ivar = ivars[i];
                NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                //有该方法实现,再排除无需归档的属性
                if ([self respondsToSelector:@selector(ignoredProperties)]) {
                    //continue 是因为需要释放ivars
                    if ([[self ignoredProperties]containsObject:key]) continue;
                }
                id value = [aDecoder decodeObjectForKey:key];
                [self setValue:value forKey:key];
            }
            //释放内存
            free(ivars);
            clazz = [clazz superclass];
        }
    
    }
    
    -(void)wxx_encode:(NSCoder*)aCoder{
        Class clazz = [self class];
        while (clazz && clazz != [NSObject class]) {
            unsigned int count = 0;
            Ivar *ivars = class_copyIvarList(clazz, &count);
            for (int i = 0; i < count; i++) {
                Ivar ivar = ivars[i];
                NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                if ([self respondsToSelector:@selector(ignoredProperties)]) {
                    if ([[self ignoredProperties]containsObject:key]) continue;
                }
                id value = [self valueForKey:key];
                [aCoder encodeObject:value forKey:key];
            }
            free(ivars);
            clazz = [clazz superclass];
        }
        
    }
    
    @end
    

    总结

    当然,我们可以发现MJExtension中:

    • 黑白名单的属性不需要加下划线,因为多了很多封装,属性也是重新封装过的。
    • 背白名单的设置方法,对外提供了两种方式。
    • 对于父类的遍历,也是不止对NSObject做判断,还有NSManagedObject,甚至:NSURLNSDateNSValueNSDataNSErrorNSArrayNSDictionaryNSStringNSAttributedString

    所以,我们的封装,只是简单的封装,简单的使用还是可以的,实际使用的话当然是MJExtension比较全面!所以对于MJExtension的其他的具体细节,下一篇我们继续吧。

    相关文章

      网友评论

        本文标题:runtime:一句代码实现对象NSCoding

        本文链接:https://www.haomeiwen.com/subject/uuvhwxtx.html