Runtime詳解(下)

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. */
};

下面實現一個UIViewCategory添加自定義屬性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爲一個類添加方法

(包括方法名稱(SEL)和方法的實現(IMP)),返回值爲BOOL類型,表示方法是否成功添加。 須要注意的地方是class_addMethod會添加一個覆蓋父類的實現,但不會取代原有類的實現。也就是說若是class_addMethod返回YES,說明子類中沒有方法originalSelector,經過class_addMethod爲其添加了方法originalSelector,並使其實現(IMP)爲咱們想要替換的實現。

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

App跳轉到某具備網絡請求的界面時,爲了用戶體驗效果常會添加加載欄或進度條來顯示當前請求狀況或進度。這種界面都會存在這樣一個問題,在請求較慢時,用戶手動退出界面,這時候須要去除加載欄。
固然能夠依次在每一個界面的viewWillDisappear方法中添加去除方法,但若是相似的界面過多,一味的複製粘貼也不是方法。這時候就能體現Method Swizzling的做用了,咱們能夠替換系統的viewWillDisappear方法,使得每當執行該方法時即自動去除加載欄。

#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方法中?
在Objective-C runtime會自動調用兩個類方法,分別爲+load與+ initialize。+load 方法是在類被加載的時候調用的,也就是必定會被調用。而+initialize方法是在類或它的子類收到第一條消息以前被調用的,這裏所指的消息包括實例方法和類方法的調用。也就是說+initialize方法是以懶加載的方式被調用的,若是程序一直沒有給某個類或它的子類發送消息,那麼這個類的+initialize方法是永遠不會被調用的。此外+load方法還有一個很是重要的特性,那就是子類、父類和分類中的+load方法的實現是被區別對待的。換句話說在 Objective-C runtime自動調用+load方法時,分類中的+load方法並不會對主類中的+load方法形成覆蓋。綜上所述,+load 方法是實現 Method Swizzling 邏輯的最佳「場所」。
(2) 爲何方法要在dispatch_once中執行?
方法交換應該要線程安全,並且保證在任何狀況下(多線程環境,或者被其餘人手動再次調用+load方法) 只交換一次,防止再次調用又將方法交換回來。除非只是臨時交換使用,在使用完成後又交換回來。 最經常使用的解決方案是在+load方法中使用dispatch_once來保證交換是安全的。以前有讀者反饋+load方法自己即爲線程安全,爲何仍需添加dispatch_once,其緣由就在於+load方法自己沒法保證其中代碼只被執行一次。
 
實例2.防止按鈕重複暴力點擊
程序中大量按鈕沒有作連續響應的校驗,連續點擊出現了不少沒必要要的問題,例如發表帖子操做,用戶手快點擊屢次,就會致使同一帖子發佈屢次。
#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的本類,且 KVONSKVONotifying_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 在調用存取方法以後總調用
}

5.消息轉發(熱更新)解決Bug(JSPatch)

JSPatch 是一個 iOS 動態更新框架,只需在項目中引入極小的引擎,就可使用 JavaScript 調用任何 Objective-C 原生接口,得到腳本語言的優點:爲項目動態添加模塊,或替換項目原生代碼動態修復 bug。

 

6.實現NSCoding的自動歸檔和自動解檔

runtime提供的函數遍歷Model自身全部屬性,並對屬性進行encodedecode操做。
核心方法:在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的知識點以及經常使用場景,博客會持續更改,歡迎指正。

相關文章
相關標籤/搜索