玩转iOS开发:装逼技术RunTime的应用(一)

2,011 阅读7分钟

文章分享至我的个人技术博客: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


最后

码字很费脑, 看官赏点饭钱可好

微信

支付宝