包罗万象的runtime(二):变量&属性&关联对象

成员变量用于类内部,无需与外界接触的变量。根据成员变量的私有性,为了方便访问,所以就有了属性变量。属性变量的好处就是允许让其他对象访问到该变量(因为属性创建过程中自动产生了set 和get方法)。当然,你可以设置只读或者可写等,设置方法也可自定义。所以,属性变量是用于与其他对象交互的变量。

简单来说,就是属性自动生成set、get方法,方面外部访问。
接触iOS的人都知道,@property声明的属性默认会生成一个_类型的成员变量,同时也会生成setter/getter方法。(苹果将默认编译器从GCC转换为LLVM(low level virtual machine)之后)
但是如果同时重写get、set方法,编译还会报错

image.png

原因是:当你复写了get和set方法之后@property默认生成的@synthesize就不会起作用了,这也就意味着你的类不会自动生成出来实例变量了,你就必须要自己声明实例变量

@implementation Person
{
    NSString *_name;
}

或者实现@synthesize name = _name;也可以

1. runtime中的变量&属性

1.1 成员变量Ivar

成员变量的实质是什么呢,在runtime中,它是一个objc_ivar类型的指针

struct objc_ivar {
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}       

Ivar的相关操作

//获取Ivar的名称
  const char *ivar_getName(Ivar v);
  //获取Ivar的类型编码,
  const char *ivar_getTypeEncoding(Ivar v)
  //通过变量名称获取类中的实例成员变量
  Ivar class_getInstanceVariable(Class cls, const char *name)
  //通过变量名称获取类中的类成员变量
  Ivar class_getClassVariable(Class cls, const char *name)
  //获取指定类的Ivar列表及Ivar个数
  Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
  //获取实例对象中Ivar的值
  id object_getIvar(id obj, Ivar ivar) 
  //设置实例对象中Ivar的值
  void object_setIvar(id obj, Ivar ivar, id value)

code for test

////定义一个Person类
@interface Person : NSObject
{
    NSString * _address;
    NSString * _idNo;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (atomic, assign) int age;
- (instancetype)initWithID:(NSString *)idNo address:(NSString *)address;
@end
@implementation Person

- (instancetype)initWithID:(NSString *)idNo address:(NSString *)address {
    if (self = [super init]) {
        _address = address;
        _idNo = idNo;
    }
    return self;
}
@end

/// 调用测试一下
- (void)test {
    Person *person = [[Person alloc] initWithID:@"3715251993098767567" address:@"山东聊城冠县"];
    person.name = @"Elaine";
    person.sex = @"F";
    [self ivarOperation:person];
}
///成员变量
- (void)ivarOperation:(id)obj {
    NSLog(@"%s的实例变量操作:",object_getClassName(obj));
    Class cls = object_getClass(obj);
    unsigned int outCount = 0;
    /// 获取成员变量列表
    Ivar *ivars = class_copyIvarList(cls, &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        /// 获取变量名
        const char *name = ivar_getName(ivar);
        /// 获取变量值,每次迭代到非objective-c对象的时候,如基本数据类型,BOOL、int、float就会报错
        const char *type = ivar_getTypeEncoding(ivar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        id value = object_getIvar(obj, ivar);
        NSLog(@"%s = %@",name, value);
        /// 修改
        NSString *key = [NSString stringWithUTF8String:name];
        if ([key isEqualToString:@"_address"]) {
            object_setIvar(obj, ivar, @"山东济南");
        }
    }
    free(ivars);
}
/// 输出
2018-06-02 13:40:20.905438+0800 RuntimeDemo[4666:839773] Person的实例变量操作:
2018-06-02 13:40:20.905609+0800 RuntimeDemo[4666:839773] _address = 山东聊城冠县
2018-06-02 13:40:20.905752+0800 RuntimeDemo[4666:839773] 修改之后_address = 山东济南
2018-06-02 13:40:20.905865+0800 RuntimeDemo[4666:839773] _idNo = 3715251993098767567
2018-06-02 13:40:20.905964+0800 RuntimeDemo[4666:839773] _name = Elaine
2018-06-02 13:40:20.906053+0800 RuntimeDemo[4666:839773] _sex = F

代码运行之后发现:runtime成员变量列表里面包含属性字段,这是因为@property会自动生成实例变量,没什么疑惑了吧,咱们继续!!

1.2 property相关操作
//替换类中的属性
  void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
  //获取类中的属性
  objc_property_t class_getProperty(Class cls, const char *name)
  //拷贝类中的属性列表
  objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
  //获取属性名称
  const char *property_getName(objc_property_t property)
  //获取属性的特性
  const char *property_getAttributes(objc_property_t property) 
  //拷贝属性的特性列表
  objc_property_attribute_t *property_copyAttributeList(objc_property_t property, unsigned int *outCount)
  //拷贝属性的特性的值
  char *property_copyAttributeValue(objc_property_t property, const char *attributeName)

同样用代码试试

- (void)test {
    Person *person = [[Person alloc] initWithID:@"3715251993098767567" address:@"山东聊城冠县"];
    person.name = @"Elaine";
    person.sex = @"F";
    [self propertyOperation:person];
}
///属性操作
- (void)propertyOperation:(id)obj {
    NSLog(@"%s的属性操作:",object_getClassName(obj));
    Class cls = object_getClass(obj);
    unsigned int outCount = 0;
    ///获取属性列表
    objc_property_t * properties = class_copyPropertyList(cls, &outCount);
    for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        /// 通过property_getName函数获得属性的名字
        const char* name = property_getName(property);
        /// 获取属性的特性
        const char* attribute = property_getAttributes(property);
        NSLog(@"%s的attribute = %s",name, attribute);
    }
    free(properties);
}
/// 输出
2018-06-02 16:03:51.565179+0800 RuntimeDemo[6210:1173876] Person的属性操作:
2018-06-02 16:03:51.565328+0800 RuntimeDemo[6210:1173876] name的attribute = T@"NSString",C,N,V_name
2018-06-02 16:03:51.565429+0800 RuntimeDemo[6210:1173876] sex的attribute = T@"NSString",C,N,V_sex
2018-06-02 16:03:51.565548+0800 RuntimeDemo[6210:1173876] age的attribute = Ti,V_age

上面输出我们可以看出,name 自然是这个属性的名称了,但是 attribute: T@"NSString",C,N,V_sex 这串字符串又是什么呢?
还是官方文档来的靠谱:Objective-C Runtime Programming Guide Property Type and Functions

属性类型  name值:T  value:变化  //T 后面是放的是该属性的数据类型
编码类型  name值:C(copy) &(strong) W(weak) 空(assign) 等 value:无
非/原子性 name值:空(atomic) N(Nonatomic)  value:无
变量名称  name值:V  value:变化  // V 后面放的是该属性的变量名称(@property 提供了 getter 和 setter 方法,并创建一个以下划线开头的变量)

2. 关联对象

参考链接:
ios动态添加属性的几种方法
Objective-C Associated Objects 的实现原理

We all know that : Category不能添加成员变量,可以通过关联对象添加属性

2.1 Category为什么不能添加成员变量?

在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。因此,可以动态添加方法,不能添加成员变量。

2.2 Category为什么可以添加属性?

我们都知道runtime里面可以通过关联对象添加属性,比如我们可以给UIView添加一个属性name:

@interface UIView (Runtime)
@property (nonatomic, copy)NSString *name;
@end

@implementation UIView (Runtime)
- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, "NAME", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
    return objc_getAssociatedObject(self, "NAME");
}
@end

/// 使用
UIView *view1 = [[UIView alloc] init];
view1.name = @"topView";
[self.view addSubview:view1];

为什么不能添加变量,而是可以添加属性呢?
我的理解:property=ivar+get+set
Category添加的属性,不会自动生成实例变量,这里添加的属性其实是添加的getter与setter方法。
category是运行时决定的,类实例在编译器已经决定了它的内存结构,所以运行时不能改变内存结构,因此,category不能添加实例变量,那么通过关联对象添加属性不应该破坏实例的内存结构,也就是说添加的属性所占的内存跟类的实例应该是没关系的。
到底是不是这样呢?看下源代码

/// set关联对象
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    /// 根据传入的value获取new_valu
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // 如果new_value存在,根据传入的对象object获取对应的ObjectAssociationMap对象
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // 如果ObjectAssociationMap存在,根据传入的key获取对应的关联对象ObjectAssociationMap
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    // 如果旧的关联对象存在,存入新的关联对象,释放旧的关联对象
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    //旧的关联对象不存在,直接存入新的关联对象
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // 如果ObjectAssociationMap不存在,为该对象创建一个新的ObjectAssociationMap
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
image.png
/// 获取关联对象
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

get_associative_reference先根据对象地址在 AssociationsHashMap 中查找其对应的 ObjectAssociationMap 对象,如果能找到则进一步根据 key 在 ObjectAssociationMap 对象中查找这个 key 所对应的关联结构 ObjcAssociation ,如果能找到则返回 ObjcAssociation 对象的 value 值,否则返回 nil

看完源码,发现跟我们想的是一样的,新添加的属性并没有改变类对象的内存结构,它是通过关联对象存储在哈希表里面,类实例通过关键字在哈希表查找关联对象。

看到这里,再思考一下:Category为什么可以添加方法?
下一篇会讲一下runtime中的method,应该能从里面找到答案

参考资料:
https://blog.csdn.net/u012946824/article/details/51788565
https://www.cnblogs.com/LeeGof/p/6674949.html
https://www.jianshu.com/p/ead476cdb828
https://www.jianshu.com/p/cefa1da5e775
https://blog.csdn.net/shengyumojian/article/details/44919695
https://www.jianshu.com/p/6ebda3cd8052
http://www.cocoachina.com/ios/20170502/19163.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容