引言相信很多同學都聽過運行時,但是我相信還是有很多同學不了解什么是運行時,到底在項目開發(fā)中怎么用?什么時候適合使用?想想我們的項目中,到底在哪里使用過運行時呢?還能想起來嗎?另外,在面試的時候,是否經(jīng)常有筆試中要求運用運行時或者在面試時面試官會問是否使用過運行時,又是如何使用的? 回想自己,曾經(jīng)在面試中被面試官拿運行時刁難過,也在筆試中遇到過。因此,后來就深入地學習了Runtime 機制,學習里面的API。所以才有了后來的組件封裝中使用運行時。 相信我們都遇到過這樣一個問題:我想在擴展(category)中添加一個屬性,如果iOS是不允許給擴展類擴展屬性的,那怎么辦呢?答案就是使用運行時機制 運行時機制Runtime 是一套比較底層的純C語言的API, 屬于C語言庫, 包含了很多底層的C語言API。 在我們平時編寫的iOS代碼中, 最終都是轉(zhuǎn)成了runtime的C語言代碼。
所謂運行時,也就是在編譯時是不存在的,只是在運行過程中才去確定對象的類型、方法等。利用Runtime 機制可以在程序運行時動態(tài)修改類、對象中的所有屬性、方法等。 還記得我們在網(wǎng)絡請求數(shù)據(jù)處理時,調(diào)用了-setValuesForKeysWithDictionary: 方法來設置模型的值。這里什么原理呢?為什么能這么做?其實就是通過Runtime 機制來完成的,內(nèi)部會遍歷模型類的所有屬性名,然后設置與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 是一個結(jié)構(gòu)體指針objc_property * ,因此我們聲明的properties 就是二維指針。在使用完成后,我們一定要記得釋放內(nèi)存,否則會造成內(nèi)存泄露。這里是使用的是C語言的API,因此我們也需要使用C語言的釋放內(nèi)存的方法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是一個數(shù)組指針,是C的語法,
// 我們需要使用free函數(shù)來釋放內(nèi)存,否則會造成內(nèi)存泄露
free(properties);
return propertiesArray;
}
現(xiàn)在,我們來測試一下,我們的方法是否正確獲取到了呢?看下面的打印結(jié)果就明白了吧! 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);
}
// 打印結(jié)果:
// 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中已經(jīng)提供了相應的結(jié)構(gòu)體封裝了C語言的指針。但是看起來好復雜,使用起來好麻煩??纯碨wift版的獲取類的屬性名稱如何做:
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中類型是嚴格檢查的,必須轉(zhuǎn)換成同一類型
for var i = 0; i < Int(count); ++i {
// UnsafeMutablePointer<objc_property_t>是
// 可變指針,因此properties就是類似數(shù)組一樣,可以
// 通過下標獲取
let property = properties[i]
let name = property_getName(property)
// 這里還得轉(zhuǎn)換成字符串
let strName = String.fromCString(name);
propertyNames.append(strName!);
}
// 不要忘記釋放內(nèi)存,否則C語言的指針很容易成野指針的
free(properties)
return propertyNames;
}
}
關(guān)于Swift中如何C語言的指針問題,這里不細說,如果需要了解,請查閱相關(guān)文章。 測試一下是否獲取正確: let p = Person()
p.name = "Lili"
// 打印結(jié)果:["name", "hasBMW"],說明成功
p.allProperties()
獲取對象的所有屬性名和屬性值對于獲取對象的所有屬性名,在上面的-allProperties 方法已經(jīng)可以拿到了,但是并沒有處理獲取屬性值,下面的方法就是可以獲取屬性名和屬性值,將屬性名作為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]);
}
看下打印結(jié)果,屬性值為空的屬性并沒有打印出來,因此字典的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
}
}
}
return resultDict
}
測試一下: let dict = p.allPropertyNamesAndValues()
for (propertyName, propertyValue) in dict.enumerate() {
print("propertyName: (propertyName), propertyValue: (propertyValue)")
}
打印結(jié)果與上面的一樣,由于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);
// 將方法名轉(zhuǎn)換成OC字符串
NSString *methodName = [NSString stringWithUTF8String:name];
// 獲取方法的參數(shù)列表
int arguments = method_getNumberOfArguments(method);
NSLog(@"方法名:%@, 參數(shù)個數(shù):%d", methodName, arguments);
}
// 記得釋放
free(methods);
}
測試一下: [p allMethods];
調(diào)用打印結(jié)果如下,為什么參數(shù)個數(shù)看起來不匹配呢?比如-allProperties 方法,其參數(shù)個數(shù)為0才對,但是打印結(jié)果為2。根據(jù)打印結(jié)果可知,無參數(shù)時,值就已經(jīng)是2了。: 方法名:allProperties, 參數(shù)個數(shù):2
方法名:allPropertyNamesAndValues, 參數(shù)個數(shù):2
方法名:allMethods, 參數(shù)個數(shù):2
方法名:setArray:, 參數(shù)個數(shù):3
方法名:.cxx_destruct, 參數(shù)個數(shù):2
方法名:name, 參數(shù)個數(shù):2
方法名:array, 參數(shù)個數(shù):2
方法名:setName:, 參數(shù)個數(shù):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)")
}
}
測試一下調(diào)用: p.allMethods()
打印結(jié)果與上面的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];
}
return results;
}
測試一下: for (NSString *varName in p.allMemberVariables) {
NSLog(@"%@", varName);
}
打印結(jié)果說明屬性也會自動生成一個成員變量: 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是有區(qū)別的。
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)
}
}
return result
}
測試一下: let array = p.allMemberVariables()
for varName in array {
print(varName)
}
打印結(jié)果,說明Swift 的屬性不會自動加下劃線,屬性名就是變量名: name
array
運行時發(fā)消息iOS中,可以在運行時發(fā)送消息,讓接收消息者執(zhí)行對應的動作??梢允褂?code style="font-size: 0.85em; font-family: Consolas, Inconsolata, Courier, monospace;margin: 0px 0.15em; padding: 0px 0.3em; white-space: pre-wrap; border: 1px solid rgb(234, 234, 234); border-radius: 3px; display: inline; background-color: rgb(248, 248, 248);">objc_msgSend方法,發(fā)送消息。 Objective-C版
Person *p = [[Person alloc] init];
p.name = @"Lili";
objc_msgSend(p, @selector(allMethods));
這樣就相當于手動調(diào)用[p allMethods]; 。但是編譯器會抱錯,問題提示期望的參數(shù)為0,但是實際上有兩個參數(shù)。解決辦法是,關(guān)閉嚴格檢查: Swift版
很抱歉,似乎在Swift中已經(jīng)沒有這種寫法了。如果有,請告訴我。 Category擴展”屬性”iOS的category是不能擴展存儲屬性的,但是我們可以通過運行時關(guān)聯(lián)來擴展“屬性”。 Objective-C版
假設擴展下面的“屬性”: // 由于擴展不能擴展屬性,因此我們這里在實現(xiàn)文件中需要利用運行時實現(xiàn)。
typedef void(^HYBCallBack)();
@property (nonatomic, copy) HYBCallBack callback;
在實現(xiàn)文件中,我們用一個靜態(tài)變量作為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 取得關(guān)聯(lián)的值,通過objc_setAssociatedObject 設置關(guān)聯(lián)。 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)
}
}
}
總結(jié)在開發(fā)中,我們比較常用的是使用關(guān)聯(lián)屬性的方式來擴展我們的“屬性”,以便在開發(fā)中簡單代碼。我們在開發(fā)中使用關(guān)聯(lián)屬性擴展所有響應事件、將代理轉(zhuǎn)換成block版等。比如,我們可以將所有繼承于UIControl的控件,都擁有block版的點擊響應,那么我們就可以給UIControl擴展一個TouchUp、TouchDown、TouchOut的block等。 對于動態(tài)獲取屬性的名稱、屬性值使用較多的地方一般是在使用第三方庫中,比如MJExtension等。這些三方庫都是通過這種方式將Model轉(zhuǎn)換成字典,或者將字典轉(zhuǎn)換成Model。
|