我的runtime学习笔记
0、簡介:
OC方法不同于C語言函數,屬于動態調用過程,在編譯的時候并不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。
至于其他理論上的東西不必講太多,編程講的就是實用性,只記錄一下各種使用場景。
使用運行時:(1)導入<objc/message.h> (2)Build Setting ->?搜索msg ->?設置屬性為No(取消消息機制的檢查)
一般見人寫runtime第一個必講消息機制,發消息是怎么回事
比如:新建一個dog類,頭文件定義三個外部調用方法,內部實現這里就不寫了,隨意
1 - (void)run; //對象方法 2 3 + (void)run; //類方法 4 5 - (void)eat:(NSString *)food; //帶參數的實例方法來到使用它的地方
1 Dog *d = [[Dog alloc] init]; 2 3 [d run]; 4 5 // 消息機制:任何方法調用,本質都是發送消息 6 // SEL:方法編號,根據方法編號就可以找到對應方法實現 7 [d performSelector:@selector(run)]; 8 9 // 讓d發送消息 10 objc_msgSend(d, @selector(run));基本可以理解為第3行代碼底層調用第7行,第7行底層調用第10行
順便也寫一下OC方法的大概調用流程吧:
簡單理解下就行了,看了下網上大神寫的資料,感覺沒必要記那么多繁復的東西
大概我理解就是:方法調用就是個發消息的過程,消息名即方法名,接收消息的對象即我們普遍認為的調用那個方法的對象。
SEL即方法編號,第3行調用方法之后去接收者那里找到對應的方法編號,通過方法編號找到方法映射表中的對應方法,最后根據方法映射表找到對應的方法實現。
來個圖更明顯點:
?
為避免誤讀,對上面的圖做個補充,以免為初學者產生一個錯誤的映射表內存模型:
類與對象相比只是多了實例變量和方法列表等,類和對象都是對象,分別是類對象和實例對象。
在class中的isa指針指向的是metaClass,metaClass中存放的是靜態成員變量和類方法(+開頭)。在object中的isa指針指向的是對應的類結構:Class,Class其中存放的是普通成員變量和實例方法(-開頭)。
所有的metaclass中isa指針都是指向根metaclass,而根metaclass則指向自身。根metaclass是通過繼承根類產生的,與根class結構體成員一致,不同的是根metaclass的isa指針指向自身。
實例對象存放對象方法的映射表,類對象存放類方法的映射表,因此上面的兩個方法其實是在不同的映射表中的。
上面的代碼是對象無參調用的方式,補充一下類方法和含參方法:
1 2 //類名調用類方法的本質就是類名轉換成類對象 3 [Dog run]; 4 5 // 獲取類對象 6 Class dogClass = [Dog class]; 7 8 //上面方法會調用這個 9 [dogClass performSelector:@selector(run)]; 10 11 //最終干的事還是發消息 12 objc_msgSend(dogClass, @selector(eat));最后說含參方法:
1 //含參方法 2 [d eat:@"789"]; 3 4 [d performSelector:@selector(eat:) withObject:@"456"]; 5 6 objc_msgSend(d, @selector(eat:),@"123");順便補充一下:含參方法performSelector最多只能傳入兩個參數,參數更多的時候多余的參數可以放到字典、數組里,這個不存在問題,demo里面有寫。
1、當你希望給系統方法擴展一些功能,并且保持原有的功能時;
直接上代碼:
分類:
#import <UIKit/UIKit.h>@interface UIColor (extension) + (__kindof UIColor *)at_redColor; @end#import "UIColor+extension.h" #import <objc/message.h>@implementation UIColor (extension)// 加載這個分類的時候就會調用load方法 + (void)load { // class:獲取這個類// SEL:獲取方法編號,根據SEL就能去對應的類找方法Method redColorMethod = class_getClassMethod([UIColor class], @selector(redColor));// 獲取類方法Method at_redColorMethod = class_getClassMethod([UIColor class], @selector(at_redColor));// 交換方法實現 method_exchangeImplementations(redColorMethod, at_redColorMethod);}//自己的方法 + (__kindof UIColor *)at_redColor {UIColor *color = [UIColor at_redColor];//添加一個打印功能NSLog(@"123");return color; } @end控制器:
1 self.view.backgroundColor = [UIColor redColor];會打印出"123"。
紅線表示方法交換;
2、動態給某個類添加方法(如果一個類方法非常多,加載類到內存的時候也比較耗費資源(感覺也耗不了什么資源,非要說的話跟懶加載的思想差不多吧,工作中基本沒這么玩過),需要給每個方法生成映射表)
?
1 #import "Dog.h" 2 #import <objc/message.h> 3 4 @implementation Dog 5 // 定義函數 6 // 默認一個方法都有兩個隱式參數, self:方法調用者, _cmd:調用方法的編號 7 void runImp(id self, SEL _cmd, NSString *param) 8 { 9 NSLog(@"調用run %@ %@ %@",self,NSStringFromSelector(_cmd),param); 10 } 11 12 // 動態添加方法,首先實現這個resolveInstanceMethod 13 // resolveInstanceMethod調用:當調用了沒有實現的方法沒有實現就會調用resolveInstanceMethod 14 // resolveInstanceMethod作用:知道哪些方法沒有實現,從而動態添加方法 15 // sel:沒有實現方法 16 + (BOOL)resolveInstanceMethod:(SEL)sel 17 { 18 // NSLog(@"%@",NSStringFromSelector(sel)); 19 // 動態添加run方法 20 21 if (sel == @selector(run:)) { 22 /* 23 cls:給哪個類添加方法 24 SEL:添加方法的方法編號是什么 25 IMP:方法實現,函數入口,函數名 26 types:方法類型 27 */ 28 // @:對象 :SEL 29 class_addMethod(self, sel, (IMP)runImp1, "v@:@"); 30 31 // 處理完 32 return YES; 33 34 } 35 return [super resolveInstanceMethod:sel]; 36 } 37 @end使用: (會調用runImp方法)
1 Dog *dog = [[Dog alloc] init]; 2 [dog performSelector:@selector(run:) withObject:@"跑啊"];說明:那兩個隱式參數可寫可不寫,types:方法類型只是對runImp方法的類型說明,具體說明可以搜官方文檔,v代表返回值void,@代表對象,:代表SEL。但是我故意把方法類型改錯了也就是和方法定義的真實類型不匹配的時候,運行也沒什么問題。
3、在分類中添加屬性?
?都知道你在分類中定義屬性的時候,只會生成get和set方法的聲明,不會生成實際的成員變量和方法實現。再順便說一句,分類沒有父類
第一個想到的辦法可能是自己定義一個全局變量,像這樣
代碼:
1 @interface NSObject (ATTest) 2 @property (nonatomic, copy) NSString *sex; 3 @end 1 @implementation NSObject (ATTest) 2 NSString *_sex; 3 - (void)setSex:(NSString *)sex { 4 _sex = sex; 5 } 6 - (NSString *)sex { 7 return _sex; 8 } 9 @end嘗試用一下:
1 NSObject *objText = [[NSObject alloc] init]; 2 objText.sex = @"女博士"; 3 NSLog(@"%@",objText.sex); 4 5 NSObject *objText1 = [[NSObject alloc] init]; 6 NSLog(@"%@",objText1.sex);兩個對象打印出來的都是“女博士”;
因此,給一個類聲明屬性,本質就是給這個類 和 屬性值 設置關聯,使類中的屬性指向屬性值的內存空間,而上面的做法是直接把這個值的內存空間添加到了這個類的內存空間。
下面是runtime做法:
1 @interface NSObject (ATDog) 2 @property (nonatomic, copy) NSString *name; 3 @end 1 #import "NSObject+ATDog.h" 2 #import <objc/message.h> 3 4 @implementation NSObject (ATDog) 5 - (void)setName:(NSString *)name { 6 //設置關聯屬性 7 /* 參數說明 8 object:添加屬性的對象 9 key:屬性名 10 value:屬性關聯的值 11 policy:屬性策略,就是strong,copy那些東西 12 */ 13 objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC); 14 } 15 - (NSString *)name { 16 //獲取關聯屬性 17 /* 參數說明 18 object:獲取屬性的對象 19 key:屬性名 20 */ 21 return objc_getAssociatedObject(self, @"name"); 22 } 23 @end使用:
1 NSObject *obj = [[NSObject alloc] init]; 2 obj.name = @"1"; 3 NSLog(@"%@",obj.name); 4 5 NSObject *obj1 = [[NSObject alloc] init]; 6 obj1.name = @"2"; 7 NSLog(@"%@",obj1.name);前面打印“1”,后面打印“2”;一個obj對象對應一個屬性
4、自動生成模型屬性代碼的工具類
不用每次手打屬性了,挺方便的
沒有訪問網絡,直接搞了個plist文件,反正都一樣的,plist文件是微博首頁的微博列表,具體見最下面demo
先復習一下讀取plist:
1 //讀取plist(最外層一個字典,字典里是個大數組) 2 NSString *path = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil]; 3 NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path]; 4 NSArray *arr = dict[@"statuses"]; 1 //調用分類方法打印模型屬性代碼 2 [NSObject createPropertyCodeWithDict:arr[2][@"user"]];打印user模型屬性是這個樣子的:
?
?現在看一下工具類是怎么實現的吧:先聲明一個類方法供外部調用,之后是實現,如果還有沒考慮到的類型自行添加就OK,具體實現就不說了,代碼已經很清楚了。也可以自己看一下下面的demo
1 @interface NSObject (ATProperty) 2 3 + (void)createPropertyCodeWithDict:(NSDictionary *)dict; 4 @end 1 @implementation NSObject (ATProperty) 2 + (void)createPropertyCodeWithDict:(NSDictionary *)dict { 3 NSMutableString *propertyCode = [NSMutableString string]; 4 5 //遍歷字典 6 [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 7 // NSLog(@"%@ %@", key, [obj class]); 8 9 NSString *code = nil; 10 //判斷不同類型的屬性定義代碼 11 if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) { 12 code = [NSString stringWithFormat:@"@property (nonatomic, copy) NSString *%@;", key]; 13 } else if ([obj isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) { 14 code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;", key]; 15 } else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]) { 16 code = [NSString stringWithFormat:@"@property (nonatomic, assign) NSUInteger %@;", key]; 17 } else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]) { 18 code = [NSString stringWithFormat:@"@property (nonatomic, copy) NSDictionary *%@;", key]; 19 } else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]) { 20 code = [NSString stringWithFormat:@"@property (nonatomic, copy) NSArray *%@;", key]; 21 } 22 //拼接字符串 23 [propertyCode appendFormat:@"\n%@\n", code]; 24 }]; 25 NSLog(@"%@",propertyCode); 26 } 27 @end5、字典轉模型
?1、直接用KVC
?1、導入plist文件,導入上面打印模型屬性代碼的工具類,新建status模型類,將打印出來的屬性代碼copy進去
打印屬性代碼的時候的一點問題
1 //先打印一下屬性代碼(有時候每個字典里不一定每個屬性都有,比如轉發微博的屬性,有的微博有轉發,有的則沒有,所以 2 // 用這個打印出的模型屬性放到模型里不一定是全部屬性都有的,還好如果崩潰的話會提醒你哪個沒有寫上去) 3 [NSObject createPropertyCodeWithDict:arr[0]];?2、解析plist
1 //解析plist 2 NSString *path = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil]; 3 NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path]; 4 NSArray *arr = dict[@"statuses"];3、轉模型
1 //轉模型 2 NSMutableArray *statuses = [NSMutableArray array]; 3 for (NSDictionary *statusDict in arr) { 4 ATStatus *status = [ATStatus statusWithDict:statusDict]; 5 [statuses addObject:status]; 6 } 7 NSLog(@"%@",statuses);在模型類中實現轉模型方法statusWithDict的代碼
1 //轉模型實現 2 + (ATStatus *)statusWithDict:(NSDictionary *)dict { 3 ATStatus *status = [[self alloc] init]; 4 //KVC 5 [status setValuesForKeysWithDictionary:dict]; 6 return status; 7 }4、還有些問題((1)服務器返回的字段不一定都用的到,但是現有方法如果不把服務器返回的所有字段屬性都寫到模型的話就會因為找不到key崩潰;(2)服務器經常返回屬性名id的字段,但是id在OC中是關鍵字,直接寫id可能會導致一些問題,所以最好在模型中給丫換個名字,比如ID)這兩個問題都能用下面的方法解決:
1 //解決KVC對應屬性崩潰找不到崩潰 2 - (void)setValue:(id)value forUndefinedKey:(NSString *)key { 3 //key:沒有找到的key 4 //value:沒有找到的key對應的value 5 6 //找不到id,把id的值賦給ID 7 if ([key isEqualToString:@"id"]) { 8 _ID = [value integerValue]; 9 } 10 //打印出沒找到的key 11 NSLog(@"unFind: key:%@ value:%@", key, value); 12 }實現效果:(最下有代碼demo)
打個斷點,看到已經成功轉模型。
?
?2、runtime轉模型
二者的區別:
? ? KVC:遍歷字典中所有key,去模型中查找有沒有對應的屬性名,沒找到就會崩
? ? runtime:遍歷模型中所有屬性名,去字典中查找,如果找不到也不會崩
?1、導入plist文件,導入上面打印模型屬性代碼的工具類,新建status模型類,將打印出來的屬性代碼copy進去
?2、新建一個分類做轉模型的工具類,具體用法注釋已經很詳細了,包括服務器返回id的問題(我把id一律在模型中定義為ID)
#import <objc/message.h>@implementation NSObject (ATObjectModel) + (__kindof NSObject *)objectModelWithDict:(NSDictionary *)dict {//創建對應模型類id obj = [[self alloc] init];//成員屬性數量unsigned int count = 0;//獲取模型類屬性列表數組Ivar *ivarList = class_copyIvarList(self, &count);//遍歷所有成員屬性for (int i = 0; i < count; i++) {//獲取成員屬性(Ivar)Ivar ivar = ivarList[i];//獲取成員屬性名NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];//去掉proprtyName前面的下劃線propertyName = [propertyName substringFromIndex:1];//獲取valueid value = nil;if ([propertyName isEqualToString:@"ID"]) {value = dict[@"id"];} else {value = dict[propertyName];}if (value) {//KVC賦值,不能傳空 [obj setValue:value forKey:propertyName];}}//C語言函數,ARC不會自動釋放,需要手動釋放 free(ivarList);return obj; } @end?
?
?使用就很簡單了:
1 NSString *path = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil]; 2 NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path]; 3 NSArray *array = dict[@"statuses"]; 4 // [NSObject createPropertyCodeWithDict:array[0]]; 5 6 NSMutableArray *statuses = [NSMutableArray array]; 7 for (NSDictionary *dict in array) { 8 //runtime轉模型 9 ATStatus *status = [ATStatus objectModelWithDict:dict]; 10 [statuses addObject:status]; 11 } 12 NSLog(@"%@",statuses);打個斷點查看statuses
OK。
?當然上面只是最外層的轉換,那么value如果是字典或者數組呢?
這就需要在上面的代碼里再加點東西:
首先是字典的情況:
1 //二級轉換 2 //獲取成員屬性類型 3 NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; 4 //如果value是字典(實質是字典類型,但是不是NSDictionary,因為如果類型還是NSDictionary沒有必要轉換) 5 if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) { 6 // NSLog(@"%@", propertyType); 7 //獲取屬性類型(剪切字符串@"@\"ATUser\"") 8 NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; 9 NSRange range = [type rangeOfString:@"\""]; 10 type = [type substringFromIndex:range.location + range.length]; 11 range = [type rangeOfString:@"\""]; 12 type = [type substringToIndex:range.location]; 13 14 Class modelClass = NSClassFromString(type); 15 if (modelClass) { //有對應的類型才需要轉 16 value = [modelClass objectModelWithDict:value]; 17 } 18 }數組的情況:
1 //三級轉換(如果value是數組,數組中再包含字典) 2 if ([value isKindOfClass:[NSArray class]]) { 3 //如果模型類實現了字典數組轉模型數組的協議 4 if ([self respondsToSelector:@selector(ModelClassInArray)]) { 5 //轉換成id類型,就能調用任何對象的方法 6 id idSelf = self; 7 //獲取數組中的模型 8 NSString *type = [idSelf ModelClassInArray][propertyName]; 9 Class modelClass = NSClassFromString(type); 10 11 NSMutableArray *dictArr = [NSMutableArray array]; 12 //遍歷數組 13 for (NSDictionary *dict in value) { 14 //轉模型 15 id model = [modelClass objectModelWithDict:dict]; 16 [dictArr addObject:model]; 17 } 18 //把模型數組賦值給value 19 value = dictArr; 20 } 21 }數組的情況需要轉模型的工具類提供一個協議供ATStatus遵守并實現,返回一個字典告訴工具類數組中是一個什么樣的字典,這樣工具類才知道給他轉成一個什么類的模型。
最后仍然是KVC賦值。
可以看見數組和字典中都轉換成model對象了,而且我特意在每層添加了id,經過上面對id的處理也沒有問題了(id的處理做的比較簡單,只要在定義模型類的時候把id的情況改成ID就行了,其他情況就沒有處理,比如ie也是個關鍵字呢?沒做到人家框架中可以讓使用者自定義模型屬性叫什么的程度)。
注意:當你把NSDictionary改成 ATUser 的時候注意策略如果不匹配也要改一下 ,我一開始就忘改了,結果用的是copy,然后就崩了,要改成strong。
?
6、利用runtime歸檔和解檔
同樣是用NSObject分類來做這個工具類;
先看頭文件提供的方法:
1 @interface NSObject (ATArchiver) 2 - (NSArray *)ignorePropertyNames; 3 - (void)encode:(NSCoder *)encoder; 4 - (void)decode:(NSCoder *)decoder; 5 @end具體實現:歸檔和解檔(比較簡單沒寫注釋,說一下大體流程:1:獲取調用類的屬性列表;2:遍歷屬性(propertyName = [propertyName substringFromIndex:1];這句代碼的意思是去掉成員變量名前面的下劃線)3:如果有需要忽略的屬性,忽略掉;4:歸解檔;5:釋放ivarList)
1 - (void)encode:(NSCoder *)encoder { 2 unsigned int count = 0; 3 Ivar *ivarList = class_copyIvarList([self class], &count); 4 for (int i = 0; i < count; i++) { 5 Ivar ivar = ivarList[i]; 6 NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)]; 7 propertyName = [propertyName substringFromIndex:1]; 8 9 if ([self respondsToSelector:@selector(ignorePropertyNames)]) { 10 if ([[self ignorePropertyNames] containsObject:propertyName]) { 11 continue; 12 } 13 } 14 id value = [self valueForKey:propertyName]; 15 [encoder encodeObject:value forKey:propertyName]; 16 } 17 free(ivarList); 18 } 19 - (void)decode:(NSCoder *)decoder { 20 unsigned int count = 0; 21 Ivar *ivarList = class_copyIvarList([self class], &count); 22 for (int i = 0; i < count; i++) { 23 Ivar ivar = ivarList[i]; 24 NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)]; 25 propertyName = [propertyName substringFromIndex:1]; 26 if ([self respondsToSelector:@selector(ignorePropertyNames)]) { 27 if ([[self ignorePropertyNames] containsObject:propertyName]) { 28 continue; 29 } 30 } 31 id value = [decoder decodeObjectForKey:propertyName]; 32 [self setValue:value forKey:propertyName]; 33 } 34 free(ivarList); 35 }控制器里面使用:
1 NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"archiver.plist"]; 2 3 Dog *d = [[Dog alloc] init]; 4 d.name = @"旺財"; 5 d.age = 12; 6 //歸檔 7 [NSKeyedArchiver archiveRootObject:d toFile:path]; 8 //解檔 9 Dog *d1 = [NSKeyedUnarchiver unarchiveObjectWithFile:path]; 10 NSLog(@"name:%@",d1.name); 11 NSLog(@"age:%ld",d1.age);效果:
更具體可以看github源碼demo
my github:https://github.com/alan12138/runtime
?
?
?
?
?
?
?
?
?
?
?
?
?
?
轉載于:https://www.cnblogs.com/alan12138/p/5624314.html
總結
以上是生活随笔為你收集整理的我的runtime学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在visual studio code中
- 下一篇: 推销员(codevs 5126)