iOS runtime機制

引言

相信很多同學都聽過運行時,但是我相信還是有很多同學不瞭解什麼是運行時,到底在項目開發中怎麼用?什麼時候適合使用?想想我們的項目中,到底在哪裏使用過運行時呢?還能想起來嗎?另外,在面試的時候,是否經常有筆試中要求運用運行時或者在面試時面試官會問是否使用過運行時,又是如何使用的?

回想自己,曾經在面試中被面試官拿運行時刁難過,也在筆試中遇到過。因此,後來就深入地學習了Runtime機制,學習裏面的API。所以纔有了後來的組件封裝中使用運行時。

相信我們都遇到過這樣一個問題:我想在擴展(category)中添加一個屬性,如果iOS是不允許給擴展類擴展屬性的,那怎麼辦呢?答案就是使用運行時機制

運行時機制

Runtime是一套比較底層的純C語言的API, 屬於C語言庫, 包含了很多底層的C語言API 在我們平時編寫的iOS代碼中, 最終都是轉成了runtimeC語言代碼。

所謂運行時,也就是在編譯時是不存在的,只是在運行過程中才去確定對象的類型、方法等。利用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;

  

  // 獲取類的所有屬性

  // 如果沒有屬性,則count0propertiesnil

  objc_property_t   *properties=class_copyPropertyList([self  class],&count);

  NSMutableArray   *propertiesArray=[NSMutableArray arrayWithCapacity:count];

  

  for (NSUInteger  i=0i<counti++){

    // 獲取屬性名稱

    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=0i<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=0i<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[propertyNamepropertyValue

        }

      }

    }

    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=0i<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=0i<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=0i<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擴展屬性


iOScategory是不能擴展存儲屬性的,但是我們可以通過運行時關聯來擴展屬性

Objective-C

假設擴展下面的屬性


// 由於擴展不能擴展屬性,因此我們這裏在實現文件中需要利用運行時實現。

typedef  void  (^HYBCallBack)  ();

@property (nonatomic,copy)  HYBCallBack  callback;


在實現文件中,我們用一個靜態變量作爲key

const  void  *s_HYBCallbackKey="s_HYBCallbackKey";

 

-(void)setCallback:(HYBCallBack)callback{

  objc_setAssociatedObject (selfs_HYBCallbackKey,callback,OBJC_ASSOCIATION_COPY_NONATOMIC);

}

 

-(HYBCallBack)callback{

  return   objc_getAssociatedObject (selfs_HYBCallbackKey);

}


其實就是通過objc_getAssociatedObject取得關聯的值,通過objc_setAssociatedObject設置關聯。


Swift

Swift版的要想擴展閉包,就比OC版的要複雜得多了。這裏只是例子,寫了一個簡單的存儲屬性擴展。


let  s_HYBFullnameKey="s_HYBFullnameKey"

 

extension  Person{

  var  fullName: String?{

    get{return   objc_getAssociatedObject (selfs_HYBFullnameKeyas?String}

    set{

      objc_setAssociatedObject (selfs_HYBFullnameKey,newValue,OBJC_ASSOCIATION_COPY_NONATOMIC)

    }

  }

}

總結

在開發中,我們比較常用的是使用關聯屬性的方式來擴展我們的屬性,以便在開發中簡單代碼。我們在開發中使用關聯屬性擴展所有響應事件、將代理轉換成block版等。比如,我們可以將所有繼承於UIControl的控件,都擁有block版的點擊響應,那麼我們就可以給UIControl擴展一個TouchUpTouchDownTouchOutblock等。

對於動態獲取屬性的名稱、屬性值使用較多的地方一般是在使用第三方庫中,比如MJExtension等。這些三方庫都是通過這種方式將Model轉換成字典,或者將字典轉換成Model


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章