看到一篇不错的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版

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
@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

 
1
2
3
4
 
/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
- (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;
}
 

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

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
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版的获取类的属性名称如何做:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 
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语言的指针问题,这里不细说,如果需要了解,请查阅相关文章。 测试一下是否获取正确:

 
1
2
3
4
5
6
7
 
let p = Person()
p.name = "Lili"
 
// 打印结果:["name", "hasBMW"],说明成功
p.allProperties()
 

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


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

Objective-C版

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
- (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:

 
1
2
3
 
propertyName: name propertyValue: Lili
 

Swift版

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
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
}
 

测试一下:

 
1
2
3
4
5
6
 
let dict = p.allPropertyNamesAndValues()
for (propertyName, propertyValue) in dict.enumerate() {
  print("propertyName: \(propertyName), propertyValue: \(propertyValue)")
}
 

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

 
1
2
3
 
propertyName: 0, propertyValue: ("name", Lili)
 

获取对象的所有方法名


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

Objective-C版

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
- (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);
}
 

测试一下:

 
1
2
3
 
[p allMethods];
 

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

 
1
2
3
4
5
6
7
8
9
10
 
方法名:allProperties, 参数个数:2
方法名:allPropertyNamesAndValues, 参数个数:2
方法名:allMethods, 参数个数:2
方法名:setArray:, 参数个数:3
方法名:.cxx_destruct, 参数个数:2
方法名:name, 参数个数:2
方法名:array, 参数个数:2
方法名:setName:, 参数个数:3
 

Swift版

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
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)
}
 

测试一下调用:

 
1
2
3
 
p.allMethods()
 

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

获取对象的成员变量名称


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

Objective-C版

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
- (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;
}
 

测试一下:

 
1
2
3
4
5
 
for (NSString *varName in p.allMemberVariables) {
  NSLog(@"%@", varName);
}
 

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

 
1
2
3
4
5
 
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是有区别的。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
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
}
 

测试一下:

 
1
2
3
4
5
6
 
let array = p.allMemberVariables()
for varName in array {
  print(varName)
}
 

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

 
1
2
3
4
 
name
array
 

运行时发消息


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

Objective-C版

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

这样就相当于手动调用[p allMethods];。但是编译器会抱错,问题提示期望的参数为0,但是实际上有两个参数。解决办法是,关闭严格检查:

Swift版

很抱歉,似乎在Swift中已经没有这种写法了。如果有,请告诉我。

Category扩展属性


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

Objective-C版

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

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

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

 
1
2
3
4
5
6
7
8
9
10
11
 
const void *s_HYBCallbackKey = "s_HYBCallbackKey";
 
- (void)setCallback:(HYBCallBack)callback {
  objc_setAssociatedObject(self, s_HYBCallbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
 
- (HYBCallBack)callback {
  return objc_getAssociatedObject(self, s_HYBCallbackKey);
}
 

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

Swift版

Swift版的要想扩展闭包,就比OC版的要复杂得多了。这里只是例子,写了一个简单的存储属性扩展。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
 
 
let s_HYBFullnameKey = "s_HYBFullnameKey"
 
extension Person {
  var fullName: String? {
    get { return objc_getAssociatedObject(self, s_HYBFullnameKey) as? String }
    set {
      objc_setAssociatedObject(self, s_HYBFullnameKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
    }
  }
}
 

总结

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

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

runtime基础知识的更多相关文章

  1. Runtime系列(一)-- 基础知识

    众所周知,Objective-C 是一种运行时语言.运行时怎么来体现的呢?比如一个对象的类型确定,或者对象的方法实现的绑定都是推迟到软件的运行时才能确定的.而运行时的诸多特性都是由Runtime 来实 ...

  2. Java基础知识(壹)

    写在前面的话 这篇博客,是很早之前自己的学习Java基础知识的,所记录的内容,仅仅是当时学习的一个总结随笔.现在分享出来,希望能帮助大家,如有不足的,希望大家支出. 后续会继续分享基础知识手记.希望能 ...

  3. IOS开发基础知识碎片-导航

    1:IOS开发基础知识--碎片1 a:NSString与NSInteger的互换 b:Objective-c中集合里面不能存放基础类型,比如int string float等,只能把它们转化成对象才可 ...

  4. 基础知识《十》unchecked异常和checked异常

    运行时异常在运行期间才能被检查出来,一般运行期异常不需要处理.也称为unchecked异常.Checked异常在编译时就能确定,Checked异常需要自己处理. checked 异常也就是我们经常遇到 ...

  5. java基础知识小总结【转】

    java基础知识小总结 在一个独立的原始程序里,只能有一个 public 类,却可以有许多 non-public 类.此外,若是在一个 Java 程序中没有一个类是 public,那么该 Java 程 ...

  6. Spring基础知识

    Spring基础知识 利用spring完成松耦合 接口 public interface IOutputGenerator { public void generateOutput(); } 实现类 ...

  7. maven基础知识

    1.maven基础知识 1.1maven坐标 maven坐标通常用冒号作为分割符来书写,像这样的格式:groupId:artifactId:packaging:version.项目包含了junit3. ...

  8. WCF入门教程:WCF基础知识问与答(转)

    学习WCF已有近两年的时间,其间又翻译了Juval的大作<Programming WCF Services>,我仍然觉得WCF还有更多的内容值得探索与挖掘.学得越多,反而越发觉得自己所知太 ...

  9. Java中实现异常处理的基础知识

    Java中实现异常处理的基础知识 异常 (Exception):发生于程序执行期间,表明出现了一个非法的运行状况.许多JDK中的方法在检测到非法情况时,都会抛出一个异常对象. 例如:数组越界和被0除. ...

随机推荐

  1. CentOS6.4安装go环境

    在官网上下载go1.6.linux-amd64.tar.gz 解压缩并拷贝程序到相应路径下 #tar -zxvf go1.6.linux-amd64.tar.gz #cp -rf go /usr/lo ...

  2. 有关app的一些小知识

    META相关 1. 添加到主屏后的标题(IOS)<meta name="apple-mobile-web-app-title" content="标题"& ...

  3. ECOS 系统查找商品详情图片存入mysql情况。

    SELECT g.goods_id, g.bn,g.name,b.brand_name,g.price,g.mktprice,c.cat_name into outfile '/tmp/xxx.xls ...

  4. php笔记(一)面向对象编程

    <?php //定义一个类 class Car { var $name = '汽车'; function getName() { return $this->name; } } //实例化 ...

  5. python 实现excel转化成json文件

    1.准备工作 python 2.7 安装 安装xlrd -- pip install xlrd 2. 直接上代码 import xlrd from collections import Ordered ...

  6. 恢复SQLSERVER被误删除的数据(转)

    恢复SQLSERVER被误删除的数据 曾经想实现Log Explorer for SQL Server的功能,利用ldf里面的日志来还原误删除的数据 这里有一篇文章做到了,不过似乎不是所有的数据类型都 ...

  7. JavaScript JSON timer(计时器) AJAX HTTP请求 同源策略 跨域请求

    JSON 介绍 1. JSON: JavaScript Object Notation 是一种轻量级的数据交换格式. 它基于ECMAScript的一个子集. JSON采用完全独立于语言的文本格式,但是 ...

  8. jmeter3 测试soap协议-webservice接口

    1.新建一个线程组 2.在线程组下新增,SOAP请求 3.设置soap请求,然后就可以测试了

  9. 安装gensim

    安装了一天的gensim,其中因为版本不一致等等各种问题纠结了好久,现记录如下: 正确安装方式: 1. 安装python2.7 2. 下载Python Extension Packages对应版本的n ...

  10. 【bug】Unable to execute dex: Multiple dex files define

    This is a build path issue. Make sure your bin folder is not included in your build path. Right clic ...