本文目錄html
由於Objc是一門動態語言,因此它老是想辦法把一些決定工做從編譯鏈接推遲到運行時。也就是說只有編譯器是不夠的,還須要一個運行時系統 (runtime system) 來執行編譯後的代碼。這就是 Objective-C Runtime 系統存在的意義,它是整個Objc運行框架的一塊基石。ios
Runtime其實有兩個版本:「modern」和 「legacy」。咱們如今用的 Objective-C 2.0 採用的是現行(Modern)版的Runtime系統,只能運行在 iOS 和 OS X 10.5 以後的64位程序中。而OS X較老的32位程序仍採用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在於當你更改一個類的實例變量的佈局時,在早期版本中你須要從新編譯它的子類,而現行版就不須要。面試
Runtime基本是用C和彙編寫的,可見蘋果爲了動態系統的高效而做出的努力。你能夠在這裏下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。api
ios的sdk中 usr/include/objc文件夾下面有這樣幾個文件數組
List.h NSObjCRuntime.h NSObject.h Object.h Protocol.h a.txt hashtable.h hashtable2.h message.h module.map objc-api.h objc-auto.h objc-class.h objc-exception.h objc-load.h objc-runtime.h objc-sync.h objc.h runtime.h
都是和運行時相關的頭文件,其中主要使用的函數定義在message.h和runtime.h這兩個文件中。 在message.h中主要包含了一些向對象發送消息的函數,這是OC對象方法調用的底層實現。 runtime.h是運行時最重要的文件,其中包含了對運行時進行操做的方法。 主要包括:框架
/// An opaque type that represents a method in a class definition. 一個類型,表明着類定義中的一個方法 typedef struct objc_method *Method; /// An opaque type that represents an instance variable.表明實例(對象)的變量 typedef struct objc_ivar *Ivar; /// An opaque type that represents a category.表明一個分類 typedef struct objc_category *Category; /// An opaque type that represents an Objective-C declared property.表明OC聲明的屬性 typedef struct objc_property *objc_property_t; // Class表明一個類,它在objc.h中這樣定義的 typedef struct objc_class *Class; struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE;
這些類型的定義,對一個類進行了徹底的分解,將類定義或者對象的每個部分都抽象爲一個類型type,對操做一個類屬性和方法很是方便。
OBJC2_UNAVAILABLE
標記的屬性是Ojective-C 2.0不支持的,但實際上能夠用響應的函數獲取這些屬性,例如:若是想要獲取Class的name屬性,能夠按以下方法獲取:
Class classPerson = Person.class; // printf("%s\n", classPerson->name); //用這種方法已經不能獲取name了 由於OBJC2_UNAVAILABLE const char *cname = class_getName(classPerson); printf("%s", cname); // 輸出:Person
對對象進行操做的方法通常以object_
開頭函數
對類進行操做的方法通常以class_
開頭佈局
對類或對象的方法進行操做的方法通常以method_
開頭測試
對成員變量進行操做的方法通常以ivar_
開頭this
對屬性進行操做的方法通常以property_開頭
開頭
對協議進行操做的方法通常以protocol_
開頭
根據以上的函數的前綴 能夠大體瞭解到層級關係。對於以objc_
開頭的方法,則是runtime最終的管家,能夠獲取內存中類的加載信息,類的列表,關聯對象和關聯屬性等操做。
例如:使用runtime對當前的應用中加載的類進行打印,別被嚇一跳。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { unsigned int count = 0; Class *classes = objc_copyClassList(&count); for (int i = 0; i < count; i++) { const char *cname = class_getName(classes[i]); printf("%s\n", cname); } }
在如下的代碼中,都用到了Person類,Person類知識簡單的定義了一個成員變量和兩個屬性
@interface Person : NSObject { @private float _height; } @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) int age; @end
class_copyIvarList
函數,若是想要獲取屬性列表可使用
class_copyPropertyList
函數,使用的示例以下:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { Class classPerson = NSClassFromString(@"Person"); // 與下面一句效果同樣,能夠不用導入頭文件 // Class clazz = Person.class; unsigned int count = 0; Ivar *ivarList = class_copyIvarList(classPerson, &count); // 獲取成員變量數組 for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivarList[i]); // 獲取成員變量的名字 NSString *name = [NSString stringWithUTF8String:cname]; NSLog(@"%@", name); } NSLog(@"-------------------分割線------------------"); objc_property_t *propertyList = class_copyPropertyList(classPerson, &count); // 獲取屬性數組 for (int i = 0; i < count; i++) { const char *cname = property_getName(propertyList[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSLog(@"%@", name); } }
以上代碼的輸出爲:
2015-06-05 22:28:16.194 runtime終極[4192:195757] _height 2015-06-05 22:28:16.195 runtime終極[4192:195757] _age 2015-06-05 22:28:16.195 runtime終極[4192:195757] _name 2015-06-05 22:28:16.195 runtime終極[4192:195757] -------------------分割線------------------ 2015-06-05 22:28:16.195 runtime終極[4192:195757] name 2015-06-05 22:28:16.195 runtime終極[4192:195757] age
爲何會有上面的輸出結果,由於@property會作三份工做:
1.生成一個帶下劃線的成員變量
2.生成這個成員變量的get方法
3.生成這個成員變量的set方法
所以會輸出三個成員變量_height、_age和_name。須要注意的是屬性名是不帶下劃線的,和定義時的名字同樣。所以能夠說:ivarList能夠獲取到@property關鍵字定義的屬性 ,而propertyList不能夠獲取到成員變量。也就是:使用ivarList是能夠將全部的成員變量和屬性都獲取的。
當屬性是readonly的並且重寫了getter時,這種狀況仍是會碰見的,好比一個屬性是計算型屬性,須要依賴其餘屬性的值計算而來。此時生成的帶下劃線的成員變量就不在了, 經過ivarList不能獲取該屬性了。所以當有這種值的時候,不管使用ivarList仍是使用propertyList都沒法獲取所有的屬性或變量。
在進行下一個話題以前:先須要弄清楚另外一個問題:對於一個readonly的屬性,究竟是didSet+set好,仍是重寫getter好?
大部分的readonly的屬性是計算型的,依舊是依賴於其餘屬性,所以可使用didSet+set,也就是在其餘屬性的set方法內,將本屬性set。 可是didSet+set有時候徹底沒有必要,不符合懶加載的規則,浪費了計算能力,用重寫getter的方法好一些。 所以重寫getter老是會好一點。
迴歸正題:在KVC時,想要獲取所有的成員變量和屬性, 怎麼辦呢?
首先要了解setValue: forKeyPath:
方法的底層實現:以name屬性爲例
1.首先先去類的方法列表去尋找有木有setName:,若是有,就直接調用[person setName:value]
2.找找有沒有帶下劃線的成員變量_name,若是有 _name = value;
3.找有沒有成員變量name,若是有 name = value;
4.若是都沒有找到,就直接報錯。
所以對於readonly的又重寫了getter的屬性而言:若是對propertyList的屬性一次使用kvc,就會報錯,所以爲保證代碼正常,不能使用propertyList的屬性進行kvc;
另外:這種屬性原本就是計算型的了,爲何還有爲它賦值呢,所以對它進行kvc也不合情理。
當使用ivaList時,直接就沒法獲取到這種屬性,所以是kvc的最佳方案。再者,使用propertyList沒法獲取成員變量(_height),沒法對成員變量進行賦值。而使用ivaList是能夠將該賦值的成員變量都獲取的。
以上就是使用ivar仍是使用property進行kvc的論證。
話題外: 不少類 有些成員變量 既沒有暴露給外部調用的getter又沒有setter,只是用@private聲明瞭一下:爲何??
猜想是:是方法調用時使用的中間變量,由於是跟隨對象產生,不適合使用靜態static,又由於外部不會使用,因此不必給外部提供接口,可是可能有好幾個方法都須要這個量,不適合作局部變量,因此就這樣定義了。
對於這種狀況,要想不對這種成員變量賦值,在KVC時又能夠這樣改進一下,經過ivarList獲取,去掉propertyList中沒有的成員變量,這樣就過濾掉了上面的那種成員變量了。
獲取屬性\成員列表一個重要的應用就是,一次取出模型中的屬性\成員變量,根據它的名字獲取字典中的key而後取出字典中這個key對應的value,使用setValue: forKeyPath:
方法設置值。爲何要這樣,而再也不使用方法setValuesForKeysWithDictionary:
。由於在setValuesForKeysWithDictionary:
方法內部會執行這樣一個過程
遍歷字典裏面的全部key,一個一個取出來,遍歷每一個key按照如下過程
1.取出key,
2.取出key的value,即dict[key],直接給模型的屬性\成員變量賦值
3.怎麼給模型的屬性賦值,使用方法setValue:value forKeyPath:key
進行賦值,這個方法的執行過程在前面已經提到。
所以,開發中常常遇到的字典中的key比模型中多時,會出現的 this class is not key-value compliant for ‘xxx’
這個bug就很好解釋了,一般是由於字典中的key,比模型中的屬性\成員變量多。那麼當模型中的屬性比字典中多時,使用setValuesForKeysWithDictionary:
會不不會有bug呢?經測試:當多出來的屬性是對象數據類型時,爲null,當屬性是基本數據類型時,會有一個系統默認值(如int爲0)。
所以使用逐一爲屬性賦值的方法進行KVC:
Class clazz = Person.class; unsigned int count = 0; Person *person = [[Person alloc]init]; NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19, @"height": @1.75}; Ivar *ivars = class_copyIvarList(clazz, &count); // NSLog(@"%tu", count); // 3 for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; // 去掉'_' [person setValue:dict[key] forKey:key]; } NSLog(@"%@", person); // 已經重寫了description方法
輸出是:
<Person, 0x7ff15b80f230>{ name = zhangsan, height = 1.750000, age = 19}
使用這種方式進行kvc,即便字典中的key多的時候也不會有bug,可是新的問題出現了,若是模型中的屬性比字典中的key多便會出現bug並且:若是多的是對象類型不會有bug,該屬性的值爲null,若是是基本數據類型就會出錯could not set nil as the value for the key ‘xxx’
。例如,將上面的字典改成:
NSDictionary *dict = @{@"age":@19, @"height": @1.75}; // 去掉了name NSString類型
修改以後輸出爲:
<Person, 0x7f996263fbd0>{ name = (null), height = 1.750000, age = 19}
若是將字典改成:
NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19}; // 去掉了height float類型
程序直接崩潰。
如何解決上面的bug:能夠在setValue:value forKeyPath:key
方法調用以前進行以下處理:取出屬性對應的類型,若是類型是基本數據類型,value替換爲默認值(如int對應默認值爲0)。
runtime提供的ivar_getTypeEncoding
函數能夠獲取到屬性的類型,返回值表明的含義以下:
Class clazz = Person.class; unsigned int count = 0; Person *person = [[Person alloc]init]; NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19, @"height": @1.75}; Ivar *ivars = class_copyIvarList(clazz, &count); for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; const char *coding = ivar_getTypeEncoding(ivars[i]); // 獲取類型 NSString *strCode = [NSString stringWithUTF8String:coding]; id value = dict[key]; if ([strCode isEqualToString:@"f"]) {// 判斷類型是不是float value = @(0.0); } [person setValue:value forKey:key]; } NSLog(@"%@", person);
這樣就能夠正常執行了,輸出爲:
<Person, 0x7fc75d004a00>{ name = zhangsan, height = 0.000000, age = 19}
- (void)encodeWithCoder:(NSCoder *)aCoder { unsigned int count = 0; Ivar *ivars = class_copyIvarList(self.class, &count); for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; id value = [self valueForKey:key]; // 取出key對應的value [aCoder encodeObject:value forKey:key]; // 編碼 } } - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) { unsigned int count = 0; Ivar *ivars = class_copyIvarList(self.class, &count); for (int i = 0; i < count; i++) { const char *cname = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:cname]; NSString *key = [name substringFromIndex:1]; id value = [aDecoder decodeObjectForKey:key]; // 解碼 [self setValue:value forKey:key];