Runtime應用git
1.Runtime 交換方法github
應用場景:當第三方框架或者系統原生方法功能不能知足咱們的時候,咱們能夠在保持系統原有功能的基礎上,添加額外的功能。安全
需求:加載一張圖片直接用系統的[UIImage imageNamed:@""];是沒法知道到底有沒有加載成功。給系統的imageNamed添加額外功能,(是否加載圖片成功,以及加載未完成的時候,用模糊的該照片代替)網絡
方法一:繼承系統的類,重寫方法:(每次使用都須要導入)多線程
方法二:使用runtime,交換方法app
實現步驟:框架
(1)給系統的方法添加分類函數
(2)本身實現一個帶有擴展功能的方法ui
(3)交換方法,只須要交換一次atom
下面是案例代碼:
- (void)viewDidLoad { [super viewDidLoad]; // 方案一:先搞個分類,定義一個能加載圖片而且能打印的方法+ (instancetype)imageWithName:(NSString *)name; // 方案二:交換 imageNamed 和 ln_imageNamed 的實現,就能調用 imageNamed,間接調用 ln_imageNamed 的實現。 UIImage *image = [UIImage imageNamed:@"123"]; } #import <objc/message.h> @implementation UIImage (Image) /** load方法: 把類加載進內存的時候調用,只會調用一次 方法應先交換,再去調用 */ + (void)load { // 1.獲取 imageNamed方法地址 // class_getClassMethod(獲取某個類的方法) Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:)); // 2.獲取 ln_imageNamed方法地址 Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:)); // 3.交換方法地址,至關於交換實現方式;「method_exchangeImplementations 交換兩個方法的實現」 method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod); } /** 看清楚下面是不會有死循環的 調用 imageNamed => ln_imageNamed 調用 ln_imageNamed => imageNamed */ // 加載圖片 且 帶判斷是否加載成功 + (UIImage *)ln_imageNamed:(NSString *)name { UIImage *image = [UIImage ln_imageNamed:name]; if (image) { NSLog(@"runtime添加額外功能--加載成功"); } else { NSLog(@"runtime添加額外功能--加載失敗"); } return image; } /** 不能在分類中重寫系統方法imageNamed,由於會把系統的功能給覆蓋掉,並且分類中不能調用super 因此第二步,咱們要 本身實現一個帶有擴展功能的方法. + (UIImage *)imageNamed:(NSString *)name { } */ @end
總結:咱們所作的就是在方法調用流程第三步的時候,交換兩個方法地址指向。並且咱們改變指向要在系統的imageNamed:方法調用前,因此將代碼寫在了分類的load方法裏,最後當運行的時候系統的方法就會去找咱們實現的方法。
2.動態添加屬性
給一個類聲明屬性,其實本質就是給這個類添加關聯,並非直接把這個值的內存空間添加到類存空間。
給系統的類添加屬性的時候,可使用runtime動態添加屬性。
註解:系統NSObject添加一個分類,咱們知道在分類中不能添加成員屬性的,雖然咱們用了@property,可是僅僅是自動生成get和set方法的聲明,並無帶下滑線的屬性和方法實現生成。咱們能夠經過runtime就能夠作到給它方法的實現。
需求:給系統NSObject動態添加屬性name字符串。
案例以下:
@interface NSObject (Property) // @property分類:只會生成get,set方法聲明,不會生成實現,也不會生成下劃線成員屬性 @property NSString *name; @property NSString *height; @end @implementation NSObject (Property) - (void)setName:(NSString *)name { // objc_setAssociatedObject(將某個值跟某個對象關聯起來,將某個值存儲到某個對象中) // object:給哪一個對象添加屬性 // key:屬性名稱 // value:屬性值 // policy:保存策略 objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSString *)name { return objc_getAssociatedObject(self, @"name"); } // 調用 NSObject *objc = [[NSObject alloc] init]; objc.name = @"123"; NSLog(@"runtime動態添加屬性name==%@",objc.name);
//結果以下: 2016-02-17 19:37:10.530 runtime[12761:543574] runtime動態添加屬性--name == 123
其實給屬性賦值的本質,就是讓屬性與一個對象產生關聯,因此要給NSObject的分類的name屬性賦值就是讓那個name和NSObject產生關聯,而Runtime能夠作到這一點。
下面再舉個例子:
關聯對象(objective-C Associated objects)給分類增長屬性
關聯對象Runtime提供了幾個接口:
//關聯對象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) //獲取關聯的對象 id objc_getAssociatedObject(id object, const void *key) //移除關聯的對象 void objc_removeAssociatedObjects(id object)
參數註釋:
id object:被關聯的對象 const void *key:關聯的key,要求惟一 id value:關聯的對象 objc_AssociationPolicy policy:內存管理的策略
內存管理的策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */ OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object. * The association is made atomically. */ OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied. * The association is made atomically. */ };
下面實現一個UIView
的Category
添加自定義屬性defaultColor
。
#import "ViewController.h" #import "objc/runtime.h" @interface UIView (DefaultColor) @property (nonatomic, strong) UIColor *defaultColor; @end @implementation UIView (DefaultColor) @dynamic defaultColor; static char kDefaultColorKey; - (void)setDefaultColor:(UIColor *)defaultColor { objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)defaultColor { return objc_getAssociatedObject(self, &kDefaultColorKey); } @end @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. UIView *test = [UIView new]; test.defaultColor = [UIColor blackColor]; NSLog(@"%@", test.defaultColor); } @end
結果以下:
打印結果: 2018-04-01 15:41:44.977732+0800 ocram[2053:63739] UIExtendedGrayColorSpace 0 1
從打印結果來看:咱們成功在分類上添加一個屬性,實現了它的setter和getter方法。
經過關聯對象實現的屬性的內存管理也是有ARC
管理的,因此咱們只須要給定適當的內存策略就好了,不須要操心對象的釋放。
3.方法魔法:(俗稱黑魔法)-method swizzling
簡單的說就是進行方法交換
在Objective-C中調用一個方法,實際上是向一個對象發送消息,查找消息的惟一依據是selector
的名字。利用Objective-C
的動態特性,能夠實如今運行時偷換selector
對應的方法實現,達到給方法掛鉤的目的。
每個類都有一個方法列表,存放着方法的名字實現的映射關係,selector的本質就是方法名,IMP有點相似函數指針,指向具體的method實現,經過selector就能夠找到對應的IMP。
交換方法的幾種實現方式:
(1)利用method_exchangeImplementations 交換兩個方法的實現
(2)利用class_replaceMethod替換方法的實現。
(3)利用method_setImplementation來直接設置某個方法的IMP。
目前已更新實例彙總:
.替換ViewController生命週期方法
.解決獲取索引、添加、刪除元素越界崩潰問題
.防止按鈕重複暴力點擊
.全局更換控件初始效果
.App熱修復
.全局修改導航欄後退(返回)按鈕
Method Swizzling通用方法封裝
咱們能夠將Method Swizzling功能封裝爲類方法,做爲NSObject的類別。
#import <Foundation/Foundation.h> #import <objc/runtime.h> @interface NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector; @end
#import "NSObject+Swizzling.h" @implementation NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getInstanceMethod(class, originalSelector); //替換原有方法的新方法 Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); //先嚐試給源SEL添加IMP,這裏是爲了不源SEL沒有實現IMP的狀況 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:說明源SEL沒有實現IMP,將源SEL的IMP替換到交換SEL的IMP class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else {//添加失敗:說明源SEL已經有IMP,直接將兩個SEL的IMP交換便可 method_exchangeImplementations(originalMethod, swizzledMethod); } } @end
解析:爲何要添加didAddMethod判斷?
先嚐試添加原SEL實際上是爲了作一層保護,由於若是這個類若是沒有實現originalSelector,但其父類實現了,那class_getInstanceMethod會返回父類的方法。這樣method_exchangeImplementations替換的是父類的那個方法。這樣method_exchangeImplementations替換的是父類的那個方法,這固然不是咱們想要。因此咱們先嚐試添加orginalSelector,若是已經存在,再用method_exchangeImplement把原方法的實現跟新的方法實現給交換掉。
大概的意思就是咱們能夠經過class_addMethod爲一個類添加方法
class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
同時再將原有的實現(IMP)替換到swizzledMethod方法上,
class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
從而實現了方法的交換,而且未影響父類方法的實現。反之若是class_addMethod返回NO,說明子類中自己就具備方法originalSelector的實現,直接調用交換便可。
method_exchangeImplementations(originalMethod, swizzledMethod);
實例1:替換ViewController
#import "UIViewController+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIViewController (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)]; }); } - (void)sure_viewWillDisappear:(BOOL)animated { [self sure_viewWillDisappear:animated]; [SVProgressHUD dismiss]; }
⚠️補充知識點
(1) 爲何方法交換調用+load方法中?#import <UIKit/UIKit.h> //默認時間間隔 #define defaultInterval 1 @interface UIButton (Swizzling) //點擊間隔 @property (nonatomic, assign) NSTimeInterval timeInterval; //用於設置單個按鈕不須要被hook @property (nonatomic, assign) BOOL isIgnore; @end
#import "UIButton+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIButton (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)]; }); } - (NSTimeInterval)timeInterval{ return [objc_getAssociatedObject(self, _cmd) doubleValue]; } - (void)setTimeInterval:(NSTimeInterval)timeInterval{ objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } //當按鈕點擊事件sendAction 時將會執行sure_SendAction - (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{ if (self.isIgnore) { //不須要被hook [self sure_SendAction:action to:target forEvent:event]; return; } if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) { self.timeInterval =self.timeInterval == 0 ?defaultInterval:self.timeInterval; if (self.isIgnoreEvent){ return; }else if (self.timeInterval > 0){ [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval]; } } //此處 methodA和methodB方法IMP互換了,實際上執行 sendAction;因此不會死循環 self.isIgnoreEvent = YES; [self sure_SendAction:action to:target forEvent:event]; } //runtime 動態綁定 屬性 - (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{ // 注意BOOL類型 須要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯,不然set方法會賦值出錯 objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnoreEvent{ //_cmd == @select(isIgnore); 和set方法裏一致 return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setIsIgnore:(BOOL)isIgnore{ // 注意BOOL類型 須要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯,不然set方法會賦值出錯 objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnore{ //_cmd == @select(isIgnore); 和set方法裏一致 return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)resetState{ [self setIsIgnoreEvent:NO]; } @end
實例3.全局修改導航欄(返回)按鈕
iOS默認的返回按鈕樣式以下,默認爲藍色左箭頭,文字爲上一界面標題文字。
閒話少說,咱們建立基於UINavigationItem
的類別,在其load
方法中替換方法backBarButtonItem
#import "UINavigationItem+Swizzling.h" #import "NSObject+Swizzling.h" static char *kCustomBackButtonKey; @implementation UINavigationItem (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(backBarButtonItem) bySwizzledSelector:@selector(sure_backBarButtonItem)]; }); } - (UIBarButtonItem*)sure_backBarButtonItem { UIBarButtonItem *backItem = [self sure_backBarButtonItem]; if (backItem) { return backItem; } backItem = objc_getAssociatedObject(self, &kCustomBackButtonKey); if (!backItem) { backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:NULL]; objc_setAssociatedObject(self, &kCustomBackButtonKey, backItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return backItem; } @end
這裏進行將返回按鈕的文字清空操做,其餘需求樣式你們也可隨意替換,如今再次運行程序,就會發現全部的返回按鈕均只剩左箭頭,並右滑手勢依然有效。如圖所示
4.KVO實現
提供了一種當其它對象屬性被修改的時候能通知當前對象的機制。
KVO
的實現依賴於 Objective-C
強大的 Runtime
,當觀察某對象 A
時,KVO
機制動態建立一個對象A
當前類的子類,併爲這個新的子類重寫了被觀察屬性 keyPath
的 setter
方法。setter
方法隨後負責通知觀察對象屬性的改變情況。
Apple
使用了
isa-swizzling
來實現
KVO
。當觀察對象
A
時,
KVO
機制動態建立一個新的名爲:
NSKVONotifying_A
的新類,該類繼承自對象A的本類,且
KVO
爲
NSKVONotifying_A
重寫觀察屬性的
setter
方法,
setter
方法會負責在調用原
setter
方法以前和以後,通知全部觀察對象屬性值的更改狀況。
KVO
的鍵值觀察通知依賴於 NSObject 的兩個方法:
willChangeValueForKey:
和
didChangeValueForKey:
,在存取數值的先後分別調用 2 個方法:
willChangeValueForKey:
被調用,通知系統該
keyPath
的屬性值即將變動;
didChangeValueForKey:
被調用,通知系統該
keyPath
的屬性值已經變動;以後,
observeValueForKey:ofObject:change:context:
也會被調用。且重寫觀察屬性的
setter
方法這種繼承方式的注入是在運行時而不是編譯時實現的。
KVO
爲子類的觀察者屬性重寫調用存取方法的工做原理在代碼中至關於:
- (void)setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; //KVO 在調用存取方法以前總調用 [super setValue:newName forKey:@"name"]; //調用父類的存取方法 [self didChangeValueForKey:@"name"]; //KVO 在調用存取方法以後總調用 }
JSPatch 是一個 iOS 動態更新框架,只需在項目中引入極小的引擎,就可使用 JavaScript 調用任何 Objective-C 原生接口,得到腳本語言的優點:爲項目動態添加模塊,或替換項目原生代碼動態修復 bug。
6.實現NSCoding的自動歸檔和自動解檔
用runtime
提供的函數遍歷Model
自身全部屬性,並對屬性進行encode
和decode
操做。
核心方法:在Model
的基類中重寫方法:
- (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) { unsigned int outCount; Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) { Ivar ivar = ivars[i]; NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; [self setValue:[aDecoder decodeObjectForKey:key] forKey:key]; } } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { unsigned int outCount; Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) { Ivar ivar = ivars[i]; NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; [aCoder encodeObject:[self valueForKey:key] forKey:key]; } }
上面就是Runtime的知識點以及經常使用場景,博客會持續更改,歡迎指正。