二十七、对于前面基础的一些总结

714 阅读16分钟

本文由快学吧个人写作,以任何形式转载请表明原文出处。

一、runtime是什么

其实前面所有文章提到的东西,无论是alloc和init、无论是对象的本质、类的本质、方法的本质、消息转发、isa、ro和rw还有rwe、包括消息转发和动态决议,甚至dyld的流程,都是或者涉及到了runtime。

那么runtime到底是什么,运行时和编译时到底是什么?

1.运行时和编译时

编译时(也可以说编译期) :

程序的编译是要经过 : 源码-->预编译-->编译-->汇编-->链接-->生成可执行文件。在整个的这个过程中,都是编译期,只要程序没运行起来的时候,都属于编译期间,这个期间要做的事情就是把你写的东西翻译成机器认识的语言,然后给你写的东西挑错。编译期是不存在分配内存给程序的,只有程序运行起来了,才需要内存。

运行时 :

运行时就是程序被加载到内存了,对于iOS来说,就是dyld的map_images和load_images,把你写的东西和相关的第三方库还有最重要的系统库都从可执行文件映射到内存上面。

为什么说Objective-C是运行时语言 :

如果是编译时的语言,那么在整个编译的阶段,也就是上面的那个流程中,你写的东西就是完全确定的了,无论是方法的实现,还是属性协议之类的,都是确定好了的,就是你写好的东西,不能再改变了,比如说ro,这东西是只能读的,不能再改了。

而Objective-C却是将一些编译和链接时候的需要做的事情放到了运行时再做,也就是说,哪怕你已经生成了可执行文件,但是在这个文件里面的一些方法、属性,它们要做什么事情,是不完全被确定的,是可以修改的。比如你调用一个方法的时候,这个方法的IMP到底是什么,其实不是完全确定的,有可能在运行的期间,在某个地方,方法的SEL就指向了其他的IMP(也是你代码写的),这就是所谓的动态修改一个方法的实现,最直击根本的就是rw和rwe。

2. 啥是runtime

上面说的,Objective-C是门运行时的语言,那么它是怎么就成了这种运行时的动态语言的?依靠的就是runtime这套API。

runtime本质上就是一套API,它就是由C和C++汇编实现的一套API。runtime为OC添加了面向对象、运行时的功能特性。我们写的所有的OC代码,在程序的运行过程中,也就是运行时,最终都会被转换成runtime中的C语言代码。

二、类的本质是什么

有关章节 :类的本质和结构

答 :

类的本质是一个objc_class结构体,objc_class是继承于 objc_object结构体的结构体,oc中的Class仿写的就是objc_class,NSObject就是仿写的objc_object。isa也是Class类型。

在objc_class的内部,有四个成员,其中第一成员isa是继承于objc_object的,isa的信息中包含了类的基本属性,也包含了类的父类。第二个成员是superClass,也就是其父类。第三个成员是cache_t,存储着类的方法的缓存,用于快速查找方法的实现。第四个成员是bits,存储着类的详细信息,包括但不限于rw、ro、rwe中存储的属性、成员变量、方法、协议等信息。

三、方法的本质是什么

答 :

方法的本质是函数指针,sel就是方法指针的索引,imp就是方法指针,通过sel就可以找到对应的imp。调用方法的本质才是objc_msgSend,也就是消息发送。objc_msgSend是汇编的代码,先通过查询调用方法的对象(可能是类,也可能是元类,但是根据类的本质,万物皆对象)的缓存,看是否可以直接找到方法的实现,如果在对象的缓存中找不到方法的实现的缓存,那么就会通过lookUpImpOrForward进行查找,lookUpImpOrForward则是在调用方法的对象的方法列表中,有可能是ro,也可能是rw或者rwe的方法列表中查找方法的实现,lookupImpOrForward会沿着继承链,查找本身的方法列表,查找父类的缓存和方法列表,如果父类没有,就会一直查找到NSObject这个根类。

如果可以在继承链中找到方法的实现,则直接返回方法的函数指针。如果继承链上找不到方法的实现,则会直接进行动态方法决议,也就是看调用方法的对象是否实现了resolveInstanceMethod方法,本质也是通过objc_msgSend给调用对象发送信息,查看调用对象的resolveInstanceMethod是否帮助方法找到了imp。

如果动态方法决议依然找不到方法的imp,那么会进行消息转发。也就是forwardTargetForSelector(也叫快速消息转发)和methodSignatureForSelector && forwardInvocation(两个一般是一起用的,是慢速的消息转发)。在这里,方法会变成信号,等待其他对象帮其进行方法实现。

sel是在objc_init()中的注册dyld的回调函数中,通过map_images的read_images将sel放入了一张表,也就是说sel进入内存的时机是read_images。

imp本质就是函数指针。也就相当于函数的实现。毕竟函数指针存的就是函数的地址吗。

四、类是如何进行加载的,分类是如何进行加载的

答 :

类的加载要分懒加载类和非懒加载类。

对于非懒加载类 :

在libobjc库进行初始化的时候,也就是objc_init中,注册dyld的回调函数中,通过回调函数,让dyld利用map_images函数中的read_images函数,将可执行文件中的类先通过readClass将其内存地址和名称进行关联,然后将其加入到两个表中,一个表存储的是没有在dyld的共享缓存中存在的类(无论这个类是否实现),另一个表存储的是已经分配内存的类。

但是非懒加载类的实现不是在readClass中进行的,非懒加载类的真正实现是在realizeClassWithoutSwift函数中。将ro中的数据拷贝一份到rw中,并将rw中ro成员设置成ro,并将类的数据指向变更成rw,也就是说我们访问到的类的数据,一般都是rw的数据。然后对其进行方法的排序。方法的排序是按照SEL的地址进行排序的。

对于懒加载类 :

懒加载类的实现则是要在懒加载类第一次被调用的时候,通过lookUpImpOrForward中的函数,进入到realizeClassWithoutSwift,进入后和非懒加载类的实现是一样的。

对于分类 :

分类的加载和类的加载有一定的关系,如果是非懒加载类和非懒加载分类,无论几个分类,只要有一个分类是非懒加载的,那么其他的分类哪怕是懒加载的,也会提前加载。非懒加载分类会在load_images中通过loadAllCategories函数,进入到attatchCategories,然后完成主类的rwe的创建和内存分配,将分类的方法进行排序,也是按照SEL的地址进行排序。最后将排序好的分类的方法和其他数据一起存入到rwe中。并且对于方法的插入来说,要按照后加入的方法放到方法列表的前面,先加入的方法平移到方法列表的后面。这也是为什么分类和类有同名方法的时候,调用到的总是分类的同名方法。

如果分类是懒加载的,非懒加载分类的数量大于等于2,而类是懒加载的,则分类是通过load_images中的prepare_load_methods函数进入到realizeClassWithoutSwift,然后经过realizeClassWithoutSwift中的methodize中的attatchToClass进入到了attatchCategories。attatchCategories中的流程和上面说的一样。

如果分类全是懒加载的,类无论是不是懒加载的,分类的数据都会在编译期就写入到类的ro中,跟随类的加载进入到内存。如果分类

如果分类只有1个是非懒加载的,其余都是懒加载的,类是懒加载的,那么分类会使类提前加载。但是分类的数据还是在编译期就存在类的ro中了。

五、能否向编译后的得到类中添加实例变量?能否向运行时创建的类中添加实例变量?

答 :

  1. 对于编译后得到的类,是不可以向其添加实例变量的,因为我们编译好的实例变量是存储在ro中,也就是在编译期就确定了的。一旦编译完成,那么内存结构就是确定的,不能修改。

  2. 能否向运行时创建的类中添加实例变量则要看类是否被注册到内存中了,如果已经注册了,那么也不行,如果还没有,也就是ro结构还没有确定,那么就是可以的。

如果添加的是属性或者方法,那是没有问题的。

六、类在内存中存在几份?

答 :

类在内存中只存在一份。

七、objc_object和对象的关系是什么?

答 :

是一种继承的关系,所有的对象,包括类,都是继承于objc_object这个结构体的,NSObject只是仿写了objc_object的结构。

八、属性、成员变量、实例变量有什么不同?

答 :

最基础的就是成员变量,就像NSString *name,就是一个成员变量,它就是单纯的一个变量,不会自动生成setter和getter方法,需要我们来进行实现它的setter和getter。

实例变量只是一种特殊的成员变量,只不过作为一个实例,可能具备更多的自己的属性,就像JDMan类中有一个实例变量是JDKid,JDKid自己也有一些属性,JDKid一般就叫做实例变量。只是一种特殊的成员变量,也是不会自动生成setter和getter方法的。需要我们手动帮助其实现。

属性则是成员变量 + setter + getter的一种融合。一般用@property来定义,自动会生成setter和getter方法。

九、什么是元类

答 :

元类是oc中的一种特殊的类,它是系统帮我们生成的,万物皆对象,类就是元类的实例对象,元类之于类,就相当于类之于对象。

十、isKindOfClass和isMemberOfClass

这个题太久远也太经典了。

图片.png

结果 :

图片.png

图片.png

1.isKindOfClass

实例方法的 :

图片.png

类方法的 :

图片.png

2. isMemberOfClass

实例方法的 :

图片.png

类方法的 :

图片.png

3. 结论 :

  1. isKindOfClass会查找继承链。参数如果在调用者的继承链上存在,就返回YES。不存在就返回NO,元类和类是不相等的。

  2. isMemberOfClass不会查找继承链,是就是是,不是就是不是。

十、runtime的关联对象是否需要我们手动释放?

答 :

不需要。关联对象的释放也在dealloc中实现了。

图片.png

图片.png

图片.png

图片.png

图片.png

图片.png

十一、方法的调用顺序

答 :

普通的方法的调用顺序 : 先调用的是最后加载到内存中的方法,也就是说,如果有分类,并且分类有主类的同名方法,那么最后一个加载到内存的分类的方法实现就会被调用。

对于load方法的调用顺序 :load方法的调用是先调用所有类的load方法,所有类的load方法调用完毕之后,再调用分类的load方法,分类之间的调用顺序则看编译顺序,在xcode中可以调整。

load方法的调用比其他一般的方法都要早,包括initialize。initialize的默认调用是在类或者分类第一次发送消息的时候调用。

所以为了不让load中加入太多的东西,也为了不影响load,有些需要提前加载的方法可以放在initialize中。

十二、[self class]和[super class]

也是经典老题,问[self class]和[super class]的打印值都是什么。例如下图 :

图片.png

图片.png

答案是一样的。都是JDMan。

原因也很简单,[self class]好分析。

先看class的源码 :

图片.png

图片.png

class的核心就是一个,getIsa,也就是拿到对象的isa指向。那么对于[self class]来说,self在JDMan里,所以self就是JDMan的实例对象,JDMan的实例对象的isa指向就是JDMan,这个很简单。

对于[super class]来说就有一点不一样。首先看一下[super class]的本质什么,通过clang来编译JDMan.m文件,查看[super class]的本质。

进入到JDMan.m所在的文件夹下执行 :clang -rewrite-objc JDMan.m -o JDMan.cpp,看JDMan.cpp

图片.png

[super class]的消息发送不是objc_msgSend,而是objc_msgSendSuper。看objc_msgSendSuper

图片.png

还有个官方的注释 :

图片.png

参数前两个,一个是objc_super结构体,一个是sel。objc_super不就是和编译后的cpp文件中的__rw_objc_super一样结构了吗。看下objc_super :

图片.png

在编译的文件中也看到了,再看一下官方的备注,reciever就是类的实例对象。结合上面的官方备注。不就是相当于receiver(objc_super的参数1)调用了super_class(objc_super的参数2)的方法吗,那不还是[self class(父类的class方法)]吗。所以[self class]和[super class]的打印结果一致。

[self class]和[super class]的区别就在于objc_msgSendobjc_msgSendSuper

objc_msgSend之前说过了,这里看下objc_msgSendSuper的源码,也是汇编 :

图片.png

直接跳转到了objc_msgSendSuper2,看objc_msgSendSuper2

图片.png

直接是去查找的父类的缓存,也就是会比[self class]要快一些,因为[self class]是沿着继承链查找class的实现,而[super class]直接就会指定查找self的父类的class的实现。

回答 :

  1. [self class]和[super Class]的打印结果一致。
  2. 不同的是[self class]本质是objc_msgSend,通过继承链查找到class的实现。而[super class]本质是objc_msgSendSuper,objc_msgSendSuper是汇编,通过跳转到objc_msgSendSuper2,直接查找self的父类的缓存,比[self class]通过继承链递归查找class的实现要快。不需要递归查找,直接找父类。
  3. [super class]是比[self class]的效率要高的。

十三、有关内存偏移的问题

1. 第一题

依然是一道经典老题 :

创建一个基本的项目,在JDMan中定义个实例方法并且实现。

图片.png

在ViewController中 :

图片.png

问是否会报错?报错会报什么错?不会报错的话,执行结果是什么?

答 :

不会报错,会正常执行,执行结果和正常调用一样。

为什么能执行?

对于正常的实例对象aMan来说,是消息发送,也就是objc_msgSend。aMan是个对象,之前说对象的本质的时候说过,对象的本质是结构体,其中第一位是isa,第二位是成员变量。aMan的isa是指向了JDMan这个类的首地址的。然后会在JDMan的内存中,经过不断的偏移,找到cache,如果cache中有方法缓存,则只会返回,如果没有方法缓存,则会找到bits中的方法列表,总之是从JDMan的首地址开始,不停的偏移,去寻找SEL的实现:

如下图

图片.png

对于mcPointer这个指针能执行,则是因为mcPointer指针指向的是JDMan的首地址,也是isa的地址,查找方式和aMan是一样的。

如下图

图片.png

就如图中所画,mcPointer的指针,指向的是JDMan这个类的地址,aMan指针指向的是[JDMan alloc]出来的对象的地址,但是这个对象在调用方法的时候,对象的isa还是会指向JDMan这个类的地址。都是按照这样的偏移去查找方法的实现,所以效果也就是一样的了。

2. 第二题

如果给上述代码做个改变,在JDMan中添加一个属性,并在earn方法中打印self.属性,那么打印的结果是什么?

代码如下图 :

JDMan的 :

图片.png

ViewController的 :

图片.png

答 :

mcPointer调用earn,earn中self.jd_name是ViewController

aMan调用earn,earn中的self.jd_name是null。

原因如下 :

  1. 首先要搞清楚self.jd_name的实质是什么,其实质是self这个指针通过内存偏移,指向了jd_name的内存。
  2. 要知道哪些东西是放在栈里面的,哪些东西是放在堆里面的。

(1). alloc出来的都是在堆里面的。

(2). 指针、对象,比如aMan是存放在栈里面的,aMan指向的JDMan通过alloc申请的内存则是存放在堆里面的。

(3). 临时变量存放在栈中。

(4). 属性的值是存放在堆中的,属性是存放在栈中的。

(5). 栈的分配是从高地址开始分配,向低地址逐渐分配。(可以理解为从下往上)

(6). 堆的分配则是从低地址开始分配,向高地址逐渐分配。(可以理解为从上往下)

  1. 所以要知道这道题到底是怎么回事,就要明白viewDidLoad中的栈里面是怎么存放的。
  2. 首先,先理清楚,viewDidLoad中都有谁入栈。

(1). viewDidLoad自带了两个压栈参数:id self 和 SEL _cmd。形参,属于临时变量。

(2). [super viewDidLoad]中,在上面说过了super的含义,调用的是objc_msgSendSuper,传入的参数是一个objc_super结构体,结构体中有两个成员是receiver和superClass,这两个是要入栈的。第二个参数SEL是不会申请栈空间的。

(3). manClass,临时变量,要入栈。

(4). mcPointer是指针,要入栈的。

(5). aMan也是指针,也要入栈的。

(6). 结构体的入栈就是按照结构体成员的顺序,下面的成员变量在高地址,上面的成员变量在低地址,就是下面的成员先入栈。

(7). 隐藏参数self和_cmd的入栈顺序则是self在高地址,_cmd在低地址,也就是前面的形参在高地址,后面的形参在低地址,就是前面的形参先入栈。

  1. 然后来看它们在栈中的顺序

图片.png

  1. 上述都理清楚了,就可以看mcPointer调用方法后,再平移8个字节,就会到objc_msgSendSuper的receiver上,也就是self,这个时候的self就是ViewController了。因为这是ViewController调用的viewDidLoad。