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

前言:什么是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类方法存储在对应的元类里面,存储形式同上;

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342