Runtime系列3之-成员变量与属性

本章的主要内容将聚集在Runtime对成员变量与属性的处理。在讨论之前,我们先介绍一个重要的概念:类型编码。

类型编码(Type Encoding)

  1. 编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。我们可以使用@encode编译器指令来获取它。当给定一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()
  2. Objective-C Runtime Programming Guide中的Type Encoding一节中,列出了Objective-C中所有的类型编码。需要注意的是这些类型很多是与我们用于存档和分发的编码类型是相同的。但有一些不能在存档时使用。
    1. 注:Objective-C不支持long double类型。@encode(long double)返回d,与double是一样的。
  3. 一个数组的类型编码位于方括号中;其中包含数组元素的个数及元素类型。如以下示例:

     float a[] = {1.0, 2.0, 3.0};
     NSLog(@"array encoding type: %s", @encode(typeof(a)));
    

    打印结果:

     2014-10-28 11:44:54.731 RuntimeTest[942:50791] array encoding type: [3f]
    
  4. 另外,还有些编码类型,@encode虽然不会直接返回它们,但它们可以作为协议中声明的方法的类型限定符。可以参考Type Encoding

成员变量、属性

  1. 从前面的类操作方法中我们可以知道,如下:
    1. 获取一个成员变量:

       //获取单个成员变量
       Ivar string = class_getInstanceVariable(cls, "_string");
      
    2. 获取一个属性:

        //获取单个属性
       objc_property_t array = class_getProperty(cls, "array");
      
  2. 我们看到成员变量用Ivar接收,属性用objc_property_t接收,那么他们分别是什么呢?

Ivar

  1. 通过runtime.h文件我们查看如下:

     /// An opaque type that represents an instance variable.
     typedef struct objc_ivar *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
     }    
    
    1. Ivar 表示一个实例变量,他是一个objc_ivar结构体指针

objc_property_t

  1. 通过runtime.h文件我们查看如下:

     /// An opaque type that represents an Objective-C declared property.
     typedef struct objc_property *objc_property_t;
    
  2. 说明是一个objc_property类型的结构体指针,至于该结构体的定义,没找到
  3. 还有一个表示属性特征的类型不得不说,那就是objc_property_attribute_t
    1. objc_property_attribute_t定义了属性的特性(attribute),它是一个结构体,定义如下:
     /// Defines a property attribute,定义一个属性的特征
     typedef struct {
         const char * _Nonnull name;           /**< The name of the attribute */
         //注意人家说了通常为空!!!usually empty
         const char * _Nonnull value;          /**< The value of the attribute (usually empty) */
     } objc_property_attribute_t;
        
    

成员变量、属性的操作方法

很显然,无非就是操作上面的三种数据类型

成员变量

  1. 成员变量操作包含以下函数:

     // 获取成员变量名
     const char * ivar_getName ( Ivar v );
     // 获取成员变量类型编码
     const char * ivar_getTypeEncoding ( Ivar v );
     // 获取成员变量的偏移量
     ptrdiff_t ivar_getOffset ( Ivar v );
    
    1. ivar_getOffset函数:
      1. 对于类型id或其它对象类型的实例变量,可以调用object_getIvar和object_setIvar来直接访问成员变量,而不使用偏移量。

属性

  1. 属性操作相关函数包括以下:

     // 获取属性名
     const char * property_getName ( objc_property_t property );
     // 获取属性特性描述字符串
     const char * property_getAttributes ( objc_property_t property );
     // 获取属性中指定的特性
     char * property_copyAttributeValue ( objc_property_t property, const char *attributeName );
     // 获取属性的特性列表
     objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
    
    1. property_copyAttributeValue函数,返回的char *,在使用完后需要调用free()释放。
    2. property_copyAttributeList函数,返回值在使用完后需要调用free()释放。

代码举例:给Test2这个类动态添加一个属性

//.h文件
#import <UIKit/UIKit.h>

@interface Test2 : NSObject
//系统定义属性
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong) UIView *vview;
+ (void)addStrPropertyForTargetClass:(Class)targetClass Name:(NSString *)propertyName;
+ (void)print;
@end
//.m文件
#import "Test2.h"
#import <objc/runtime.h>
@implementation Test2
//给一个类添加一个属性
+ (void)addStrPropertyForTargetClass:(Class)targetClass Name:(NSString *)propertyName{
    objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
    objc_property_attribute_t ownership0 = { "C", "" }; // C = copy & = strong
    objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
    objc_property_attribute_t backingivar  = { "V", [[NSString stringWithFormat:@"_%@", propertyName] UTF8String] };  //variable name
    objc_property_attribute_t attrs[] = { type, ownership0, ownership, backingivar };
    class_addProperty(targetClass, [propertyName UTF8String], attrs, 4);
}
//打印这个类的成员变量跟属性
+(void)print{
    unsigned int soutCount, i;
    //获取所有的成员变量打印
    // 成员变量列表
    Ivar *ivars = class_copyIvarList([self class], &soutCount);
    for (int i = 0; i < soutCount; i++) {
        Ivar ivar = ivars[i];
        NSLog(@"成员变量===: %s ==第 %d 个", ivar_getName(ivar), i);
    }
    free(ivars);
    
    //获取所有的属性打印
    objc_property_t *propertiess = class_copyPropertyList([self class], &soutCount);
    for (i = 0; i < soutCount; i++) {
        objc_property_t property = propertiess[i];
        NSLog(@"属性有===%s===属性特征==%s", property_getName(property),property_getAttributes(property));
    }
    free(propertiess);
}
@end
//调用:
[Test2 addStrPropertyForTargetClass:[Test2 class] Name:@"name2"];
[Test2 print];

打印:

2017-11-30 17:09:27.195201+0800 RuntimeTest2[19094:488223] 成员变量===: _name ==第 0 个
2017-11-30 17:09:27.195382+0800 RuntimeTest2[19094:488223] 成员变量===: _vview ==第 1 个
2017-11-30 17:09:27.195499+0800 RuntimeTest2[19094:488223] 属性有===name2===属性特征==T@"NSString",C,N,V_name2
2017-11-30 17:09:27.195580+0800 RuntimeTest2[19094:488223] 属性有===name===属性特征==T@"NSString",C,N,V_name
2017-11-30 17:09:27.195691+0800 RuntimeTest2[19094:488223] 属性有===vview===属性特征==T@"UIView",&,N,V_vview

从打印可以看到,属性添加成功了,但是却没有成员变量_name2

关联对象(Associated Object)

  1. 关联对象是Runtime中一个非常实用的特性,不过可能很容易被忽视。他看起来像跟成员变量/属性有关,简单的来说,就是让一个对象(或者基本数据类型)成为另一个对象的成员变量.
  2. 关联对象类似于成员变量,不过是在运行时添加的。我们通常会把成员变量(Ivar)放在类声明的头文件中,或者放在类实现的@implementation后面。但这有一个缺点,我们不能在分类中添加成员变量。如果我们尝试在分类中添加新的成员变量,编译器会报错。
    1. 分类不能添加成员变量
    2. class_addIvar只能在运行时创建时添加
  3. 但是当我们想给一个已存在的类添加成员变量时,扩充这个类,怎么办呢?
    1. 通常的解决办法是我们用全局变量,然后自己实现set/get方法,达到具有属性的效果
    2. 但这些都不是Ivar,因为他们不会连接到一个单独的实例。因此,这种方法很少使用。
  4. Objective-C针对这一问题,提供了一个解决方案:即关联对象(Associated Object)。
  5. 我们可以把关联对象想象成一个Objective-C对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void指针(const void *)。我们还需要指定一个内存管理策略,以告诉Runtime如何管理这个对象的内存。这个内存管理的策略可以由以下值指定:

     OBJC_ASSOCIATION_ASSIGN
     OBJC_ASSOCIATION_RETAIN_NONATOMIC
     OBJC_ASSOCIATION_COPY_NONATOMIC
     OBJC_ASSOCIATION_RETAIN
     OBJC_ASSOCIATION_COPY
    
  6. 当宿主对象被释放时,会根据指定的内存管理策略来处理关联对象。如果指定的策略是assign,则宿主释放时,关联对象不会被释放;而如果指定的是retain或者是copy,则宿主释放时,关联对象会被释放。我们甚至可以选择是否是自动retain/copy。当我们需要在多个线程中处理访问关联对象的多线程代码时,这就非常有用了。
  7. 关联对象操作函数包括以下:

     // 设置关联对象
     void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
     // 获取关联对象
     id objc_getAssociatedObject ( id object, const void *key );
     // 移除关联对象
     void objc_removeAssociatedObjects ( id object );
    
  8. 我们将一个对象连接到其它对象所需要做的就是下面两行代码:

     static char myKey;
     objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);
     //通过key取出这个关联对象
     id anObject = objc_getAssociatedObject(self, &myKey);
    
    1. 在这种情况下,self对象将获取一个新的关联的对象anObject,且内存管理策略是自动retain关联对象,当self对象释放时,会自动release关联对象。另外,如果我们使用同一个key来关联另外一个对象时,也会自动释放之前关联的对象,这种情况下,先前的关联对象会被妥善地处理掉,并且新的对象会使用它的内存。
    2. 我们可以使用objc_removeAssociatedObjects函数来移除一个关联对象,或者使用objc_setAssociatedObject函数将key指定的关联对象设置为nil。

代码使用举例:创建一个NSObject分类,给NSObject添加一个testproperty属性

//.h文件
#import <Foundation/Foundation.h>

@interface NSObject (Addtional)
/*
 @property (nonatomic,copy) NSString *testproperty;
 这句话的作用有3点:
 1.在当前类的.m文件中(注意一定是.m文件中)的类扩展(@interface)或者@implementation中添加了一个_testproperty成员变量
 2.在当前类的.h文件中添加了get/set方法的声明
     -(void)setTestproperty:(NSString *)testproperty;
     -(NSString *)testproperty;
 3. 在当前类的.m文件中添加了set/get方法的实现:
     -(void)setTestproperty:(NSString *)testproperty{
     _testproperty = testproperty;
     }
     -(NSString *)testproperty{
     return _testproperty;
     }
 但是,如果这句话用在了分类中:
 只会添加上面的第2步,其余的1,3步需要我们自己来完成
 但是由于在分类中我们只能添加方法,不能添加成员变量,但是仍然为了实现,我们采用以下两种方式:
 1.用静态变量,或者全局静态变量代替.缺点整个程序运行过程中该变量一直存活,值会一直保留,不是真正意义上的成员变量
 2.用runtime,可以达到真正意义上的成员变量,随着当前对象的销毁而销毁
 */
@property (nonatomic,copy) NSString *testproperty;

@end

//.m文件
#import "NSObject+Addtional.h"
#import <objc/runtime.h>
@implementation NSObject (Addtional)
/*
//方式1:采用静态变量
static NSString *_testproperty;
-(void)setTestproperty:(NSString *)testproperty{
    _testproperty = testproperty;
    
}
-(NSString *)testproperty{
    return _testproperty;
}
 */
//方式2:采用runtime
-(void)setTestproperty:(NSString *)testproperty{
    //这里是通过key动态绑定
  //  objc_setAssociatedObject(self, @"addedProps", testproperty, OBJC_ASSOCIATION_COPY_NONATOMIC);
    //或者
    //Associated Objects的key要求是唯一并且是常量,而SEL是满足这个要求的,所以上面的采用隐藏参数_cmd作为key。

      objc_setAssociatedObject(self, 
@selector(testproperty), testproperty, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)testproperty{
    //这里通过key来取得绑定的值
    //return objc_getAssociatedObject(self, @"addedProps");
    //或者
      return objc_getAssociatedObject(self, _cmd); 
}
@end

//调用:
NSObject *obj = [[NSObject alloc] init];
obj.testproperty = @"我是测试属性";
NSLog(@"---%@",obj.testproperty);
Table of Contents