Runtime

https://github.com/starainDou 歡迎點星

Runtime簡介

Sheep.png

runtime是什么(原理)

runtime是一套比較底層的純C語言API, 屬于1個C語言庫, 包含了很多底層的C語言API。
在我們平時編寫的OC代碼中, 程序運行過程時, 其實最終都是轉成了runtime的C語言代碼, runtime算是OC的幕后工作者

發送消息(消息機制)

// 方法調用的本質,就是讓對象發送消息,使用消息機制前提,必須導入#import <objc/message.h>
// 對象方法
Person *person = [[Person alloc] init];
[person eat];
 //就是讓實例對象發送消息 objc_msgSend(person, @selector(eat));

// 類方法
[Person run];
// 等價   [[Person class] run];
// 就是讓類對象發送消息 objc_msgSend([Person class], @selector(run));

可以新建一個類MyClass證明

#import "MyClass.h"
@implementation MyClass
-(instancetype)init{
    if (self = [super init]) {
        [self showUserName];
    }
    return self;
}
-(void)showUserName{
    NSLog(@"Dave Ping");
}

然后使用clang重寫命令

clang -rewrite-objc MyClass.m

得到MyClass.cpp文件

static instancetype _I_MyClass_init(MyClass * self, SEL _cmd) {
    if (self = ((MyClass *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyClass"))}, sel_registerName("init"))) {
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"));
    }
    return self;
}

消息轉發

當[person eat];時如果ea方法不存在,會報經典錯誤 unrecognized selector sent to instance,此時用消息轉發解決
消息轉發機制三個步驟(方案): 動態方法解析,備用接受者,完整轉發

方法在調用時,系統會查看這個對象能否接收這個消息(查看這個類有沒有這個方法,或有沒有實現這個方法。),如果不能且只在不能的情況下,就會調用下面這幾個方法,給你“補救”的機會,先理解為幾套防止程序crash的備選方案,我們就是利用這幾個方案進行消息轉發,注意一點,前一套方案實現后一套方法就不會執行。如果這幾套方案你都沒有做處理,那么程序就會報錯crash。

方案一:動態方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

方案二:備用接收者

- (id)forwardingTargetForSelector:(SEL)aSelector;

方案三:完整轉發

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

詳解:

新建一個Person的類,定義兩個未實現的方法:

@interface Person : NSObject
- (void)eat;
+ (Person *)run;
@end

1.動態方法解析
對象在接收到未知的消息時,首先會調用所屬類的類方法+resolveInstanceMethod:(實例方法)或+resolveClassMethod:(類方法)。在這個方法中,我們有機會為該未知消息新增一個”處理方法”“。不過使用該方法的前提是我們已經實現了該”處理方法”,只需要在運行時通過class_addMethod函數動態添加到類里面就可以了。

void functionForMethod(id self, SEL _cmd) {
 NSLog(@"%@:%s", self, sel_getName(_cmd));
}

Class functionForClassMethod(id self, SEL _cmd) {
 NSLog(@"%@:%s", self, sel_getName(_cmd));
 return [Person class];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod");
NSString *selString = NSStringFromSelector(sel);
if ([selString isEqualToString:@"run"]) {
Class metaClass = objc_getMetaClass("Person");
// 動態添加方法
class_addMethod(metaClass, @selector(run), (IMP)functionForClassMethod, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod");
if (sel == @selector(eat)) {
class_addMethod(self, sel, (IMP)functionForMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

2備用接受者
動態方法解析無法處理消息,則會走備用接受者。這個備用接受者只能是一個新的對象,不能是self本身,否則就會出現無限循環。如果我們沒有指定相應的對象來處理aSelector,則應該調用父類的實現來返回結果。

@interface Dog : NSObject
- (void)eat;
@end

@implementation Dog
- (void)eat {
 NSLog(@"%@, %p", self, _cmd);
}
@end
- (id)forwardingTargetForSelector:(SEL)sel {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 將消息交給_helper來處理
if ([selectorString isEqualToString:@"eat"]) {
 return [[Dog alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}

3 完整轉發

// 必須重寫這個方法,消息轉發機制的使用從這個方法中獲取的信息來創建NSInvocation對象
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSString *sel = NSStringFromSelector(aSelector);
    if ([sel isEqualToString:@"eat"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    Dog *dog = [[Dog alloc] init];
    if ([dog respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:dog];
    }
}

KVC

KVC全稱是Key Value Coding (鍵值編碼),定義在NSKeyValueCoding.h文件中,是一個非正式協議。KVC提供了一種間接訪問其屬性方法或成員變量的機制,可以通過字符串來訪問對應的屬性方法或成員變量,KVO 就是基于 KVC 實現的關鍵技術之一。
NSKeyValueCoding中提供了KVC通用的訪問方法,分別是getter方法valueForKey:和setter方法setValue:forKey:,以及其衍生的keyPath方法,這兩個方法各個類通用的。并且由KVC提供默認的實現,我們也可以自己重寫對應的方法來改變實現。

KVC最典型的兩個應用場景:
1,對私有變量進行賦值(setValue:forKey:)
2,字典轉模型(如 [self setValuesForKeysWithDictionary:dict];)
但要注意
1,字典轉模型時,字典中的某個key一定要在模型中有對應的屬性,否則重寫- setValue: forUndefinedKey:
2,如果一個模型中包含了另外的模型對象,是不能直接轉化成功的。
3,通過kvc轉化模型中的模型,也是不能直接轉化成功的。

安全性檢查

KVC存在一個問題在于,因為傳入的key或keyPath是一個字符串,這樣很容易寫錯或者屬性自身修改后字符串忘記修改,這樣會導致Crash。

可以利用iOS的反射機制來規避這個問題,通過@selector()獲取到方法的SEL,然后通過NSStringFromSelector()將SEL反射為字符串。這樣在@selector()中傳入方法名的過程中,編譯器會有合法性檢查,如果方法不存在或未實現會報黃色警告。

KVC原理剖析

KVO

KVO,即key-value-observing,利用一個key來找到某個屬性并監聽其值得改變。其實這也是一種典型的觀察者模式。

KVO的用法

1,添加觀察者
2,在觀察者中實現監聽方法,observeValueForKeyPath: ofObject: change: context:
3,移除觀察者

KVO原理(底層實現)

KVO是基于runtime機制實現的,當一個類的屬性被觀察的時候,系統會通過runtime動態的創建一個該類的派生類NSKVONotifying_class,并且會在這個派生類中重寫基類被觀察的屬性的setter方法,而且系統將這個類的isa指針指向了派生類,從而實現了給監聽的屬性賦值時調用的是派生類的setter方法。重寫的setter方法會在調用原setter方法前后,通知觀察對象值得改變。
鍵值觀察通知依賴于NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey:;在一個被觀察屬性發生改變之前, willChangeValueForKey:一定會被調用,這就 會記錄舊的值。而當改變發生后,didChangeValueForKey:會被調用,繼而 observeValueForKey:ofObject:change:context: 也會被調用

KVC和KVO

方法交換(Method Swizzling 黑魔法)

方法交換實現的需求場景:自己創建了一個功能性的方法,在項目中多次被引用,當項目的需求發生改變時,要使用另一種功能代替這個功能,要求是不改變舊的項目(也就是不改變原來方法的實現)。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    // 需求:給imageNamed方法提供功能,每次加載圖片就判斷下圖片是否加載成功。
    // 步驟一:先搞個分類,定義一個能加載圖片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 步驟二:交換imageNamed和imageWithName的實現,就能調用imageWithName,間接調用imageWithName的實現。
    UIImage *image = [UIImage imageNamed:@"123"];
}

@end

@implementation UIImage (Image)
// 加載分類到內存的時候調用
+ (void)load
{
    // 交換方法

    // 獲取imageWithName方法地址
    Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));

    // 獲取imageWithName方法地址
    Method imageName = class_getClassMethod(self, @selector(imageNamed:));

    // 交換方法地址,相當于交換實現方式
    method_exchangeImplementations(imageWithName, imageName);
    // 實例方法 Method originalMethod = class_getInstanceMethod([self class], @selector(size));
}

// 不能在分類中重寫系統方法imageNamed,因為會把系統的功能給覆蓋掉,而且分類中不能調用super.

// 既能加載圖片又能打印
+ (instancetype)imageWithName:(NSString *)name
{
    // 這里調用imageWithName,相當于調用imageName
    UIImage *image = [self imageWithName:name];

    if (image == nil) {
        NSLog(@"加載空的圖片");
    }

    return image;
}
@end

交換方法的實現原理:

這還是要從方法調用的流程說起,
1,首先會獲取當前對象的isa指針,然后去isa指向的類中查找,
2,根據傳入的SEL找到對應方法名(函數入口)
3,然后去方法區直接調用函數實現

最優實現,防止子類中交換出現unrecognized selector sent to instance 0x...
.

Method originalMethod = class_getInstanceMethod([self class], @selector(setImage:));
    Method swizzleMethod = class_getInstanceMethod([self class], @selector(setMaskImage:));
    BOOL didAddMethod = class_addMethod([self class], @selector(setImage:), method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    if (didAddMethod) {
        class_replaceMethod([self class], @selector(setMaskImage:), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzleMethod);
    }

方法關聯

1 給分類添加屬性

// 定義關聯的key
static const char *key = "name";

@implementation NSObject (Property)

- (NSString *)name
{
    // 根據關聯的key,獲取關聯的值。
    return objc_getAssociatedObject(self, key);
}

- (void)setName:(NSString *)name
{
    // 第一個參數:給哪個對象添加關聯
    // 第二個參數:關聯的key,通過這個key獲取
    // 第三個參數:關聯的value
    // 第四個參數:關聯的策略
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

為category添加屬性2

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface NSObject (CategoryWithProperty)
@property (nonatomic, strong) NSObject *property;
@end

@implementation NSObject (CategoryWithProperty)
- (NSObject *)property { 
return objc_getAssociatedObject(self, @selector(property));
}
- (void)setProperty:(NSObject *)value { 
objc_setAssociatedObject(self, @selector(property), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

2 給對象添加關聯對象

// block
typedef void (^testBlock)(void);
if (resultBlock) objc_setAssociatedObject(self, "testBlockKey", resultBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
testBlock resultBlock = objc_getAssociatedObject(self, "testBlockKey");

// BOOL ,int,枚舉值等
objc_setAssociatedObject(self, "locationTypeKey", @(type), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
DDYCLLocationType type = (DDYCLLocationType)[objc_getAssociatedObject(self, "locationTypeKey") integerValue];

static char const * const ObjectTagKey = "ObjectTag";
@implementation ClassName (CategoryName)
- (void) setBoolProperty:(BOOL) property {
    NSNumber *number = [NSNumber numberWithBool: property];
    objc_setAssociatedObject(self, ObjectTagKey, number , OBJC_ASSOCIATION_RETAIN);
}

- (BOOL) boolProperty {
    NSNumber *number = objc_getAssociatedObject(self, ObjectTagKey);
    return [number boolValue]; 
}
@end

// 用全局key
static void *testNumKey = &testNumKey;
 objc_setAssociatedObject(self, testNumKey, @(testNum), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
NSNumber *tempNum = objc_getAssociatedObject(self, testNumKey); 
NSInteger num = tempNum ? tempNum integerValue] : 0;

static const char alertKey;
typedef void (^successBlock)(NSInteger buttonIndex);
objc_setAssociatedObject(self, &alertKey, block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
successBlock block = objc_getAssociatedObject(self, &alertKey);

比如 :我們想把更多的參數傳給alertView代理

- (void)shopCartCell:(FFShopCartCell *)shopCartCell didDeleteClickedAtRecId:(NSString *)recId
{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"確認刪除" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"確定", nil];
    
    // 傳遞多參數
    objc_setAssociatedObject(alert, "suppliers_id", @"1", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(alert, "warehouse_id", @"2", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    alert.tag = [recId intValue];
    [alert show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex == 1) {
        
        NSString *warehouse_id = objc_getAssociatedObject(alertView, "warehouse_id");
        NSString *suppliers_id = objc_getAssociatedObject(alertView, "suppliers_id");
        NSString *recId = [NSString stringWithFormat:@"%ld",(long)alertView.tag];
    }
}

再比如讓UIButton SEL點擊轉block回調
.h

#import <UIKit/UIKit.h>

typedef void (^btnBlock)();

@interface UIButton (Block)

- (void)handelWithBlock:(btnBlock)block;

.m

#import "UIButton+Block.h"
#import <objc/runtime.h>

static const char btnKey;

@implementation UIButton (Block)

- (void)handelWithBlock:(btnBlock)block
{
    if (block)
    {
        objc_setAssociatedObject(self, &btnKey, block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    [self addTarget:self action:@selector(btnAction) forControlEvents:UIControlEventTouchUpInside];
}

- (void)btnAction
{
    btnBlock block = objc_getAssociatedObject(self, &btnKey);
    block();
}

@end

獲取實例變量、屬性、對象方法、類方法等

#pragma mark 獲取一個類的屬性列表
- (void)getPropertiesOfClass:(NSString *)classString {
    Class class = NSClassFromString(classString);
    unsigned int count = 0;
    objc_property_t *propertys = class_copyPropertyList(class, &count);
    for(int i = 0;i < count;i ++)
    {
        objc_property_t property = propertys[i];
        NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        NSLog(@"uialertion.property = %@",propertyName);
    }
}
#pragma mark 獲取一個類的成員變量列表
- (void)getIvarListOfClass:(NSString *)classString {
    Class class = NSClassFromString(classString);
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(class, &count);
    for(int i =0;i < count;i ++)
    {
        Ivar ivar = ivars[i];
        NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        const char *type = ivar_getTypeEncoding(ivar);
        NSLog(@"uialertion.ivarName = %@   type = %s",ivarName,type);
    }
}

#pragma mark 獲取一個類的所有方法
- (void)getMethodsOfClass:(NSString *)classString {
    Class class = NSClassFromString(classString);
    unsigned int count = 0;
    Method *methods = class_copyMethodList(class, &count);
    for (int i = 0; i < count; i++) {
        SEL sel = method_getName(methods[i]);
        NSLog(@"Methods = %@",NSStringFromSelector(sel));
    }
    
    free(methods);
}

#pragma mark 獲取一個類的所有類方法
- (void)getClassMethodsOfClass:(NSString *)classString {
    Class class = NSClassFromString(classString);
    // Class class  = [NSString class];
    unsigned int count = 0;
    Method *classMethods = class_copyMethodList(objc_getMetaClass(class_getName(class)), &count);
    for (int i = 0; i < count; i++) {
        SEL sel = method_getName(classMethods[i]);
        NSLog(@"Class Methods = %@",NSStringFromSelector(sel));
    }
}

#pragma mark 獲取協議列表
- (void)getProtocolsOfClass:(NSString *)classString {
    Class class = NSClassFromString(classString);
    unsigned int count;
    __unsafe_unretained Protocol **protocols = class_copyProtocolList(class, &count);
    for (unsigned int i = 0; i < count; i++) {
        const char *name = protocol_getName(protocols[i]);
        printf("Protocols = %s\n",name);
    }
}

SEL、Method、IMP的含義及區別

在運行時,類(Class)維護了一個消息分發列表來解決消息的正確發送。每一個消息列表的入口是一個方法(Method),這個方法映射了一對鍵值對,其中鍵是這個方法的名字(SEL),值是指向這個方法實現的函數指針 implementation(IMP)。

推薦

西木 runtime完整總結
iOS-Runtime-Headers
為什么object_getClass(obj)與[OBJ class]返回的指針不同
動手實現objc_msgSend
Runtime對方法的操作
運行時簡介
神經病院Objective-C Runtime住院第二天——消息發送與轉發
iOS面試題
Runtime Method Swizzling開發實例匯總
Runtime 10種用法
Runtime知識點概括以及使用場景

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容