一、objc对象的isa的指针指向什么?有什么作用?
指向他的类对象,从而可以找到对象上的方法
详解:下图很好的描述了对象,类,元类之间的关系:
图中实线是 super_class
指针,虚线是 isa
指针。
1. Root class (class)
其实就是 NSObject
,NSObject
是没有超类的,所以 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 字节
本质是
获取 Obj-C
指针所指向的内存的大小,实际上是 16 字节
对象在分配内存空间时,会进行内存对齐,所以在 iOS
中,分配内存空间都是 16 字节 的倍数。可以通过以下网址 :openSource.apple.com/tarballs来查看源代码。
三、说一下对class_rw_t的理解?
rw
代表可读可写。
ObjC
类中的属性、方法还有遵循的协议等信息都保存在class_rw_t
中:
四、说一下对class_ro_t的理解?
存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
五、说一下对isa指针的理解
说一下对 isa
指针的理解, 对象的isa
指针指向哪里?isa
指针有哪两种类型?
isa
等价于 is kind of
· 实例对象 isa
指向类对象
· 类对象指 isa
向元类对象
· 元类对象的 isa
指向元类的基类
isa
有两种类型
· 纯指针,指向内存地址
· NON_POINTER_ISA
,除了内存地址,还存有一些其他信息
isa源码分析
在 Runtime
源码查看isa_t
是共用体。简化结构如下:
六、说一下Runtime的方法缓存?存储的形式、数据结构以及查找的过程?
首先作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号: 编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)
cache_t
增量扩展的哈希表结构。哈希表内部存储的 bucket_t
.
bucket_t
中存储的是 SEL
和 IMP
的键值对。
如果是有序方法列表,采用二分查找
如果是无序方法列表,直接遍历查找
cache_t 结构体
散列表查找过程,在 objc-cache.mm
文件中
上面是查询散列表函数,其中cache_hash(k, m)
是静态内联方法,将传入的key
和mask
进行&操作返回uint32_t
索引值。do-while
循环查找过程,当发生冲突 cache_next
方法将索引值减 1。
七、使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
无论在 MRC
下还是 ARC
下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc
调用的 object_dispose()
方法中释放。
详解:
八、实例对象的数据结构?
具体可以参看 Runtime
源代码,在文件 objc-private.h
的第 127-232 行。
本质上 objc_object
的私有属性只有一个 isa
指针。指向 类对象 的内存地址。
九、什么是method swizzling(俗称黑魔法)
简单说就是进行方法交换
在 Objective-C
中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是 selector
的名字。利用 Objective-C
的动态特性,可以实现在运行时偷换selector
对应的方法实现,达到给方法挂钩的目的。每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系 selector
的本质其实就是方法名 IMP
有点类似函数指针,指向具体的 Method
实现,通过 selector
就可以找到对应的 IMP
。
换方法的几种实现方式
· 利用 method_exchangeImplementations
交换两个方法的实现
· 利用class_replaceMethod
替换方法的实现
· 利用 method_setImplementation
来直接设置某个方法的 IMP
十、什么时候会报unrecognized selector的异常?
objc
在向一个对象发送消息时,runtime
库会根据对象的 isa
指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常 unrecognized selector sent to XXX
。
十一、如何给 Category添加属性?关联对象以什么形式进行存储?
查看的是 关联对象 的知识点。详细的说一下 关联对象。
关联对象 以哈希表的格式,存储在一个全局的单例中。
十二、能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量? 为什么?
不能向编译后得到的类中增加实例变量; 能向运行时创建的类中添加实例变量;
1. 因为编译后的类已经注册在 runtime
中,类结构体中的 objc_ivar_list
实例变量的链表和 instance_size
实例变量的内存大小已经确定,同时 runtime
会调用 class_setvarlayout
或 class_setWeaklvarLayout
来处理 strong weak
引用.所以不能向存在的类中添加实例变量。
2. 运行时创建的类是可以添加实例变量,调用 class_addIvar
函数. 但是的在调用`objc_allocateClassPair之后,
objc_registerClassPair`之前,原因同上.
十三、类对象的数据结构?
具体可以参看 Runtime
源代码。类对象就是 objc_class
。
它的结构相对丰富一些。继承自 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
表中搜索,找到所有以 a
为键的 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指针指向对象的地址。
当我们初始化一个 weak
变量时,runtime
会调用 NSObject.mm
中的 objc_initWeak
函数。
通过 objc_initWeak
函数初始化“附有 weak
修饰符的变量(obj1)
”,在变量作用域结束时通过
objc_destoryWeak
函数释放该变量(obj1)
。
2. 添加引用时:objc_initWeak函数会调用objc_storeWeak()函数,objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
objc_initWeak
函数将“附有 weak
修饰符的变量(obj1)
”初始化为 0(nil)
后,会将“赋值对象”(obj)
作为参数,调用 objc_storeWeak
函数。
也就是说
weak
修饰的指针默认值是 nil
(在Objective-C
中向 nil
发送消息是安全的) 然后 obj_destroyWeak
函数将 0(nil)
作为参数,调用objc_storeWeak
函数。
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
时,a
和b
指向同一个内存地址,在b
变nil
时,a
变nil
。此时向a
发送消息不会崩溃:在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*)
,float
,double
,long double
或者 long long
的整型标量,发送给 nil
的消息将返回 0;
如果方法返回值为结构体,发送给 nil
的消息将返回 0。结构体中各个字段的值将都是 0;
如果方法的返回值不是上述提到的几种情况,那么发送给 nil
的消息的返回值将是未定义的。
十七、objc在向一个对象发送消息时,发生了什么?
objc
在向一个对象发送消息时,runtime
会根据对象的 isa
指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到 ,就去执行它的实现 IMP
。
十八、isKindOfClass与isMemberOfClass
下面代码输出什么?
答案: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++
之后,实际上是:
Category
在刚刚编译完的时候,和原来的类是分开的,只有在程序运行起来后,通过 Runtime
,
Category
和原来的类才会合并到一起。
mememove
,memcpy
:这俩方法是位移、复制,简单理解就是原有的方法移动到最后,根根新开辟的控件, 把前面的位置留给分类,然后分类中的方法,按照倒序依次插入,可以得出的结论就就是,越晚参与编译的分类,里面的方法才是生效的那个。
二十二、_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]
下面的代码输出什么?
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
;
在 objc_msgSendSuper
方法中,第一个参数是一个objc_super
的结构体,这个结构体里面有两个变量, 一个是接收消息的 receiver
,一个是当前类的父类super_class
。
objc_msgSendSuper
的工作原理应该是这样的:
从 objc_super
结构体指向的 superClass
父类的方法列表开始查找 selector
,找到后以 objc->receiver
去调用父类的这个 selector
。注意,最后的调用者是 objc->receiver
,而不是 super_class
!
那么 objc_msgSendSuper
最后就转变成:
由于找到了父类 NSObject
里面的class
方法的 IMP
,又因为传入的入参 objc_super->receiver= self
。self
就是son
,调用 class
,所以父类的方法 class
执行 IMP
之后,输出还是 son
,最后输出两个都一样,都是输出 son
。