iOS runtime机制

相信很多人都听过运行时,但是我相信还是有很多人不了解什么是运行时,到底在项目开发中怎么用?什么时候适合使用?想想我们的项目中,到底在哪里使用过运行时呢?还能想起来吗?--(收集总结网上关于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: namepropertyValue: Lili

Swift版

func  allPropertyNamesAndValues()->[String: AnyObject]{

    var  count: UInt32=0

    let   properties=class_copyPropertyList(Person.self,&count)

    

    var  resultDict:[String: AnyObject]=[:]

    for  vari=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);

}

测试一下:

[pall Methods];

调用打印结果如下,为什么参数个数看起来不匹配呢?比如-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-2323:54:00.896 PropertiesDemo[46966:3856655] _variableString

2015-10-2323:54:00.897 PropertiesDemo[46966:3856655] _name

2015-10-2323: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  var  Name  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,但是实际上有两个参数。解决办法是,关闭严格检查:

Swift版

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

Category扩展“属性”

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

Objective-C版

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

// 由于扩展不能扩展属性,因此我们这里在实现文件中需要利用运行时实现。

typedef  void  (^HYBCallBack)  ();

@property (nonatomic,copy)  HYBCallBack  callback;



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

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版的要复杂得多了。这里只是例子,写了一个简单的存储属性扩展。

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。

大千世界,求同存异;相遇是缘,相识是份,相知便是“猿粪”(缘分)
From MZou

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

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,623评论 4 59
  • (一) 瀑布的水逆流而上, 蒲公英种子从远处飘回,聚成伞的模样, 太阳从西方升起,落向东方。 子弹退回枪膛, 运动...
    苏冽阅读 263评论 1 6
  • 当和人合作久了就容易产生依赖心理,人在协作中相互磨合,然后形成边界,慢慢会把着种默认的边界当成一种习惯,人在舒适的...
    蓝色孤枫阅读 691评论 0 0
  • 生活就是让自己忙起来,不是瞎忙,忙着虚度光阴,忙着打王者,如果你觉得有意义未尝不可。 忙自己喜欢的事,哪怕一早上出...
    秘密树洞阅读 136评论 0 2