Part 0
看以下习题前可以先看看下面的别人整理好的面试题:
iOSInterviewQuestions【强烈推荐看】
Part 1
习题来自:iOS-InterviewQuestion-collection
0x01 内存管理
@autoreleasrPool
的释放时机?
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop
迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
参考:
自动引用计(ARC)
数应该遵循的原则?
不能使用 retain/release/retainCount/autorelease
不能使用 NSAllocateObject/NSDeallocateObject
须遵守内存管理的方法命名规则
不要显示调用dealloc
使用 @autorelease 块代替NSAutoreleasePool
不能使用区域(NSZone)
对象型变量不能作为C语言结固体的成员
显示转换“id”和”void *”
参考:
访问__weak
修饰的变量,是否已经被注册在了 @autoreleasePool
中?为什么?
在访问 __weak
修饰的变量时,实际上必定会访问注册到 Autorelease Pool
的对象。如下来年两段代码是相同的效果:
1 | id __weak obj1 = obj0; |
为什么会这样呢?因为 __weak
修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 Autorelease Pool 中,就能保证 Autorelease Pool 被销毁前对象是存在的。
参考:
ARC 的 retainCount 怎么存储的?
the reference counts for most objects are stored in hash tables. Have a look at the _objc_rootRetain
function in runtime/objc-arr.mm
参考:
Where is the retain count stored for NSObjects in Objective C
简要说一下 @autoreleasePool 的数据结构
1 | AutoreleasePoolPage { |
magic用来校验AutoreleasePoolPage结构是否完整;
next指向第一个可用的地址;
thread指向当前的线程;
parent指向父类
child指向子类
参考:
__weak
和 _Unsafe_Unretain
的区别
weak与unsafe_unretained的区别在于,weak
会将被释放指针赋值为nil,而unsafe_unretained
则会成为野指针。
为什么已经有了 ARC ,但还是需要 @AutoreleasePool 的存在?
ARC 并不是舍弃了 @autoreleasepool
,而是在编译阶段帮你插入必要的 retain
/release
/autorelease
的代码调用。
所以,跟你想象的不一样,ARC 之下依然是延时释放的,依然是依赖于 NSAutoreleasePool
,跟非 ARC 模式下手动调用那些函数本质上毫无差别,只是编译器来做会保证引用计数的正确性。
__weak
属性修饰的变量,如何实现在变量没有强引用后自动置为 nil
runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
说一下对 retain,copy,assign,weak,_Unsafe_Unretain 关键字的理解
assign
表明 setter 仅仅是一个简单的赋值操作,通常用于基本的数值类型,例如CGFloat和NSInteger。
weak
表明属性定义了一个非拥有者关系。当给属性设定一个新值的时候,这个值不会进行 retain,旧值也不会进行 release, 而是进行类似 assign 的操作。不过当属性指向的对象被销毁时,该属性会被置为nil。
strong
表明属性定义一个拥有者关系。当给属性设定一个新值的时候,首先这个值进行 retain ,旧值进行 release ,然后进行赋值操作。
copy
类似于 strong,不过在赋值时进行 copy 操作而不是 retain 操作。通常在需要保留某个不可变对象(NSString最常见),并且防止它被意外改变时使用。
unsafe_unretained
的语义和 assign 类似,不过是用于对象类型的,表示一个非拥有(unretained)的,同时也不会在对象被销毁时置为nil的(unsafe)关系。
参考:
NSString属性什么时候用copy,什么时候用strong?
ARC 在编译时做了哪些工作
根据代码执行的上下文语境,在适当的位置插入 retain
,release
ARC 在运行时做了哪些工作
待查~
函数返回一个对象时,会对对象 autorelease 么?为什么?
参考 arc 内存管理要点 这篇。
说一下什么是 悬垂指针?什么是 野指针?
悬垂指针(dangling pointer)一般是说指向已经被释放的自由区内存(free store)的指针,野指针(wild pointer)则一般是未经初始化的指针。前者曾经有效过,后者从未有效过。
参考:
悬垂指针(Dangling pointer)和野指针(Wild pointer)
内存管理默认的关键字是什么?
strong
内存中的5大区分别是什么?
内存5大区:堆,栈,方法区,全局区,常量区
栈:不需要手动管理内存,会自动清理栈中的内存
堆: 需要手动管理内存
静态区:又称全局区
常量区: 储存常量的地方
方法区: 存放函数体的二进制代码
是否了解 深拷贝 和 浅拷贝 的概念,集合类深拷贝如何实现?
深拷贝
是内存拷贝,指向的是不同的内存浅拷贝
是指针拷贝,指向的是同一块内存
集合类深拷贝是通过归档、解档实现。
BAD_ACCESS 在什么情况下出现?
- 访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。
- 死循环
0x02 Runtime
类对象的数据结构?
Objective-C类是由Class
类型来表示的,它实际上是一个指向objc_class
结构体的指针。它的定义如下:
1 | typedef struct objc_class *Class; |
查看objc/runtime.h
中objc_class
结构体的定义如下:
1 | struct objc_class { |
在这个定义中,下面几个字段是我们感兴趣的
isa
:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class
里面也有一个isa
指针,它指向metaClass
(元类),我们会在后面介绍它。super_class
:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class
为NULL。cache
:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache
就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache
列表中,下次调用的时候runtime就会优先去cache中查找,如果cache
没有,才去methodLists
中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。version
:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
参考:
Objective-C Runtime 运行时之一:类与对象
实例对象的数据结构?
实例对象都是一个id
类型的对象,查看objc.h
中对id
的描述:
1 | #if !OBJC_TYPES_DEFINED |
查看源文件,可以看出id
其实就是一个指向objc_object
结构体指针,它包含一个Class
isa 成员。所以实例对象的数据结构实际上就是一个Class
的数据结构。
元类对象的数据结构?
meta-class 是 Class 对象的类。每个 Class 都有个不同的自己的 meta-class(因此每个 Class 都可以有一个自己不同的方法列表)。也就是说每个类的 Class 不完全相同。
参考:
Objective-C 中的 Meta-class 是什么?
Category
的实现原理?以及如何实现添加一个属性?
参考:
如何运用 Runtime
字典转模型?
参考:
简单的版本如上,其他还有一些关键字的不能作为key值得还要映射一下的。还有嵌套的等等其他的要考虑。
Category
有哪些用途?
1)无需创建继承类,实现对已有类扩展。并且可以被扩展的类的所有子类自动继承。
2)可以用来修复没有源码类的bug。
3)对于一个类多个开发人员维护的情况,可以根据不同用途创建不同分类。
注意点:
1)不能在分类中重写系统方法,因为会把系统的功能给覆盖掉,而且分类中不能调用super。但是,这种情况可以用来修复,没有源码的类中方法有Bug的情况。
Category
和 Extension
有什么区别?
1)Category
的加载在运行时,Extension
的加载在编译时。
2) Extension
不能给没有源码的类添加方法。
3) Extension
是一个匿名的 Category
。
说一下 Method Swizzling
? 说一下在实际开发中你在什么场景下使用过?
参考:
关于Method Swizzling
踩过的坑,
参考这里
如何实现动态添加方法和属性?
1 | //动态创建类 |
上述代码,我们通过objc_allocateClassPair()
创建了一个继承NSObject
的person
的子类,然后通过objc_registerClassPair()
这个函数注册了person
类,下面我们就可以使用这个类了,使用之前我们在给person
类中添加一个name
属性和sayHi:
方法,分别通过class_addIvar()
和class_addMethod()
来添加,接下来我们要实现我们添加的sayHi:
方法,如下:
1 | //这个函数必须写,要不然xcode会报错,实际运行的时候,这个函数是不会调用的 |
static void sayHi(id self, SEL _cmd, NSString *name)
这个就是我们运行时候调用的方法,其中self
使我们使用person
创建的对象Tom
,_cmd
是调用的方法名,name
就是传过来的参数,如果有多个参数,可以写成static void sayHi(id self, SEL _cmd, NSString *name,...)
省略号可以填写你愿意添加的参数。
这样我们就利用runtime动态的创建了一个person类,包括了name属性和sayHi:方法,运行如结果如下:
1 | RunTimeDemo[46329:7242266] hello Jeck,my name is Tom |
参考:
说一下对 isa 指针的理解, 对象的isa 指针指向哪里?(注意区分不同对象)
网上有2种说法:
1)isa
是指向元类的指针,不了解元类的可以看 Objective-C 中的 Meta-class 是什么?
2) 任何直接或间接继承了NSObject
的类,它的实例对象(instacne objec)中都有一个isa
指针,指向它的类对象(class object)。这个类对象(class object)中存储了关于这个实例对象(instace object)所属的类的定义的一切:包括变量,方法,遵守的协议等等。
参考:
Objective-C 中的 Meta-class 是什么?
Obj-C
中的类信息存放在哪里?
类方法存储在元类。
当前类的信息都在 data
里。看看源码就知道了。
一个 NSObject 对象占用多少内存空间?
通过 size_t class_getInstanceSize ( Class cls )
接口可以获取NSObject
对象占用内存空间大小。
1 | size_t z = class_getInstanceSize([NSObject class]); |
说一下对 class_rw_t
的理解?
rw
代表可读可写。
ObjC
类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t
中:
1 | struct class_rw_t { |
其中还有一个指向常量的指针 ro
,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议
参考:
说一下对 class_ro_t
的理解?
储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
1 | struct class_ro_t { |
参考:
说一下 Runtime
消息解析和转发
先看一下流程图:
在 Object-C 中调用方法最终都会翻译成调用方法实现的的函数指针,并传递给它一个对象指针、一个选择器和一组函数参数。
objc_msgSend 的工作方式如下:
- 检查接受对象是否为
nil
。如果是,调用nil
处理程序。 - 在垃圾收集环境中(ios 不支持,写在这里是为了内容的完整性),检查有没有锻炼选择器(retain、release、autorelease、retainCount),如果有,返回 self。是的,这意味着在垃圾收集环境中 retainCount 会返回 self,不过你应该用不到。
- 检查类缓存中是不是已经有方法实现了,有的话,直接调用。
- 比较请求的选择器和类定义的选择器,如果找到了,调用方法实现。
- 比较请求的选择器和父类中定义的选择器,然后是父类的父类,以此类推。如果找到选择器,调用方法实现。
- 调用
resolveInstanceMethod:
( 或者resolveClassMethod:
)。如果它返回YES
,那么重新开始。这一次对象会相应这个选择器,一般是因为它已经调用过class_addMethod
。 - 调用
forwardingTargetForSelector:
,如果返回非 nil
,那就把消息发送到返回的对象上。这里不要返回 self,否则会形成死循环。 - 调用
methodSignatureForSelector:
,如果返回一个非 nil
,创建一个NSInvocation
并传给forwardInvocation
。 - 调用
doesNotRecognizeSelector:
,默认是抛出异常。
例子🌰
Xcode何时会报unrecognized selector
的错误
1 | - (void)viewDidLoad { |
当向AAPerson
发送test
这个消息时,runtime
会根据对象的isa指针
找到该对象实际所属的类,然后在该类的方法列表以及父类的方法列表里面找相应的方法运行,如果在最顶层的父类中依然找不到相应的方法实现时,程序在运行时就会报unrecognized selector sent to
的错误并且崩溃,但是在此之前,objc的运行时给出了三次避免程序崩溃的机会。
1、Method resolution
objc运行时会调用+resolveInstanceMethod:
或者+resolveClassMethod:
,让我们有机会提供一个函数实现而不导致程序崩溃,如果在这里面添加了函数,系统就会重新启动一次消息发送的过程,否则就会进入到消息的快速转发流程。
1 | + (BOOL)resolveInstanceMethod:(SEL)sel { |
2、Fast forwarding
如果目标对象
实现了-forwardingTargetForSelector:
的方法,runtime就会调用这个方法,给我们一个机会把这个消息转发给其他的对象,只要这个方法返回值不是nil和self,整个消息发送的过程就会被重启,这时发送的对象会变成我们返回的这个对象,否则就会移到下一步。
1 | - (id)forwardingTargetForSelector:(SEL)aSelector { |
3、Normal Fowarding
如果上面两种方法都没有被实现的话,就会来到第三步 —— 普通转发,这是runtime给我们最后一次避免崩溃的机会,首先它会-methodSignatureForSelector:
来获得函数的参数和返回值类型,如果返回值为nil
,则runtime会发出-doesNotRecognizeSelector:
的消息,程序崩溃。如果返回了一个函数签名,runtime会创建一个NSInvocation
对象并发送-forwardInvocation:
的消息给目标对象。
1 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { |
如果上面的三步都没有实现的话,就会调用-doesNotRecognizeSelector:
,程序崩溃。
如何运用 Runtime
字典转模型?
Runtime
遍历 ivar_list
,结合 KVC
赋值。
Note:
对于特殊的(如id)或者自定义的属性和字典中的key不一样的时候会报错,这时候需要重写- (void)setValue:(id)value forUndefinedKey:(NSString *)key
方法。
参考:
如何运用 Runtime 进行模型的归解档?
Runtime
遍历 ivar_list
。
参考:
在 Obj-C 中为什么叫发消息而不叫函数调用?
结合 objc_msgsend
讲一下,接收消息的过程。
说一下 Runtime 的方法缓存?存储的形式、数据结构以及查找的过程?
cache_t
增量扩展的哈希表结构。哈希表内部存储的 bucket_t
。
如果是有序方法列表,采用二分查找
如果是无序方法列表,直接遍历查找
0x03 RunLoop
Runloop
和线程的关系
一个线程对应一个
Runloop
。主线程的默认就有了
Runloop
。子线程的
Runloop
以懒加载的形式创建。Runloop
存储在一个全局的可变字典里,线程是key
,Runloop
是value
讲一下 Runloop 的 Mode?(越详细越好)
苹果文档中提到的 Mode 有五个,分别是:
- NSDefaultRunLoopMode
- NSConnectionReplyMode
- NSModalPanelRunLoopMode
- NSEventTrackingRunLoopMode
- NSRunLoopCommonModes
iOS 中公开暴露出来的只有 NSDefaultRunLoopMode
和 NSRunLoopCommonModes
。 NSRunLoopCommonModes
实际上是一个 Mode
的集合,默认包括 NSDefaultRunLoopMode
和 NSEventTrackingRunLoopMode
。
参考:
讲一下 Observer ?(Mode中的重点)
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
讲一下 Runloop 的内部实现逻辑?(运行过程)
参考:
你所知的哪些三方框架使用了 Runloop?(AFNetworking、Texture 等)
解释一下 事件响应
的过程
苹果注册了一个 Source1
(基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()
。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework
生成一个 IOHIDEvent
事件并由 SpringBoard
接收。这个过程的详细情况可以参考这里。SpringBoard
只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event
,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1
就会触发回调,并调用 _UIApplicationHandleEventQueue()
进行应用内部的分发。
_UIApplicationHandleEventQueue()
会把 IOHIDEvent
处理并包装成 UIEvent
进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
详细参考(推荐阅读):
解释一下 手势识别 的过程?
当上面的 _UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用 Cancel
将当前的 touchesBegin/Move/End
系列回调打断。随后系统将对应的 UIGestureRecognizer
标记为待处理。
苹果注册了一个 Observer
监测 BeforeWaiting
(Loop即将进入休眠) 事件,这个 Observer
的回调函数是 _UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的 GestureRecognizer
,并执行GestureRecognizer
的回调。
当有 UIGestureRecognizer
的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
解释一下 GCD 在 Runloop 中的使用?
1、RunLoop 的超时时间
这是 RunLoop
的 API:
1 | //mode默认为defaultMode、超时时间是100亿秒、false |
RunLoop
的超时时间就是使用 GCD
中的 dispatch_source_t
来实现的。
2、执行GCD MainQueue
上的异步任务,即:dispatch_async(dispatch_get_main_queue(), block)
产生的任务。
参考:
RunLoop总结:RunLoop 与GCD 、Autorelease Pool之间的关系
解释一下 NSTimer,以及 NSTimer 的循环引用
NSTimer
其实就是 CFRunLoopTimerRef
,他们之间是 toll-free bridged
的。一个 NSTimer
注册到 RunLoop
后,RunLoop
会为其重复的时间点注册好事件。例如 10:00
, 10:10
, 10:20
这几个时间点。RunLoop
为了节省资源,并不会在非常准确的时间点回调这个Timer
。Timer
有个属性叫做 Tolerance (宽容度)
,标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。
引起 NSTimer
循环引用的主要是捕获了self
了,如下:
1 | self.timer = [NSTimer scheduledTimerWithTimeInterval:1 |
就算是将上面的self
改为weakSelf
也没有用,最好的解决办法是用GCD
的dispatch_source
解决。
如下:
1 | #import "RNTimer.h" |
参考链接:
拓展:
AFNetworking
中如何运用Runloop
?AFURLConnectionOperation
这个类是基于 NSURLConnection
构建的,其希望能在后台线程接收 Delegate
回调。为此 AFNetworking
单独创建了一个线程,并在这个线程中启动了一个 RunLoop
:
1 | + (void)networkRequestThreadEntryPoint:(id)__unused object { |
RunLoop
启动前内部必须要有至少一个 Timer/Observer/Source
,所以 AFNetworking
在 [runLoop run]
之前先创建了一个新的NSMachPort
添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port)
并在外部线程通过这个 port
发送消息到 loop
内;但此处添加 port
只是为了让 RunLoop
不至于退出,并没有用于实际的发送消息。
1 | - (void)start { |
当需要这个后台线程执行任务时,AFNetworking
通过调用 [NSObject performSelector:onThread:..]
将这个任务扔到了后台线程的 RunLoop
中。
PerformSelector 的实现原理
当调用 NSObject
的 performSelecter:afterDelay:
后,实际上其内部会创建一个 Timer
并添加到当前线程的 RunLoop
中。所以如果当前线程没有 RunLoop
,则这个方法会失效。
当调用 performSelector:onThread:
时,实际上其会创建一个 Timer
加到对应的线程去,同样的,如果对应线程没有 RunLoop
该方法也会失效。
其他的performSelector
系列方法是类似的
1 | - (void)viewDidLoad { |
执行结果:
1 | 2018-05-16 11:38:20.939516+0800 Test[6410:1215068] execute -[ViewController mainThreadMethod] |