趁着开年的空闲时间,找了一些面试题写写,算是回顾总结一下iOS开发方面的知识, 水平渣,答案仅作参考! 欢迎指导讨论,写的不对或不妥的地方望及时提出! 原题在这里
iOS常见面试题基础篇(附参考答案)看这里
-
Block
-
block的实质是什么?一共有几种block?都是什么情况下生成的?
-
简单的来讲,block在OC中的表现可以看作为带有自动变量(局部变量)的匿名函数。
C语言函数定义的时候,可以将函数的地址赋值给函数指针类型的变量,如下:
int func(int count){ return count + 1; } // 赋值 int (*funcprt)(int) = &func; int result = (*funcprt)(10);
同理,我们也可以将block语法赋值给声明为block类型的变量中。如下:
// 声明一个block类型的变量,其与函数指针类型的变量不同之处只有'*'改为'^' int (^blk)(int); // 赋值 int (^blk)(int) = ^(int count){return count + 1;}; int result = blk(10);
还可以通过typedef来声明
blk_t
类型变量,如下:typedef int (^blk_t)(int); blk_t blk = ^(int count){return count + 1;}; int result = blk(10)
以上解释了匿名函数,现在来解释一下带有自动变量
int value = 10; void(^blk)() = ^{ NSLog(@"value === %d", value); }; blk(); value = 2; NSLog(@"%@", value);
value结果为 10 。在block中,block表达式截获所使用的自动变量的值,即保存该变量的瞬间值,所以在执行了block后,改变block外自动变量的值,并不会影响block执行时自动变量的值。
-
block的本质
关于block的本质, ibireme大神objc 中的 block这篇博客里,有详细的分析。
struct Block_descriptor_1 { uintptr_t reserved; uintptr_t size; }; struct Block_layout { void *isa; volatile int32_t flags; // contains ref count int32_t reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 *descriptor; // imported variables };
根据block的数据结构可以发现,它是含有
*isa
指针的,在OC中根据对象的定义,凡是首地址是*isa的结构体指针,都可以认为是对象(id)。这样在objc中,block实际上就算是对象。 -
既然OC处理Block是按照对象来处理的。在iOS中,常见的就是
_NSConcreteStackBlock
,_NSConcreteMallocBlock
,_NSConcreteGlobalBlock
这3种,还有另外几种,暂不做讨论。 -
ARC下:
void(^blockA)() = ^{ NSLog(@"just a block"); }; NSLog(@"%@", blockA); int value = 10; void(^blockB)() = ^{ NSLog(@"just a block === %d", value); }; NSLog(@"%@", blockB);
ARC下打印结果如下:
<__NSGlobalBlock__: 0x10c81c308> <__NSMallocBlock__: 0x60400025d160>
MRC下打印结果如下:
<__NSGlobalBlock__: 0x1056bf308> <__NSStackBlock__: 0x7ffeea540a48>
我们对栈上的block做copy操作:
NSLog(@"%@", [blockB copy])
结果为:
<__NSMallocBlock__: 0x600000444b60>
由此我们可以得出以下结果:
-
当block没有引用外部局部变量的时候,block为
全局block
-
当block引用外部局部变量的时候,ARC下为
堆block
,MRC下为栈block
,此时对MRC下的栈block进行copy,栈block
就变为堆block
。在ARC下,编译器会把block从栈拷贝到堆。 -
经验证,当block只引用静态变量,全局变量的时候,block均为
全局block
-
-
-
为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?
- 默认情况下,在block中是无法修改被block捕获的自动变量,因为block捕获自动变量时,仅仅捕获到该自动变量的值,并非是内存地址,因此早block内部无法改变自动变量的值。
- __block的实现原理详见深入研究 Block 捕获外部变量和 __block 实现原理。大致意思是说: 带有
__block
的自动变量,经过编译后会变成一个结构体,通过结构体中的__forwarding
指针可以访问到变量,自然就可以修改变量了。
-
模拟一下循环引用的一个情况?block实现界面反向传值如何实现?
-
循环引用场景
// 当block作为属性时: @property(nonatomic, copy) void(^block)(); self.block = ^{ NSLog(@"%@",self); }
此时会出现警告:
这时,可以用__weak修饰self,避免循环引用: -
界面反向传值
//SecondViewController.h #import < UIKit/UIKit.h> typedef void(^CallBackBlock) (NSString *string); @interface SecondViewController : UIViewController @property (nonatomic,strong)UItextField *textField; @property (nonatomic,copy)CallBackBlcok callBackBlock; @end // 在implementation中添加一个点击事件: - (IBAction)click:(id)sender { self.callBackBlock(_textField.text); [self.navigationController popToRootViewControllerAnimated:YES]; }
在FirstViewController中:
// FirstViewController.m - (IBAction)push:(id)sender { SecondViewController *secondVC = [[SecondViewController alloc]init] secondVC.callBackBlock = ^(NSString *string){ NSLog(@"string is %@",string); self.label.text = string; }; [self.navigationController pushViewController:secondVC animated:YES]; }
-
-
思考一下这个问题: ARC下会发生什么? MRC呢?若
blockA
并没有引用自动变量val
的话情况又是什么样?@property(nonatomic, weak) void(^block)(); - (void)viewDidLoad { [superviewDidLoad]; int val = 10; void(^blockA)() = ^{ NSLog(@"val:%d", val); }; NSLog(@"%@", blockA); _block = blockA; } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"%@", _block); }
-
Runtime
-
objc在向一个对象发送消息时,发生了什么?
-
众所周知,在objc中方法调用的本质是发消息,例如:
[obj message]; // 运行时会转化为: objc_msgSend(obj, selector) // 当有参数时: [obj message:(id)arg...]; // 运行时会转化为: objc_msgSend(obj, selector, arg1, arg2, ...)
消息在运行时才会与方法绑定
当向一个对象发送消息时:
1.首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
2.检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
3.如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。
4.如果 cache 找不到就找类的方法列表中是否有对应的方法。 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。
5.如果还找不到,就要开始进入消息转发流程
如下图所示:
-
-
什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
-
当调用某对象上某个方法,而该对象并没有实现这个方法的时候, 可以通过消息转发进行解决。
-
消息转发步骤如下:
-
objc运行时会调用
+resolveInstanceMethod:
或者+resolveClassMethod:
,让我们有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则,运行时就会移到下一步,消息转发(Message Forwarding)。 -
调用
forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。 -
调用
methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果返回了一个方法签名,Runtime就会创建一个NSInvocation对象,然后继续进行第四步 -
调用
forwardInvocation:
方法,将地3步获取到的方法签名包装成Invocation
传入,如何处理就在这里面了。
如图所示:
-
-
-
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
- 不能向编译后得到的类中增加实例变量
- 能向运行时创建的类中添加实例变量
- 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量。运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。
-
runtime如何实现weak变量的自动置nil?
- weak修饰符表示该属性是非拥有关系,运行期系统会将每一个类的weak变量放入相应的一个hash表中,在这个表中以weak变量所指向的对象的内存地址为key,当weak指向的对象引用计数为0执行dealloc方法,对象被销毁,运行期系统通过key去hash表中找到相应的weak对象将他们设置成nil。
-
给类添加一个属性后,在类结构体里哪些元素会发生变化?
- 类的结构体如下:
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; /* Use `Class` instead of `struct objc_class *` */
- 当我们给类添加属性后,实例对象的内存大小:
instance_size
和属性列表:objc_ivar_list *ivars
会发生改变。
- 类的结构体如下:
RunLoop
-
runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
-
runloop字面的意思就是跑圈,实际上App能一直不停的运行下去,runloop功不可没!我们来分析一下main函数:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
如果没有runloop,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以,我们就有了RunLoop,其中
UIApplicationMain
函数内部帮我们开启了主线程的RunLoop,UIApplicationMain
内部拥有一个无线循环的代码。 -
runloop与线程对应关系如下:
- 一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的RunLoop对象。
- 我们只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。
- RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候。
- 主线程的RunLoop对象系统自动帮助我们创建好了(原理如下),而子线程的RunLoop对象需要我们主动创建。
-
-
runloop的mode是用来做什么的?有几种mode?
-
在ibireme大神的深入理解 Runloop一文中,详细的介绍了一个Runloop包含了哪些东西,如下图所示:
Mode代表RunLoop的运行模式,一个RunLoop可以包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。 -
常见的Mode如下:
1.
kCFRunLoopDefaultMode
:App的默认运行模式,通常主线程是在这个运行模式下运行 2.UITrackingRunLoopMode
:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响) 3.UIInitializationRunLoopMode
:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用 4.GSEventReceiveRunLoopMode
:接受系统内部事件,通常用不到 5.kCFRunLoopCommonModes
:伪模式,不是一种真正的运行模式
-
-
为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
- 1.当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下。
- 2.而当我们拖动scrollview的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加NSTimer,所以我们的NSTimer就不工作了。
- 3.但当我们松开鼠标的时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了
- 4.我们可以把NSTimer也添加到UITrackingRunLoopMode模式下,或者开启新的线程,把NSTimer添加到子线程的Runloop中,这样就可以解决滑动时NSTimer失效的问题
-
苹果是如何实现Autorelease Pool的?
-
autorelease
一个被autorelease修饰的对象会被加到最近的
autoreleasePool
中,当这个autoreleasePool
自身drain的时候,其中的autoreleased对象会被release
-
autoreleasePool是怎么实现的?
1.
AutoreleasePool
并没有单独的结构,而是由若干个AutoreleasePoolPage
以双向链表的形式组合而成2.
AutoreleasePoolPage
对象会记录autorelease
对象地址3.AutoreleasePool的操作时通过以下这几个函数实现的:
objc_autoreleasepoolPush
,objc_autoreleasepoolPop
,objc_autorelease
4.
Autorelease
对象是在当前的runloop
迭代结束时释放的,而它能够释放的原因是系统在每个runloop
迭代中都加入了autorelease
的Push和Pop
推荐大家阅读这篇黑幕背后的Autorelease
-
类结构
-
isa指针?(对象的isa,类对象的isa,元类的isa都要说)
- 上面题目中写了类经过编译后的结构体,其中包含有
isa
指针,在OC中类也是一种对象,它属于元类metaClasss
,对象的isa
指针指向类,类的isa
指针指向元类,元类的isa
指针指向父类的元类,一直到根元类,最后根元类的isa
指针指向了自身,如图:
- 上面题目中写了类经过编译后的结构体,其中包含有
-
类方法和实例方法有什么区别?
- 调用方式不同,类方法由类名直接调用,实例方法由该类生成的对象调用。原因是类方法是在元类结构体的
methodLists
里面,而实例方法位于类结构体的methodLists
中。 - 类方法不能使用该类的属性,实例方法可以使用属性
- 类方法中不能调用实例方法,而实例方法中可以调用类方法
- 类方法中self代表类本身,实例方法中self代表实例对象本身
- 调用方式不同,类方法由类名直接调用,实例方法由该类生成的对象调用。原因是类方法是在元类结构体的
-
介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法?
-
- 扩展已有的类
- 分散原类的实现
- 声明私有方法
- 模拟多继承
- 公开framework的部分私有方法
-
分类经过编译后也会成为一个结构体:
struct category_t { const char *name; // 类名 classref_t cls; // 分类所属的类 struct method_list_t *instanceMethods; // 实例方法列表 struct method_list_t *classMethods; // 类方法列表 struct protocol_list_t *protocols; // 遵循的协议列表 struct property_list_t *instanceProperties; // 属性列表 };
在运行时,
category
会被附加到类上面,包括把category
的实例方法、协议以及属性添加到类上和category
的类方法和协议添加到类的metaclass
上。 -
category
的方法没有完全替换掉原来类已经有的方法,也就是说如果category
和原来类都有methodA,那么category
附加完成之后,类的方法列表里会有两个methodA,category
的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category
的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。推荐大家阅读美团出品的深入理解Objective-C:Category,我是知识的搬运工~ 也欢迎大家给我推荐高质量的文章!独乐乐不如众乐乐~
-
-
运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么?
class_addIvar
给指定的类添加成员变量,但是不能为已经生成的类添加,运行时规定,只能在objc_allocateClassPair
与objc_registerClassPair
两个函数之间为类添加变量。原因在上面的题目中有过解释。class_addProperty
给指定的类添加属性,可以成功添加了属性但是不能用点调用法调用,可以利用KVC/关联方式来该表这个属性的值runtime
的objc_setAssociatedObject
和objc_getAssociatedObject
方法来实现关联,给分类添加属性就是利用这个方法实现的。
-
objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体)
- 向 nil 发送消息并不会引起程序crash,只是在运行时不会有任何作用。但是对
[NSNull null]
对象发送消息时,是会crash的。 - 当方法返回值为对象的时候, 给nil发消息返回nil
- 当方法返回值为结构体的时候,给nil发消息返回0,结构体中的各个参数也是0
- 当方法返回值为指针类型的时候, 给nil发消息返回0
- 向 nil 发送消息并不会引起程序crash,只是在运行时不会有任何作用。但是对
参考博客: