前言

之前在看一些第三方源码的时候,时不时的能碰到一些关于运行时相关的代码。于是乎,就阅读了一些关于运行时的文章,感觉写的都不错,写此篇文章为了记录一下,同时也重新学习一遍。

Runtime简介

  • Runtime简称运行时,OC就是运行时机制。
  • C语言中函数的调用在编译的时候就会决定调用哪个函数。
  • 对于OC来说,属于动态调用过程,在编译的时候并不能决定调用哪个函数,只有真正运行的时候才会根据函数的名称找到对应的函数来调用。
  • 事实证明:
    1. 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明就不会报错。
    2. 在编译阶段,C语言调用未实现的函数就会报错。

Runtime的作用

发送消息

  • 方法调用的本质就是向对象发送消息。

  • objc_msgSend,只有对象才能发送消息,因此以objc开头。注意:在oc中,不论是实例对象还是Class,都是id类型的对象

  • 让我们来看看方法调用转化成运行时的代码,看看调用方法的真面目吧。

    1. 新建一个命令行工程

    2. 然后在main方法里面写上

      int main(int argc, const char * argv[]) {
      @autoreleasepool {
      NSObject *obj = [[NSObject alloc] init];
      }
      return 0;
      }
    3. 用终端跳转到工程所在的根目录,然后命令行运行clang -rewrite-objc main.m

    4. 然后ls查看一下当前目录可以看到有一个main.cpp文件,我们用open main.cpp打开该文件,可以看到一大串代码,我们可以直接翻到底部,可以看到这样的代码:

      int main(int argc, const char * argv[]) {
      /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
      NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
      }
      return 0;
      }
    5. 删除掉一些强制转换,将上面的代码简化后:

      int main(int argc, const char * argv[]) {
      /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
      NSObject *obj = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
      }
      return 0;
      }

总结:到这里,我们可以看到调用方法的本质就是发送消息了,并且可以看到我们写的

NSObject *obj = [[NSObject alloc] init];
> 上面这条语句发送了两次消息,第一次发送了`alloc`消息,第二次发送了`init`消息。

### 给分类添加属性
相信大家都知道分类是不可以添加属性的,不过我们可以通过运行时,给分类动态的添加属性。 **原理:**给一个分类声明属性,其本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。 #### 实践
- 我们给`NSObject`添加一个分类,然后声明一个`name`属性。 ```objc
#import <Foundation/Foundation.h>
@interface NSObject (Extension)
@property (nonatomic, copy) NSString *name;
@end
``` - 我们创建一个`NSObject`对象,然后给`name`属性赋值,并且打印`name`的值。
我们可以看到编译成功,但是运行的时候就会华丽丽的崩溃,这就是所谓的不能给分类添加属性的原因了。不过我们可以通过运行时实现。
- 接下来我们在.m文件重写`name`属性的`setter`以及`getter`方法。 ```objc
#import "NSObject+Extension.h"
#import <objc/runtime.h>
static const char *key = "name";
@implementation NSObject (Extension)
-(NSString *)name {
// 根据关联的key,获取关联的值
return objc_getAssociatedObject(self, key);
}
-(void)setName:(NSString *)name {
// 第一个参数:给哪个对象添加关联
// 第二个参数:关联的key,通过这个key获取
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
``` - 运行程序,编译成功,运行也成功。 ### 动态添加方法
* **开发使用场景:** 如果一个类方法非常多,加载该类到内存的时候比较耗费资源,需要给每个方法生成映射表,可以使用运行时给该类动态添加方法来解决。(**ps:我感觉这个在开发中不常用**) #### 实践
- 创建一个继承`NSObject`的`Student`类,声明一个`study`方法。 ```objc
#import <Foundation/Foundation.h>
@interface Student : NSObject
-(void)study;
@end
``` - 创建一个`Student`对象,调用`study`方法。 ```objc
Student *s = [[Student alloc] init];
[s performSelector:@selector(study)];
```
此时会出现一个经典的报错:
```objc
-[Student study]: unrecognized selector sent to instance 0x7fd719cbb2f0
``` > **错误原因:调用一个未实现的实例方法** - 我们在`Student`类.m文件动态添加study方法的实现。 ```objc
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
// void(*)()
// 默认方法都有两个隐式参数,
void studyStudent(id self, SEL sel){
NSLog(@"%@--%@",self,NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用该方法处理,并且会把对应的方法列表传进来,我们可以在这个方法里判断,未实现的方法是不是我们想要动态添加的方法。
+(BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(study)) {
// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, @selector(study), studyStudent, "v@:");
// 注意:此处需要马上结束此方法(否则,如果存在继承关系的话,会调用到父类去)
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
```
### 交换方法实现
* 交换方法实现,也就是所谓的`Method Swizzling`。
* **场景一:**如果你整个项目都做完了,然后产品经理告诉你想统计每一个页面停留的时长。
* **场景二:**系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有功能。 #### 实践
**场景一:**统计整个项目每一个页面停留时长。(**解决方法也不是唯一的**) - 方式一:找到所有的控制器,然后写上: ```objc
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[MobClick beginLogPageView:NSStringFromClass([self class])];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[MobClick endLogPageView:NSStringFromClass([self class])];
}
``` 然后我们一个项目可能有几十个甚至上百个页面需要统计,我们总不可能每个页面都这样写吧。于是乎,就有了**方式二**。 - 方式二:所有的界面都继承于一个基类,然后在基类中写上 ```objc
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[MobClick beginLogPageView:NSStringFromClass([self class])];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[MobClick endLogPageView:NSStringFromClass([self class])];
}
``` 可是一开始写项目的时候,并没有使用到继承,所以又papapa地就整个项目的控制器都继承于一个基类,重复地将每一个控制器的继承都该成了我们创建的基类。但是,这样解决真的好么,有可能我们有些界面是继承自`UITableViewController`的,`UICollectionViewController`,等等。那么你就可能会对这些控制器再单独的写上面的代码了。 好不容易将整个项目改过来了,然后某天,公司来了一位新人,你告诉他所有的类都要继承自你写的那个基类,新手总是会不经意地犯错误(**也有可能是人家还没有习惯**),有些类忘记继承了,后期排查起来费力费时。那么有没有更好地解决方式呢?**方式三**就可以处理这种问题。 - 方式三:使用`Method Swizzling`实现,给`UIViewController`写一个分类。 ```objc
#import "UIViewController+Help.h"
#import <objc/runtime.h>
@implementation UIViewController (Help)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
methodSwizzling(class, @selector(viewWillAppear:), @selector(scott_viewWillAppear:));
methodSwizzling(class, @selector(viewWillDisappear:), @selector(scott_viewWillDisappear:));
});
}
void methodSwizzling(Class class, SEL originSelector, SEL swizzSelector){
Method originMethod = class_getInstanceMethod(class, originSelector);
Method swizzMethod = class_getInstanceMethod(class, swizzSelector);
BOOL isAddMethod = class_addMethod(class, originSelector, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isAddMethod) {
class_replaceMethod(class, swizzSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else{
method_exchangeImplementations(originMethod, swizzMethod);
}
}
-(void)scott_viewWillAppear:(BOOL)animated {
[self scott_viewWillAppear:animated];
NSLog(@"调用自定义viewWillAppear");
}
-(void)scott_viewWillDisappear:(BOOL)animated {
[self scott_viewWillDisappear:animated];
NSLog(@"调用自定义viewWillDissappear");
}
@end
``` ### 字典转模型
这个单独开一篇给大家讲讲吧。 ## 结束语
希望通过本文能让大家学习到一些关于Runtime的知识,如果有什么疑问,欢迎大家一起讨论。

初识 Runtime的更多相关文章

  1. 初识runtime

    首先需要加上头文件#import<objc/runtime.h>和#Import<objc/message.h>   将A中的某个方法替换成B中某个方法,且没有任何的耦合 这里 ...

  2. Runtime-b

    感谢大神分享 依旧是网上很多runtime的资料,依旧是看不懂,,,这里给大家转化一下runtime,使它由隐晦难懂变得通俗易懂. (虽然截图和语言组织的有些凌乱,但是大家还是一点一点的阅读下去吧,可 ...

  3. Objective-C 中的Runtime的使用

    Runtime的使用 一直以来,OC被大家冠以动态语言的称谓,其实是因为OC中包含的runtime机制.Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平 ...

  4. Objective-C runtime初识

    Objective-C Runtime Describes the macOS Objective-C runtime library support functions and data struc ...

  5. Runtime初识

    什么是Runtime   我们写的代码在程序运行过程中都会被转化成runtime的C代码执行,例如[target doSomething];会被转化成objc_msgSend(target, @sel ...

  6. Java初识

    基础概念 特点: 完全面向对象,动态 解释性,简单.易移植,跨平台 安全健壮,高性能 多线程,分布式 三种核心机制: Java虚拟机 Java Virtual Machine 垃圾收集机制 Garba ...

  7. C#脚本引擎 CS-Script 之(一)——初识

    最近在做新产品,这个产品需要满足不同项目对于系统的定制性数据处理需求,比如有的要统计一段时间内某开关打开关闭了多少次,有的要统计一段时间内空调的使用率,有的希望根据温度来控制空调的开还是关,有的则是希 ...

  8. XSS 自动化检测 Fiddler Watcher & x5s & ccXSScan 初识

    一.标题:XSS 自动化检测 Fiddler Watcher & x5s  & ccXSScan 初识     automated XSS testing assistant 二.引言 ...

  9. iOS 开发-- Runtime 1小时入门教程

    1小时让你知道什么是Objective-C Runtime,并对它有一定的基本了解,可以在开发过程中运用自如. 三.Objective-C Runtime到底是什么东西? 简而言之,Objective ...

随机推荐

  1. 深度学习之 mnist 手写数字识别

    深度学习之 mnist 手写数字识别 开始学习深度学习,先来一个手写数字的程序 import numpy as np import os import codecs import torch from ...

  2. 如何将portfolio产品图片上的悬停去掉?

    在Avada主题里,文章和portfolio的分类界面的图片,鼠标移入后都会出现这个东西 那么如何把它去掉,改为直接点击产品图片后进入产品详情页呢? 在theme option里搜索image rol ...

  3. api-gateway实践(15)3.6JL分支和3.7并行改造需求

    一.名称改为"API网关" --哪个地方的名称?二.开发者视图中,API网关显示两个视图. 1. 服务分类视图:支持按照业务分为多个类别,分类方式参照应用服务化的分类:人像比对.自 ...

  4. windows7.0旗舰版安装后控制面板自带的Microsoft程序

    1.不要卸载,否则会出现安装其他软件时缺少动态链接库

  5. Ubuntu下安装最新sublime

    1. Install the GPG key: wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | sudo apt-key ...

  6. python Http协议

    Http协议 一 HTTP概述 HTTP(hypertext transport protocol),即超文本传输协议.这个协议详细规定了浏览器和万维网服务器之间互相通信的规则. HTTP就是一个通信 ...

  7. python--socket粘包

    socket粘包 1 什么是粘包 须知:只有TCP有粘包现象,UDP永远不会粘包,首先需要掌握一个socket收发消息的原理, 所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少 ...

  8. Spark:导入数据到oracle

    方案一: //overwrite JdbcDialect fitting for Oracle val OracleDialect = new JdbcDialect { override def c ...

  9. ROS系统MoveIt玩转双臂机器人系列(一)

    一.ROS系统的MoveIt模块简介 机器人操作系统ROS目前最受关注的两个模块是导航(Navigation)和机械臂控制(MoveIt!),其中,机械臂控制模块(后面简称MoveIt)可以让用户快速 ...

  10. hdu-2639 Bone Collector II---第k大背包

    题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=2639 题目大意: 求第k大背包. 思路: 由01背包递推式dp[i][j] = max(dp[i][ ...