iOS Runtime幾種常見的應用場景
OC的動態性
OC 的函數是屬於動態調用,在編譯的時候是不能決定真正去調用那個函數的,只有在運行的時候才能決定去調用哪一個函數 ,在編譯階段,OC可以調用任何的函數,即使這個函數沒有實現,只要聲明過也就不會報錯。
場景一:防止button重複事件點擊
Method Swizzle執行點擊事件,我們利用runtime來方法交換,來達到防止重複點擊的目的:
- runtime添加時間間隔屬性
//UIButton添加一個Category
//.h文件
@interface UIButton (YY_FixMultiClick)
@property (nonatomic, assign) NSTimeInterval yy_acceptEventInterval; // 時間間隔
@property (nonatomic, assign) NSTimeInterval yy_acceptEventTime;
@end
注意:Category不能給類添加屬性, 所以以上的yy_acceptEventInterval和yy_acceptEventTime只會有對應的getter和setter方法, 不會添加真正的成員變量.
如果我們不在實現文件中添加其getter和setter方法, 則採用*** btn.yy_acceptEventInterval = 0.5; *** 這種方法訪問程序會崩潰.
2019-07-15 08:30:52.538 RuntimeDemo[17380:1387981] -[UIButton setYy_acceptEventInterval:]: unrecognized selector sent to instance 0x7fe8e198e880
因此:實現文件中通過runtime的關聯對象的方式, 爲UIButton添加以上兩個屬性.
#import "UIControl+YY_FixMultiClick"
#import <objc/runtime.h>
@implementation UIButton (YY_FixMultiClick)
// 因category不能添加屬性,只能通過關聯對象的方式。
static const char *UIControl_acceptEventInterval = "UIControl_acceptEventInterval";
- (NSTimeInterval)yy_acceptEventInterval {
return [objc_getAssociatedObject(self, UIControl_acceptEventInterval) doubleValue];
}
- (void)setYy_acceptEventInterval:(NSTimeInterval)yy_acceptEventInterval {
objc_setAssociatedObject(self, UIControl_acceptEventInterval, @(yy_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
static const char *UIControl_acceptEventTime = "UIControl_acceptEventTime";
- (NSTimeInterval)yy_acceptEventTime {
return [objc_getAssociatedObject(self, UIControl_acceptEventTime) doubleValue];
}
- (void)setYy_acceptEventTime:(NSTimeInterval)yy_acceptEventTime {
objc_setAssociatedObject(self, UIControl_acceptEventTime, @(yy_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
- 方法交換
//文件:.m
// 在load時執行
+ (void)load {
[super load];
Method normal = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method newMethod = class_getInstanceMethod(self, @selector(yy_sendAction:to:forEvent:));
method_exchangeImplementations(normal, newMethod);
}
- (void)yy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if ([NSDate date].timeIntervalSince1970 - self.yy_acceptEventTime < self.yy_acceptEventInterval) {
return;
}
if (self.yy_acceptEventInterval > 0) {
self.yy_acceptEventTime = [NSDate date].timeIntervalSince1970;
}
[self yy_sendAction:action to:target forEvent:event];
}
- button防止重複點擊使用:
btn.yy_acceptEventInterval = 0.5f; //UIButton指定了0.5s的時間間隔用於防止重複點擊.
場景二:訪問NSArray越界問題
爲NSArray添加Category代碼如下:
//
// NSArray+Crash.m
// YMallShop
//
// Created by bruce on 2018/8/8.
// Copyright © 2018年 bruce. All rights reserved.
//
#import "NSArray+Crash.h"
@implementation NSArray (Crash)
+ (void)load{
[super load];
//替換不可變數組方法
Method oldObjectAtIndex = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method newObjectAtIndex = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtSafeIndex:));
method_exchangeImplementations(oldObjectAtIndex, newObjectAtIndex);
//替換可變數組方法
Method oldMutableObjectAtIndex = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
Method newMutableObjectAtIndex = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(mutableObjectAtSafeIndex:));
method_exchangeImplementations(oldMutableObjectAtIndex, newMutableObjectAtIndex);
}
- (id)objectAtSafeIndex:(NSUInteger)index {
if (index > self.count - 1 || !self.count) {
@try {
return [self objectAtSafeIndex:index];
}
@catch (NSException *exception) {
NSLog(@"exception: %@", exception.reason);
return nil;
}
}else {
return [self objectAtSafeIndex:index];
}
}
- (id)mutableObjectAtSafeIndex:(NSUInteger)index{
if (index > self.count - 1 || !self.count) {
@try {
return [self mutableObjectAtSafeIndex:index];
}
@catch (NSException *exception) {
NSLog(@"exception: %@", exception.reason);
return nil;
}
}else {
return [self mutableObjectAtSafeIndex:index];
}
}
@end
場景三:快速接手新工程ViewController中添加Hook
//
// UIViewController+Swizzling.m
// qzy
//
// Created by BruceYao on 2017/1/17.
// Copyright © 2017年 BruceYao. All rights reserved.
//
#import "UIViewController+Swizzling.h"
@implementation UIViewController (Swizzling)
+ (void)load {
// 只在開發模式下,纔會出現交換
#ifdef DEBUG
// 原來的viewWillAppear方法
Method viewWillAppear = class_getInstanceMethod(self, @selector(viewWillAppear:));
// 需要替換成,能夠輸出的日誌的viewWillAppear
Method logViewWillAppear = class_getInstanceMethod(self, @selector(logViewWillAppear:));
// 兩個方法進行交換
method_exchangeImplementations(viewWillAppear, logViewWillAppear);
// 原來的viewWillDisappear方法
Method viewWillDisappear = class_getInstanceMethod(self, @selector(viewWillDisappear:));
// 需要替換成,能夠輸出的日誌的viewWillAppear
Method logViewWillDisAppear = class_getInstanceMethod(self, @selector(logViewWillDisAppear:));
// 兩個方法進行交換
method_exchangeImplementations(viewWillDisappear, logViewWillDisAppear);
#endif
}
- (void)logViewWillAppear:(BOOL)animater {
NSString *className = NSStringFromClass([self class]);
//[MobClick beginLogPageView:className]; 統計的時候會用到、友盟統計
NSLog(@"%@ will appear", className);
}
- (void)logViewWillDisAppear:(BOOL)animater {
NSString *className = NSStringFromClass([self class]);
//[MobClick endLogPageView:className];統計的時候會用到
}
@end
###場景四:歸檔、反歸檔
#import "EncodeAndUnEncode.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation EncodeAndUnEncode
//解
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
unsigned int count = 0;
Ivar * ivarlist = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
Ivar alvar = ivarlist[i];
const char * name = ivar_getName(alvar);
id value = [aDecoder decodeObjectForKey:[NSString stringWithUTF8String:name]];
if value != nil {
[self setValue:value forKey:[NSString stringWithUTF8String:name]];
}
}
free(ivarlist);
}
return self;
}
//歸
-(void)encodeWithCoder:(NSCoder *)aCoder{
//獲取某個類的成員變量
unsigned int count = 0;
Ivar * ivarlist = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i ++) {
Ivar alvar = ivarlist[i];
//獲取成員變量的名稱
const char * ivarname = ivar_getName(alvar);
id value = [self valueForKey:[NSString stringWithUTF8String:ivarname]];
if value != nil {
[aCoder encodeObject:value forKey:[NSString stringWithUTF8String:ivarname]];
}
}
free(ivarlist);
}
動態的添加方法
開發使用場景:如果一個類方法非常多,加載類到內存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態給某個類,添加方法解決。
調用:
Person *per = [[Person alloc] init];
per.name = [[NSString alloc] initWithFormat:@"張三"];
[per eat];
objc_msgSend(per, @selector(eat));
[per performSelector:@selector(sleeping)];
對應的類中:Person,或者在分類中添加也可以
void sleeping(id self, SEL sel) {
NSLog(@"%@ %@", self, NSStringFromSelector(sel));
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sleeping)) {
// type: 方法類型:void用v來表示,id參數用@來表示,SEL用:來表示
class_addMethod(self, @selector(sleeping), sleeping, "v@:");
}
return [super resolveInstanceMethod:sel];
}
發送消息
注意點:在工程配置頁面中:Enable Strict Checking of objc_msgSend Calls 設置爲NO即可,要不然會引起crash
Person *per = [[Person alloc] init];
per.name = [[NSString alloc] initWithFormat:@"張三"];
[per eat];
objc_msgSend(per, @selector(eat));