Runtime重要知識點以及使用場景(編譯過程和Runtime兩者如何連線的)

Deft_MKJing宓珂璟發表於2016-12-22

Runtime概念介紹

Objective-C 擴充套件了 C 語言,並加入了物件導向特性和 Smalltalk 式的訊息傳遞機制。而這個擴充套件的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 物件導向和動態機制的基石,因此它是一門動態語言。

高階程式語言想要成為可執行檔案需要先編譯為組合語言再彙編為機器語言,機器語言也是計算機能夠識別的唯一語言,但是OC並不能直接編譯為組合語言,而是要先轉寫為純C語言再進行編譯和彙編的操作,從OC到C語言的過渡就是由runtime來實現的。然而我們使用OC進行物件導向開發,而C語言更多的是程式導向開發,這就需要將物件導向的類轉變為程式導向的結構體。

首先語言需要編譯,總結了一套編譯全過程,可以看下Runtime是在什麼時候參與的以及編譯過程詳細介紹 Command + B 編譯全過程

總結:
dyld是動態連結器,啟動編譯後的二進位制檔案(所有.o檔案的集合),然後dyld參與進來,初始化二進位制,把一些動態庫,例如Fundation,UIKit,Runtime
lib庫,GCD,Block等連結進來,然後修正地址偏移(ASLR,Apple為了防止攻擊,可執行檔案和動態連結庫的每次載入地址都不同),然後dyld把這些符號,類,方法等載入進記憶體,runtime向dyld註冊了回撥,當全部載入進記憶體的時候,交給runtime來處理,runtime根據載入進來的類遞迴層級遍歷,根據runtime中的objc定義的結構體載入成對應的格式(例如Class結構體,Objc結構體,objc_msgSend方法呼叫等)以及呼叫+load方法完成初始化,至此,可執行檔案中和動態連結庫的符號(Class,Protocol,SEL,IMP)按runtime格式載入進記憶體了,後續的那些方法例如動態新增Class和Swizzle才會生效

底層結構(物件導向和訊息分發)

類,物件,Method結構體介紹

//物件
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
//類
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;
//方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
//方法
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// A pointer to an instance of a class.
typedef struct objc_object *id;
Class是一個指向objc_class(類)結構體的指標,而id是一個指向objc_object(物件)結構體的指標。 objec_objct(物件)中isa指標指向的類結構稱為objc_class(該物件的類),其中存放著普通成員變數與物件方法 (“-”開頭的方法)。 objc_class(類)中isa指標指向的類結構稱為metaclass(該類的元類),其中存放著static型別的成員變數與static型別的方法 (“+”開頭的方法)
/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

objec_object(物件)結構體中只有isa一個成員屬性,指向objec_class(該物件的類)。

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;   //isa指標,指向metaclass(該類的元類)
 
#if !__OBJC2__
 
    Class super_class   //指向objc_class(該類)的super_class(父類)
 
    const char *name    //objc_class(該類)的類名
 
    long version        //objc_class(該類)的版本資訊,初始化為0,可以通過runtime函式class_setVersion和class_getVersion進行修改和讀取
 
    long info           //一些標識資訊,如CLS_CLASS表示objc_class(該類)為普通類。ClS_CLASS表示objc_class(該類)為metaclass(元類)
 
    long instance_size  //objc_class(該類)的例項變數的大小
 
    struct objc_ivar_list *ivars    //用於儲存每個成員變數的地址
 
    struct objc_method_list **methodLists   //方法列表,與info標識關聯
 
    struct objc_cache *cache        //指向最近使用的方法的指標,用於提升效率
 
    struct objc_protocol_list *protocols    //儲存objc_class(該類)的一些協議
#endif
 
} OBJC2_UNAVAILABLE;

objec_class(類)比objec_object(物件)的結構體中多了很多成員,上面就是介紹各個成員的作用。

所有的metaclass(元類)中isa指標都是指向根metaclass(元類),而根metaclass(元類)中isa指標則指向自身。

根metaclass(元類)中的superClass指標指向根類,因為根metaclass(元類)是通過繼承根類產生的。
在這裡插入圖片描述

作用

  • 當我們呼叫某個物件的物件方法時,它會首先在自身isa指標指向的objc_class(類)的methodLists中查詢該方法,如果找不到則會通過objc_class(類)的super_class指標找到其父類,然後從其methodLists中查詢該方法,如果仍然找不到,則繼續通過
    super_class向上一級父類結構體中查詢,直至根class;
  • 當我們呼叫某個類方法時,它會首先通過自己的isa指標找到metaclass(元類),並從其methodLists中查詢該類方法,如果找不到則會通過metaclass(元類)的super_class指標找到父類的metaclass(元類)結構體,然後從methodLists中查詢該方法,如果仍然找不到,則繼續通過super_class向上一級父類結構體中查
    找,直至根metaclass(元類);
  • 這裡有個細節就是要說執行的時候編譯器會將程式碼轉化為objc_msgSend(obj, @selector
    (makeText)),在objc_msgSend函式中首先通過obj(物件)的isa指標找到obj(物件)對應的class(類)。在class(類)中先去cache中通過SEL(方法的編號)查詢對應method(方法),若cache中未找到,再去methodLists中查詢,若methodists中未找到,則去superClass中查詢,若能找到,則將method(方法)加入到cache中,以方便下次查詢,並通過method(方法)中的函式指標跳轉到對應的函式中去執行。

具體的理解就是網上那張圖,理解的struct結構體就很容易理解了。這裡寫連結內容

類物件(objc_class)

Objective-C類是由Class型別來表示的,它實際上是一個指向objc_class結構體的指標。

typedef struct objc_class *Class;

objc/runtime.h中定義的結構如下

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

struct objc_class結構體定義了很多變數
結構體裡儲存了指向父類的指標、類的名字、版本、例項大小、例項變數列表、方法列表、快取、遵守的協議列表等,
類物件就是一個結構體struct objc_class,這個結構體存放的資料稱為後設資料(metadata),

凡是首地址是*isa的struct指標,都可以被認為是objc中的物件,因此我們稱之為類物件,類物件在編譯期產生用於建立例項物件,是單例。

例項物件(objc_object)

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

例項物件的結構很簡單,就是一個指向類或者元類的isa指標。例項化資訊都存放在類或者元類中。

元類(Meta Class)

元類(Meta Class)是一個類物件的類。
在上面我們提到,所有的類自身也是一個物件,我們可以向這個物件傳送訊息(即呼叫類方法)。
為了呼叫類方法,這個類的isa指標必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念,元類中儲存了建立類物件以及類方法所需的所有資訊。
任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指標是指向它自己。

Method(objc_method)

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}      

我們來看下objc_method這個結構體的內容:

SEL method_name 方法名
char *method_types 方法型別
IMP method_imp 方法實現

在這個結構體重,我們已經看到了SEL和IMP,說明SEL和IMP其實都是Method的屬性。

SEL(objc_selector)

objc.h
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

其實selector就是個對映到方法的C字串,你可以用 Objective-C 編譯器命令@selector()或者 Runtime 系統的sel_registerName函式來獲得一個 SEL 型別的方法選擇器。
selector既然是一個string,我覺得應該是類似className+method的組合,命名規則有兩條:

同一個類,selector不能重複
不同的類,selector可以重複

這也帶來了一個弊端,我們在寫C程式碼的時候,經常會用到函式過載,就是函式名相同,引數不同,但是這在Objective-C中是行不通的,因為selector只記了method的name,沒有引數,所以沒法區分不同的method。

比如:

- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

複製程式碼是會報錯的。
我們只能通過命名來區別:

- (void)caculateWithInt(NSInteger)num;
- (void)caculateWithFloat(CGFloat)num;

複製程式碼在不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變數型別不同也會導致它們具有相同的方法選擇器。

IMP

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

就是指向最終實現程式的記憶體地址的指標。
在iOS的Runtime中,Method通過selector和IMP兩個屬性,實現了快速查詢方法及實現,相對提高了效能,又保持了靈活性。

類快取(objc_cache)

為了避免每次查詢帶來的效能問題,一般都會設計快取結構,所以類實現一個快取,每當你搜尋一個類分派表,並找到相應的選擇器,它把它放入它的快取。所以當objc_msgSend查詢一個類的選擇器,它首先搜尋類快取。這是基於這樣的理論:如果你在類上呼叫一個訊息,你可能以後再次呼叫該訊息。
為了加速訊息分發, 系統會對方法和對應的地址進行快取,就放在上述的objc_cache,所以在實際執行中,大部分常用的方法都是會被快取起來的,Runtime系統實際上非常快,接近直接執行記憶體地址的程式速度。這裡其實就是和NSDictionary一樣,實現了Hash表來儲存,如果用物件一樣的開放定址法,他的時間複雜度可以達到O(1)

Category(objc_category)

/// An opaque type that represents a category.
typedef struct objc_category *Category;
struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}             

清晰明瞭,其實某個角度看,這隻能新增類方法和例項方法以及協議,單單根據底層結構來看,屬性得用objc_setAssociatedObject掛載上去,不能新增成員變數

Runtime訊息轉發

根據上面物件導向中的訊息分發,首先會根據類方法還是例項方法進行isa指標訊息橫向查詢,然後在進行繼承關係縱向查詢,如果都沒找到,doesNotRecognizeSelector:方法報unrecognized selector錯,丟擲異常,那麼當橫向縱向都沒有的時候就會進入如下訊息轉發流程,進行最後的補救措施
1.動態方法解析
2.備用接收者
3.完整訊息轉發
在這裡插入圖片描述
第一步:動態方法解析並新增
首先,Objective-C執行時會呼叫 +resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函式實現。如果你新增了函式並返回YES, 那執行時系統就會重新啟動一次訊息傳送的過程。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //執行foo函式
    [self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo:)) {//如果是執行foo函式,就動態解析,指定新的IMP
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");//新的foo函式
}

一樣的例子可以看下面第五點,如果這裡resolve返回NO,就會進入第二步forwardingTargetForSelector

第二步:備用接收者

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函式
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //執行foo函式
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;//返回YES,進入下一步轉發
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [Person new];//返回Person物件,讓Person物件接收這個訊息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end

可以看到我們通過forwardingTargetForSelector把當前ViewController的方法轉發給了Person去執行了。列印結果也證明我們成功實現了轉發。

第三步:完整訊息轉發

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函式
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //執行foo函式
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;//返回YES,進入下一步轉發
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;//返回nil,進入下一步轉發
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//簽名,進入forwardInvocation
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }
    else {
        [self doesNotRecognizeSelector:sel];
    }

}

@end

從列印結果來看,我們實現了完整的轉發。通過簽名,Runtime生成了一個物件anInvocation,傳送給了forwardInvocation,我們在forwardInvocation方法裡面讓Person物件去執行了foo函式。簽名引數v@:怎麼解釋呢,這裡蘋果文件Type Encodings有詳細的解釋。
以上就是Runtime的三次轉發流程

上面的流程摘錄自掘金的一片文章

簡單總結下Runtime

Runtime是用C++編寫的執行時庫,為 C 新增了物件導向的能力,這也是區別於C語言這樣靜態語言的重要特性,C語言函式呼叫在編譯期間就已經決定了,在編譯完成之後直接順序執行。OC是動態語言(多型和執行時),函式呼叫就是訊息傳送,在編譯期間不知道具體呼叫哪個函式,所以Runtime就是去解決執行時找到呼叫哪個方法的問題
多型:OC中的多型是不同物件對同一訊息的不同響應方式,子類通過重寫父類的方法來改變同一方法的實現,體現多型性
總結:三個能力
首地址是isa的struct指標,都可以被認為是objc中的物件
1.物件導向能力 (繼承,封裝,多型)
2.動態載入類資訊,進行訊息的分發 (isa橫向查詢,繼承縱向查詢,找不到呼叫_objc_msgForward用於訊息轉發)
3.訊息轉發 1.動態方法解析並動態新增方法 2.備用接收者 3.完整訊息轉發 4.丟擲異常

順序:
instance—>class—->method—–>sel—(Cache)–>imp—->實現函式(找不到就進入轉發流程)

例項物件中存放isa指標,有isa指標就可以找到實力變數對應的所屬類,類中存放著例項方法列表,SEL為Key,IMP為value,在編譯期間,根據方法名會生成一個唯一的int識別符號,這就是SEL標識,IMP就是函式指標,指向最終函式實現。runtime核心就是objc_msgSend函式,通過給SEL傳遞訊息,找到匹配的IMP

程式碼介紹

因為Objc是一門動態語言,所以它總是想辦法把一些決定工作從編譯連線推遲到執行時。也就是說只有編譯器是不夠的,還需要一個執行時系統 (runtime system) 來執行編譯後的程式碼。這就是 Objective-C Runtime 系統存在的意義,它是整個Objc執行框架的一塊基石。

1.OC程式碼如何轉換成runtime執行時

/*
     * 以下程式碼runtime轉換如下  clang -rewrite-objc 檔名,例如我們要轉換的是FViewcontroller的檔案,會出現一個.cpp字尾的檔案,開啟拉倒最後就是轉換程式碼
     */
    People *p1 = [[People alloc] init];
    [p1 eat:@"屎"];
    
    People *p1 = ((People *(*)(id, SEL))(void *)objc_msgSend)((id)((People *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("People"), ), sel_registerName("init"));
    
    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p1, sel_registerName("eat:"), (NSString *)&__NSConstantStringImpl__var_folders_c2_z9kbf6xs7x3g3qc4vwt8b10w0000gn_T_main_c68e89_mi_1);

注:這裡只是簡單介紹下如何轉換,如果需要詳細的可以傳送門

2.看下如何用runtime來建立物件

// 換這個方式建立下
    // 1.objc_getClass("People") 獲取類
    // 2.sel_registerName("alloc") 登錄檔中拿方法 可以用成@selector
    // 3.objc_msgSend(類名, @selector(), 引數);
    
    // 根據類名獲取到類
    Class class = objc_getClass("People");
    // 這裡直接寫會報錯 請將 Build Setting -> Enable Strict Checking of objc_msgSend Calls 改為 NO
    People *people = objc_msgSend(class,@selector(alloc));
    // runtime初始化物件
    people = objc_msgSend(people, @selector(init));
    // 方法呼叫  這裡的類方法不暴露都能呼叫到  但是這裡會出現警告 最終還是能到方法列表裡面找到該方法列印
    objc_msgSend(people, @selector(eat:),@"屎");
    
    // 單引數
    People *people2 = objc_msgSend(objc_msgSend(objc_getClass("People"), sel_registerName("alloc")), sel_registerName("init"));
    objc_msgSend(people2, sel_registerName("eat:"),@"香蕉");
    
    // 多引數
    People *people3 = objc_msgSend(objc_msgSend(objc_getClass("People"), sel_registerName("alloc")), sel_registerName("init"));
    objc_msgSend(people3, sel_registerName("play:age:block:"),@"美女",18,[^{NSLog(@"我來了");} copy]);
    
    // 列印如下
//    2016-12-19 16:45:34.974 RuntimeKJ[11526:324368] People eat 屎
//    2016-12-19 16:45:34.974 RuntimeKJ[11526:324368] People eat 香蕉
//    2016-12-19 16:45:34.975 RuntimeKJ[11526:324368] name = 美女,age = 18,<__NSGlobalBlock__: 0x10ecd90a0>
//    2016-12-19 16:45:34.975 RuntimeKJ[11526:324368] 我來了美女

這裡的People物件是隻是在.m檔案中實現了方法而已,可以不在.h裡面實現,都能進行runtime呼叫 從這些定義中可以看出傳送一條訊息也就 objc_msgSend 做了什麼事。舉 objc_msgSend(obj, play) 這個例子來說:
  • 首先,通過 obj 的 isa 指標找到它的 class ;
  • 在 class 的 method list 找 play ;
  • 如果 class 中沒到 play,繼續往它的 superclass 中找 ;
  • 一旦找到 play 這個函式,就去執行它的實現IMP .


    但這種實現有個問題,效率低。但一個 class 往往只有 20% 的函式會被經常呼叫,可能佔總呼叫次數的 80% 。每個訊息都需要遍歷一次 objc_method_list 並不合理。如果把經常被呼叫的函式快取下來,那可以大大提高函式查詢的效率。這也就是 objc_class 中另一個重要成員 objc_cache 做的事情 - 再找到 play 之後,把 play 的 method_name 作為 key ,method_imp 作為 value 給存起來。當再次收到 play 訊息的時候,可以直接在 cache 裡找到,避免去遍歷 objc_method_list.

4.動態交換兩個方法(swizzle和KVO isa交換)


交換方法實現的需求場景:自己建立了一個功能性的方法,在專案中多次被引用,當專案的需求發生改變時,要使用另一種功能代替這個功能,要求是不改變舊的專案(也就是不改變原來方法的實現)。 可以在類的分類中,再寫一個新的方法(是符合新的需求的),然後交換兩個方法的實現。這樣,在不改變專案的程式碼,而只是增加了新的程式碼 的情況下,就完成了專案的改進。 交換兩個方法的實現一般寫在類的load方法裡面,因為load方法會在程式執行前載入一次,而initialize方法會在類或者子類在 第一次使用的時候呼叫,當有分類的時候會呼叫多次
// Load的時候如果下面的方法是-方法,那麼是無效的,類方法對例項方法無法操作
+ (void)load
{
    Method eatM = class_getClassMethod(self, sel_registerName("eat:"));
    
    Method sleepM = class_getClassMethod(self, @selector(sleep:));
    
    method_exchangeImplementations(eatM, sleepM);
}

// 如果要在自己的方法裡面呼叫另個一個方法,直接呼叫自己的方法名就好了
+ (void)eat:(NSString *)food
{
    
    NSLog(@"%@大口吃%@",NSStringFromClass([self class]),food);
}

// 如果這樣呼叫直接死迴圈了
+ (void)sleep:(NSString *)name
{
//    [self eat:@"屎"]; 死迴圈
    
    NSLog(@"%@睡了%@",NSStringFromClass([self class]),name);
    
    [self sleep:@"屎"];
}
objc_msgSend(objc_getClass("Dog"), sel_registerName("eat:"),@"aaa");
    
    // 列印如下
    // 2016-12-19 17:25:15.699 RuntimeKJ[12219:363270] Dog睡了aaa
    // 2016-12-19 17:25:15.700 RuntimeKJ[12219:363270] Dog大口吃屎
    
    // 先是呼叫eat的方法,但是由於方法的調換,先呼叫了sleep方法,在sleep方法裡面繼續呼叫sleep,實際上呼叫的是eat方法,這樣就完成的方法調換

動態交換方法,也可以交換isa指標,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 方法之前和之後,通知所有觀察物件屬性值的更改情況。

NSKVONotifying_A 類剖析

NSLog(@"self->isa:%@",self->isa);  
NSLog(@"self class:%@",[self class]);  

在建立KVO監聽前,列印結果為:

self->isa:A
self class:A

建立之後列印

self->isa:NSKVONotifying_A
self class:A

在這個過程,被觀察物件的 isa 指標從指向原來的 A 類,被KVO 機制修改為指向系統新建立的子類NSKVONotifying_A 類,來實現當前類屬性值改變的監聽;
所以當我們從應用層面上看來,完全沒有意識到有新的類出現,這是系統“隱瞞”了對 KVO 的底層實現過程,讓我們誤以為還是原來的類。但是此時如果我們建立一個新的名為“NSKVONotifying_A”的類,就會發現系統執行到註冊 KVO 的那段程式碼時程式就崩潰,因為系統在註冊監聽的時候動態建立了名為 NSKVONotifying_A 的中間類,並指向這個中間類了。
子類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.動態新增方法

<font color = black size = 4>通常做法都是在resolve方法內部指定sel的IMP,前提是該方法未實現會被攔截下來,就能實現動態建立的過程</font>
```objective-c
void run (id self, SEL _cmd,NSNumber *meter,NSString *name)
{
    // implementation .....
    NSLog(@"%@跑了%@",name,meter);
}


//對實現(abc)的前兩個引數的說明
//每個方法的內部都預設包含兩個引數,被稱為隱式引數
//id型別self(代表類或物件)和SEL型別的_cmd(方法編號)
//class_addMethod函式引數的含義:
//第一個引數Class cls, 型別
//第二個引數SEL name, 被解析的方法
//第三個引數 IMP imp, 指定的實現
//第四個引數const char *types,方法的型別,具體參照型別的codeType那張圖,但是要注意一點:Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).譯為:因為函式必須至少有兩個引數self和_cmd,第二個和第三個字元必須是“@:”。如果想要再增加引數,就可以從實現的第三個引數算起,看下面的例子就明白。
// 當呼叫有未實現的例項方法的時候會進到這裡來
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
    // 多引數就是"run:"無引數就是run
    if (aSEL == @selector(run:))
    {
        class_addMethod([self class], aSEL, (IMP) run, "v@:@:@");// 增加了2個物件型別引數 增加了@
        return YES;
    }
//    return [super resolveInstanceMethod:aSEL];
    return YES;
}

// 當呼叫類方法未實現的時候+ (BOOL)resolveClassMethod:(SEL)sel 在這裡攔截
+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"類方法未實現");
    return NO;
}

## 6.由第五點衍生出動態訊息轉發 和上面一樣我們建立的物件呼叫未實現的方法時,類和例項變數的內部是可以這樣進行轉發的
mark - 物件方法
// 沒有實現firstMethod的方法

// 1. 在沒有找到方法時,會先呼叫此方法,和DynicmicInsatance的方法一樣就可以動態新增方法3
//    返回YES 表示響應的Selector的實現已經被找到並新增到子類中了
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
//    if (sel == @selector(touch)) {
//        class_addMethod([self class], sel, (IMP)touch, "v@:");
//    }
    return YES;
}

// 2.第二步
//   第一步裡面返回之後沒有新增新方法,該方法就會被呼叫,在這個方法中,我們可以指定一個可以返回一個響應方法的獨享
//   不能返回Self 死迴圈
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return nil;
}

// 3.第三步
//   如果上面返回了nil則該方法會被呼叫,給系統一個需要的編碼
//   如果這裡放回的是nil,那是無法執行下一波的,下次無法得到處理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 4.第四步
// 呼叫轉發訊息方法
- (void)second
{
    NSLog(@"物件方法first方法未被呼叫,訊息轉發成了second方法");
}

// 該方法不進行重寫就直接進入第五步
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation setSelector:@selector(second)];
    [anInvocation invokeWithTarget:self];
}

// 5.第五步
// 如果沒有呼叫第四步的轉發,那麼會進入異常
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
    
    NSLog(@"無法處理的訊息:%@",NSStringFromSelector(aSelector));
}

  • 先在這裡攔截resolveInstanceMethod
  • 第一步未動態新增的話就呼叫forwardingTargetForSelector
  • 第二步返回nil來到這裡呼叫methodSignatureForSelector簽名
  • 重定向訊息指標,實現訊息轉發
  • 如果沒有實現第四步就doesNotRecognizeSelector異常


    這幾個階段特性都不太一樣:resolvedInstanceMethod 適合給類/物件動態新增一個相應的實現,forwardingTargetForSelector 適合將訊息轉發給其他物件處理,相對而言,forwardInvocation 是裡面最靈活,最能符合需求的。因此 Aspects 的方案就是,對於待 hook 的 selector,將其指向 objc_msgForward / _objc_msgForward_stret ,同時生成一個新的 aliasSelector 指向原來的 IMP,並且 hook 住 forwardInvocation 函式,使他指向自己的實現。按照上面的思路,當被 hook 的 selector 被執行的時候,首先根據 selector 找到了 objc_msgForward / _objc_msgForward_stret ,而這個會觸發訊息轉發,從而進入 forwardInvocation。同時由於 forwardInvocation 的指向也被修改了,因此會轉入新的 forwardInvocation 函式,在裡面執行需要嵌入的附加程式碼,完成之後,再轉回原來的 IMP。

面向切面Aspects專案應用

7.動態關聯屬性

// 獲取成員變數資訊
    unsigned count = 0;
    Ivar *ivarLists = class_copyIvarList(objc_getClass("Tree"), &count);
    for (NSInteger i = 0; i < count; i ++)
    {
        const char *name = ivar_getName(ivarLists[i]);
        NSString *str = [NSString stringWithUTF8String:name];
        NSLog(@"111%@",str);
    }
    
    // 獲取屬性
    objc_property_t *property = class_copyPropertyList(objc_getClass("Tree"), &count);
    for (NSInteger i = 0; i < count; i++)
    {
        const char *name = property_getName(property[i]);
        NSString *str  = [NSString stringWithUTF8String:name];
        NSLog(@"222%@",str);
    }
    
    // 獲取方法
    Method *methods = class_copyMethodList(objc_getClass("Tree"), &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methods[i];
        NSLog(@"333%@",NSStringFromSelector(method_getName(method)));
    }
    
    // 獲取協議
    __unsafe_unretained Protocol **protocol = class_copyProtocolList([self class], &count);
    for (NSInteger i = 0; i < count; i ++) {
        Protocol *pro = protocol[i];
        const char *nameP = protocol_getName(pro);
        NSLog(@"444%@",[NSString stringWithUTF8String:nameP]);
    }

給Category新增屬性以及方法,但是不能新增成員變數的理解 新增一個屬性,呼叫的時候崩潰了,說是找不到getter、setter方法。查了下文件發現,OC的分類允許給分類新增屬性,但不會自動生成getter、setter方法。有沒有解決方案呢?有,通過執行時建立關聯引用 第一個引數id object, 當前物件 第二個引數const void *key, 關聯的key,可以是任意型別 第三個引數id value, 被關聯的物件 第四個引數objc_AssociationPolicy policy關聯引用的規則 關聯物件不是為類\物件新增屬性或者成員變數(因為在設定關聯後也無法通過ivarList或者propertyList取得)
- (void)setSubName:(NSString *)subName
{
    objc_setAssociatedObject(self, "subname", subName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}


- (NSString *)subName
{
    return objc_getAssociatedObject(self, "subname");
}

那麼為什麼不能新增成員變數能,但是能新增屬性和方法呢?
意思是說,這個函式只能在“構建一個類的過程中”呼叫。一旦完成類定義,就不能再新增成員變數了。經過編譯的類在程式啟動後就runtime載入,沒有機會呼叫addIvar。程式在執行時動態構建的類需要在呼叫objc_registerClassPair之後才可以被使用,同樣沒有機會再新增成員變數。那為什麼可以在類別中新增方法和屬性呢?
因為方法和屬性並不“屬於”類例項,而成員變數“屬於”類例項。我們所說的“類例項”概念,指的是一塊記憶體區域,包含了isa指標和所有的成員變數。所以假如允許動態修改類成員變數佈局,已經建立出的類例項就不符合類定義了,變成了無效物件。但方法定義是在objc_class中管理的,不管如何增刪類方法,都不影響類例項的記憶體佈局,已經建立出的類例項仍然可正常使用。


8.使用Runtime和KVC字典轉模型

+ (instancetype)configModelWithDict:(NSDictionary *)jsonDict replaceDict:(NSDictionary *)replaceDict
{
    id obj = [[self alloc] init];
    unsigned int count = 0;
    // 獲取變數列表
    Ivar *ivarLists = class_copyIvarList(self, &count);
    // 遍歷逐個進行使用
    for (NSInteger i = 0; i < count; i ++)
    {
        // 獲取變數物件
        Ivar ivar = ivarLists[i];
        const char *name = ivar_getName(ivar);
        const char *coding = ivar_getTypeEncoding(ivar); // 判斷型別
        // 獲取自己寫的屬性變數字串 _name
        NSString *nameStr = [[NSString stringWithUTF8String:name] substringFromIndex:1];
        NSString *codingstr = [NSString stringWithUTF8String:coding];
        // 根據字串在原生字典取值
        id value = jsonDict[nameStr];
        // 如果未取到值  說明欄位已經修改了
        if (!value) {
            if (replaceDict) {
                // 然後把修改之前的原生欄位拿出來進行取值
                NSString *originValue = replaceDict[nameStr];
                // 再賦值
                value = jsonDict[originValue];
            }
        }
        // 避免屬性數量大於資料數量的時候,如果多出來的屬性是物件型別的那正好是null,無影響,如果多出來的屬性是普通型別的,那會把nil賦值過去,直接崩潰
        if ([codingstr isEqualToString:@"f"] || [codingstr isEqualToString:@"d"]) {
            value = @(0.0);
        }
        // kvc進行模型組裝 這裡的value型別和property裡面給的屬性效果是一致的,如果屬性是BOOL,你強行給字串,實際型別還是BOOL
        [obj setValue:value forKey:nameStr];
    }
    return obj;
}

上面就是轉換的核心程式碼,分析下主要功能引數
1.通過class_copyIvarList拿到屬性列表的陣列,ivar_getName這方法拿到屬性C型別字元去掉_,轉換成OC

2.這裡會有個問題,如果自己建的model欄位和Json返回的欄位完全一致,那麼就問題不大,但是由於可讀性的關係,我們一般都會做一次對映,這就是replaceDict存在的意義,用例如下:
當你的屬性名字是SubName,但是Json返回的字典key是sub_name,顯然是不同的,需要對映,我們根據runtime拿到的key也是SubName,那麼你根據字典取值,就會出現空值的問題,因此
replaceDict就用到了@{@"SubName":@"sub_name"},只要對映好傳進去,我們裡面就能進一步做判斷了

3.直接看程式碼註釋

/*
 *  1.首先屬性小於資料來源的時候是肯定沒問題的
 *  2.當屬性大於資料來源的時候,屬性是物件,列印出來就是nil,但是如果屬性是基本資料型別,直接崩潰
 *  一定要這麼判斷是否是基本資料型別
 *  避免屬性數量大於資料數量的時候,如果多出來的屬性是物件型別的那正好是null,無影響,如果多出來的屬性是普通型別的,那會把nil賦值過去,直接崩潰
 *  if ([codingstr isEqualToString:@"f"] || [codingstr isEqualToString:@"d"]) {
 *  value = @(0.0);
 }
 */

這裡的F,D什麼型別可以參考官方文件型別[type型別文件](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html)

理解了欄位和原理,呼叫程式碼如下

// 1.URL
    NSString *githubAPI = @"https://api.github.com/users/Tuccuay";
    
    // 2.建立請求物件
    // 物件內部已經包含了請求方法和請求頭(GET)
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:githubAPI]];
    
    // 3.建立session單例
    NSURLSession *session = [NSURLSession sharedSession];
    
    // 4.根據會話物件傳送請求
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
        MKJModel *mkj = [MKJModel configModelWithDict:jsonDict replaceDict:@{@"mkjLogin":@"login",
                                                                             @"mkjID":@"id",
                                                                             @"mkjAvatar_url":@"avatar_url",
                                                                             @"mkjGravatar_id":@"gravatar_id",
                                                                             @"mkjUrl":@"url",
                                                                             @"mkjHtml_url":@"html_url",
                                                                             @"mkjFollowers_url":@"followers_url",
                                                                             @"mkjFollowing_url":@"following_url",
                                                                             @"mkjGists_url":@"gists_url",
                                                                             @"mkjStarred_url":@"starred_url",
                                                                             }];
        NSLog(@"%@",mkj);
        
    }];
    
    // 5.task resume
    [dataTask resume];

8.runtime實現歸檔和解檔

self.fish = [NSKeyedUnarchiver unarchiveObjectWithFile:self.path];
    [NSKeyedArchiver archiveRootObject:self.fish toFile:self.path];

當外部呼叫上面歸檔解檔的程式碼時會走如下方法
- (NSArray *)ignoredNames
{
    return @[];
}
// 解檔的時候呼叫
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self == [super init]) {
        [self mkj_initWithCoder:aDecoder];
    }
    return self;
}
// 歸檔的時候呼叫
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [self mkj_encodeWithCoder:aCoder];
}

#pragma mark - 普通模式下的歸檔和解檔
//實現NSCoding協議中的歸檔方法,需要一個個列出屬性來
//- (void)encodeWithCoder:(NSCoder *)aCoder {
//    [aCoder encodeObject:self.name forKey:@"name"];
//    [aCoder encodeObject:self.age forKey:@"age"];
//}
//
//
////實現NSCoding協議中的解檔方法
//- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
//    if (self = [super init]) {
//        self.name = [aDecoder decodeObjectForKey:@"name"];
//        self.age = [aDecoder decodeObjectForKey:@"age"];
//    }
//    return self;
//}
- (NSArray *)ignoredProperty
{
    return @[];
}

- (void)mkj_encodeWithCoder:(NSCoder *)aCoder
{
    Class selfClass = self.class;
    while (selfClass && selfClass != [NSObject class]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList(selfClass, &count);
        for (NSInteger i = 0; i < count; i ++)
        {
            Ivar ivar = ivars[i];
            const char *ivarName = ivar_getName(ivar);
            NSString *ivarStr = [[NSString stringWithUTF8String:ivarName] substringFromIndex:1];
            if ([self respondsToSelector:@selector(ignoredProperty)]) {
                // 如果歸檔key為空
                if ([[self ignoredProperty] containsObject:ivarStr]) {
                    continue;
                }
            }
            id value = [self valueForKey:ivarStr];
            [aCoder encodeObject:value forKey:ivarStr];
        }
        free(ivars);
        selfClass = [selfClass superclass];
    }
}

- (void)mkj_initWithCoder:(NSCoder *)aDecoder
{
    Class selfClass = self.class;
    while (selfClass && selfClass != [NSObject class]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList(selfClass, &count);
        for (NSInteger i = 0; i < count; i ++)
        {
            Ivar ivar = ivars[i];
            const char *ivarName = ivar_getName(ivar);
            NSString *ivarStr = [[NSString stringWithUTF8String:ivarName] substringFromIndex:1];
            if ([self respondsToSelector:@selector(ignoredProperty)]) {
                // 如果歸檔key為空
                if ([[self ignoredProperty] containsObject:ivarStr]) {
                    continue;
                }
            }
            id value = [aDecoder decodeObjectForKey:ivarStr];
            [self setValue:value forKey:ivarStr];
            
        }
        free(ivars);
        selfClass = [selfClass superclass];
    }
    
}

Runtime動態關聯的實際應用1cell值存取

這裡寫圖片描述
點選每個cell的時候彈出Alert,然後根據不同cell撥打對應的電話

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MKJTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MKJTableViewCell" forIndexPath:indexPath];
    
    MKJModel *model = self.dataSources[indexPath.row];
    cell.phoneNumber.text = model.phoneNumber;
    cell.callBack = ^(UIButton *button){
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"warning" message:model.phoneNumber delegate:self cancelButtonTitle:@"cancel" otherButtonTitles:@"call", nil];
        [alert show];
        // static const void *CallBack = &CallBack;
        // 動態關聯
        objc_setAssociatedObject(alert, CallBack, model.phoneNumber, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    };
    return cell;
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    // 取值
    NSString *string = objc_getAssociatedObject(alertView, CallBack);
    NSLog(@"電話%@",string);
}

Runtime動態關聯的實際應用2Button新增Block事件

// 宣告
typedef void(^MKJBlock)(id sender);

@interface UIControl (MKJBlockEvent)

- (void)tapButtonWithAction:(UIControlEvents)events withBlock:(MKJBlock)block;
// 實現
// 靜態變數
static const void *sMKJTouchUpInsideEventKey = "sMKJTouchUpInside";

@implementation UIControl (MKJBlockEvent)

- (void)tapButtonWithAction:(UIControlEvents)events withBlock:(MKJBlock)block
{
    objc_setAssociatedObject(self, sMKJTouchUpInsideEventKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self removeTarget:self action:@selector(tapAction:) forControlEvents:events];
    if (block) {
        [self addTarget:self action:@selector(tapAction:) forControlEvents:events];
    }
}

- (void)tapAction:(UIButton *)button
{
    MKJBlock block = objc_getAssociatedObject(self, sMKJTouchUpInsideEventKey);
    if (block) {
        block(button);
    }
}
// 呼叫自定義,不用系統的addtarget
self.mkjControl = [[UIControl alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    self.mkjControl.backgroundColor = [UIColor redColor];
    self.mkjControl.tag = 10086;
    [self.mkjControl tapButtonWithAction:UIControlEventTouchUpInside withBlock:^(UIButton * sender) {
        NSLog(@"custom Action %ld",sender.tag);
        
    }];
    [self.view addSubview:self.mkjControl];

RuntimeMethodSwizzle3給每個頁面增加全屏拖反手勢

//啟用hook,自動對每個導航器開啟拖返功能,整個程式的生命週期只允許執行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //設定記錄type,並且執行hook
        __MLTransitionGestureRecognizerType = type;
        
        __MLTransition_Swizzle([UINavigationController class],@selector(viewDidLoad),@selector(__MLTransition_Hook_ViewDidLoad));
    });

// 具體實現
void __MLTransition_Swizzle(Class c, SEL origSEL, SEL newSEL)
{
    //獲取例項方法
    Method origMethod = class_getInstanceMethod(c, origSEL);
    Method newMethod = nil;
	if (!origMethod) {
        //獲取靜態方法
		origMethod = class_getClassMethod(c, origSEL);
        newMethod = class_getClassMethod(c, newSEL);
    }else{
        newMethod = class_getInstanceMethod(c, newSEL);
    }
    
    if (!origMethod||!newMethod) {
        return;
    }
    
    //自身已經有了就新增不成功,直接交換即可
    if(class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))){
        //新增成功一般情況是因為,origSEL本身是在c的父類裡。這裡新增成功了一個繼承方法。
        class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    }else{
        method_exchangeImplementations(origMethod, newMethod);
	}
}

// 實現Nvc的Category,實現交換之後的方法,然後再找個方法裡面增加返回手勢
#pragma mark - UINavigationController category interface
@interface UINavigationController(__MLTransition)<UIGestureRecognizerDelegate>

/**
 *  每個導航器都新增一個拖動手勢
 */
@property (nonatomic, strong) UIPanGestureRecognizer *__MLTransition_panGestureRecognizer;

- (void)__MLTransition_Hook_ViewDidLoad;

@end

#pragma mark - UINavigationController category implementation
NSString * const k__MLTransition_GestureRecognizer = @"__MLTransition_GestureRecognizer";

@implementation UINavigationController(__MLTransition)

#pragma mark getter and setter
- (void)set__MLTransition_panGestureRecognizer:(UIPanGestureRecognizer *)__MLTransition_panGestureRecognizer
{
    [self willChangeValueForKey:k__MLTransition_GestureRecognizer];
	objc_setAssociatedObject(self, &k__MLTransition_GestureRecognizer, __MLTransition_panGestureRecognizer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self didChangeValueForKey:k__MLTransition_GestureRecognizer];
}

- (UIPanGestureRecognizer *)__MLTransition_panGestureRecognizer
{
	return objc_getAssociatedObject(self, &k__MLTransition_GestureRecognizer);
}

#pragma mark hook
- (void)__MLTransition_Hook_ViewDidLoad
{
    [self __MLTransition_Hook_ViewDidLoad];
    
    //初始化拖返手勢
    if (!self.__MLTransition_panGestureRecognizer&&[self.interactivePopGestureRecognizer.delegate isKindOfClass:[UIPercentDrivenInteractiveTransition class]]) {
        UIPanGestureRecognizer *gestureRecognizer = nil;

#define kHandleNavigationTransitionKey [@"nTShMTkyGzS2nJquqTyioyElLJ5mnKEco246" __mlDecryptString]
        if (__MLTransitionGestureRecognizerType == MLTransitionGestureRecognizerTypeScreenEdgePan) {
            gestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self.interactivePopGestureRecognizer.delegate action:NSSelectorFromString(kHandleNavigationTransitionKey)];
            ((UIScreenEdgePanGestureRecognizer*)gestureRecognizer).edges = UIRectEdgeLeft;
        }else{
            gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self.interactivePopGestureRecognizer.delegate action:NSSelectorFromString(kHandleNavigationTransitionKey)];
        }
        
        gestureRecognizer.delegate = self;
        gestureRecognizer.__MLTransition_NavController = self;
        
        self.__MLTransition_panGestureRecognizer = gestureRecognizer;
        
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    
    [self.view addGestureRecognizer:self.__MLTransition_panGestureRecognizer];
}

#pragma mark GestureRecognizer delegate
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)recognizer
{
    UINavigationController *navVC = self;
    if ([navVC.transitionCoordinator isAnimated]||
        navVC.viewControllers.count < 2) {
        return NO;
    }
    
    UIView* view = recognizer.view;
    if (view.disableMLTransition) {
        return NO;
    }
    CGPoint loc = [recognizer locationInView:view];
    UIView* subview = [view hitTest:loc withEvent:nil];
    UIView *superView = subview;
    while (superView!=view) {
        if (superView.disableMLTransition) { //這個view忽略了拖返
            return NO;
        }
        superView = superView.superview;
    }
    
    //普通拖曳模式,如果開始方向不對即不啟用
    if (__MLTransitionGestureRecognizerType==MLTransitionGestureRecognizerTypePan){
        CGPoint velocity = [recognizer velocityInView:navVC.view];
        if(velocity.x<=0) {
            //NSLog(@"不是右滑的");
            return NO;
        }
        
        CGPoint translation = [recognizer translationInView:navVC.view];
        translation.x = translation.x==0?0.00001f:translation.x;
        CGFloat ratio = (fabs(translation.y)/fabs(translation.x));
        //因為上滑的操作相對會比較頻繁,所以角度限制少點
        if ((translation.y>0&&ratio>0.618f)||(translation.y<0&&ratio>0.2f)) {
            //NSLog(@"右滑角度不在範圍內");
            return NO;
        }
    }
    
    return YES;
}

runtime的基本介紹和使用案例最基本的差不多就這些了,簡單概括下方法列表和用法

1、objc_getClass 獲取類名

2、objc_msgSend 呼叫物件的sel

3、class_getClassMethod 獲取類方法

4、method_exchangeImplementations 交換兩個方法

5、class_addMethod 給類新增方法

6、class_copyIvarList 獲取成員變數資訊

7、class_copyPropertyList獲取屬性資訊

8、class_copyMethodList獲取方法資訊

9、class_copyProtocolList獲取協議資訊

10、objc_setAssociatedObject 動態關聯set方法

11、objc_getAssociatedObject 動態關聯get方法

12、ivar_getName 獲取變數名 char * 型別

13、ivar_getTypeEncoding 獲取到屬性變數的型別詳情型別介紹

Demo傳送門

用到過的案例
1.json轉model
2.Category動態關聯屬性
3.MethodSwizzling (1.全域性VC容器 2.無碼埋點 3.全域性拖翻手勢 4.避免崩潰越界啥的替換原有方法 5.通過hook alloc new dealloc等,主要思路是在一個時間切片內檢測物件的宣告週期以觀察記憶體是否會無限增長。通過 hook 掉 alloc,dealloc,retain,release 等方法,來記錄物件的生命週期。)
這個hook可以操作很多東西
3.1.JSPatch AOP
3.2.手勢返回 method swizziling
3.3.錨點支付返回,切換不同頁面跳轉
4.訊息轉發失敗檢測

新增一個功能,Hook sendAction:to:forEvent 給按鈕動態設定連續點選觸發的間隔
http://southpeak.github.io/2015/12/13/cocoa-uikit-uicontrol/
https://juejin.im/entry/58971b578d6d81005842867b
https://icetime17.github.io/page/4/

在新增一個 3D touch崩潰imagePicker的問題
https://icetime17.github.io/2016/03/19/2016-03/iOS-使用runtime解決3D-Touch導致UIImagePicker崩潰的問題/

參考:
isa
runtime詳解

相關文章