几道iOS面试题 (三)
- block的实质是什么?一共有几种block?都是什么情况下生成的?
- 为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?
- 模拟循环引用
- objc在向一个对象发送消息时,发生了什么?
- 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
- 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
- runtime如何实现weak变量的自动置nil?
- 给类添加一个属性后,在类结构体里哪些元素会发生变化?
- runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢
- runloop的mode是用来做什么的?有几种mode?
- 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
- 苹果是如何实现Autorelease Pool的?
- 讲一讲isa指针?
- 类方法和实例方法有什么区别?
- 介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法?
- 运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么?
- objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体)
block的实质是什么?一共有几种block?都是什么情况下生成的?
block的实质是什么?
- block本质上也是一个
OC对象
,它内部也有个isa指针 - block是封装了函数调用以及函数调用环境的OC对象
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
一共有几种block?

都是什么情况下生成的?
-
在全局区域声明定义一个block
-
block表达式中没有使用捕获的自动变量时
以上情况生成的block都是NSConcreteGlobalBlock类型,只生成一个结构体实例;
除此情况下,生成的都是NSConcreteStackBlock类型的block,且都是存储在栈上。
为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?
-
默认情况下,block里面的变量,拷贝进去的是
变量的值
(值传递),而不是指向变量的内存的指针(引用传递)。 -
但经过__block修饰后的变量,拷贝到block里面的就是
指向变量
的内存的指针,所以我们就可以修改变量的值。
模拟循环引用
Student *stu = [[Student alloc]init];
stu.name = @"Hello World";
stu.block = ^{
NSLog(@"my name is = %@",stu.name); // 互相持有 循环引用
}
objc在向一个对象发送消息时,发生了什么?
触发objc_msgSend,Runtime会
- 通过对象isa指针去查找SEL
- 实例对象在
class
的方法列表里查找 - 类对象则在
meta_class
的方法列表里查找
- 实例对象在
- 找到SEL后,继续找IMP实现
- 在cache中查找
- 在method_list中查找,找到后加入cache
- 如果找不到就去superClass中如上地方查找,找到后加入cache,向上传递
- 最后仍然找不到就进入 消息转发
- 动态方法解析 (询问所有接收者的类能否动态添加实现)
- 快速转发(询问是否其他对象的类是否可以实现)
- 标准转发(这是最后一步,找不到crash。找到发送 forward invocation向前调用)
什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
当调用该对象上某个方法,而该对象上没有实现这个方法的时候就会报unrecognized selector错误。
解决:
-
创建一个继承至NSObject的消息接收类作子类
当调用方法的消息转发给该类后,会调用**resolveInstanceMethod:**方法,在消息接收类中重写方法,拦截消息。
+ (BOOL)resolveInstanceMethod:(SEL)sel { // 如果没有动态添加方法的话,还会调用forwardingTargetForSelector:方法,从而造成死循环 class_addMethod([self class], sel, (IMP)addMethod, "v@:@"); return YES; } id addMethod(id self, SEL _cmd) { NSLog(@"WOCrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd)); return 0; }
-
为NSObject添加分类
以此拦截NSObject的 **forwardingTargetForSelector:**方法
/** 对实例方法进行拦截并替换 @param kClass 具体的类 @param originalSelector 原有实例方法 @param replaceSelector 自定义实例替换方法 */ - (void)instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector { Method O_Method = class_getInstanceMethod(kClass, originalSelector); Method R_Method = class_getInstanceMethod(kClass, replaceSelector); // class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现 BOOL didAddMethod = class_addMethod(kClass, O_Method, method_getImplementation(R_Method), method_getTypeEncoding(R_Method)); if (didAddMethod) { // 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换) class_replaceMethod(kClass, replaceSelector, method_getImplementation(O_Method), method_getTypeEncoding(O_Method)); } else { // 添加失败(原本就有原方法, 直接交换两个方法) method_exchangeImplementations(O_Method, R_Method); } }
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
- 不能向编译后得到的类中增加实例变量。
- 能向运行时创建的类中添加实例变量。
原因:
-
因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。
所以不能向编译后得到的类中添加实例变量。
-
而运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。
runtime如何实现weak变量的自动置nil?
- 初始化时runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
- 添加引用时objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
- 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
其实Weak表是一个hash(哈希)表,Key是weak所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。
给类添加一个属性后,在类结构体里哪些元素会发生变化?
给类添加属性,本质上也是通过setter和getter生成了属性变量
objc2源码里有实例的数组信息的属性 所以
- instance_size :实例的内存大小会变化
- objc_ivar_list *ivars:属性列表会变化
但objc4源码 已经没有这两个属性了。
来自objc4源码
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
... ...
}
struct objc_class : objc_object{
// Class ISA; // 8字节
Class superclass; // 8字节
cache_t cache; // 16字节
class_data_bits_t bits;
class_rw_t * data() const {
return bits.data();
}
}
runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢
- RunLoop 的作用就是来
管理线程
的。 - 当线程的 RunLoop开启后,线程就会在执行完任务后,处于休眠状态,随时等待接受新的任务。
- 主线程默认启动runloop。
- 子线程默认未开启。
runloop的mode是用来做什么的?有几种mode?
runloop的model 主要是用来指定事件在运行循环中的
优先级
的
-
NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
-
UITrackingRunLoopMode:ScrollView滑动时
-
UIInitializationRunLoopMode:启动时
-
NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的 Mode 有两个:
-
NSDefaultRunLoopMode
(kCFRunLoopDefaultMode) :默认,空闲状态 -
NSRunLoopCommonModes
(kCFRunLoopCommonModes) :Mode集合
为什么把NSTimer对象以NSDefaultRunLoopMode添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
滑动时runloop模式会切换到UITrackingRunLoopMode(ScrollView滑动时)。
苹果是如何实现Autorelease Pool的?
autoreleasePool作用域结束的时候,会将里面所有的对象的引用计数器-1,以此释放对象、回收内存。
实际上是调用
AutoreleasePoolPage
的push
和pop
两个类方法
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
讲一讲isa指针?
isa指针的作用就是描述OC对象结构体里所有信息的。

- 实例对象的isa指针指向所属的类
- 类对象的isa指针指向了所属的元类
- 元类的isa指向了根元类,根元类指向了自己。
类方法和实例方法有什么区别?
-
调用的方式不同。
类方法必须使用
类对象
调用,不能在类方法里面调用属性,类方法里面也必须调用类方法。实例方法必须使用
实例对象
调用,可以在实例方法里面使用属性,实例方法也必须调用实例方法。 -
存储位置不同。
类方法 存储在
元类
结构体里面的methodLists里面实例方法 存储在
类
结构体里面的methodLists里面
介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法?
实现
Category实际上变成了一个
method_list
, 被插入到类的信息内, 这样查表的时候就能找到Category内的方法。
- 将 Category 和它的主类(或元类)注册到
哈希表
中 - 如果主类(或元类)已实现,那么重建它的方法列表
此处需要知道是,它分为两种情况:
- Category中的实例方法、协议以及属性添加到
类
的方法列表上 - 而Category的类方法和协议添加到类的
元类
的方法列表上的
分类可以在不影响原方法的基础上
- 添加新的方法
- 交换系统方法
- 添加成员属性
- ... ...
为什么会覆盖?
系统是在运行时将分类中对应的实例方法、类方法等插入
到了原来类或元类的方法列表中,且靠前
!
所以,方法调用时通过isa指针
去对应的类或元类的列表中查找对应的方法时先查到的是分类中的方法!查到后就直接调用不在继续查找。这即是覆盖的本质!
运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么?
-
运行时不能在增加成员变量了。
确实有一个class_addIvar()函数用于给类添加成员变量,但苹果已经给出不能添加的原因:
这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。
经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。
程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。
-
能增加属性
但必须我们实现它的
getter
和setter
方法。然后通过runtime去关联对象实现#import "Person+mutable.h" @implementation Person (mutable) /// 要关联的对象的键值,一般设置成静态的,用于获取关联对象的值 static char name; - (void)setNick:(NSString *)nick{ /* 关联 (应用场景:为分类添加“属性”,为UI控件关联事件Block体,为了不重复获得某种数据) id object :表示关联者,是一个对象,变量名理所当然也是object const void *key :获取被关联者的索引key id value :被关联者,这里是一个block objc_AssociationPolicy policy :关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC */ objc_setAssociatedObject(self, &name, nick, OBJC_ASSOCIATION_COPY_NONATOMIC); /// 断开关联 /// objc_removeAssociatedObjects(self) /// objc_setAssociatedObject(self, &name, nil, OBJC_ASSOCIATION_ASSIGN); } - (NSString *) nick{ return objc_getAssociatedObject(self, &name); }
objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体)
-
如果方法返回值是一个
对象
,那么发送给nil的消息将返回0(nil)
。例如:Person * motherInlaw = [ aPerson spouse] mother];如果spouse对象为nil,那么发送给nil的消息mother也将返回nil。
-
如果方法返回值为
指针类型
,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者long long的整型标量,发送给nil的消息将返回0
。 -
如果方法返回值为
结构体
,正如在《Mac OS X ABI 函数调用指南》,发送给nil的消息将返回0
。结构体中各个字段的值将都是0。其他的结构体数据类型将不是用0填充的。 -
如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是
未定义
的。
转载自:https://juejin.cn/post/6986217181734240264