阅读 380

iOS底层面试总结

前言:这篇文章是我看李明杰老师的iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化总结所得,断断续续历时3个月左右,把课堂听的东西给做了一下笔记。

总结不易,耗时耗力,您的一颗小星星✨是我无限的动力。原文地址

iOS底层原理.png


我们经常会看一些面试题,但是好多面试题我们都是知其然不知其所以然,你如果认真的看了我上面总结的几十篇文章,那么你也会知其所以然。

OC对象本质

1、一个NSObject对象占用多少内存?

系统分配了16个字节给NSObject对象(通过malloc_size函数获得),但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

2、对象的isa指针指向哪里?

  • instance对象的isa指向class对象
  • class对象的isa指向meta-class对象
  • meta-class对象的isa指向基类的meta-class对象

3、OC的类信息存放在哪里?

  • 对象方法、属性、成员变量、协议信息,存放在class对象中
  • 类方法,存放在meta-class对象中
  • 成员变量的具体值,存放在instance对象

具体实现请参考: 1、一个NSObject对象占用多少内存 2、OC对象的分类

KVO

1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

  • 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
    • 1、调用willChangeValueForKey方法
    • 2、调用setAge方法
    • 3、调用didChangeValueForKey方法
    • 4、didChangeValueForKey方法内部调用oberser的observeValueForKeyPath:ofObject:change:context:方法

2、如何手动触发KVO?

手动调用willChangeValueForKey:和didChangeValueForKey:

3、直接修改成员变量会触发KVO么?

不会触发KVO

具体实现请参考:3、KVO实现原理

KVC

1、通过KVC修改属性会触发KVO么?

会触发KVO,因为KVC是调用set方法,KVO就是监听set方法

2、KVC的赋值和取值过程是怎样的?原理是什么?

KVO的setValue:forKey原理

KVC2.png

  • 1、按照setKey,_setKey的顺序查找成员方法,如果找到方法,传递参数,调用方法
  • 2、如果没有找到,查看accessInstanceVariablesDirectly的返回值(accessInstanceVariablesDirectly的返回值默认是YES),
    • 返回值为YES,按照_Key,_isKey,Key,isKey的顺序查找成员变量, 如果找到,直接赋值,如果没有找到,调用setValue:forUndefinedKey:,抛出异常
    • 返回NO,直接调用setValue:forUndefinedKey:,抛出异常

KVO的ValueforKey原理

KVC3.png

  • 1、按照getKey,key,isKey,_key的顺序查找成员方法,如果找到直接调用取值
  • 2、如果没有找到,查看accessInstanceVariablesDirectly的返回值
    • 返回值为YES,按照_Key,_isKey,Key,isKey的顺序查找成员变量,如果找到,直接取值,如果没有找到,调用setValue:forUndefinedKey:,抛出异常
    • 返回NO,直接调用setValue:forUndefinedKey:,抛出异常

具体实现请参考:4、KVC实现原理

Category

1、Category的实现原理

  • Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
  • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

2、Category和Class Extension的区别是什么?

  • Class Extension在编译的时候,它的数据就已经包含在类信息中
  • Category是在运行时,才会将数据合并到类信息中

3、load、initialize方法的区别什么?

  • 1.调用方式

    • 1> load是根据函数地址直接调用
    • 2> initialize是通过objc_msgSend调用
  • 2.调用时刻

    • 1> load是runtime加载类、分类的时候调用(只会调用1次 )
    • 2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

4、load、initialize的调用顺序

1.load

  • 1> 先调用类的load
    • a) 先编译的类,优先调用load
    • b) 调用子类的load之前,会先调用父类的load
  • 2> 再调用分类的load
    • a) 先编译的分类,优先调用load

2.initialize

  • 1> 先初始化父类
  • 2> 再初始化子类(可能最终调用的是父类的initialize方法)

5、如何实现给分类“添加成员变量”?

默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现

关联对象提供了以下API
添加关联对象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

获得关联对象
id objc_getAssociatedObject(id object, const void * key)

移除所有的关联对象
void objc_removeAssociatedObjects(id object)
复制代码

具体实现请参考: 5.1、分类的实现原理 5.2、Load和Initialize实现原理

Block

1、block的原理是怎样的?本质是什么?

  • block本质上也是一个OC对象,它内部也有个isa指针
  • block是封装了函数调用以及函数调用环境的OC对象

block的底层.png

2、block的(capture)

变量捕获.png

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

3、Block类型有哪几种 block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

  • 1、NSGlobalBlock ( _NSConcreteGlobalBlock
  • 2、NSStackBlock ( _NSConcreteStackBlock )
  • 3、NSMallocBlock ( _NSConcreteMallocBlock )

Block类型.png

4、block的copy

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  • 1、block作为函数返回值时
  • 2、将block赋值给__strong指针时
  • 3、block作为Cocoa API中方法名含有usingBlock的方法参数时
  • 4、block作为GCD API的方法参数时
MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

复制代码

5、__block修饰符

  • __block可以用于解决block内部无法修改auto变量值的问题

  • __block不能修饰全局变量、静态变量(static)

  • 编译器会将__block变量包装成一个对象

  • 当__block变量在栈上时,不会对指向的对象产生强引用

  • 当__block变量被copy到堆时

    • 会调用__block变量内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
  • 如果__block变量从堆上移除

    • 会调用__block变量内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放指向的对象(release)

6、循环引用

  • 用__weak、__unsafe_unretained解决
__unsafe_unretained typeof(self) weakSelf = self;
self.block = ^{
 print(@"%p", weakSelf);
}
复制代码
__weak typeof(self) weakSelf = self;
self.block = ^{
 print(@"%p", weakSelf);
}
复制代码
  • 用__block解决(必须要调用block)
__block id weakSelf = self;
self.block = ^{
weakSelf = nil;
}
self.block();
复制代码

具体实现请参考:6、Block底层解密

RunTime

1、讲一下 OC 的消息机制

  • OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
  • objc_msgSend底层有3大阶段:消息发送(当前类、父类中查找)、动态方法解析、消息转发

2、消息转发机制流程

  • 1、消息发送
  • 2、动态方法解析
  • 3、消息转发

消息发送阶段

消息发送流程是我们平时最经常使用的流程,其他的像动态方法解析和消息转发其实是补救措施。具体流程如下

消息发送1.png

  • 1、首先判断消息接受者receiver是否为nil,如果为nil直接退出消息发送
  • 2、如果存在消息接受者receiverClass,首先在消息接受者receiverClass的cache中查找方法,如果找到方法,直接调用。如果找不到,往下进行
  • 3、没有在消息接受者receiverClass的cache中找到方法,则从receiverClass的class_rw_t中查找方法,如果找到方法,执行方法,并把该方法缓存到receiverClass的cache中;如果没有找到,往下进行
  • 4、没有在receiverClass中找到方法,则通过superClass指针找到superClass,也是现在缓存中查找,如果找到,执行方法,并把该方法缓存到receiverClass的cache中;如果没有找到,往下进行
  • 5、没有在消息接受者superClass的cache中找到方法,则从superClass的class_rw_t中查找方法,如果找到方法,执行方法,并把该方法缓存到receiverClass的cache中;如果没有找到,重复4、5步骤。如果找不到了superClass了,往下进行
  • 6、如果在最底层的superClass也找不到该方法,则要转到动态方法解析

动态方法解析

消息发送2.png

开发者可以实现以下方法,来动态添加方法实现

  • +resolveInstanceMethod:
  • +resolveClassMethod: 动态解析过后,会重新走“消息发送”的流程,从receiverClass的cache中查找方法这一步开始执行

消息转发

如果方法一个方法在消息发送阶段没有找到相关方法,也没有进行动态方法解析,这个时候就会走到消息转发阶段了。

消息发送6.png

  • 调用forwardingTargetForSelector,返回值不为nil时,会调用objc_msgSend(返回值, SEL)
  • 调用methodSignatureForSelector,返回值不为nil,调用forwardInvocation:方法;返回值为nil时,调用doesNotRecognizeSelector:方法
  • 开发者可以在forwardInvocation:方法中自定义任何逻辑
  • 以上方法都有对象方法、类方法2个版本(前面可以是加号+,也可以是减号-)

3、什么是Runtime?平时项目中有用过么?

  • OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
  • OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
  • 平时编写的OC代码,底层都是转换成了Runtime API进行调用

具体应用

  • 利用关联对象(AssociatedObject)给分类添加属性
  • 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  • 交换方法实现(交换系统的方法)
  • 利用消息转发机制解决方法找不到的异常问题

4、super的本质

  • super调用,底层会转换为objc_msgSendSuper2函数的调用,接收2个参数
    • struct objc_super2
    • SEL
  • receiver是消息接收者
  • current_class是receiver的Class对象

具体实现请参考:

RunLoop

1、讲讲 RunLoop,项目中有用到吗? 1、定时器切换的时候,为了保证定时器的准确性,需要添加runLoop 2、在聊天界面,我们需要持续的把聊天信息存到数据库中,这个时候需要开启一个保活线程,在这个线程中处理

2、runloop内部实现逻辑

每次运行RunLoop,线程的RunLoop会自动处理之前未处理的消息,并通知相关的观察者。具体顺序

  • 1、通知观察者(observers)RunLoop即将启动
  • 2、通知观察者(observers)任何即将要开始的定时器
  • 3、通知观察者(observers)即将处理source0事件
  • 4、处理source0
  • 5、如果有source1,跳到第9步
  • 6、通知观察者(observers)线程即将进入休眠
  • 7、将线程置于休眠知道任一下面的事件发生
    • 1、source0事件触发
    • 2、定时器启动
    • 3、外部手动唤醒
  • 8、通知观察者(observers)线程即将唤醒
  • 9、处理唤醒时收到的时间,之后跳回2
    • 1、如果用户定义的定时器启动,处理定时器事件
    • 2、如果source0启动,传递相应的消息
  • 10、通知观察者RunLoop结束

RunLoop7.png

3、RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

4、timer 与 runloop 的关系?

  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个Mode,作为currentMode
  • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
  • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

解决定时器在滚动视图上面失效问题NSTimer添加到两种RunLoop中

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
复制代码
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
复制代码

5、RunLoop有几种状态

kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop 
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer 
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source 
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠 
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
 kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
复制代码

**6、RunLoop的mode的作用 **

RunLoop的mode的作用 系统注册了5中mode

kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode // 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode // 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes //这是一个占位用的Mode,不是一种真正的Mode
复制代码

但是我们只能使用两种mode

kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
复制代码

具体实现请参考:7、RunLoop实现原理

多线程

1、你理解的多线程? 2、iOS的多线程方案有哪几种?你更倾向于哪一种? 3、你在项目中用过 GCD 吗? 4、GCD 的队列类型 5、说一下 OperationQueue 和 GCD 的区别,以及各自的优势 6、线程安全的处理手段有哪些? 使用线程锁

  • 1、OSSpinLock
  • 2、os_unfair_lock
  • 3、pthread_mutex
  • 4、dispatch_semaphore
  • 5、dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • 6、NSLock
  • 7、NSRecursiveLock
  • 8、NSCondition
  • 9、NSConditionLock
  • 10、@synchronized
  • 11、pthread_rwlock
  • 12、dispatch_barrier_async
  • 13、atomic

7、线程通讯 线程间通信的体现

  • 1、一个线程传递数据给另一个线程
  • 2、在一个线程中执行完特定任务后,转到另一个线程继续执行任务

1、NSThread 可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
 
复制代码

2、GCD

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      
 });
复制代码

内存管理

1、使用CADisplayLink、NSTimer有什么注意点? CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用

2、介绍下内存的几大区域

  • 代码段:编译之后的代码
  • 数据段
    • 字符串常量:比如NSString *str = @"123"
    • 已初始化数据:已初始化的全局变量、静态变量等
    • 未初始化数据:未初始化的全局变量、静态变量等
  • 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
  • 堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大

3、讲一下你对 iOS 内存管理的理解 在iOS中,使用引用计数来管理OC对象的内存

  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

内存管理的经验总结

  • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
  • 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1

可以通过以下私有函数来查看自动释放池的情况 extern void _objc_autoreleasePoolPrint(void);

4、ARC 都帮我们做了什么 LLVM + Runtime

  • LVVM生成release代码
  • RunTime负责执行

5、weak指针的实现原理 runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组

  • 1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址
  • 2、添加引用时:objc_initWeak函数会调用 storeWeak() 函数, storeWeak() 的作用是更新指针指向,创建对应的弱引用表
  • 3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录

6、autorelease对象在什么时机会被调用release

  • 1、iOS在主线程的Runloop中注册了2个Observer
  • 2、第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
  • 3、第2个Observer监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush() 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop() autoreleased 对象是在 runloop 的即将进入休眠时进行释放的

7、方法里有局部对象, 出了方法后会立即释放吗 在ARC情况下会立即释放 在MRC情况下,对象是在 runloop 的即将进入休眠时进行释放的

文章中可以提炼出来的题目太多了,我这里也就简单的总结几道题,想要了解具体实现请到我的github中找到相关文章进行阅读。欢迎点赞哦,如果里面有什么我理解的不太正确,欢迎提出,我们相互印证

关注下面的标签,发现更多相似文章
评论

查看更多 >