善用runtime,可以解决自动归档解档。想想以前归档是手动写的,确实太麻烦了。现在有了runtime,我们可以做到自动化了。本篇文章旨在学习如何通过runtime实现自动归档和解档,因此不会对所有类型适用,而是对我们指定的几种类型适用。
定义模型
我们这里只是写一个例子,用于学习如何用runtime实现自动归档以及解档,因此,我们需要定义一个模型类,然后在里面实现自动归档和解档。
我们这里只处理了普通的几种类型,这里只测试int
、NSString
、const void *
、NSNumber
、float
类型。对于const void *
是不支持kvc
的,因此我们是不能通过kvc
完成的,但是我们可以通过runtime发送消息实现。
声明头文件
我们首先得要定义一个类,声明一下我们要测试的类型。首先,我们必须要遵守协议NSCoding
,这是归档必须要遵守的:
@interfaceHDFArchiveModel:NSObject<NSCoding> @property(nonatomic,assign)intreferenceCount; @property(nonatomic,copy)NSString*archive; @property(nonatomic,assign)constvoid*session; @property(nonatomic,strong)NSNumber*totalCount; //注意,这里只是为了测试一下属性使用下划线的情况 @property(nonatomic,assign)float_floatValue; +(void)test; @end
实现代码
遵守了NSCoding
协议之后,我们就可以在实现文件中实现-encodeWithCoder:
方法来归档和-initWithCoder:
解档。
实现代码如下:
#import<objc/runtime.h> #import<objc/message.h> @implementationHDFArchiveModel -(void)encodeWithCoder:(NSCoder*)aCoder{ unsignedintoutCount=0; Ivar*ivars=class_copyIvarList([selfclass],&outCount); for(unsignedinti=0;i<outCount;++i){ Ivarivar=ivars[i]; //获取成员变量名 constvoid*name=ivar_getName(ivar); NSString*ivarName=[NSStringstringWithUTF8String:name]; //去掉成员变量的下划线 ivarName=[ivarNamesubstringFromIndex:1]; //获取getter方法 SELgetter=NSSelectorFromString(ivarName); if([selfrespondsToSelector:getter]){ constvoid*typeEncoding=ivar_getTypeEncoding(ivar); NSString*type=[NSStringstringWithUTF8String:typeEncoding]; //constvoid* if([typeisEqualToString:@"r^v"]){ constchar*value=((constvoid*(*)(id,SEL))(void*)objc_msgSend)((id)self,getter); NSString*utf8Value=[NSStringstringWithUTF8String:value]; [aCoderencodeObject:utf8ValueforKey:ivarName]; continue; } //int elseif([typeisEqualToString:@"i"]){ intvalue=((int(*)(id,SEL))(void*)objc_msgSend)((id)self,getter); [aCoderencodeObject:@(value)forKey:ivarName]; continue; } //float elseif([typeisEqualToString:@"f"]){ floatvalue=((float(*)(id,SEL))(void*)objc_msgSend)((id)self,getter); [aCoderencodeObject:@(value)forKey:ivarName]; continue; } idvalue=((id(*)(id,SEL))(void*)objc_msgSend)((id)self,getter); if(value!=nil&&[valuerespondsToSelector:@selector(encodeWithCoder:)]){ [aCoderencodeObject:valueforKey:ivarName]; } } } free(ivars); } -(instancetype)initWithCoder:(NSCoder*)aDecoder{ if(self=[superinit]){ unsignedintoutCount=0; Ivar*ivars=class_copyIvarList([selfclass],&outCount); for(unsignedinti=0;i<outCount;++i){ Ivarivar=ivars[i]; //获取成员变量名 constvoid*name=ivar_getName(ivar); NSString*ivarName=[NSStringstringWithUTF8String:name]; //去掉成员变量的下划线 ivarName=[ivarNamesubstringFromIndex:1]; //生成setter格式 NSString*setterName=ivarName; //那么一定是字母开头 if(![setterNamehasPrefix:@"_"]){ NSString*firstLetter=[NSStringstringWithFormat:@"%c",[setterNamecharacterAtIndex:0]]; setterName=[setterNamesubstringFromIndex:1]; setterName=[NSStringstringWithFormat:@"%@%@",firstLetter.uppercaseString,setterName]; } setterName=[NSStringstringWithFormat:@"set%@:",setterName]; //获取getter方法 SELsetter=NSSelectorFromString(setterName); if([selfrespondsToSelector:setter]){ constvoid*typeEncoding=ivar_getTypeEncoding(ivar); NSString*type=[NSStringstringWithUTF8String:typeEncoding]; NSLog(@"%@",type); //constvoid* if([typeisEqualToString:@"r^v"]){ NSString*value=[aDecoderdecodeObjectForKey:ivarName]; if(value){ ((void(*)(id,SEL,constvoid*))objc_msgSend)(self,setter,value.UTF8String); } continue; } //int elseif([typeisEqualToString:@"i"]){ NSNumber*value=[aDecoderdecodeObjectForKey:ivarName]; if(value!=nil){ ((void(*)(id,SEL,int))objc_msgSend)(self,setter,[valueintValue]); } continue; }elseif([typeisEqualToString:@"f"]){ NSNumber*value=[aDecoderdecodeObjectForKey:ivarName]; if(value!=nil){ ((void(*)(id,SEL,float))objc_msgSend)(self,setter,[valuefloatValue]); } continue; } //object idvalue=[aDecoderdecodeObjectForKey:ivarName]; if(value!=nil){ ((void(*)(id,SEL,id))objc_msgSend)(self,setter,value); } } } free(ivars); } returnself; } +(void)test{ HDFArchiveModel*archiveModel=[[HDFArchiveModelalloc]init]; archiveModel.archive=@"标哥学习自动归档"; archiveModel.session="http://www.henishuo.com"; archiveModel.totalCount=@(123); archiveModel.referenceCount=10; archiveModel._floatValue=10.0; NSString*path=NSHomeDirectory(); path=[NSStringstringWithFormat:@"%@/archive",path]; [NSKeyedArchiverarchiveRootObject:archiveModel toFile:path]; HDFArchiveModel*unarchiveModel=[NSKeyedUnarchiverunarchiveObjectWithFile:path]; } @end
自动归档解析
我们这里获取对象的成员变量,通过class_copyIvarList
可以得到所有成员变量:
unsignedintoutCount=0; Ivar*ivars=class_copyIvarList([selfclass],&outCount);
我们获取到的属性所生成的成员变量是带下划线,因此我们需要在获取到成员变量名称后,需要去掉下划线:
//获取成员变量名 constvoid*name=ivar_getName(ivar); NSString*ivarName=[NSStringstringWithUTF8String:name]; //去掉成员变量的下划线 ivarName=[ivarNamesubstringFromIndex:1];
接下来,在归档的时候我们通过属性的getter方法来获取值,然后归档。但是,成员变量是是有类型的,并不是所有类型都可以归档,比如const void *
就不支持归档,那么我们需要根据类型转换成支持归档的类型再存储。另外,我们可以通过ivar_getTypeEncoding
函数获取成员变量的类型,但是这个类型不一定是我们常见的NSString
之类的,可能会出现r^v
这样代表const void *
。这些都是runtime系统所定义的,因此它是固定的,我们只要去按照苹果给出的类型编码表就可以知道哪些字符代表什么类型了。
constvoid*typeEncoding=ivar_getTypeEncoding(ivar); NSString*type=[NSStringstringWithUTF8String:typeEncoding]; //constvoid* if([typeisEqualToString:@"r^v"]){ constchar*value=((constvoid*(*)(id,SEL))(void*)objc_msgSend)((id)self,getter); NSString*utf8Value=[NSStringstringWithUTF8String:value]; [aCoderencodeObject:utf8ValueforKey:ivarName]; continue; } //int elseif([typeisEqualToString:@"i"]){ intvalue=((int(*)(id,SEL))(void*)objc_msgSend)((id)self,getter); [aCoderencodeObject:@(value)forKey:ivarName]; continue; } //float elseif([typeisEqualToString:@"f"]){ floatvalue=((float(*)(id,SEL))(void*)objc_msgSend)((id)self,getter); [aCoderencodeObject:@(value)forKey:ivarName]; continue; } idvalue=((id(*)(id,SEL))(void*)objc_msgSend)((id)self,getter); if(value!=nil&&[valuerespondsToSelector:@selector(encodeWithCoder:)]){ [aCoderencodeObject:valueforKey:ivarName]; }
像上面,我们只额外处理了const void *
、int
、float
类型,当我们调用objc_msgSend
函数时,一定要转换类型,如果类型不匹配无法转换则是会崩溃的。从这里看到objc_msgSend
函数是不是很强大?是的,真的太强大了。一会可以是有返回值的,一会可以是带多个参数的,还可以是不带返回值的。
最后,我们就可以调用归档方法来实现了归档:
[NSKeyedArchiverarchiveRootObject:archiveModel toFile:path];
到此,我们归档的工作已经完成了。如果要写一个通用的自动归档扩展,那么我们就需要处理完苹果中所有的数据类型,包括基本类型、C指针类型等。
自动解档解析
不知道这里叫解档是否合适,但是似乎已经习惯这么叫了。要实现解档,其实就是实现NSCoding
协议中的-initWithCoder:
方法。
首先我们要生成setter
方法,但是setter
方法的生成是有规则的。若属性名称不带下划线,那么生成的setter
方法就是set
+属性名称,其中需要将属性名称的首字母变成大写。若属性名称带下划线,则不需要处理。我们看看如何生成:
//那么一定是字母开头 if(![setterNamehasPrefix:@"_"]){ NSString*firstLetter=[NSStringstringWithFormat:@"%c",[setterNamecharacterAtIndex:0]]; setterName=[setterNamesubstringFromIndex:1]; setterName=[NSStringstringWithFormat:@"%@%@",firstLetter.uppercaseString,setterName]; } setterName=[NSStringstringWithFormat:@"set%@:",setterName];
我们知道,苹果中的变量只是是字母、数字和下划线,其中第一个字符只能是字母或者是下划线。因此,上面判断第一个是否是下划线,若不是下划线,则说明一定是字母。然后我们需要获取首字母,以便转换成大写字母。当然,上面替换,我们也可以这样写:
[setterNamestringByReplacingCharactersInRange:NSMakeRange(0,0)withString:firstLetter.uppercaseString];
接下来就是判断属性的类型,然后发送消息。这里只说明设置const void *
属性值:
if([typeisEqualToString:@"r^v"]){ NSString*value=[aDecoderdecodeObjectForKey:ivarName]; if(value){ ((void(*)(id,SEL,constvoid*))objc_msgSend)(self,setter,value.UTF8String); } continue; }
我们一定要强转objc_msgSend
函数为(void (*)(id, SEL, const void *))
这样的类型,然后其中第三个参数就是我们要设置的属性的值的类型。
最后,我们就可以通过解档方法来实现解档了:
HDFArchiveModel*unarchiveModel=[NSKeyedUnarchiverunarchiveObjectWithFile:path];
源代码
小伙伴们可以到我的GITHUB下载demo看看效果:RuntimeDemo喜欢,就给个star吧!
小结
在学习通过runtime自动归档和解档的过程中,也遇到了不少问题。尤其是在生成setter方法时,由于调用了capitalizedString
方法,结果将属性名称中只有首字母大写,其它全变成了小写,导致笔者调试了好久才发现这个小细节。这再次说明了,细节决定成败!!!