前言

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

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. 增加Linux虚拟机的硬盘空间

    原配置为40G,现需要增加到60G,操作方法如下: 一.虚拟机关机,在编辑设置里调整硬盘空间到60G 二.虚拟机开机,扩展硬盘空间 1.安装gparted,命令如下 sudo apt-get inst ...

  2. Python Tornado初学笔记之表单与模板(一)

    Tornado中的表单和HTML5中的表单具有相同的用途,同样是用于内容的填写.只是不同的是Tornado中的表单需要传入到后台,然后通过后台进行对模板填充. 模板:是一个允许嵌入Python代码片段 ...

  3. angular2 学习笔记 ( animation 动画 )

    refer : https://angular.io/guide/animations https://github.com/angular/angular/blob/master/packages/ ...

  4. 使用 vi 命令

    一.vi是什么 vi命令是UNIX操作系统和类UNIX操作系统中最通用的全屏幕纯文本编辑器. Linux中的vi编辑器叫vim,它是vi的增强版(vi Improved),与vi编辑器完全兼容,而且实 ...

  5. api-gateway实践(06)新服务网关 - 请求监控

    一.实时监控 用户点击服务实例,系统显示服务实例-version下的api列表, 用户点击某个api的如下两个图标 1.API请求次数监控 横轴:时间,粒度为分钟 纵轴:请求访问次数 展示:失败数(红 ...

  6. Tomcat(1-1)重置Tomcat8.5管理员的用户名和密码

    1.访问 http://localhost:8080/,点击 [manager app],提示输入用户名和密码,admin/admin后报错.  2.解决办法:重置Tomcat8.5管理员的用户名和密 ...

  7. io流的关闭顺序

    1.一般先打开的后关闭,后打开的先关闭 2.可以只关闭处理流,因为io流使用了装饰模式,所以关闭处理流时,会调用节点流的close()方法.

  8. awk、变量、运算符、if多分支

    awk.变量.运算符.if多分支 awk: 语法 awk [options] 'commands' files option -F 定义字段分隔符,默认的分隔符是连续的空格或制表符 使用option中 ...

  9. uva 11636 Hello World!

    https://vjudge.net/problem/UVA-11636 题意: 希望输出n条语句,但是并不会循环,所以只能复制粘贴,一条语句经过复制粘贴后可以变为2条,2条变成4条....每次可以只 ...

  10. oracle:批量插入不同方案对比

    实时测试的速度: --48466条数据 --1.297 inline view更新法 inline view更新法就是更新一个临时建立的视图 update (select a.join_stateas ...