runtime基础应用

引言

相信很多同学都听过运行时,但是我相信还是有很多同学不了解什么是运行时,到底在项目开发中怎么用?什么时候适合使用?想想我们的项目中,到底在哪里使用过运行时呢?还能想起来吗?另外,在面试的时候,是否经常有笔试中要求运用运行时或者在面试时面试官会问是否使用过运行时,又是如何使用的?

回想自己,曾经在面试中被面试官拿运行时刁难过,也在笔试中遇到过。因此,后来就深入地学习了Runtime机制,学习里面的API。所以才有了后来的组件封装中使用运行时。

相信我们都遇到过这样一个问题:我想通过分类(category)追加一个属性,而iOS是不允许给已有类通过分类的方式追加属性的,那怎么办呢?答案就是使用运行时机制

运行时机制

Runtime是一套比较底层的纯C语言的API, 属于C语言库, 包含了很多底层的C语言API。 在我们平时编写的iOS代码中, 最终都是转成了runtime的C代码。

所谓运行时,也就是在编译时是不确定的,只是在运行过程中才去确定对象的类型、方法等。利用Runtime机制可以在程序运行时动态修改类、对象中的所有属性、方法等。

还记得我们在网络请求数据处理时,调用了-setValuesForKeysWithDictionary:方法来设置模型的值。这里什么原理呢?为什么能这么做?其实就是通过Runtime机制来完成的,内部会遍历模型类的所有属性名,然后设置与key对应的属性名的值。

我们在使用运行时的地方,都需要包含头文件:#import <objc/runtime.h>。如果是Swift就不需要包含头文件,就可以直接使用了。

获取对象所有属性名

利用运行时获取对象的所有属性名是可以的,但是变量名获取就得用另外的方法了。我们可以通过class_copyPropertyList方法获取所有的属性名称。

下面我们通过一个Person类来学习,这里的方法没有写成扩展,只是为了简化,将获取属性名的方法直接作为类的实例方法:

Objective-C版

@interface Person : NSObject {
  NSString *_variableString;
}

// 默认会是什么类型呢?
@property (nonatomic, copy) NSString *name;

// 默认是strong类型
@property (nonatomic, strong) NSMutableArray *array;

// 获取所有的属性名
- (NSArray *)allProperties;

@end

下面主要是写如何获取类的所有属性名的方法。注意,这里的objc_property_t是一个结构体指针objc_property *,因此我们声明的properties就是二维指针。在使用完成后,我们一定要记得释放内存,否则会造成内存泄露。这里是使用的是C语言的API,因此我们也需要使用C语言的释放内存的方法free

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;
- (NSArray *)allProperties {
  unsigned int count;
  
  // 获取类的所有属性
  // 如果没有属性,则count为0,properties为nil
  objc_property_t *properties = class_copyPropertyList([self class], &count);
  NSMutableArray *propertiesArray = [NSMutableArray arrayWithCapacity:count];
  
  for (NSUInteger i = 0; i < count; i++) {
    // 获取属性名称
    const char *propertyName = property_getName(properties[i]);
    NSString *name = [NSString stringWithUTF8String:propertyName];
    
    [propertiesArray addObject:name];
  }
  
  // 注意,这里properties是一个数组指针,是C的语法,
  // 我们需要使用free函数来释放内存,否则会造成内存泄露
  free(properties);
  
  return propertiesArray;
}

现在,我们来测试一下,我们的方法是否正确获取到了呢?看下面的打印结果就明白了吧!

Person *p = [[Person alloc] init];
p.name = @"Lili";

size_t size = class_getInstanceSize(p.class);
NSLog(@"size=%ld", size);

for (NSString *propertyName in p.allProperties) {
  NSLog(@"%@", propertyName);
}
// 打印结果:
// 2015-10-23 17:28:38.098 PropertiesDemo[1120:361130] size=48
// 2015-10-23 17:28:38.098 PropertiesDemo[1120:361130] copiedString
// 2015-10-23 17:28:38.098 PropertiesDemo[1120:361130] name
// 2015-10-23 17:28:38.098 PropertiesDemo[1120:361130] unsafeName
// 2015-10-23 17:28:38.099 PropertiesDemo[1120:361130] array

Swift版

对于Swift版,使用C语言的指针就不容易了,因为Swift希望尽可能减少C语言的指针的直接使用,因此在Swift中已经提供了相应的结构体封装了C语言的指针。但是看起来好复杂,使用起来好麻烦。看看Swift版的获取类的属性名称如何做:

class Person: NSObject {
  var name: String = ""
  var hasBMW = false
  
  override init() {
    super.init()
  }
  
  func allProperties() ->[String] {
    // 这个类型可以使用CUnsignedInt,对应Swift中的UInt32
    var count: UInt32 = 0
    
    let properties = class_copyPropertyList(Person.self, &count)
    
    var propertyNames: [String] = []
    
    // Swift中类型是严格检查的,必须转换成同一类型
    for var i = 0; i < Int(count); ++i {
      // UnsafeMutablePointer<objc_property_t>是
      // 可变指针,因此properties就是类似数组一样,可以
      // 通过下标获取
      let property = properties[i]
      let name = property_getName(property)
      
      // 这里还得转换成字符串
      let strName = String.fromCString(name);
      propertyNames.append(strName!);
    }
    
    // 不要忘记释放内存,否则C语言的指针很容易成野指针的
    free(properties)
    
    return propertyNames;
  }
}

关于Swift中如何C语言的指针问题,这里不细说,如果需要了解,请查阅相关文章。
测试一下是否获取正确:

let p = Person()
p.name = "Lili"

// 打印结果:["name", "hasBMW"],说明成功
p.allProperties()

获取对象的所有属性名和属性值

对于获取对象的所有属性名,在上面的-allProperties方法已经可以拿到了,但是并没有处理获取属性值,下面的方法就是可以获取属性名和属性值,将属性名作为key,属性值作为value

Objective-C版

- (NSDictionary *)allPropertyNamesAndValues {
  NSMutableDictionary *resultDict = [NSMutableDictionary dictionary];
  
  unsigned int outCount;
  objc_property_t *properties = class_copyPropertyList([self class], &outCount);
  
  for (int i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];
    const char *name = property_getName(property);
    
    // 得到属性名
    NSString *propertyName = [NSString stringWithUTF8String:name];
    
    // 获取属性值
    id propertyValue = [self valueForKey:propertyName];
    
    if (propertyValue && propertyValue != nil) {
      [resultDict setObject:propertyValue forKey:propertyName];
    }
  }
  
  // 记得释放
  free(properties);
  
  return resultDict;
}

测试一下:

// 此方法返回的只有属性值不为空的属性
NSDictionary *dict = p.allPropertyNamesAndValues;
for (NSString *propertyName in dict.allKeys) {
  NSLog(@"propertyName: %@ propertyValue: %@", 
        propertyName, 
        dict[propertyName]);
}

看下打印结果,属性值为空的属性并没有打印出来,因此字典的key对应的value不能为nil:

propertyName: name propertyValue: Lili

Swift版

func allPropertyNamesAndValues() ->[String: AnyObject] {
    var count: UInt32 = 0
    let properties = class_copyPropertyList(Person.self, &count)
    
    var resultDict: [String: AnyObject] = [:]
    for var i = 0; i < Int(count); ++i {
      let property = properties[i]
      
      // 取得属性名
      let name = property_getName(property)
      if let propertyName = String.fromCString(name) {
        // 取得属性值
        if let propertyValue = self.valueForKey(propertyName) {
          resultDict[propertyName] = propertyValue
        }
      }
    }
    
    free(properties)
    
    return resultDict
}

测试一下:

let dict = p.allPropertyNamesAndValues()
for (propertyName, propertyValue) in dict.enumerate() {
  print("propertyName: \(propertyName), propertyValue: \(propertyValue)")
}

打印结果与上面的一样,由于array属性的值为nil,因此不会处理。

propertyName: 0, propertyValue: ("name", Lili)

获取对象的所有方法名

通过class_copyMethodList方法就可以获取所有的方法。

Objective-C版

- (void)allMethods {
  unsigned int outCount = 0;
  Method *methods = class_copyMethodList([self class], &outCount);
  
  for (int i = 0; i < outCount; ++i) {
    Method method = methods[i];
    
    // 获取方法名称,但是类型是一个SEL选择器类型
    SEL methodSEL = method_getName(method);
    // 需要获取C字符串
    const char *name = sel_getName(methodSEL);
   // 将方法名转换成OC字符串
    NSString *methodName = [NSString stringWithUTF8String:name];
    
    // 获取方法的参数列表
    int arguments = method_getNumberOfArguments(method);
    NSLog(@"方法名:%@, 参数个数:%d", methodName, arguments);
  }
  
  // 记得释放
  free(methods);
}

测试一下:

[p allMethods];

调用打印结果如下,为什么参数个数看起来不匹配呢?比如-allProperties方法,其参数个数为0才对,但是打印结果为2。根据打印结果可知,无参数时,值就已经是2了。:

方法名:allProperties, 参数个数:2
方法名:allPropertyNamesAndValues, 参数个数:2
方法名:allMethods, 参数个数:2
方法名:setArray:, 参数个数:3
方法名:.cxx_destruct, 参数个数:2
方法名:name, 参数个数:2
方法名:array, 参数个数:2
方法名:setName:, 参数个数:3

Swift版

func allMethods() {
  var count: UInt32 = 0
  let methods = class_copyMethodList(Person.self, &count)
  
  for var i = 0; i < Int(count); ++i {
    let method = methods[i]
    let sel = method_getName(method)
    let methodName = sel_getName(sel)
    let argument = method_getNumberOfArguments(method)
    
    print("name: \(methodName), arguemtns: \(argument)")
  }
  
  free(methods)
}

测试一下调用:

p.allMethods()

打印结果与上面的Objective-C版的一样。

获取对象的成员变量

要获取对象的成员变量,可以通过class_copyIvarList方法来获取,通过ivar_getName来获取成员变量的名称。对于属性,会自动生成一个成员变量。

Objective-C版

- (NSArray *)allMemberVariables {
  unsigned int count = 0;
  Ivar *ivars = class_copyIvarList([self class], &count);
  
  NSMutableArray *results = [[NSMutableArray alloc] init];
  for (NSUInteger i = 0; i < count; ++i) {
    Ivar variable = ivars[i];
    
    const char *name = ivar_getName(variable);
    NSString *varName = [NSString stringWithUTF8String:name];
    
    [results addObject:varName];
  }
  
  free(ivars);
  
  return results;
}

测试一下:

for (NSString *varName in p.allMemberVariables) {
  NSLog(@"%@", varName);
}

打印结果说明属性也会自动生成一个成员变量:

2015-10-23 23:54:00.896 PropertiesDemo[46966:3856655] _variableString
2015-10-23 23:54:00.897 PropertiesDemo[46966:3856655] _name
2015-10-23 23:54:00.897 PropertiesDemo[46966:3856655] _array

Swift版

Swift的成员变量名与属性名是一样的,不会生成下划线的成员变量名,这一点与Oc是有区别的。

func allMemberVariables() ->[String] {
  var count:UInt32 = 0
  let ivars = class_copyIvarList(Person.self, &count)
  
  var result: [String] = []
  for var i = 0; i < Int(count); ++i {
    let ivar = ivars[i]
    
    let name = ivar_getName(ivar)
    
    if let varName = String.fromCString(name) {
      result.append(varName)
    }
  }
  
  free(ivars)
  
  return result
}

测试一下:

let array = p.allMemberVariables()
for varName in array {
  print(varName)
}

打印结果,说明Swift的属性不会自动加下划线,属性名就是变量名:

name
array

运行时发消息

iOS中,可以在运行时发送消息,让接收消息者执行对应的动作。可以使用objc_msgSend方法,发送消息。

Objective-C版

Person *p = [[Person alloc] init];
p.name = @"Lili";
objc_msgSend(p, @selector(allMethods));

这样就相当于手动调用[p allMethods];。但是编译器会抱错,问题提示期望的参数为0,但是实际上有两个参数。

Category追加"属性"

iOS的category是不能追加存储属性的,但是我们可以通过运行时关联来追加“属性”。

Objective-C版

假设扩展下面的“属性”:

// 由于扩展不能扩展属性,因此我们这里在实现文件中需要利用运行时实现。
typedef void(^GGCallBack)();
@property (nonatomic, copy) GGCallBack callback;

在实现文件中,我们用一个静态变量作为key:

const void *s_GGCallbackKey = "s_GGCallbackKey";

- (void)setCallback:(GGCallBack)callback {
  objc_setAssociatedObject(self, s_GGCallbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (GGCallBack)callback {
  return objc_getAssociatedObject(self, s_GGCallbackKey);
}

其实就是通过objc_getAssociatedObject取得关联的值,通过objc_setAssociatedObject设置关联。

总结

在开发中,我们比较常用的是使用关联属性的方式来扩展我们的“属性”,以便在开发中简单代码。我们在开发中使用关联属性扩展所有响应事件、将代理转换成block版等。比如,我们可以将所有继承于UIControl的控件,都拥有block版的点击响应,那么我们就可以给UIControl分类追加TouchUp、TouchDown、TouchOut的block等。

对于动态获取属性的名称、属性值使用较多的地方一般是在使用第三方库中,比如MJExtension等。这些三方库都是通过这种方式将Model转换成字典,或者将字典转换成Model。

写在最后

如果文章中出现有疑问的地方,请在评论中评论,笔者会在第一时间回复您的!

阅读容易,写文章难!且看且珍惜!

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

推荐阅读更多精彩内容