利用runtime为setter方法添加存储到本地功能
近来换了一家离家很近的公司工作,接手了一个老项目,独立进行二次开发。
项目中存在许多用户信息,且时常需要更新存储在本地,方便二次访问。
我的上一任是在每一次对其赋值后,使用userdefaults进行存储,没有封装,没有重写setter,直接在后面写上[NSUserD....],典型copy党···
我感觉我的膝盖中了一箭。
问题:
如何将已成型的类的属性更方便快捷的存储到本地?
解决方案分析:
1.重写setter方法,在每一个方法中都存储到本地:
- (void)setName:(NSString *)name
{
_name = name;
[[NSUserDefaults standardUserDefaults] setObject:name forKey:@"name"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
工作量大,代码冗余度高。
2.写一个方法对用户数据类进行统一存储到本地操作
- (void)savaUserData
{
[[NSUserDefaults standardUserDefaults] setObject:_name forKey:@"name"];
[[NSUserDefaults standardUserDefaults] setObject:_password forKey:@"password"];
......
......
[[NSUserDefaults standardUserDefaults] synchronize];
}
工作量小,但只更改一个属性也需要进行整体存储,效率低。
3. 无视之~~
是虽然不是处女~~座,但是这尼玛能忍!!?
4.运用运行时直接修改其setter,为其添加存储本地功能
可以试试~~
懒,又追求效率,SO选择了方案4! 果然懒才是程序猿的第一生产力啊。`
实践
既然方案选择好了,Just do it。
步骤1: 书写通用new_setter方法
setter方法的本质是用属性的新值去替换掉旧值。
setter方法在C层面是一个带三个参数的函数
static void new_setter(id self, SEL _cmd, id newValue) OC类专用
static void new_setter(id self, SEL _cmd, long long newValue) 基本类型使用
self是实例本身。
_cmd是方法对应的SEL
newValue顾名思义。
1.1 得到类型中对应属性的相关信息
由于实际项目中可以会不适用系统自动生成setter和getter方法自定义,则需要做一个通用的方法来获得对应的setter方法名m,在demo中我适用了一个类存储需要的相关信息,便于拓展
objc_property_t * propertys = class_copyPropertyList(classs, &count);
WKClassPropertyModel * model = [self new];
model.name = [NSString stringWithUTF8String:property_getName(property)];
NSString * attrStr = [NSString stringWithFormat:@"%@",[NSString stringWithUTF8String:property_getAttributes(property)]];
NSArray * attrs = [attrStr componentsSeparatedByString:@","];
for (NSString * str in attrs) {
if([str hasPrefix:@"T"])//类型
{
model.type = [str substringFromIndex:1];
}
if([str hasPrefix:@"S"])//自定义setter
{
model.setterName = [str substringFromIndex:1];
}
if([str hasPrefix:@"G"])//自定义getter
{
model.getterName = [str substringFromIndex:1];
}
if([str hasPrefix:@"V"])//属性转换的变量名
{
model.varName = [str substringFromIndex:1];
}
}
if (!model.setterName) {
NSString * header = [[model.name substringToIndex:1] uppercaseString];
NSString * footer = [model.name substringFromIndex:1];
model.setterName = [NSString stringWithFormat:@"set%@%@:",header,footer];
}
if (!model.getterName) {
model.getterName = model.name;
}
return model;
1.2 遍历成员变量列表,替换成员变量值
//得到变量列表
Ivar * members = class_copyIvarList([self class], &count);
int index = -1;
//遍历变量
for (int i = 0 ; i < count; i++) {
Ivar var = members[i];
//获得变量名
const char *memberName = ivar_getName(var);
//生成string
NSString * memberNameStr = [NSString stringWithUTF8String:memberName];
if ([varName isEqualToString:memberNameStr]) {
index = i;
break ;
}
}
//变量存在则赋值
if (index > -1) {
Ivar member= members[index];
object_setIvar(self, member, newValue);
}
1.3 存储到本地——任意自由发挥阶段
[[NSUserDefaults standardUserDefaults] setObject:newValue forKey:getterName];
[[NSUserDefaults standardUserDefaults ]synchronize];
步骤2: 替换setter方法
unsigned int count = 0;
NSArray <WKClassPropertyModel *> * arr = [WKClassPropertyManager getClassPropertysWithClass:[self class]];
//获得方法列表
Method * a = class_copyMethodList([self class], &count);
//遍历方法列表
for (unsigned int i = 0; i < count; i ++) {
NSString * methodName = NSStringFromSelector(method_getName(a[i]));
for (WKClassPropertyModel * model in arr) {
if ([model.setterName isEqualToString:methodName]) {
if ([model.type containsString:@"@"])
{
method_setImplementation(a[i], (IMP)new_setter_object);
}
else
{
method_setImplementation(a[i], (IMP)new_setter_long);
}
}
}
}
难点
1. setter方法如何通用
2. 在C层面如何替换方法
其实这两个问题都在于我对OC底层不熟悉导致。
OC的方法在底层是以method方法的形式存储在方法列表中,每一个方法实际对应一个IMP。
IMP实质就是一个函数指针。
SEL则类似方法名称,和实例以及IMP是一一对应关系。
一个实例不能有两个相同的SEL(方法名不能重复),一个SEL对应一个IMP。
所以我们可以通过SEL得到方法名称,进而找到成员变量名,完成setter方法的通用——解决难点1
同理由于method对应一个IMP,只需要将menthod的IMP更改为我们写的函数即可——解决难点2