前言:什么是runtime
?根据官方文档的解释,Objective-C语言将决定尽可能的从编译和链接时推迟到运行时。只要有可能,Objective-C总是使用动态的方式来解决问题。这意味着Objective-C语言不仅需要一个编译器,同时也需要一个运行时系统来执行编译好的代码,这儿的运行时系统扮演的角色类似于Objective-C语言的操作系统,Objective-C基于该系统来工作。
一、Runtime初识
1、什么是Runtime?
`Runtime`是一套由C,C++,及汇编语言写成的一套`API`,为我们的Objective-C增加运行时功能,OC的所有代码在编译时最终都会转化成直接执行`Runtime`中`API`的代码。
比如下面这个方法的调用部分
BMPerson * person = [[BMPerson alloc]init];
[person run];
经过转化就会变成这样
objc_msgSend(person, sel_registerName("run"));
2、什么是运行时?什么是编译时?
1、编译时:
编译就是一系列工作,作用就是把我们可读性非常强的源代码,比如`Objective`、`Swift`等高级语言编译成机器语言的过程。比如汇编语言再到最后的二进制从而被我们的系统识别。
2、运行时:
运行时就是我们的代码run起来后被装载到内存上。
值得注意的是,将静态语言编译和链接时期需要做的事放到了运行时来处理之后,我们写的代码的灵活度就很高了,比如我们可以选择把消息转发给我们想要的对象,或者随意交换方法的实现等等。
3、Runtime版本和平台
Runtime运行时系统有两个已知版本:早期版本(Legacy)和现行版本(Modern),早期版本对应的编程接口 Objective 1.0;现行版本对应的编程接口 Objective-C 2.0;早期版本和现行版本的区别就是:早期版本中,如果你想改变类中实例变量的布局,您必须重新编译该类的所有子类;而现行版本中则无需编译该类的任何子类就可以达到原来的效果。
iPhone程序和Mac OS X v10.5及以后的系统中的64位程序使用的都是Objective-C系统的现行版本。
其他情况(Mac OS X系统中的32位程序)使用的是早期版本。
4、和运行时系统的交互
Objective-C程序有三种途径和运行时系统交互:
1、通过Objective-C源代码
2、通过类NSObject的方法
3、通过运行时系统的函数
二、Objective-C 的对象和方法的本质
先创建一个工程,创建一个类BMPerson
继承自NSObject
,声明并实现BMPerson
的实例方法- (void)run;,接着创建一个类BMStudent
继承自BMPerson
,声明并实现实例方法- (void)learn;,
接着在main.m
执行一段程序并run运行一下
//
// main.m
// RuntimeProjectTest
//
// Created by battleMage on 2019/7/22.
// Copyright © 2019 battleMage. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "BMPerson.h"
#import "BMStudent.h"
#include <objc/runtime.h>
void study(){
printf("跑呀跑呀!!!\n");
}
int main(int argc, char * argv[]) {
@autoreleasepool {
//调用person对象方法
BMPerson * person = [[BMPerson alloc]init];
[person run];
//调用BMPerson子类BMStudent的对象方法
BMStudent * student = [[BMStudent alloc] init];
[student learn];
//调用C函数
study();
}
}
运行完成后,可以看到打印台打印如下信息
跑呀跑呀!!!
2019-07-22 23:00:18.827778+0800 RuntimeProjectTest[11943:1704750] 跑步
2019-07-22 23:00:18.828556+0800 RuntimeProjectTest[11943:1704750] 好好学习,天天向上
把C
函数study()
的实现部分注释,就可以发现study()
调用那一行编译直接提示报错 Implicit declaration of function 'study' is invalid in C99,而把BMPerson.m
的实现部分注掉,编译时是不会报错的,但是点击run运行时就会打印台信息报错:
2019-07-22 23:31:15.114209+0800 RuntimeProjectTest[12107:1729913] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BMPerson run]: unrecognized selector sent to instance 0x6000019f8130'
报错信息提示的是BMPerson
的run
没有实现
上述对比一下,就能明白运行时和编译时明显的区别。
接着删除main.m
中的其他代码只留下BMPerson
的创建和方法调用,如下
int main(int argc, char * argv[]) {
@autoreleasepool {
//调用person对象方法
BMPerson * person = [[BMPerson alloc]init];
[person run];
}
}
接着showinfinder,打开终端,使用clang
编译输出main.cpp
文件,在终端输入命令
clang -rewrite-objc main.m -o main.cpp
看到该文件夹下多出了一个main.cpp
的C++
文件,打开文件,在最底部找到下面代码
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
BMPerson * person = ((BMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((BMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("BMPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
}
}
找到这一层先不要着急,然后接着在文件内全局搜索BMPerson
,能够找到这部分代码
#ifndef _REWRITER_typedef_BMPerson
#define _REWRITER_typedef_BMPerson
typedef struct objc_object BMPerson;//注意这一行!!!
typedef struct {} _objc_exc_BMPerson;
#endif
struct BMPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
看到 typedef struct objc_object BMPerson; 这里其实就很明白了,
Objective-C对象的本质其实就是结构体!
再接着看这一块
BMPerson * person = ((BMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((BMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("BMPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
很明显:
Objective-C方法的本质就是发送消息!
接下来我们拿[person run]
的调用,仔细分析消息的组成
(void ()(id, SEL))(void )objc_msgSend)((id)person --- 就是消息的接收者
sel_registerName("run") --- SEL是方法编号,具体底层是一个字符串name,这里顺便提一下imp
,imp
是函数实现的指针,实际过程中是要用SEL
方法编号去找到imp
,拿到函数实现,然后直接调用实现部分
我们可以接着在main.m
下面打印一下地址就能看出来
//调用person对象方法
BMPerson * person = [[BMPerson alloc]init];
[person run];
NSLog(@"%p----%p", sel_registerName("run"), @selector(run));
打印台打印信息:
2019-07-23 22:30:22.259406+0800 RuntimeProjectTest[14063:1897197] 0x1077c2483----0x1077c2483
可以看出这两个地址是完全一致的,这也就是说@selector(run)在编译之后就是sel_registerName("run")
三、Objective-C 的对象的结构
Objective-C对象大致分为三类:
实例对象,类对象,元类对象
方法对应有:
实例方法,类方法
上一块,我们通过clang
大致了解了一下对象的本质,我们继续深入探索对象的详细结构部分。
接下来我们command+B
点击 Class
进入objc.h
文件,发现一个结构体重定义
typedef struct objc_class *Class;
继续command+B
点击查看,进入runtime.h
文件的55行,看到这段代码,为了直观,我把注释直接加在代码里面
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //isa指针
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;//父类
const char * _Nonnull name OBJC2_UNAVAILABLE;//类名
long version OBJC2_UNAVAILABLE;//类的版本信息,默认0
long info OBJC2_UNAVAILABLE;//类的信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE;//该类的实例变量大小
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;//该类的成员变量的链表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; //方法定义的链表
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; //方法缓存
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; //协议链表
#endif
} OBJC2_UNAVAILABLE;
什么是isa
? 一个经过特殊处理优化过的指针,指向对象的类。 如果是实例对象,指向的就是它的类;如果是类对象,就指向该类对象的元类,如果是元类对象,就指向另一个基类的元类。(什么是元类?在Objective-C中,每当我们创建一个类,编译时就会创建一个元类,而这个元类的对象就是我们创建的这个类)
我们创建的实例对象,在C语言中就是
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
这里的isa
指针就指向了其类地址;下面这图就是对类,元类,对象的isa指向说明:
通过以上isa
指针走位图,我们可以结合一些问题,来加深一下我们对runtime isa指针相关知识点的理解:
问题:OC对象方法存在哪里?类方法存在哪里?
OC对象方法存储在对应的类里面,具体存储形式是以散列表(hash table)的形式;
OC类方法存储在对应的元类里面,存储形式同上;
对象在类里面,是以一个实例对象的姿态出现的;同理,类在元类里面也是以一个实例对象的姿态出现的。
准确描述类比对象、类以及元类之间的关系:对象是类的一个实例,类是元类的一个实例!
所以类方法是存储在元类里面的,并且是以元类的一个实例方法的形式进行存储的。