https://github.com/starainDou 歡迎點星
Runtime簡介
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()中傳入方法名的過程中,編譯器會有合法性檢查,如果方法不存在或未實現會報黃色警告。
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: 也會被調用
方法交換(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知識點概括以及使用場景