[toc]php
Runtime
是一套比較底層的純C語言API,包含了不少底層的C語言API。在咱們平時編寫的OC代碼中,程序運行時,其實最終都是轉成了Runtime
的C語言代碼。Runtime
是開源的,你能夠去這裏下載Runtime的源碼。html
實例方法被調用的時候,會經過其持有的isa指針找到對應的類,而後在其中的class_data_bits_t中查找對應的方法。ios
*執行NSArray array = [[NSArray alloc] init];的流程:git
realizeClass方法的主要做用是對類進行第一次初始化(分配可讀寫數據空間、返回真正的類結構) 類在內存中的位置是編譯期肯定的,只要代碼不改變,類在內存中的位置就會不變github
class_rw_t
中class_ro_t
中realizeClass
執行以後,才放到了 class_rw_t
指向的只讀區域 class_ro_t
,這樣咱們便可以在運行時爲 class_rw_t
添加方法,也不會影響類的只讀結構。class_ro_t
中的屬性在運行期間就不能改變了,再添加方法時,會修改 class_rw_t
中的 methods 列表,而不是 class_ro_t 中的 baseMethodsstruct objc_object {
Class isa;
} *id;
複製代碼
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
}
複製代碼
- 實例方法調用時,經過對象的 isa 在類中獲取方法的實現
- 類方法調用時,經過類的 isa 在元類中獲取方法的實現
typedef struct objc_selector *SEL;
複製代碼
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE; // 方法類型,主要存儲着方法的參數類型和返回值類型
IMP method_imp OBJC2_UNAVAILABLE; // 方法的實現,函數指針
}
複製代碼
class_copyMethodList(Class cls, unsigned int *outCount)
可使用這個方法獲取某個類的成員方法列表。objective-c
typedef id (*IMP)(id, SEL, ...);
複製代碼
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
複製代碼
class_copyIvarList(Class cls, unsigned int *outCount)
可使用這個方法獲取某個類的成員變量列表。sql
// ivar 的修飾信息存放在了 Class 的 Ivar Layout 中
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout; // <- 記錄了哪些是 strong 的 ivar
const char * name;
const method_list_t * baseMethods;
const protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout; // <- 記錄了哪些是 weak 的 ivar
const property_list_t *baseProperties;
};
複製代碼
typedef struct objc_property *objc_property_t;
複製代碼
class_copyPropertyList(Class cls, unsigned int *outCount)
可使用這個方法獲取某個類的屬性列表。編程
typedef struct objc_category *Category;
typedef struct objc_category {
const char *name; // 類的名字
classref_t cls; // 類
struct method_list_t *instanceMethods; // category中全部給類添加的實例方法的列表
struct method_list_t *classMethods; // category中全部添加的類方法的列表
struct protocol_list_t *protocols; // category實現的全部協議的列表
struct property_list_t *instanceProperties; // category中添加的全部屬性
};
複製代碼
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
複製代碼
每調用一次方法後,不會直接在isa指向的類的方法列表(methodLists)中遍歷查找可以響應消息的方法,由於這樣效率過低。它會把該方法緩存到cache列表中,下次的時候,就直接優先從cache列表中尋找,若是cache沒有,才從isa指向的類的方法列表(methodLists)中查找方法。提升效率。json
Class
是一個指向objc_class
結構體的指針,而id
是一個指向objc_object
結構體的指針,其中的isa
是一個指向objc_class
結構體的指針。其中的id就是咱們所說的對象,Class
就是咱們所說的類。isa
指針不老是指向實例對象所屬的類,不能依靠它來肯定類型,而是應該用isKindOfClass
:方法來肯定實例對象的類。由於KVO
的實現機制就是將被觀察對象的isa指針指向一箇中間類而不是真實的類。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; // 類的版本信息,默認爲0
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;
複製代碼
objc_allocateClassPair
class_addMethod
、class_addIvar
、class_addProperty
objc_registerClassPair
注意:運行時只能添加屬性,不能添加成員變量,不然會打亂類的內存結構。segmentfault
[TOC]
@(runtime)[runTime, 溫故而知新]
轉自:iOS動態特性初研究(利用JSON動態建立類型和對象)
程序能夠訪問,檢測和修改它自己狀態或行爲的能力。用我本身的理解,這裏的狀態和行爲,理解成變量,屬性和方法,會更加形象一點。
Class: 從語法形式上看,和UIButton,NSString同樣,是一種類型。
Class被定義爲一個指向objc_class的結構體指針。
它是指向對象的類結構體的指針,該類結構體含有一個指向其父類類結構的指針,訪類方法的鏈表,該類方法的緩存以及其餘必要信息。見下圖
除了靜態方法來建立對象,還可使用string來建立,NSClassFromString。
SEL:定義成一個指向objc_selector指針
運行時,會在方法鏈表中根據SEL查找具體的實現方法IMP。爲何不用函數指針直接調用,而加了一層SEL?個人理解,首先Object-C的類不能直接應用函數指針,這樣只能作一個@selector語法來取(本人在OC中寫過狀態機,用函數指針形式寫action,但一直報錯,只能用selector代替);其次,SEL還能夠配合動態方法來使用,例如NSSelectorFromString,performSelector,動態添加方法,並執行。
IMP:就是定義一個函數指針的形式
它包含一個接受消息的對象(self指針),調用方法SEL,以及若干參數,並返回一個id。
用動態方法,得到該對象的屬性/變量列表(class_copyPropertyList
/class_copyIvarList
),遍歷得到每一個屬性的名稱(property_getName),而後將JSON轉換Dic,用key-value(setvalueForkey,valueForKey)方法,對對象進行賦值,取值操做。
此種方法,抽象出了公用的setter方法(用dictionary給對象賦值),可是缺點是,類型要事先定義。沒法動態生成類型。這種例子,網上不少,並且不明白爲何例子中都把property name和attribute值打印出來,至於怎麼用,半個字都沒提?
(上面是最長見的使用方式,有人問我可否不事先定義類型,而後利用JSON來建立類型呢?這個還把我問住了)後來查閱OC runtime guide,發現有動態添加變量的方法(class_addIvar
),因而思路由此打開:
(沒有屬性,變量,方法),只有一個類名,而後運行時,給該類添加變量(當時沒有查到能夠動態添加屬性的方法,後來發現有,可是要到iOS4.3之後才行),隨後用給變量賦值。可是結果讓人失望,沒法動態添加變量。緣由是class_addIvar只能在動態建立類型的時候,添加變量,也就是「class_addIvar"This function may only be called after objc_allocateClassPair and beforeobjc_registerClassPair.Adding an instance variable to an existing class is notsupported」,而事先定義類是靜態建立的類,故沒法在runtime時添加變量objective-c-add-property-in-runtime
因而,只能放棄事先定義類的方式,轉而利用在動態建立類時(objc_allocateClassPair
),添加變量 。而後用給變量賦值和取值的方式(object_setInstanceVariable
,object_getIvar
,注意,沒法用key-value的方式操做,這種方法只有靜態定義屬性後才行),但這種方式,就只能用純C的方式封裝,賦值,取值都要傳進obj參數,比較繁瑣,沒有面向對象那麼方便。
結論:3.2中的結論,若是編譯前定義類,那麼沒法用runtime添加變量,這種方法行不通;只有在runtime時,在objc_allocateClassPair
和objc_registerClassPair
之間用class_addIvar
添加變量
(class_addProperty
),在4.3以後。因而想到一種動態建立類型,而且能夠用OC語法的方式訪問變量。
首先,動態建立類型,添加變量(這個很重要,由於當咱們訪問property時,其實是要對變量操做,若是沒有添加變量,那麼就是null),註冊類型,而後往裏動態添加屬性,隨後就能夠象OC同樣方便訪問屬性了 (由於靜態類中屬性會默認有一個和它同名的變量,對屬性操做,其實是對該變量操做)。
但實際上對該屬性賦值後,取值倒是null。由於只有在編譯前定義的屬性纔會默認一個變量,property實際上只是提供了setter和getter的方法,至於你要把值存貯在哪裏,須要本身設定,因此還須要在class_addProperty
方法後,添加property的setter,getter,並在其中肯定須要把值保存到哪裏,從哪裏取值。
這個例子有以下幾個特色:1.能夠動態生成類型 2.能夠用OC的方式訪問屬性。純粹的「動態」。
固然也有美中不足的地方,首先動態建立對象的類型都是id類型(由於是動態建立,事先沒有定義具體類型),視覺上不直觀。其次編譯過程當中,會報warning,由於property是動態添加的,不是編譯以前肯定的,因此編譯器不知道setter,getter方法哪裏來的。(固然能夠用performSelector來調用就沒有warning問題,可是調用方式太繁瑣)
可是不影響使用。
結果
結論:3.3的方法比3.2,3.1的方法牛逼,直接動態建立類型和對象,可是犧牲的是code的可讀性和可維護性,研究的意義大於實用意義。
注意:這裏須要你們研究的是,如何經過JSON的值,肯定動態添加的變量和property的類型,個人思路是,能夠容易區分NSString和NSNumber,可是若是肯定int,long,float, long long等類型?應該能夠經過值的大小範圍來肯定,例如int -256~255
@property 和 Ivar 的區別:@property = Ivar + setter + getter
相關函數
objc_setAssociatedObject
、objc_getAssociatedObject
、objc_removeAssociatedObjects
,下面的代碼經過給UIButton添加一個分類的方式關聯兩個屬性clickInterval
、clickTime
,來實現按鈕的防連點操做。
// .h文件
#import <UIKit/UIKit.h>
@interface UIButton (FixMultiClick)
@property (nonatomic, assign) NSTimeInterval clickInterval;
@end
複製代碼
// .m文件
#import "UIButton+FixMultiClick.h"
#import <objc/runtime.h>
#import <Aspects/Aspects.h>
@interface UIButton ()
@property (nonatomic, assign) NSTimeInterval clickTime;
@end
@implementation UIButton (FixMultiClick)
-(NSTimeInterval)clickTime {
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickTime:(NSTimeInterval)clickTime {
objc_setAssociatedObject(self, @selector(clickTime), @(clickTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSTimeInterval)clickInterval {
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickInterval:(NSTimeInterval)clickInterval {
objc_setAssociatedObject(self, @selector(clickInterval), @(clickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+(void)load {
[UIButton aspect_hookSelector:@selector(sendAction:to:forEvent:)
withOptions:AspectPositionInstead
usingBlock:^(id<AspectInfo> info){
UIButton *obj = info.instance;
if(obj.clickInterval <= 0){
[info.originalInvocation invoke];
}
else{
if ([NSDate date].timeIntervalSince1970 - obj.clickTime < obj.clickInterval) {
return;
}
obj.clickTime = [NSDate date].timeIntervalSince1970;
[info.originalInvocation invoke];
}
} error:nil];
}
@end
複製代碼
能夠快速爲一個已有的class
添加一個動態屬性或者block塊
不能遍歷全部的關聯對象列表,不能移除指定的關聯對象,只能經過objc_removeAssociatedObjects
一次移除全部的關聯對象。
相關函數objc_alloctateClassPair
、class_addIvar
、objc_registerClassPaire
// 一:爲Class分配內存空間
Class myClass = objc_allocateClassPair([NSObject class], "myClass", 0);
// 二:添加方法
class_addMethod(myClass, @selector(method), (IMP)myMethod, "v@:");
// 三:註冊Class
objc_registerClassPair(myClass);
// 建立對象調用方法
id obj = [[myClass alloc] init];
[obj performSelector:@selector(method)];
複製代碼
動態添加Ivar咱們可以經過遍歷Ivar獲得咱們所添加的屬性
必須經過class_allocatePair動態建立一個class,才能調用class_addIvar建立Ivar,最後經過class_registClassPair註冊class。不能爲已存在的類添加Ivar,不然會涉及到OC中類的成員變量的偏移量問題,若是在類註冊以後class_adddIvar的話會破壞原來類成員變量的正確偏移量,這樣的話會致使你訪問的成員變量並非你想訪問的成員變量(用KVC賦值和取值直接報錯, 用getIvar的話取值爲null),如圖:
相關函數class_addProperty
、class_addMethod
、objc_getAssociatedObject
、objc_getAssociatedObject
僅僅添加屬性是沒什麼用的,由於還須要添加屬性對應的實例變量。雖然runtime提供了class_addIvar方法來給類添加實例變量,可是注意,該方法只能在建立新的類的時候才能使用;對於已經存在的類,是不容許添加實例變量的。鑑於上述緣由,因此能夠採用動態添加關聯對象來存儲屬性對應的實例變量。實現策略以下:
- 一、因爲咱們確定會在interface 中提供生的property(因爲沒有合成實現與ivar,在此稱爲生的),因此這樣對於在外部訪問時和普通property相同。
- 二、因爲缺少的是實現以及能夠存取的數據量,這裏咱們能夠直接實現這些set與get。
- 三、set與get的實現能夠經過 associatedObject 進行對對象的存取操做。
#import "RuntimeTest.h"
#import <objc/runtime.h>
@interface RuntimeTest()
{
NSString* _address;
}
@end
@implementation RuntimeTest
+(void)load {
[self runtimeTest];
}
void myMethod(id self, SEL _cmd) {
NSLog(@"self = %@", self);
NSLog(@"self.name = %@", [self valueForKey:NSStringFromSelector(@selector(name))]);
NSLog(@"self.addres = %@", [self valueForKey:NSStringFromSelector(@selector(addres))]);
}
NSString *nameGetter(id self, SEL _cmd) {
NSString* result = objc_getAssociatedObject(self, _cmd);
return result;
}
void nameSetter(id self, SEL _cmd, NSString *value) {
NSString *propertyStr = NSStringFromSelector(_cmd);
// 去掉 set
NSString *realProperty = [propertyStr substringFromIndex:3];
// 去掉 :
realProperty = [realProperty substringToIndex:realProperty.length - 1];
// 首字母小寫
realProperty = [realProperty lowercaseString];
// 關聯對象
objc_setAssociatedObject(self, NSSelectorFromString(realProperty), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+ (void) runtimeTest {
// 一、Class分配內存空間
Class myClass = objc_allocateClassPair([NSObject class], "myClass", 0);
// 2.一、添加方法
class_addMethod(myClass, @selector(method), (IMP)myMethod, "v@:");
// 2.二、添加變量(ivar)
class_addIvar(myClass, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));
// 三:註冊Class
objc_registerClassPair(myClass);
// 2.三、添加屬性(property)(能夠在類的註冊完成以後)
NSString* propertyName = @"addres";
objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
objc_property_attribute_t ownership = { "N", "" }; // N = nonatomic
objc_property_attribute_t backingivar = { "V", [[NSString stringWithFormat:@"_%@", propertyName] UTF8String] }; //variable name
objc_property_attribute_t attrs[] = { type, ownership0, ownership, backingivar };
if (class_addProperty(myClass, [propertyName UTF8String], attrs, sizeof(attrs)/sizeof(objc_property_attribute_t))) {
//添加get和set方法
NSString *setFunc = [NSString stringWithFormat:@"set%@:",[propertyName capitalizedString]];
class_addMethod(myClass, NSSelectorFromString(propertyName), (IMP)nameGetter, "@@:");
class_addMethod(myClass, NSSelectorFromString(setFunc), (IMP)nameSetter, "v@:@");
}
// 建立對象調用方法
id obj = [[myClass alloc] init];
[obj setValue:@"xiaoMing" forKey:NSStringFromSelector(@selector(name))];
[obj setValue:@"宇宙1" forKey:NSStringFromSelector(@selector(addres))];
NSLog(@"addres1 = : %@", [obj valueForKey:NSStringFromSelector(@selector(addres))]);
[obj setValue:@"宇宙2" forKey:@"addres"];
NSLog(@"addres1 = : %@", [obj valueForKey:@"addres"]);
[obj performSelector:@selector(method)];
}
@end
複製代碼
能都在已有的類中添加property,而且能可以遍歷到動態添加的屬性。這種操做因爲提供了生的property,因此在第三方的json轉model庫遍歷property時能夠直接遍歷到,因爲手動實現了set和get方法,因此在遍歷後的KVC賦值時也能起到做用,保證了和普通成員變量操做的一致性。
比較麻煩class_addProperty只是聲明瞭get和set方法(缺乏實現和Ivar),get和set方法須要本身實現,值也須要本身存儲(可使用關聯對象或者存儲到已存在的ivar上)。
這種方法相似於property,須要重寫setValue:forUndefinedKey
和valueForUndefinedKey:
,存值方式也同樣,須要藉助一個其餘對象。因爲這種方式沒有藉助於runtime,因此也比較容易理解。
[obj setValue:@"張三" forKey:@"name"];
// =======================================
// 就會被編譯器處理成:
// =======================================
SEL sel = sel_get_uid ("setValue:forKey:");
IMP method = objc_msg_lookup (obj->isa, sel);
method(obj, sel, @"張三", @"name");
複製代碼
KVC運用了isa_swizzling(類型混合指針機制)技術,來實現其內部查找定位的。
- (void)setValue:(id)value forKey:(NSString *)key;
複製代碼
- (id)valueForKey:(NSString *)key;
複製代碼
// value的值爲OC對象,若是是基本數據類型要包裝成NSNumber
- (void)setValue:(id)value forKey:(NSString *)key;
// keyPath鍵路徑,類型爲xx.xx
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
// 它的默認實現是拋出異常,能夠重寫這個函數作錯誤處理。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
複製代碼
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
// 若是Key不存在,且沒有KVC沒法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常
- (id)valueForUndefinedKey:(NSString *)key;
複製代碼
// 容許直接訪問實例變量,默認返回YES。若是某個類重寫了這個方法,且返回NO,則KVC不能夠訪問該類。
+ (BOOL)accessInstanceVariablesDirectly;
// 這是集合操做的API,裏面還有一系列這樣的API,若是屬性是一個NSMutableArray,那麼能夠用這個方法來返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 若是你在setValue方法時面給Value傳nil,則會調用這個方法
- (void)setNilValueForKey:(NSString *)key;
// 輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
// KVC提供屬性值確認的API,它能夠用來檢查set的值是否正確、爲不正確的值作一個替換值或者拒絕設置新值並返回錯誤緣由。
- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;
複製代碼
//JSON數據:
//{
// "username": "lxz",
// "age": 25,
// "id": 100
//}
@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSString age;
@property (nonatomic, assign) NSInteger userId;
@end
@implementation User
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if ([key isEqualToString:@"id"]) {
self.userId = [value integerValue];
}
}
@end
複製代碼
賦值時會遇到一些問題,例如服務器會返回一個id字段,可是對於客戶端來講id是系統保留字段,能夠重寫setValue:forUndefinedKey:方法並在內部處理id參數的賦值。
轉換時須要服務器數據和類定義匹配,字段數量和字段名都應該匹配。若是User比服務器數據多,則服務器沒傳的字段爲空。若是服務端傳遞的數據User中沒有定義,則會致使崩潰。
在KVC進行屬性賦值時,內部會對基礎數據類型作處理,不須要手動作NSNumber的轉換。須要注意的是,NSArray和NSDictionary等集合對象,value都不能是nil,不然會致使Crash。
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
[self setValue:@"" forKey:@」age」];
} else {
[super setNilValueForKey:key];
}
}
複製代碼
當經過KVC給某個非對象的屬性賦值爲nil時,此時KVC會調用屬性所屬對象的setNilValueForKey:方法,並拋出NSInvalidArgumentException的異常,並使應用程序Crash。
咱們能夠經過重寫下面方法,在發生這種異常時進行處理。例如給name賦值爲nil的時候,就能夠重寫setNilValueForKey:方法並表示name是空的。
KVC修改readonly的系統隱藏變量。將UIPageControl的圓形替換爲長條形。
[pageControler setValue:[UIImage imageNamed:@"line"] forKeyPath:@"pageImage"];
[pageControler setValue:[UIImage imageNamed:@"current"] forKeyPath:@"currentPageImage"];
複製代碼
一、對數組求和、平均值、最大值、最小值。
NSArray *array = @[@1, @3, @5, @7, @9,@11, @13];
NSInteger sumPath = [[array valueForKeyPath:@"@sum.floatValue"] integerValue];
NSInteger avgPath = [[array valueForKeyPath:@"@avg.floatValue"] integerValue];
NSInteger maxPath = [[array valueForKeyPath:@"@max.floatValue"] integerValue];
NSInteger minPath = [[array valueForKeyPath:@"@min.floatValue"] integerValue];
NSLog(@"sum = %ld, avg = %ld, max = %ld, min = %ld",(long)sumPath, (long)avgPath, (long)maxPath, (long)minPath);
// 上述例子經驗證是可取的,但下面的寫法不可取(將引發崩潰)
NSInteger sum = [[array valueForKey:@"@sum.floatValue"] integerValue];
NSInteger avg = [[array valueForKey:@"@avg.floatValue"] integerValue];
NSInteger max = [[array valueForKey:@"@max.floatValue"] integerValue];
NSInteger min = [[array valueForKey:@"@min.floatValue"] integerValue];
NSLog(@"sum = %ld, avg = %ld, max = %ld, min = %ld",(long)sum, (long)avg, (long)max, (long)min);
複製代碼
二、刪除數組中重複的數據
NSArray *array = @[@1, @3, @5, @7, @9, @11, @13, @7, @9,@11];
NSLog(@"deleteKeyPath = %@",[array valueForKeyPath:@"@distinctUnionOfObjects.self"]);
// 下述寫法不可取,會引發崩潰
NSLog(@"deleteKey = %@",[array valueForKey:@"@distinctUnionOfObjects.self"]);
複製代碼
三、深層次取出字典中的屬性
NSDictionary *dic = @{@"dic1":@{@"dic2":@{@"name":@"zhangsanfeng",@"info":@{@"age":@"13"}}}};
NSLog(@"KeyPath = %@",[dic valueForKeyPath:@"dic1.dic2.info.age"]); // 能夠深層次的取到子層級屬性
NSLog(@"Key = %@",[dic valueForKey:@"dic1.dic2.info.age"]); // 沒法深層次取到子層級屬性
複製代碼
struct method_t {
SEL name; // 方法名(一個類裏面可能有多個name相同的method,好比分類中重寫的方法)
const char *types; // 存儲着方法的參數類型和返回值類型的描述字串
IMP imp; // 方法的函數指針(方法實現,相同的name可能對應不一樣的實現)
};
複製代碼
// Method method = class_getInstanceMethod([self class], sel);
Method method = someMethod;
IMP imp = method_getImplementation(method);
複製代碼
// 第一種:methodForSelector(SEL) (內部是用 class_getMethodImplementation 實現)
SEL sel = @selector(myFunc);
IMP imp = [self methodForSelector:sel];
// 第二種:class_getMethodImplementation(Class, SEL)
SEL sel = @selector(myFunc);
IMP imp = class_getMethodImplementation(self, sel);
複製代碼
SEL sel = @selector(someFunc:);
複製代碼
objc_msgSend(接受者+選擇器+參數)
#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
#else
OBJC_EXPORT id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
#endif
複製代碼
performSelector
(儘可能不使用)performSelector系列方法在內存管理上容易有缺失,它沒法肯定將要執行的選擇子是什麼,於是ARC編譯器也沒法插入適當的內存管理方法,這是一個大坑,使用GCD則不存在這個問題。
// 沒有參數
- (id)performSelector:(SEL)aSelector;
// 傳遞一個參數
- (id)performSelector:(SEL)aSelector withObject:(id)object;
// 傳遞兩個參數
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製代碼
// 不一樣的返回值使用不一樣的宏,不然會報EXC_BAD_ACCESS錯誤
typedef id (*_IMP) (id, SEL, ...);
typedef int (*_INT_IMP) (id, SEL, ...);
typedef bool (*_BOOL_IMP) (id, SEL, ...);
typedef void (*_VOID_IMP) (id, SEL, ...);
Method mthod = class_getInstanceMethod([Obj class], sel);
_IMP imp = (_IMP)method_getImplementation(mthod);
imp(Obj, sel, 參數列表)
複製代碼
NSMethodSignature * methodSignature = [[myObj class] instanceMethodSignatureForSelector:@selector(myFunc)];
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:myObj];
[invocation setSelector:@selector(myFunc)];
NSString *a=@"111";
int b=2;
[invocation setArgument:&a atIndex:2];
[invocation setArgument:&b atIndex:3];
[invocation retainArguments];
[invocation invoke];
複製代碼
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
// 關鍵代碼(a)
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); // 調用這個函數,僞代碼...
}
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) {
... // 執行動態綁定
}
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; // 這個是用於消息轉發的
return imp;
}
// 遍歷繼承鏈,查找IMP
IMP lookUpImpOrNil(Class cls, SEL sel) {
if (!cls->initialize()) {
_class_initialize(cls);
}
Class curClass = cls;
IMP imp = nil;
do { // 先查緩存,緩存沒有時重建,仍舊沒有則向父類查詢
if (!curClass) break;
if (!curClass->cache) fill_cache(cls, curClass);
imp = cache_getImp(curClass, sel);
if (imp) break;
} while (curClass = curClass->superclass); // 關鍵代碼(b)
return imp;
}
複製代碼
/**
若是分類中重寫了類的方法,找到原有方法,而且執行獲取結果
@param aString 須要比較的NSString
@return YES or NO
*/
-(BOOL)excuteoRiginalIsEqualToString:(NSString*)aString {
unsigned int count;
Method originalMethod = {0};
// 獲取類的全部方法列表,根據SEL匹配,可能找到多個method,最後一個即原有method
Method *methods = class_copyMethodList([self class], &count);
for (int i = 0; i < count; i++) {
const char* funcName = sel_getName(method_getName(methods[i]));
if ( 0 == strcmp(funcName, "isEqualToString:") ) {
// category中的方法在方法列表中的下標小,最後一個爲原來的方法
originalMethod = methods[i];
}
}
_BOOL_IMP imp = (_BOOL_IMP)method_getImplementation(originalMethod);
BOOL res = NO;
if (imp) {
res = imp(self, method_getName(originalMethod), aString);
}
free(methods);
return res;
}
複製代碼
#import "UIViewController+viewDidLoad.h"
#import <objc/runtime.h>
@implementation UIViewController (viewDidLoad)
+ (void)load
{
//保證交換方法只執行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//獲取原始方法
Method viewDidLoad = class_getInstanceMethod(self, @selector(viewDidLoad));
//獲取方法實現
_VIMP viewDidLoad_IMP = (_VIMP)method_getImplementation(viewDidLoad);
//從新設置方法實現
method_setImplementation(viewDidLoad,imp_implementationWithBlock(^(id target,SEL action){
viewDidLoad_IMP(target,@selector(viewDidLoad));
//自定義代碼
NSLog(@"%@ did load",target);
}));
});
}
複製代碼
@interface Man : NSObject
@property (nonatomic, assign) NSInteger p_mustacheLength;
// 直接修改爲員變量
- (void)set_P_mustacheLength:(NSInteger)p_mustacheLength;
// 手動觸發
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength;
@end
@implementation Man
// 直接修改爲員變量
- (void)set_P_mustacheLength:(NSInteger)p_mustacheLength {
_p_mustacheLength = p_mustacheLength;
}
// 手動觸發
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength {
[self willChangeValueForKey:@"p_mustacheLength"];
_p_mustacheLength = p_mustacheLength;
[self didChangeValueForKey:@"p_mustacheLength"];
}
// 重寫set方法
- (void)setP_mustacheLength:(NSInteger)p_mustacheLength {
_p_mustacheLength = p_mustacheLength;
}
// 是否自動對屬性p_mustacheLength觸發KVO
+(BOOL)automaticallyNotifiesObserversOfP_mustacheLength {
// 默認返回YES
return YES;
}
@end
@interface KVOVC : UIViewController
@end
// 同一個屬性觀察了屢次,用來區分是哪一次觀察操做
// const*:不能改變內容
// const:不能改變地址
char const* const context_p_man_p_mustacheLength_1 = "context_p_man_p_mustacheLength_1";
char const* const context_p_man_p_mustacheLength_2 = "context_p_man_p_mustacheLength_2";
@interface KVOVC ()
@property (nonatomic, strong) Man* p_man;
@end
@implementation KVOVC
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
NSLog(@"-----------------------------------------");
NSLog(@"keyPath = %@", keyPath);
NSLog(@"object = %@", object);
NSLog(@"change = %@", change);
NSLog(@"context = %s", context);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.p_man = [[Man alloc] init];
[self.p_man addObserver:self
forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
options:NSKeyValueObservingOptionNew
context:(void*)context_p_man_p_mustacheLength_1];
[self.p_man addObserver:self
forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
options:NSKeyValueObservingOptionNew
context:(void*)context_p_man_p_mustacheLength_2];
// (觸發)set方法能夠觸發(不管是否重寫Man的p_mustacheLengthset方法。由於此時p_man的isa = NSKVONotifying_Man而不是Man 查看連接)
self.p_man.p_mustacheLength = 10;
// (觸發)kvc能夠觸發(kvc首先查找調用的也是set方法 查看連接)
[self.p_man setValue:@20 forKey:NSStringFromSelector(@selector(p_mustacheLength))];
// (不能觸發)直接修改爲員變量不能觸發(沒有走set方法)
[self.p_man set_P_mustacheLength:30];
// (觸發)手動觸發
[self.p_man set_P_mustacheLength_manual:40];
}
- (void)dealloc {
NSLog(@"%@-%s-%d", NSStringFromClass([self class]), __func__, __LINE__);
[self.p_man removeObserver:self
forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
context:(void*)context_p_man_p_mustacheLength_1];
[self.p_man removeObserver:self
forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
context:(void*)context_p_man_p_mustacheLength_2];
}
@end
複製代碼
自動鍵值觀察是使用isa- swizzle
技術實現的。顧名思義,isa
指針指向維護分派表的對象的類。這個分派表本質上包含指向類實現的方法以及其餘數據的指針。當觀察者爲一個對象的屬性註冊時,被觀察對象的isa
指針被修改,指向一箇中間類而不是真正的類。所以,isa
指針的值不必定反映實例的實際類。 您永遠不該該依賴isa
指針來肯定類的繼承關係。相反,您應該使用類方法來肯定對象實例的類。
二:在self.p_man添加KVO以後,查看其繼承關係。結果:isa = NSKVONotifying_Man,superClass = NSObject
官方文檔中說起作多的關鍵字就是isa
,Objective-C的消息機制就是經過isa查找方法的。其實在添加KVO以後,isa
已經替換成了NSKVONotifying_Man
。所以調用屬性的set方法的時候,根據isa找到的方法實際上是NSKVONotifying_Man
中的set方法。
KVO是基於runtime機制實現的,當某個實例的屬性第一次被觀察的時候,系統會在運行時期動態的建立一個該類的子類(類名=NSKVONotifying_XXX
)並將isa指針指向新建立的子類。在這個派生類中重寫全部被觀察屬性的set方法,在成員變量被改變前調用NSObject的willChangeValueForKey:
,被改變後調用didChangeValueForKey:
。從而致使observeValueForKey:ofObject:change:context
被調用。
KVO的這套實現機制中蘋果還偷偷重寫了class方法,讓咱們誤認爲仍是使用的當前類,從而達到隱藏生成的派生類
![]()
-(void) addObserver: forKeyPath: options: context: 這個部分就是觀察者的註冊了。經過如下類圖能夠很方便得看到,全部的類的KVO觀察都是經過infoTable管理的。以被觀察對象實例做key,GSKVOInfo對象爲value的形式保存在infoTable表裏,每一個被觀察者實例會對應多個keypath,每一個keypath會對應多個observer對象。順帶提一下,關於Notification的實現也相似,也是全局表維護通知的註冊監聽者和通知名。 GSKVOInfo的結構能夠看出來,一個keyPath能夠對應有多個觀察者。其中觀察對象的實例和option打包成GSKVOObservation對象保存在一塊兒。
// 手動觸發
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength {
[self willChangeValueForKey:@"p_mustacheLength"];
_p_mustacheLength = p_mustacheLength;
[self didChangeValueForKey:@"p_mustacheLength"];
}
複製代碼
若是要禁止KVO對某個屬性自動觸發,返回NO就能夠
// 是否自動對屬性p_mustacheLength觸發KVO
+(BOOL)automaticallyNotifiesObserversOfP_mustacheLength {
// 默認返回YES
return YES;
}
複製代碼
context
來removeObserver)@selector
彌補)從本身實現isa-swizzling到說一些Runtime的內容
爲啥能夠對nil對象發送消息?
NilTest
宏,判斷被髮送消息的對象是否爲nil
的。若是爲nil
,那就直接返回nil
。
weak
不管是用做property
修飾符仍是用來修飾一個變量的聲明其做用是同樣的,就是不增長新對象的引用計數,被釋放時也不會減小新對象的引用計數,同時在新對象被銷燬時,weak修飾的屬性或變量均會被設置爲nil,這樣能夠防止野指針錯誤,本文要講解的也正是這個特性,runtime如何將weak修飾的變量的對象在銷燬時自動置爲nil。
那麼
runtime
是如何實如今weak
修飾的變量的對象在被銷燬時自動置爲nil的呢?一個廣泛的解釋是:
runtime
對註冊的類會進行佈局,對於weak
修飾的對象會放入一個hash
表中。用weak
指向的對象內存地址做爲key,當此對象的引用計數爲0的時候會dealloc
,假如weak
指向的對象內存地址是a,那麼就會以a爲鍵在這個weak
表中搜索,找到全部以a爲鍵的weak
對象,從而設置爲nil