美文网首页
Runtime系列之OC对象和方法的本质

Runtime系列之OC对象和方法的本质

作者: 溪浣双鲤 | 来源:发表于2019-07-24 23:40 被阅读0次

前言:什么是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'

报错信息提示的是BMPersonrun没有实现

上述对比一下,就能明白运行时和编译时明显的区别。

接着删除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.cppC++文件,打开文件,在最底部找到下面代码


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,这里顺便提一下impimp是函数实现的指针,实际过程中是要用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指针走位图

通过以上isa指针走位图,我们可以结合一些问题,来加深一下我们对runtime isa指针相关知识点的理解:
问题:OC对象方法存在哪里?类方法存在哪里?

OC对象方法存储在对应的类里面,具体存储形式是以散列表(hash table)的形式;

OC类方法存储在对应的元类里面,存储形式同上;

对象在类里面,是以一个实例对象的姿态出现的;同理,类在元类里面也是以一个实例对象的姿态出现的。
准确描述类比对象、类以及元类之间的关系:对象是类的一个实例,类是元类的一个实例!
所以类方法是存储在元类里面的,并且是以元类的一个实例方法的形式进行存储的。

溪浣双鲤的技术摸爬滚打之路

相关文章

网友评论

      本文标题:Runtime系列之OC对象和方法的本质

      本文链接:https://www.haomeiwen.com/subject/qhmqrctx.html