文章分享至我的個人技術部落格:https://cainluo.github.io/15065147177317.html
前面我們把RunTime
的一些基本知識都瞭解了一遍, 知道了在Objective-C
的方法呼叫是屬於訊息傳送的機制.
接著呢, 我們知道了每個類都有一個isa
的結構體指標, 在這個結構體裡, 我們得到指定類的所有屬性, 所有方法的列表, 也可以知道這個所屬的父類是什麼等等的.
這只是RunTime
黑魔法的一丟丟應用, 如果沒有看過之前那些文章的朋友可以去這裡看看:
轉載宣告:如需要轉載該文章, 請聯絡作者, 並且註明出處, 以及不能擅自修改本文.
RunTime中的訊息應用
在之前的文章裡, 我們就有接觸過RunTime
的訊息機制, 通過Clang
把Object.m
檔案轉成Object.mm
檔案, 然後就可以看得到裡面的所有東西, 包括是怎麼呼叫方法的也可以明確的看到.
這次我們換一個方式來實現, 首先我們宣告一個類, 內部實現兩個小方法:
#import "RunTimeMessageModel.h"
@implementation RunTimeMessageModel
- (void)cl_post {
NSLog(@"被呼叫了: %@, 當前物件為: %@", NSStringFromClass([self class]), self);
}
- (void)cl_getWithCount:(NSInteger)count {
NSLog(@"被%ld人呼叫了", count);
}
@end
複製程式碼
在這裡我們還需要修改一點東西, 不然我們沒法用RunTime
的訊息機制:
搞定完一切之後, 我們就來實現一下:
#import "RunTimeMessageController.h"
#import "RunTimeMessageModel.h"
#import <objc/message.h>
@interface RunTimeMessageController ()
@end
@implementation RunTimeMessageController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = NSStringFromClass([self class]);
Class getClass = objc_getClass("RunTimeMessageModel");
NSLog(@"Get The Class is: %@", getClass);
// Xcode 會自動遮蔽通過objc_msgSend建立物件, 我們可以去到工程裡設定
// Build Setting -> Enable Strict Checking of objc_msgSend Calls 改成No就好了.
RunTimeMessageModel *messageModel = objc_msgSend(getClass, @selector(alloc));
NSLog(@"alloc Object: %@", messageModel);
// 在不呼叫init方法, 我們也可以通過發訊息呼叫想用的方法, 這裡呼叫沒有在.h檔案裡宣告的方法會警告該方法沒有宣告
objc_msgSend(messageModel, @selector(cl_post));
messageModel = objc_msgSend(messageModel, @selector(init));
NSLog(@"init Object: %@", messageModel);
objc_msgSend(messageModel, @selector(cl_post));
// 還有另外一種寫法, 就是把所有東西都集合在一起, 也就是我們常用的[[NSObject alloc] init];的原型
RunTimeMessageModel *messageModelTwo = objc_msgSend(objc_msgSend(objc_getClass("RunTimeMessageModel"), @selector(alloc)), @selector(init));
objc_msgSend(messageModelTwo, @selector(cl_getWithCount:), 5);
}
@end
複製程式碼
列印結果:
2017-09-27 22:55:44.713329+0800 RunTimeExample[74324:4732411] -[RunTimeMessageController viewDidLoad] 第27行
Get The Class is: RunTimeMessageModel
2017-09-27 22:55:44.714726+0800 RunTimeExample[74324:4732411] -[RunTimeMessageController viewDidLoad] 第33行
alloc Object: <RunTimeMessageModel: 0x60400001b9f0>
2017-09-27 22:55:44.715881+0800 RunTimeExample[74324:4732411] -[RunTimeMessageModel cl_post] 第15行
被呼叫了: RunTimeMessageModel, 當前物件為: <RunTimeMessageModel: 0x60400001b9f0>
2017-09-27 22:55:44.716663+0800 RunTimeExample[74324:4732411] -[RunTimeMessageController viewDidLoad] 第40行
init Object: <RunTimeMessageModel: 0x60400001b9f0>
2017-09-27 22:55:44.718265+0800 RunTimeExample[74324:4732411] -[RunTimeMessageModel cl_post] 第15行
被呼叫了: RunTimeMessageModel, 當前物件為: <RunTimeMessageModel: 0x60400001b9f0>
2017-09-27 22:55:44.719543+0800 RunTimeExample[74324:4732411] -[RunTimeMessageModel cl_getWithCount:] 第20行
被5人呼叫了
複製程式碼
由於之前的文章裡已經有做過解釋了, 這裡就不詳細講解了.
RunTime方法交換
在這裡還有比較有意思的用處, 就是交換兩個方法, 這裡另外建一個類:
// RunTimeMethodModel.h檔案
#import <Foundation/Foundation.h>
@interface RunTimeMethodModel : NSObject
@property (nonatomic, copy) NSString *cl_height;
@property (nonatomic, copy) NSString *cl_weight;
- (NSString *)cl_height;
- (NSString *)cl_weight;
@end
// RunTimeMethodModel.m檔案
#import "RunTimeMethodModel.h"
@implementation RunTimeMethodModel
- (NSString *)cl_height {
return @"我身高180";
}
- (NSString *)cl_weight {
return @"我體重280";
}
@end
複製程式碼
- (void)viewDidLoad {
[super viewDidLoad];
self.title = NSStringFromClass([self class]);
RunTimeMethodModel *methodModel = [[RunTimeMethodModel alloc] init];
NSLog(@"身高: %@", methodModel.cl_height);
NSLog(@"體重: %@", methodModel.cl_weight);
Method methodOne = class_getInstanceMethod([methodModel class], @selector(cl_height));
Method methodTwo = class_getInstanceMethod([methodModel class], @selector(cl_weight));
method_exchangeImplementations(methodOne, methodTwo);
NSLog(@"列印的內容: %@", [methodModel cl_height]);
}
複製程式碼
列印結果:
2017-09-28 00:36:20.277653+0800 RunTimeExample[76955:4827313] -[RunTimeMethodController viewDidLoad] 第27行
身高: 我身高180
2017-09-28 00:36:20.279144+0800 RunTimeExample[76955:4827313] -[RunTimeMethodController viewDidLoad] 第28行
體重: 我體重280
2017-09-28 00:36:20.283090+0800 RunTimeExample[76955:4827313] -[RunTimeMethodController viewDidLoad] 第35行
列印的內容: 我體重280
複製程式碼
PS: 但這裡需要注意一點, 由於這裡的ViewController
會銷燬, 但method_exchangeImplementations
會一直存在, 再次進來的時候, 就會再次根據上次交換過的順序再次交換.
那怎麼辦呢? 查了一下資料, 發現有兩個解決的方案:
+load交換方法
我們可以把交換方法的步驟放在+load
, 試試看:
+ (void)load {
Method methodOne = class_getInstanceMethod(self, @selector(cl_height));
Method methodTwo = class_getInstanceMethod(self, @selector(cl_weight));
method_exchangeImplementations(methodOne, methodTwo);
}
- (NSString *)cl_height {
return @"我身高180";
}
- (NSString *)cl_weight {
return @"我體重280";
}
複製程式碼
列印結果:
2017-09-30 20:32:48.054168+0800 RunTimeExample[81266:5241395] -[RunTimeMethodController viewDidLoad] 第23行
身高: 我體重280
2017-09-30 20:32:48.054436+0800 RunTimeExample[81266:5241395] -[RunTimeMethodController viewDidLoad] 第24行
體重: 我身高180
2017-09-30 20:33:19.179724+0800 RunTimeExample[81266:5241395] -[RunTimeMethodController viewDidLoad] 第23行
身高: 我體重280
2017-09-30 20:33:19.179947+0800 RunTimeExample[81266:5241395] -[RunTimeMethodController viewDidLoad] 第24行
體重: 我身高180
複製程式碼
PS: 雖然在+load
這個方法裡的確是可以保證方法交換隻有一次, 但這裡有一個弊端, 就是當程式一執行就會執行這個方法交換了, 這並不是一個好的方案.
+initialize交換方法
這裡我們嘗試第二個方案, 使用+initialize
方法:
+ (void)initialize {
Method methodOne = class_getInstanceMethod(self, @selector(cl_height));
Method methodTwo = class_getInstanceMethod(self, @selector(cl_weight));
method_exchangeImplementations(methodOne, methodTwo);
}
- (NSString *)cl_height {
return @"我身高180";
}
- (NSString *)cl_weight {
return @"我體重280";
}
複製程式碼
列印結果:
2017-09-30 20:42:49.750880+0800 RunTimeExample[81385:5249133] -[RunTimeMethodController viewDidLoad] 第23行
身高: 我體重280
2017-09-30 20:42:49.752335+0800 RunTimeExample[81385:5249133] -[RunTimeMethodController viewDidLoad] 第24行
體重: 我身高180
複製程式碼
ok, 這滿足了我們的需要了, 這解釋一下+load
和+initialize
的區別:
- +load: 程式一開始就會去執行, 只執行一次.
- +initialize: 當類被初始化的時候會才會去執行, 該類只會執行一次.
當然並不是說在+load
上用是不對的, 也不是說+initialize
就一定是對的, 根據場景的需要來使用才是王道.
RunTime方法攔截
從剛剛我們就知道, 可以使用method_exchangeImplementations
交換兩個方法, 但只應用在本類, 現在我們來看看別的應用:
@implementation BaseModel
- (void)cl_logBaseModel {
NSLog(@"Base Model Log");
}
@end
複製程式碼
@implementation InterceptModel
- (void)cl_logInterceptModel {
NSLog(@"Intercept You Method ");
}
@end
複製程式碼
最終的實現:
+ (void)initialize {
Method mehtodOne = class_getInstanceMethod([BaseModel class], @selector(cl_logBaseModel));
Method mehtodTwo = class_getInstanceMethod([InterceptModel class], @selector(cl_logInterceptModel));
method_exchangeImplementations(mehtodOne, mehtodTwo);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
BaseModel *baseModel = [[BaseModel alloc] init];
[baseModel cl_logBaseModel];
}
複製程式碼
列印結果:
2017-10-01 12:36:03.488764+0800 RunTimeExample[82538:5345309] -[InterceptModel cl_logInterceptModel] 第15行
Intercept You Method
複製程式碼
發現方法是被InterceptModel
這個類攔截, 並且替換了InterceptModel
的方法.
補充點小知識
這裡我們都是用例項方法來作為例子, 那是不是說只能使用例項方法呢?
其實並不是的, 類方法也可以交換和攔截:
#import "BaseModel.h"
@implementation BaseModel
- (void)cl_logBaseModel {
NSLog(@"Base Model Log");
}
+ (void)cl_logBaseModelClass {
NSLog(@"Base Model Class Log");
}
@end
複製程式碼
@implementation InterceptModel
- (void)cl_logInterceptModel {
NSLog(@"Intercept You Method ");
}
+ (void)cl_logInterceptModelClass {
NSLog(@"Intercept Class You Method ");
}
@end
複製程式碼
最終實現:
+ (void)initialize {
// 攔截例項方法
Method mehtodOne = class_getInstanceMethod([BaseModel class], @selector(cl_logBaseModel));
Method mehtodTwo = class_getInstanceMethod([InterceptModel class], @selector(cl_logInterceptModel));
method_exchangeImplementations(mehtodOne, mehtodTwo);
// 攔截類方法
Method classMehtodOne = class_getClassMethod([BaseModel class], @selector(cl_logBaseModelClass));
Method classMehtodTwo = class_getClassMethod([InterceptModel class], @selector(cl_logInterceptModelClass));
method_exchangeImplementations(classMehtodOne, classMehtodTwo);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
BaseModel *baseModel = [[BaseModel alloc] init];
[baseModel cl_logBaseModel];
[BaseModel cl_logBaseModelClass];
}
複製程式碼
2017-10-01 12:59:13.229912+0800 RunTimeExample[82996:5374475] -[InterceptModel cl_logInterceptModel] 第15行
Intercept You Method
2017-10-01 12:59:13.230480+0800 RunTimeExample[82996:5374475] +[InterceptModel cl_logInterceptModelClass] 第20行
Intercept Class You Method
複製程式碼
工程地址
專案地址: https://github.com/CainRun/iOS-Project-Example/tree/master/RunTime/Four