包罗万象的runtime(二):变量&属性&关联
成员变量用于类内部,无需与外界接触的变量。根据成员变量的私有性,为了方便访问,所以就有了属性变量。属性变量的好处就是允许让其他对象访问到该变量(因为属性创建过程中自动产生了set 和get方法)。当然,你可以设置只读或者可写等,设置方法也可自定义。所以,属性变量是用于与其他对象交互的变量。
简单来说,就是属性自动生成set、get方法,方面外部访问。
接触iOS的人都知道,@property
声明的属性默认会生成一个_类型的成员变量,同时也会生成setter/getter
方法。(苹果将默认编译器从GCC转换为LLVM(low level virtual machine)之后)
但是如果同时重写get、set方法,编译还会报错
原因是:当你复写了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