美文网首页梦想者实用iOS 开发进阶干货
利用runtime为setter方法添加功能

利用runtime为setter方法添加功能

作者: 莫道别离伤 | 来源:发表于2016-01-05 22:19 被阅读689次

    利用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

    附:Demo地址

    相关文章

      网友评论

      • 说干就干:[NSString stringWithUTF8String:property_getName(property)]这个里面的property怎么指定的
        莫道别离伤:是在WKClassManager类里面,利用runtime 获取的 class的属性名 来的。 你可以看看WKClassManager.m文件
      • leo_guo:demo地址没有了,求地址
        莫道别离伤:已更新·· 实现方式也做了很大的变更·
      • b260664e8436:作者您好,我遇到一个问题不知道怎么解决,当property 为 assign 的BOOL或者 int类型时,用你的代码会导致崩溃。有好的处理方式么
        莫道别离伤:@莫道别离伤 新demo 已经解决了这个情况·· 可以看看 实现方法也做了优化,基本可以适应任何环境,比如动态生成,重新设置了setter getter名字等等
        莫道别离伤:不好意思 才看见,看看是不是第1.4步 我用的是setobject 对基本类型得封装成nsobject旗下的类型
      • zhiyi:这种办法以前也用过,不过这样替换setter方法就将该class下所有set方法都替换了哦,所有属性的赋值都会存储本地。如果这个model的属性比较多的话这样做还是不错的。
        莫道别离伤:@zhiyi 嗯 确实是这样 我就是用来存储 用户数据·· 这·样还蛮好用的,如果是混合属性的话 就需要做一些判断了。

        但是总的来说 在重构阶段用起来还是很顺手的··
      • 笨鸟后飞了:runtime 一直不懂
      • dispath_once:厉害啊,有时间我也要看看runtime了
      • 曾樑:good~

      本文标题:利用runtime为setter方法添加功能

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