最近對一些ios的apm系統比較感興趣,因此就研究了一些相關的技術。首先從最基本的Method Swizzling
開始。php
Method Swizzling
是OC runtime提供的一種動態替換方法實現的技術,咱們利用它能夠替換系統或者咱們自定義類的方法實現,進而達到咱們的特殊目的。html
代碼地址-github: MethodSwizzlingios
爲何Method Swizzling
能替換一個類的方法呢?咱們首先要理解一下其替換的原理。git
OC中的方法在runtime.h
中的定義以下:github
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
}
複製代碼
由此,咱們也能夠發現 OC 中的方法名是不包括參數類型的,也就是說下面兩個方法在 runtime 看來就是同一個方法:objective-c
- (void)viewWillAppear:(BOOL)animated;
- (void)viewWillAppear:(NSString *)string;
複製代碼
原則上,方法的名稱 method_name
和方法的實現 method_imp
是一一對應的,而 Method Swizzling 的原理就是動態地改變它們的對應關係,以達到替換方法實現的目的。數組
Method Swizzling
應用OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
複製代碼
OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);
複製代碼
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m);
複製代碼
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m);
複製代碼
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types);
複製代碼
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
複製代碼
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types) ;
複製代碼
eg: 替換UIViewController
中的viewDidLoad
方法。bash
#import "UIViewController+MI.h"
#import <objc/runtime.h>
@implementation UIViewController (MI)
+ (void)load{
// 替換ViewController中的viewDidLoad方法
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL origin_selector = @selector(viewDidLoad);
SEL swizzed_selector = @selector(mi_viewDidLoad);
Method origin_method = class_getInstanceMethod([self class], origin_selector);
Method swizzed_method = class_getInstanceMethod([self class], swizzed_selector);
BOOL did_add_method = class_addMethod([self class],
origin_selector,
method_getImplementation(swizzed_method),
method_getTypeEncoding(swizzed_method));
if (did_add_method) {
NSLog(@"debugMsg: ViewController類中沒有viewDidLoad方法(可能在其父類h中),因此先添加後替換");
class_replaceMethod([self class],
swizzed_selector,
method_getImplementation(origin_method),
method_getTypeEncoding(origin_method));
}else{
NSLog(@"debugMsg: 直接交換方法");
method_exchangeImplementations(origin_method, swizzed_method);
}
});
}
- (void)mi_viewDidLoad
{
[self mi_viewDidLoad];
NSLog(@"debugMsg: 替換成功");
}
複製代碼
對以上代碼作簡要說明:app
+(void)load
方法中添加替換的代碼,在類初始加載的時候會被自動調用。class_addMethod
方法,保證即便父類中存在要替換的方法,也能替換成功替換成功。控制檯信息:函數
2019-04-17 17:25:16.937849+0800 MethodSwizzling[4975:639584] debugMsg: 直接交換方法
2019-04-17 17:25:17.025214+0800 MethodSwizzling[4975:639584] debugMsg: 替換成功
複製代碼
當咱們對私有類庫(不知道該類的頭文件,只知道有這個類而且已知該類中的一個方法),此時咱們須要hook這個類的方法到一個新類中。
eg: 咱們要hook person類中有一個speak:
方法方法:
#import "Person.h"
@implementation Person
- (void)speak:(NSString *)language
{
NSLog(@"person speak language: %@",language);
}
+ (void)sleep:(NSUInteger)hour
{
NSLog(@"person sleep: %lu",hour);
}
@end
複製代碼
咱們新建ChinesePerson
,hook speak:
方法到ChinesePerson
中。
#import "ChinesePerson.h"
#import <objc/runtime.h>
@implementation ChinesePerson
+ (void)load
{
Class origin_class = NSClassFromString(@"Person");
Class swizzed_class = [self class];
SEL origin_selector = NSSelectorFromString(@"speak:");
SEL swizzed_selector = NSSelectorFromString(@"mi_speak:");
Method origin_method = class_getInstanceMethod(origin_class, origin_selector);
Method swizzed_method = class_getInstanceMethod(swizzed_class, swizzed_selector);
BOOL add_method = class_addMethod(origin_class,
swizzed_selector,
method_getImplementation(swizzed_method),
method_getTypeEncoding(swizzed_method));
if (!add_method) {
return;
}
swizzed_method = class_getInstanceMethod(origin_class, swizzed_selector);
if (!swizzed_method) {
return;
}
BOOL did_add_method = class_addMethod(origin_class,
origin_selector,
method_getImplementation(swizzed_method),
method_getTypeEncoding(swizzed_method));
if (did_add_method) {
class_replaceMethod(origin_class,
swizzed_selector,
method_getImplementation(origin_method),
method_getTypeEncoding(origin_method));
}else{
method_exchangeImplementations(origin_method, swizzed_method);
}
}
- (void)mi_speak:(NSString *)language
{
if ([language isEqualToString:@"Chinese"]) {
[self mi_speak:language];
}
}
複製代碼
替換成功。控制檯信息(只打印漢語):
2019-04-17 17:25:17.025362+0800 MethodSwizzling[4975:639584] person speak language: Chinese
複製代碼
eg: 咱們替換person類中的sleep:
方法:
#import "Person+MI.h"
#import <objc/runtime.h>
@implementation Person (MI)
+ (void)load
{
Class class = [self class];
SEL origin_selector = @selector(sleep:);
SEL swizzed_selector = @selector(mi_sleep:);
Method origin_method = class_getClassMethod(class, origin_selector);
Method swizzed_method = class_getClassMethod(class,swizzed_selector);
if (!origin_method || !swizzed_method) {
return;
}
IMP origin_imp = method_getImplementation(origin_method);
IMP swizzed_imp = method_getImplementation(swizzed_method);
const char* origin_type = method_getTypeEncoding(origin_method);
const char* swizzed_type = method_getTypeEncoding(swizzed_method);
// 添加方法到MetaClass中
Class meta_class = objc_getMetaClass(class_getName(class));
class_replaceMethod(meta_class, swizzed_selector, origin_imp, origin_type);
class_replaceMethod(meta_class, origin_selector, swizzed_imp, swizzed_type);
}
+ (void)mi_sleep:(NSUInteger)hour
{
if (hour >= 7) {
[self mi_sleep:hour];
}
}
@end
複製代碼
控制檯打印(睡眠大於等於7小時纔打印----呼籲健康睡眠):
2019-04-17 17:25:17.025465+0800 MethodSwizzling[4975:639584] person sleep: 8
複製代碼
類方法的hook和實例方法的hook有兩點不一樣:
class_getClassMethod(Class cls, SEL name)
,不是class_getInstanceMethod(Class cls, SEL name)
;#import "MIMutableDictionary.h"
#import <objc/runtime.h>
@implementation MIMutableDictionary
+ (void)load
{
Class origin_class = NSClassFromString(@"__NSDictionaryM");
Class swizzed_class = [self class];
SEL origin_selector = @selector(setObject:forKey:);
SEL swizzed_selector = @selector(mi_setObject:forKey:);
Method origin_method = class_getInstanceMethod(origin_class, origin_selector);
Method swizzed_method = class_getInstanceMethod(swizzed_class, swizzed_selector);
IMP origin_imp = method_getImplementation(origin_method);
IMP swizzed_imp = method_getImplementation(swizzed_method);
const char* origin_type = method_getTypeEncoding(origin_method);
const char* swizzed_type = method_getTypeEncoding(swizzed_method);
class_replaceMethod(origin_class, swizzed_selector, origin_imp, origin_type);
class_replaceMethod(origin_class, origin_selector, swizzed_imp, swizzed_type);
}
- (void)mi_setObject:(id)objContent forKey:(id<NSCopying>)keyContent
{
if (objContent && keyContent) {
NSLog(@"執行了進去");
[self mi_setObject:objContent forKey:keyContent];
}
}
@end
複製代碼
不推薦在項目中過多的使用Method Swizzling
,否則的話原生的類都被hook的很是亂,項目出問題時很是難定位問題。有一篇文章說它是ios中的一個毒瘤。 iOS屆的毒瘤-MethodSwizzling
儘管如此,咱們仍是瞭解並梳理一下其應用場景來感覺一下這種技術。
不單單是數組,NSDictionary
的利用runtime防止崩潰是一樣的原理。
#import "NSArray+Safe.h"
#import <objc/runtime.h>
@implementation NSArray (Safe)
+ (void)load
{
// objectAtIndex: 方式取元素
Method origin_method = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method replaced_method = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safeObjectAtIndex:));
method_exchangeImplementations(origin_method, replaced_method);
Method origin_method_muta = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
Method replaced_method_muta = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(safeMutableObjectAtIndex:));
method_exchangeImplementations(origin_method_muta, replaced_method_muta);
// 直接經過數組下標的方式取元素
Method origin_method_sub = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
Method replaced_method_sub = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safeObjectAtIndexedSubscript:));
method_exchangeImplementations(origin_method_sub, replaced_method_sub);
Method origin_method_muta_sub = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndexedSubscript:));
Method replaced_method_muta_sub = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(safeMutableObjectAtIndexedSubscript:));
method_exchangeImplementations(origin_method_muta_sub, replaced_method_muta_sub);
}
- (id)safeObjectAtIndex:(NSUInteger)index
{
if (self.count > index && self.count) {
return [self safeObjectAtIndex:index];
}
NSLog(@"errorMsg:數組[NSArray]越界...");
return nil;
}
- (id)safeMutableObjectAtIndex:(NSUInteger)index
{
if (self.count > index && self.count) {
return [self safeMutableObjectAtIndex:index];
}
NSLog(@"errorMsg:數組[NSMutableArray]越界...");
return nil;
}
-(id)safeObjectAtIndexedSubscript:(NSUInteger)index
{
if (self.count > index && self.count) {
return [self safeObjectAtIndexedSubscript:index];
}
NSLog(@"errorMsg:數組[NSArray]越界...");
return nil;
}
- (id)safeMutableObjectAtIndexedSubscript:(NSUInteger)index
{
if (self.count > index && self.count) {
return [self safeMutableObjectAtIndexedSubscript:index];
}
NSLog(@"errorMsg:數組[NSMutableArray]越界...");
return nil;
}
@end
複製代碼
使用:
- (void)test2
{
NSArray *arr = @[@"a",@"b",@"c",@"d",@"e",@"f"];
NSLog(@"atIndex方式: %@",[arr objectAtIndex:10]);
NSLog(@"下標方式: %@",arr[10]);
}
複製代碼
控制檯輸出log:
2019-04-18 19:14:18.139417+0800 MethodSwizzling[25379:1703659] errorMsg:數組[NSArray]越界...
2019-04-18 19:14:18.139536+0800 MethodSwizzling[25379:1703659] atIndex方式: (null)
2019-04-18 19:14:18.139793+0800 MethodSwizzling[25379:1703659] errorMsg:數組[NSArray]越界...
2019-04-18 19:14:18.139868+0800 MethodSwizzling[25379:1703659] 下標方式: (null)
複製代碼
常規作法是遍歷視圖中的全部子視圖,把全部的button總體改變。 此時咱們使用runtime改變按鈕的大小。
#import "UIButton+Size.h"
#import <objc/runtime.h>
@implementation UIButton (Size)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 增大全部按鈕大小
Method origin_method = class_getInstanceMethod([self class], @selector(setFrame:));
Method replaced_method = class_getInstanceMethod([self class], @selector(miSetFrame:));
method_exchangeImplementations(origin_method, replaced_method);
});
}
- (void)miSetFrame:(CGRect)frame
{
frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width+20, frame.size.height+20);
NSLog(@"設置按鈕大小生效");
[self miSetFrame:frame];
}
@end
複製代碼
若是重複過快的點擊同一個按鈕,那麼就會屢次觸發和按鈕綁定的事件。處理這種case的方式有不少種,經過Method Swillzing
也能解決這種問題。
.h文件:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIButton (QuickClick)
@property (nonatomic,assign) NSTimeInterval delayTime;
@end
NS_ASSUME_NONNULL_END
複製代碼
#import "UIButton+QuickClick.h"
#import <objc/runtime.h>
@implementation UIButton (QuickClick)
static const char* delayTime_str = "delayTime_str";
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method replacedMethod = class_getInstanceMethod(self, @selector(miSendAction:to:forEvent:));
method_exchangeImplementations(originMethod, replacedMethod);
});
}
- (void)miSendAction:(nonnull SEL)action to:(id)target forEvent:(UIEvent *)event
{
if (self.delayTime > 0) {
if (self.userInteractionEnabled) {
[self miSendAction:action to:target forEvent:event];
}
self.userInteractionEnabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(self.delayTime * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
self.userInteractionEnabled = YES;
});
}else{
[self miSendAction:action to:target forEvent:event];
}
}
- (NSTimeInterval)delayTime
{
return [objc_getAssociatedObject(self, delayTime_str) doubleValue];
}
- (void)setDelayTime:(NSTimeInterval)delayTime
{
objc_setAssociatedObject(self, delayTime_str, @(delayTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
複製代碼
github: MethodSwizzling