玩轉iOS開發:裝逼技術RunTime的應用(一)

CainLuo發表於2019-03-04

文章分享至我的個人技術部落格:https://cainluo.github.io/15065147177317.html


前面我們把RunTime的一些基本知識都瞭解了一遍, 知道了在Objective-C的方法呼叫是屬於訊息傳送的機制.

接著呢, 我們知道了每個類都有一個isa的結構體指標, 在這個結構體裡, 我們得到指定類的所有屬性, 所有方法的列表, 也可以知道這個所屬的父類是什麼等等的.

這只是RunTime黑魔法的一丟丟應用, 如果沒有看過之前那些文章的朋友可以去這裡看看:

轉載宣告:如需要轉載該文章, 請聯絡作者, 並且註明出處, 以及不能擅自修改本文.


RunTime中的訊息應用

在之前的文章裡, 我們就有接觸過RunTime的訊息機制, 通過ClangObject.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的訊息機制:

1

搞定完一切之後, 我們就來實現一下:

#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


最後

碼字很費腦, 看官賞點飯錢可好

微信

支付寶

相關文章