14:属性/变量赋值的本质 & KVC流程

593 阅读5分钟

拓展:person.name = @"MD" 底层本质是什么?

  • 看过clang知道系统会为属性在编译时添加setter方法,使用=来赋值就相当于调用这个setter方法。它具体是怎么编译出来的呢?

    @interface LGPerson : NSObject
    
    // 属性
    @property (nonatomic, copy) NSString *name;
    
    @end
    

本质的 setter方法 从何而来?

  • 编译后,可以从 ro 里取出对应的setter方法,断点调试看到,它来源于 objc底层 的这里(这里开一下上帝视角,跟汇编很繁琐的)

  • 这几个方法区分 atomic/nonatomic copy/notcopy,他们内部都调用下面这个 reallySetProperty:

  • reallySetProperty:,设置 newValue,释放 oldValue 等 常规操作

  • 为什么不是 clang 看到的简简单单的 setName ? 答案:通用入口,需要考虑atomic/nonatomic copy/notcopy

系统如何走进 reallySetProperty通用入口

  • 说白了就是 我们写的 person.name = @"MD"objc_setProperty_nonatomic_copy 之间有断层,objc源码 看不到,那么估计是 汇编、LLVM 或者 宏

使用 Visual Studio Code 查找 LLVM代码

  • LLVM源码,搜 objc_setProperty_nonatomic_copy,这个图没意思,下面那个有意思

  • 根据 atomiccopy 标示 来设置方法的名字

  • 最后 return CGM.CreateRuntimeFunction(FTy, name);

  • 回溯搜索

  • 回溯搜索

  • 也没啥好看的

结论

  • 编译时,LLVM 分析 @property (nonatomic, copy) NSString *name;,根据 nonatomic/copy 赋予它一个合适的 setter方法,这次是 objc_setProperty_nonatomic_copy

  • 运行时,person.name = @"MD" 时调起该 setter方法

KVC

成员属性和成员变量的KVC区别

  • setValue:forKey:时,成员属性nameobjc_setProperty_nonatomic_copy,成员变量sex不走

  • 很正常,因为属性才在rogetter/setter

    @interface LGPerson : NSObject {
        @public
        // 成员变量
        NSString *sex;
    }
    
    // 属性
    @property (nonatomic, copy) NSString *name;
    
    @end
    
    LGPerson *person = [[LGPerson alloc] init];
        
    [person setValue:@"male" forKey:@"sex"];
    [person setValue:@"Mark" forKey:@"name"];
    
  • sex也通过 KVC 成功赋值了,这是咋回事?

    -w268

猜测

  • 这个图也只是猜测

  • setValue:forKey:和前面的person.name = @"MD"一样,会在LLVM鬼鬼祟祟地做处理,很有可能调用了这个↓

    // 在汇编看到 msgSend,不一定直接是这个方法,但有可能 msgSend 的那个方法包含这个方法
    object_setIvar(self , ivar, value);
    
    /** 
     * Sets the value of an instance variable in an object.
     * 
     * @param obj The object containing the instance variable whose value you want to set.
     * @param ivar The Ivar describing the instance variable whose value you want to set.
     * @param value The new value for the instance variable.
     * 
     * @note Instance variables with known memory management (such as ARC strong and weak)
     *  use that memory management. Instance variables with unknown memory management 
     *  are assigned as if they were unsafe_unretained.
     * @note \c object_setIvar is faster than \c object_setInstanceVariable if the Ivar
     *  for the instance variable is already known.
     */
    OBJC_EXPORT void
    object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 
        OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
  • 注意:编译时系统会给成员属性创建getter/setter,所以接下来使用成员变量进行探索

根据文档验证 setValue:forKey: 流程

  1. 找对象方法:setSex:_setSex:(必须有参数),并执行。↓确实可以,.h文件不声明也可以

    - (void)setSex:(NSString *)sex {
        NSLog(@"AAA%s",__func__);
    }
    
    - (void)_setSex:(NSString *)sex {
        NSLog(@"BBB%s",__func__);
    }
    

    2020-03-24 09:05:17.352322+0800 objc-debug[8662:705820] AAA-[LGPerson setSex:]

  2. 如果找不到,且如果accessInstanceVariablesDirectly返回YES,则系统自行赋值给_sex_isSexsexisSex。↓确实可以,把Person类sex变量改成_isSex,明明 KVC 设 的是sex,结果_isSex被赋值了

    [person setValue:@"male" forKey:@"sex"];    // 赋值给 _isSex
    NSLog(@"%@", person->_isSex);
    

    2020-03-23 23:18:18.125211+0800 objc-debug[8152:636122] male

    • 这应该是为了容错,防止人为加is或者运行时系统加_
  3. 如果上面2个情况都不满足,则调用setValue:forUndefinedKey:,默认会报异常,但NSObject的子类可能会重写该方法

    [person setValue:@"male" forKey:@"xxsex"];
    
    • 默认会报异常(不重写)

    2020-03-24 08:49:54.644587+0800 objc-debug[8625:696532] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<LGPerson 0x102d5a010> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxsex.'

    • Person.m重写
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
        NSLog(@"value is: %@, key is: %@", value, key);
    }
    

    2020-03-24 08:48:24.367977+0800 objc-debug[8597:694937] value is: male, key is: xxsex

根据文档验证 valueForKey: 流程

  1. 找对象方法:getSexsexisSex或者_sex,如果找到就赋值,然后跳转到第5步,否则进入下一步。确实可以↓,刻意用了isSex方法.h文件不声明也可以

    - (void)isSex {
        NSLog(@"LLL%s",__func__);
    }
    

    2020-03-24 09:27:06.501039+0800 objc-debug[8831:720436] LLL-[LGPerson isSex]

  2. 关于NSArray的,这次先不看

  3. 关于NSSet的,这次先不看

  4. 如果accessInstanceVariablesDirectly返回YES,则系统自行找成员变量_sex_isSexsexisSex;如果能找到,则进行取值,然后进入下一步,否则跳转到第6步。↓确实可以,把Person类sex变量改成isSex,明明 KVC 取 的是sex,结果能返回isSex的值

    @interface LGPerson : NSObject {
        @public
        // 成员变量
        NSString *isSex;
    }
    
    [person setValue:@"male" forKey:@"sex"];    // 赋值给 isSex
    NSLog(@"%@", [person valueForKey:@"sex"]);  // 取出 isSex
    

    2020-03-24 09:20:45.443098+0800 objc-debug[8788:716312] male

  5. 如果第1步第4步顺利执行,会直接跳到这里。分3种情况:

    • 对象指针,则直接返回
    • 能被NSNumber支持的值类型,则存入并返回一个NSNumber对象
    • 不能被NSNumber支持的值类型,则转成NSValue对象并返回(PS:NSNumberNSValue的子类)
    [person setValue:@18 forKey:@"age"];    // age是成员变量int
    NSLog(@"%d", [[person valueForKey:@"age"] intValue]);    // NSNumber
    

    2020-03-24 09:56:39.050970+0800 objc-debug[8921:735692] 18

  6. 如果上面的情况都不满足,则调用valueForUndefinedKey:,默认会报异常,但NSObject的子类可能会重写该方法。上面setValue示范过了。

KVC 对一些特殊情况的处理

自动转换类型

// [person setValue:@18 forKey:@"age"];    // 正常情况
[person setValue:@"20" forKey:@"age"];     // 把 int 设值成 string
[[person valueForKey:@"age"] class];       // __NSCFNumber

设置空值

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"设 %@ 为空值",key);
}
  • setNilValueForKey:(推测)原意是为了对值类型容错,所以只对NSNumberNSValue生效,如果对一个String类型的name设为nil,不会走这个方法

设值找不到 Key

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"设值,但没有这个key: %@ ",key);
}

取值找不到 Key

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"取值,但没有这个key: %@ ; 返回一个自定义的值",key);
    return @"自定义值";
}