iOS开发面试只需知道这些,技术基本通关!(Runtime篇)

832 阅读14分钟

runtime.png

一、objc对象的isa的指针指向什么?有什么作用?

指向他的类对象,从而可以找到对象上的方法

详解:下图很好的描述了对象,类,元类之间的关系:

image.png

图中实线是 super_class 指针,虚线是isa指针。

1. Root class (class)其实就是 NSObjectNSObject 是没有超类的,所以 Root class(class)superclass指向 nil

2. 每个 Class 都有一个 isa 指针指向唯一的 Meta class

3. Root class(meta) superclass 指向 Root class(class),也就是 NSObject,形成一个回路。

4. 每个 Meta class 的 isa 指针都指向 Root class (meta)

二、一个NSObject对象占用多少内存空间?

受限于内存分配的机制,一个 NSObject对象都会分配 16byte 的内存空间。

但是实际上在 64 位 下,只使用了 8byte;在 32 位下,只使用了 4byte

一个 NSObject 实例对象成员变量所占的大小,实际上是 8 字节

image.png

本质是

image.png

获取 Obj-C指针所指向的内存的大小,实际上是 16 字节

image.png

对象在分配内存空间时,会进行内存对齐,所以在 iOS 中,分配内存空间都是 16 字节 的倍数。可以通过以下网址 :openSource.apple.com/tarballs来查看源代码。

三、说一下对class_rw_t的理解?

rw 代表可读可写。

ObjC 类中的属性、方法还有遵循的协议等信息都保存在class_rw_t 中:

image.png

四、说一下对class_ro_t的理解?

存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

image.png

五、说一下对isa指针的理解

说一下对 isa 指针的理解, 对象的isa 指针指向哪里?isa 指针有哪两种类型?

isa等价于 is kind of

· 实例对象 isa 指向类对象

· 类对象指 isa 向元类对象

· 元类对象的 isa 指向元类的基类

isa 有两种类型

· 纯指针,指向内存地址

· NON_POINTER_ISA,除了内存地址,还存有一些其他信息

##isa源码分析

Runtime源码查看isa_t 是共用体。简化结构如下:

image.png

六、说一下Runtime的方法缓存?存储的形式、数据结构以及查找的过程?

cache_t 增量扩展的哈希表结构。哈希表内部存储的 bucket_t.

bucket_t 中存储的是 SEL IMP 的键值对。

如果是有序方法列表,采用二分查找

如果是无序方法列表,直接遍历查找

cache_t 结构体

image.png

image.png

散列表查找过程,在 objc-cache.mm 文件中

image.png

上面是查询散列表函数,其中cache_hash(k, m)是静态内联方法,将传入的key mask 进行&操作返回uint32_t 索引值。do-while循环查找过程,当发生冲突 cache_next 方法将索引值减 1。

七、使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

无论在 MRC 下还是 ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc调用的 object_dispose()方法中释放。

详解:

image.png

八、实例对象的数据结构?

具体可以参看 Runtime源代码,在文件 objc-private.h  的第  127-232 行。

image.png

本质上 objc_object 的私有属性只有一个 isa 指针。指向 类对象 的内存地址。

九、什么是method swizzling(俗称黑魔法)

简单说就是进行方法交换

Objective-C 中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是 selector 的名字。利用 Objective-C 的动态特性,可以实现在运行时偷换selector 对应的方法实现,达到给方法挂钩的目的。每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系 selector 的本质其实就是方法名 IMP 有点类似函数指针,指向具体的 Method 实现,通过 selector 就可以找到对应的 IMP

换方法的几种实现方式

· 利用 method_exchangeImplementations 交换两个方法的实现

· 利用class_replaceMethod 替换方法的实现

· 利用 method_setImplementation 来直接设置某个方法的 IMP

image.png

十、什么时候会报unrecognized selector的异常?

objc 在向一个对象发送消息时,runtime库会根据对象的 isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常 unrecognized selector sent to XXX 。

十一、如何给 Category添加属性?关联对象以什么形式进行存储?

查看的是 关联对象 的知识点。详细的说一下 关联对象。

关联对象 以哈希表的格式,存储在一个全局的单例中。

image.png

十二、能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量? 为什么?

不能向编译后得到的类中增加实例变量; 能向运行时创建的类中添加实例变量;

1. 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setvarlayout class_setWeaklvarLayout 来处理 strong weak 引用.所以不能向存在的类中添加实例变量。

2. 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数. 但是的在调用objc_allocateClassPair 之后,objc_registerClassPair之前,原因同上.

十三、类对象的数据结构?

具体可以参看 Runtime源代码。类对象就是 objc_class

image.png

它的结构相对丰富一些。继承自 objc_object 结构体,所以包含 isa 指针

· isa:指向元类

· superClass: 指向父类

· Cache: 方法的缓存列表

· data: 顾名思义,就是数据。是一个被封装好的 class_rw_t 

十四、runtime如何通过selector找到对应的IMP地址?

每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实 selector 本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.

十五、runtime如何实现weak变量的自动置nil?知道SideTable 吗?

runtime 对注册的类会进行布局,对于  weak 修饰的对象会放入一个  hash  表中。  用  weak指向的对象内存地址作为 key,当此对象的引用计数为 0 的时候会 dealloc,假如 weak指向的对象内存地址是 a,那么就会以 a为键, 在这个 weak表中搜索,找到所有以 为键的 weak对象,从而设置为 nil

##更细一点的回答:

1. 初始化时:runtime会调用 objc_initWeak函数,初始化一个新的 weak 指针指向对象的地址。

2. 添加引用时:objc_initWeak 函数会调用objc_storeWeak()函数, objc_storeWeak()的作用是更新指针指向, 创建对应的弱引用表。

3. 释放时,调用 clearDeallocating 函数。clearDeallocating函数首先根据对象地址获取所有 weak指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry weak 表中删除,最后清理对象的记录。

SideTable 结构体是负责管理类的引用计数表和 weak 表,

详解:参考自《Objective-C 高级编程》一书

###1. 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

image.png

当我们初始化一个 weak 变量时,runtime会调用 NSObject.mm中的 objc_initWeak 函数。

image.png

通过 objc_initWeak 函数初始化“附有 weak 修饰符的变量(obj1)”,在变量作用域结束时通过

objc_destoryWeak 函数释放该变量(obj1)

2. 添加引用时:objc_initWeak函数会调用objc_storeWeak()函数,objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。

objc_initWeak函数将“附有 weak修饰符的变量(obj1)”初始化为 0(nil)后,会将“赋值对象”(obj) 作为参数,调用 objc_storeWeak 函数。

image.png

也就是说

weak 修饰的指针默认值是 nil (在Objective-C 中向 nil 发送消息是安全的) 然后 obj_destroyWeak 函数将 0(nil)作为参数,调用objc_storeWeak 函数。

image.png

前面的源代码与下列源代码相同。

image.png

objc_storeWeak 函数把第二个参数的赋值对象(obj)的内存地址作为键值,将第一个参数weak 修饰的属性变量(obj1)的内存地址注册到weak表中。如果第二个参数(obj)0(nil),那么把变量(obj1) 的地址从 weak 表中删除。

由于一个对象可同时赋值给多个附有weak 修饰符的变量中,所以对于一个键值,可注册多个变量的地址。

可以把 objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当 key nil,将 value 置 nil。在b 非nil 时,b 指向同一个内存地址,在b 变nil 时,nil。此时向发送消息不会崩溃:在Objective-C中向 nil 发送消息是安全的。

3. 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

weak 引用指向的对象被释放时,又是如何去处理weak 指针的呢?当释放对象时,其基本流程如下:

1. 调用 objc_release

2. 因为对象的引用计数为 0,所以执行 dealloc

3. 在 dealloc 中,调用了_objc_rootDealloc 函数

4. 在_objc_rootDealloc 中,调用了 object_dispose 函数

5. 调用 objc_destructInstance

6. 最后调用 objc_clear_deallocating

对象被释放时调用的 objc_clear_deallocating 函数:

1. 从 weak 表中获取废弃对象的地址为键值的记录

2. 将包含在记录中的所有附有 weak 修饰符变量的地址,赋值为 nil

3. 将 weak 表中该记录删除

4. 从引用计数表中删除废弃对象的地址为键值的记录

总结:

其实 Weak 表是一个 hash(哈希)表,Key weak 所指对象的地址,Value  weak 指针的地址(这个地址的值是所指对象指针的地址)数组。

十六、objc中向一个nil对象发送消息将会发生什么?

如果向一个 nil对象发送消息,首先在寻找对象的 isa 指针时就是 0 地址返回了,所以不会出现任何错误。也不会崩溃。

详解:

如果一个方法返回值是一个对象,那么发送给 nil 的消息将返回 0(nil)

如果方法返回值为指针类型,其指针大小为小于或者等于 sizeof(void*) floatdoublelong double 或者 long long 的整型标量,发送给 nil 的消息将返回 0;

如果方法返回值为结构体,发送给 nil 的消息将返回 0。结构体中各个字段的值将都是 0;

如果方法的返回值不是上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。

十七、objc在向一个对象发送消息时,发生了什么?

objc 在向一个对象发送消息时,runtime会根据对象的 isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到 ,就去执行它的实现 IMP 。

十八、isKindOfClass与isMemberOfClass

下面代码输出什么?

image.png

答案:1000

详解:

isKindOfClass 中有一个循环,先判断class 是否等于 meta class,不等就继续循环判断是否等于meta class super class,不等再继续取super class,如此循环下去。

[NSObject class]执行完之后调用isKindOfClass,第一次判断先判断 NSObject 和  NSObject 的meta class是否相等,之前讲到 meta class 的时候放了一张很详细的图,从图上我们也可以看出,NSObject 的metaclass 与本身不等。 接着第二次循环判断 NSObject 与 meta class  superclass 是否相等。还是从那张图上面我们可以看到:Root class(meta) 的 superclass 就是 Root class(class),也就是 NSObject 本身。所以第二次循环相等,于是第一行 res1 输出应该为 YES

同理,[Sark class]执行完之后调用isKindOfClass,第一次 for 循环,Sark 的Meta Class [Sark class]不等,第二次 for 循环,Sark Meta Class super class 指向的是 NSObject Meta Class, 和 Sark Class 不相等。第三次 for 循环,NSObject Meta Class 的 super class 指向的是 NSObject Class,和Sark Class不相等。第四次循环,NSObject Class super class 指向 nil, 和 Sark Class 不相等。第四次循环之后,退出循环,所以第三行的 res3 输出为NO

isMemberOfClass 的源码实现是拿到自己的 isa 指针和自己比较,是否相等。

第二行 isa 指向 NSObject  Meta Class,所以和 NSObject Class 不相等。第四行,isa 指向 Sark 的Meta Class,和 Sark Class 也不等,所以第二行 res2 和第四行 res4 都输出 NO

十九、Category在编译过后,是在什么时机与原有的类合并到一起的?

1. 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init

2. 然后会 map_images

3. 接下来调用 map_images_nolock

4. 再然后就是 read_images,这个方法会读取所有的类的相关信息。

5. 最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。

6. 在 reMethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传入Class Category ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t 结构体中。

二十、Category有哪些用途?

· 给系统类添加方法、属性(需要关联对象)。

· 对某个类大量的方法,可以实现按照不同的名称归类。

二十一、Category的实现原理?

被添加在了 class_rw_t 的对应结构里。

Category 实际上是 Category_t 的结构体,在运行时,新添加的方法,都被以倒序插入到原有方法列表的最前面,所以不同的 Category,添加了同一个方法,执行的实际上是最后一个。

拿方法列表举例,实际上是一个二维的数组。

Category 如果翻看源码的话就会知道实际上是一个 _catrgory_t 的结构体。

例如我们在程序中写了一个 Nsobject+Tools 的分类,那么被编译为 C++ 之后,实际上是:

image.png

Category 在刚刚编译完的时候,和原来的类是分开的,只有在程序运行起来后,通过 Runtime 

Category 和原来的类才会合并到一起。

mememovememcpy:这俩方法是位移、复制,简单理解就是原有的方法移动到最后,根根新开辟的控件, 把前面的位置留给分类,然后分类中的方法,按照倒序依次插入,可以得出的结论就就是,越晚参与编译的分类,里面的方法才是生效的那个。

二十二、_objc_msgForward 函数是做什么的,直接调用它将会发生什么?

_objc_msgForward 是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,

_objc_msgForward 会尝试做消息转发。

详解:_objc_msgForward 在进行消息转发的过程中会涉及以下这几个方法:

1. List itemresolveInstanceMethod:方法 (或 resolveClassMethod:)。

2.List itemforwardingTargetForSelector:方法

3. List itemmethodSignatureForSelector:方法

4. List itemforwardInvocation:方法

5. List itemdoesNotRecognizeSelector: 方 法

具体请见:请看 Runtime 在工作中的运用 第三章 Runtime 方法调用流程;

二十三、[self class] 与 [super class]

下面的代码输出什么?

image.png

NSStringFromClass([self class]) = Son NSStringFromClass([super class]) = Son

详解:这个题目主要是考察关于 Objective-C 中对 self  super 的理解。

self 是类的隐藏参数,指向当前调用方法的这个类的实例;

super 本质是一个编译器标示符,和  self 是指向的同一个消息接受者。不同点在于:super 会告诉编译器, 当调用方法时,去调用父类的方法,而不是本类中的方法。

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super时,则从父类的方法列表中开始找。然后调用父类的这个方法。

在调用[super class]的时候,runtime 会去调用 objc_msgSendSuper 方法,而不是 objc_msgSend

image.png

objc_msgSendSuper 方法中,第一个参数是一个objc_super 的结构体,这个结构体里面有两个变量, 一个是接收消息的 receiver,一个是当前类的父类super_class

objc_msgSendSuper 的工作原理应该是这样的:

objc_super 结构体指向的 superClass 父类的方法列表开始查找 selector,找到后以 objc->receiver 去调用父类的这个 selector。注意,最后的调用者是 objc->receiver,而不是 super_class

那么 objc_msgSendSuper 最后就转变成:

image.png

由于找到了父类 NSObject 里面的class方法的 IMP,又因为传入的入参 objc_super->receiver= selfself 就是son,调用 class,所以父类的方法 class执行 IMP之后,输出还是 son,最后输出两个都一样,都是输出 son