〇、引言
前面一步步学习了对象、类、方法、类的加载等,这些其实都是Runtime 的基础,而Runtime 又是iOS开发语言 Objective C 的精髓,因此关于Runtime 的面试题举不胜举。
下面就简单的介绍几个,并提供解题思路,希望可以帮读者更清晰的理解Runtime。
一、什么是Runtime
这个问题,问的是对Runtime 的基础理解。
Runtime是由C和C++汇编实现的一套API,为 OC 语言加入了面向对象,运行时的功能。
运行时(Runtime) 是指将数据类型确定推迟到了运行时。
举个例子:
关于类——类的扩展,在编译时作为类的一部分(ro)就已经确定好了,所以可以添加分类;而分类由于是运行时加载,只能添加属性以及对应的setter 和getter 方法,来达到模拟属性的目的。
二、方法的本质是什么?
方法的本质,就是发送消息/消息传递
让一个类(对象/类)执行一个方法的过程,就是向它发送消息,它便开始消息查找的过程。主要包含以下几个过程:
- 快速查找(objc_msgSend),主要向类 cache_t 查找缓存的过的方法。
- 慢速查找:执行 lookUpImpOrForward 方法,递归自己,以及自己的父类,属性
rw
里的methodlist
中查找方法。 - 动态方法加解析(还是查找不到):resolveInstanceMethod 方法,看是否自定义实现过
- 消息转发阶段:
- 快速转发—— forwardingTargetForSelector 寻找特定对象来执行方法
- 慢速转发—— methodSignatureForSelector(获取方法的签名)以及生成相应的invocation,由 forwardInvocation 方法进行消息分发,让有能力执行的类来执行该方法。
三、简述SEL 和IMP和之间关系
定义:SEL是内存中方法编号,IMP 是方法的具体实现
两者个点关系就好比一本书:SEL 是目录页里的 章节标题,而IMP 则是文章的页码,通过页码,可以看到具体内容。
SEL 是由read_images 就已经加载注册到内存表里。
四、能否往已注册的类添加成员变量?
Q:已经注册好的类,能否再动态添加成员变量?为什么?
答案是不可以。
我们通过Runtime还原一下场景。
注册好的类,实现的方法是,注册到内存里:
objc_registerClassPair(LGPerson); |
而实现objc_registerClassPair
这个方法,又实现了下面的内容:
// Clear "under construction" bit, set "done constructing" bit |
即对类更改了状态,更改了什么状态?RW_CONSTRUCTED这个状态,即让类处于内存开辟&注册到内存中——
// class allocated and registered |
接下来,根据创建成员变量的函数为addIvar
,创建业务代码如下
class_addIvar(LGPerson, "lgName", sizeof(NSString *), log2(sizeof(NSString *)), "@"); |
在源码中找到相对应的函数:
走到这一步,就戛然而止了……添加ivars 被拒绝——因为内存已固定,无法再添加新属性了。
原因:因为注册好的类,内存容量已经固定,无法动态添加了。
五、isKindOFClass 和 isMemberOfClass 的区别
5.1 题目
关于这两个函数,我们知道他们各自概念是:
isKindOfClass——某个对象是否是类的成员,或者继承自该类的成员(即父子关系)
isMemberOfClass——某个对象是否当前类的成员,并不考虑回溯的父子类关系。
这里有一道面试题,题目如下,要求回答各打印结果:
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];//1 |
答案是什么呢?先别忙,先冷静分析一下
5.2 概念分析
5.2.1 类方法的区别分析
很明显,上半部4个判断,是判断类与类的归属,执行的是类方法判断,先看一下涉及到的两个方法的源码:
类方法的区别
+(void)isKindOfClass** 的实现
+ (BOOL)isKindOfClass:(Class)cls { |
+(BOOL)isMemberOfClass 的实现
+ (BOOL)isMemberOfClass:(Class)cls { |
从上面源码分析,可以看出,isKindOfClass 多了一步 tcls = tcls->superclass
的循环,即如果当前类不等于目标类,向上查找父类,看看父类与目标是否相等。
5.2.2 实例方法的区别
-(void)isKindOfClass 的实现
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}步骤为:先判断对象的类,看是否与目标类相同;否则通过
tcls = tcls->superclass
,递归寻找父类及其父类,是否与本类相等-(void)isMemberOfClass 的实现
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}步骤为:判断对象指向的类,是否与目标类相同
5.2.3 答题
好的,根据这个,一个一个来进行解答。
返回1:因为根元类的父类=根元类
左边的
NSObject class
为 NSObject 的元类,判断是否与本类NSobject 相等?答案是不一致。
但是此时会进入
tcls = tcls->superclass
这个循环,查找本类的父类,而我们知道 NSObject 的元类的父类就是NSObject ,见下图。所以绕了一圈回来,NSObject = NSObject返回0:因为元类与本类不相等
和上一个问题一样,但是
isMemberOfClass
在第一步就停下了,object_getClass((id)self) == cls
这里问到元类与本类是否相等,当然是否。返回0:因为普通类的元类的父类与本类是不相等的
我们看右边是 Person Class
左边的 Person Class
判断条件是
isKindOfClass
所以,Person 开始找它的元类,看是否等于本类。我们看isa 的指向图可以得出,它的元类递归查找父类,一直都与本类不相等。所以返回为0
返回为0。因为 元类不等于本类
这个和第2条是一样的
**返回为1: ** 因为本类等于本类
给出的判断代码为:
[(id)[NSObject alloc] isKindOfClass:[NSObject class]]
其中左边的[NSObject alloc] ,创建了一个 NSObject 的对象,而对他指向 isKindOfClass 即执行以下方法:
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}创建的
Class tcls = [self class]
,其中tcls 即为 其父类 NSObject,取其与 NSObject 相比,相等,所以返回为1。返回为1: 因为本类等于本类,同第5 题
返回为1:同第5题
返回为1: 同第5题
五、[self class] 与[super class] 区别
5.1 提问:以下打印什么?
|
5.2 源码分析
因为问题都涉及到了 class
这个方法,在NSObject.mm 这个类里找一下,方法实现如下:
- (Class)class { |
可以见到,在object_getClass
方法里,如果给的对象self
——obj
存在,返回的是它的元类,指针指向的是它的类,所以打印应该是 Student
。
而第一行中,向super
发送消息了。
- [self class] 方法,是向 对象(
self
)发送消息(class
),走的流程是 objc_msgSend - [super class],是向 对象(
self
)发送消息(class
),走的流程是 objc_msgSendSuper
那继续探寻objc_msgSendSuper
这个方法,可以看到他的结构如下:
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ ) |
可见它执行的是 struct objc_super
类型的super,
原因:在OC中,super只是个符号标识符,objc_msgSendSuper最终接受对象还是
self
5.3 解答
打印都是NSObject
六、weak 是什么及其实现原理
6.1 分析
在OC语言到开发中,我们经常对对象前面添加 weak
来实现弱引用,从而达到避免循环引用造成的内存泄漏。那么这个weak 究竟实现了什么,作为高级开发者,不得不仔细探究一下。
6.1.1 代码创建
为了搞清楚weak 创建对象时内部的实现,创建一个weak 对象,打印试试,这时要把汇编断点打开
NSArray *arr = @[@"jack", @"tiga", @"jade", @"obu"]; |
可以看到运行后,执行了这行代码:
可见创建后执行了关键代码 objc_initWeak
,贴到源码里查看,实现如下:
id |
返回到时storeWeak(id *location, objc_object *newObj)
这个函数,依稀可以看出来,是一个将newObj
储存到location
的动作。继续往下分析如下:
6.1.2 源码解读 storeWeak
如果有新对象,创建新的散列表
if (haveNew) {
newTable = &SideTables()[newObj];散列表结构如下:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
/*精简以后的内容*/
};可见散列表中有重要的3个属性
slock —— 自旋锁
refcnts——引用计数表,这里是App 维护的一张全局表,类型是
weak_table —— 全局的弱引用表,存储所有的弱引用对象,把对象当作
key
来保持,打开看看,它的结构体实现是这样的:/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};共包含了整个应用里,弱引用实体和数量。
如果当前是创建的新弱引用
haveNew
,添加弱引用。newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);记录弱引用实体保存的内存地址:
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}插入对象到弱引用过程如下:
创建数组,插入到weak 表里
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);循环弱引用表,将引用实体插入,弱引用的计数增加
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
assert(weak_entries != nil);
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;
}
weak_entries[index] = *new_entry;
weak_table->num_entries++;
if (hash_displacement > weak_table->max_hash_displacement) {
weak_table->max_hash_displacement = hash_displacement;
}
}
将弱引用的位值,存放在引用计数表里
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}并对isa 的weak引用属性设置为TRUE
newisa.weakly_referenced = true;
添加到计数表的方法如下:
void
objc_object::sidetable_setWeaklyReferenced_nolock()
{
assert(!isa.nonpointer);
SideTable& table = SideTables()[this];
table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
}最终返回存储的内存地址的指针,指向这个新的对象
*location = (id)newObj;
6.3 weak 的释放
6.1 查看 dealloc
我们知道,对象的销毁,一般是在类的dealloc 后进行,所以目光放在 dealloc 的方法是先上
查看NSObject.mm 中,得知dealloc 如下
- (void)dealloc { |
继续探寻
void |
继续继续,得到一个内联函数如下:
inline void |
目标放在object_dispose((id)this);
这一行,函数的名字——销毁对象,即当前对象的isa还有些弱引用(isa.weakly_referenced
)或者关联对象(isa.has_assoc
)未处理的业务,会比较复杂,需要特别处理。继续探寻如下:
id |
继续查看解构过程objc_destructInstance(obj)
这个函数:
void *objc_destructInstance(id obj) |
- if (cxx) object_cxxDestruct(obj); —— 处理析构C++ 的对象
- _object_remove_assocations(obj); ——移除类的关联对象
那么要找寻如何移除弱引用,把目光放在obj->clearDeallocating();
这行代码——
又是一个内联函数,解释了如何dealloc 对象
inline void |
很明显 clearDeallocating_slow()
才是需要找的,因为注释已经解释的很清楚:
Slow path for non-pointer isa with weak refs and/or side table data.
带弱引用或散列表数据的非指针的isa 慢速路径
NEVER_INLINE void |
分析关键代码:
如果当前isa 有弱引用,在弱引用表中,弱引用表清除当前的弱引用
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}weak_clear_no_lock
这个核心函数,具体做了什么工作,继续探究一下void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
//
}
}
weak_entry_remove(weak_table, entry);
}这里看上去业务挺多,其实核心也就两个:
移除指针
if (*referrer == referent) {
*referrer = nil;
}移除实体:
weak_entry_remove(weak_table, entry);
/**
* Remove entry from the zone's table of weak references.
*/
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
// remove entry
if (entry->out_of_line()) free(entry->referrers);
bzero(entry, sizeof(*entry));
weak_table->num_entries--;
weak_compact_maybe(weak_table);
}
如果当前对象有引用计数存表不为0,引用计数表清楚本对象
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
6.3 回答
weak 的创建,在内存的散列表中的弱引用表里,存储关于该对象的弱引用,按照key-value 存入。
weak 的创建,正好相反,中dealloc 里,去弱引用表格里,找到引用进行弱引用实体销毁,以及弱引用的引用计数减少。
七、黑魔法·方法交换(Method Swizzling)坑点
7.1 一般使用
如下所示,对数组越界做保护:
|
试验一下结果如何:
NSArray *arr = @[@"jack", @"tiga", @"jade", @"obu"]; |
打印结果如下:
2020-05-03 23:52:50.759731+0800 SWZ[7438:461811] 朋友,数组越界了 |
可以看到,经过方法交换,本来使用 objectAtIndex
的方法,它的实现被交换了,被交换到 lj_objectAtIndex
的实现里,执行了越界保护,保证了代码的鲁棒性。
7.2 坑点1 - 重复使用
以上是方法交换的一个简单的例子,对NSArray 添加方法实现方法交换。
7.2.1 症状
那么会不会有什么漏洞呢?留意到方法实现是在 load 方法就实现,假设在实现方法之前,主动加载一次load 会怎么样?
假设是这样:
NSArray *arr = @[@"jack", @"tiga", @"jade", @"obu"]; |
结果,出现了越界崩溃
7.2.2 追踪分析
往核心方法load 里添加一段打印标记,看看是否与他有关:
通过追踪,得知load 方法,执行了2次。想一想,执行load 本来是为了交换方法,那执行2次,意思是将原本交换过的方法实现,又交换回去了——白干了,这也是出现越界崩溃的原因。
7.2.3 解决
解决的方法很简单,将该方法设置成单例,通过 onceToken 来保证交换过程只会走一次:
+ (void)load{ |
这样,继续执行虽然执行2次load
, 但是内部的交换实现,只会执行一次。
2020-05-04 00:13:53.061523+0800 SWZ[7778:482862] 执行load! |
结果如下,虽然load 执行2次,交换方法只执行1次
7.3 坑点2 - 待交换子类未实现
7.3.1 症状
先创建一个案发现场,如下:
- 父类某方法A并实现
- 子类继承父类
- 其他业务向子类交换该A方法
- 执行父类该方法A,查看结果
如下所示:
创建父类
Animal
,以及子类Dog
, 其中父类拥有并实现run
的类方法@interface Animal : NSObject
- (void)run;
@end
@interface Dog : Animal
@end其他业务场景,向子类申请交换了 run 的实现,run 换成了play,在Dog.m 执行:
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(run));
Method swiMethod = class_getInstanceMethod(self, @selector(play));
method_exchangeImplementations(oriMethod, swiMethod);
});
}
- (void)play{
NSLog(@"%s", __func__);
}检查此时子类与父类的的run 方法
生成两个实例,执行run
方法
Animal *a = [[Animal alloc] init]; |
结果如下:
2020-05-04 00:51:33.474658+0800 SWZ[8188:509782] -[Dog(Exchange) play] |
总结:可见子类方法执行了新方法play
,但是同时父类的run 方法也给换走了,执行了新的play方法
7.3.2. 追踪分析
在分析之前,先明白现在的场景:
- 父类有该方法及其实现
- 子类并无该方法极其实现
可见,业务场景向子类交换了该方法,子类查询方法并未找到,于是递归向父类查找,在父类的方法列表里查找成功,进而完成了交换过程。而父类下次调用该方法,结果使用了交换来的新方法。
好一个坑爹滴子类……
7.3.3 解决
面对如此坑爹的子类,解决方法只有一个,方法没有——自己实现。
当子类需要交换某方法的时候,尝试向自己添加待交换走的方法,以防自己未实现,不得不去找父类交换:
BOOL success = class_addMethod([Dog class], oriSEL, swiIMP, method_getTypeEncoding(oriMethod)); |
然后,如果添加成功,即自身本来并未实现,借着添加的机会实现了。
接下来做的就是,将新添加的方法与目标方法交换:
if (success) |
当然, 如果添加失败,即原本就有,那么按原来的逻辑,直接交换两种方法即可
贴一下完整的完善的逻辑如下:
+ (void)load{ |
7.4 坑点2 - 待交换子类父类实现
7.4.1 症状
上面的代码,解决了子类未实现不得不向父类索取,并拿出来交换的弊端。
那么如果该方法,父类都没实现呢?就是将父类的 run
方法实现注释,会如何,再走一遍——
不出意外,瘪犊子了。
7.4.2 追踪分析
这里很明白得出结论是,想要像某个类交换方法,结果该类以及向上的父类都没实现,毫无意外会造成崩溃。该怎么办呢?
先看一下一级一级的关系:
- 交换方法——>找当前类要
- 找不到当前类的方法实现——找父类要
- 找不到父类的方法实现——崩溃
- 找不到当前类的方法实现——找父类要
结合7.4 里的逻辑,追根溯源,是父类没有实现,那么需要做的是,先给父类添加一个空的实现,以避免崩溃。
然后在子类交换方法的过程中,子类会完善自身的添加方法实现,再去交换方法(这部分的逻辑是7.3)
7.4.3 解决
在交换方法时,先判断原方法是否存在,如果不存在,添加一个空方法实现。
添加部分如下:
if (!oriIMP) { |
全文如下:
+ (void)load{ |
Z、总结
在这篇文章,初步汇总了一下关于Runtime 几个典型的问题,比如基础的SEL、IMP 的关系,以及self 和super 的区别,以及后面深一点的weak 的底层实现,还有业务上用得最多的黑魔法——方法交换使用过程中的几点坑,如果深刻理解了其内部实现,自然能避开这些坑。
希望在日后更深刻的理会这些原理,欢迎大家有问题留言交流。