引言
相信很多同學都聽過運行時,但是我相信還是有很多同學不瞭解什麼是運行時,到底在項目開發中怎麼用?什麼時候適合使用?想想我們的項目中,到底在哪裏使用過運行時呢?還能想起來嗎?另外,在面試的時候,是否經常有筆試中要求運用運行時或者在面試時面試官會問是否使用過運行時,又是如何使用的?
回想自己,曾經在面試中被面試官拿運行時刁難過,也在筆試中遇到過。因此,後來就深入地學習了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。