iOS底层面试总结

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

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

iOS底层原理.png


咱们常常会看一些面试题,可是好多面试题咱们都是知其然不知其因此然,你若是认真的看了我上面总结的几十篇文章,那么你也会知其因此然。面试

OC对象本质

一、一个NSObject对象占用多少内存?数据库

系统分配了16个字节给NSObject对象(经过malloc_size函数得到),但NSObject对象内部只使用了8个字节的空间(64bit环境下,能够经过class_getInstanceSize函数得到)编程

二、对象的isa指针指向哪里?数组

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

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

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

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

KVO

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

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

二、如何手动触发KVO?bash

手动调用willChangeValueForKey:和didChangeValueForKey:

三、直接修改为员变量会触发KVO么?

不会触发KVO

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

KVC

一、经过KVC修改属性会触发KVO么?

会触发KVO,由于KVC是调用set方法,KVO就是监听set方法

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

KVO的setValue:forKey原理

KVC2.png

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

KVO的ValueforKey原理

KVC3.png

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

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

Category

一、Category的实现原理

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

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

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

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

  • 1.调用方式

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

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

四、load、initialize的调用顺序

1.load

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

2.initialize

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

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

默认状况下,由于分类底层结构的限制,不能添加成员变量到分类中。但能够经过关联对象来间接实现

关联对象提供了如下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.一、分类的实现原理 5.二、Load和Initialize实现原理

Block

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

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

block的底层.png

二、block的(capture)

变量捕获.png

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

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

  • 一、NSGlobalBlock ( _NSConcreteGlobalBlock
  • 二、NSStackBlock ( _NSConcreteStackBlock )
  • 三、NSMallocBlock ( _NSConcreteMallocBlock )

Block类型.png

四、block的copy

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

  • 一、block做为函数返回值时
  • 二、将block赋值给__strong指针时
  • 三、block做为Cocoa API中方法名含有usingBlock的方法参数时
  • 四、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);

复制代码

五、__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)

六、循环引用

  • 用__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();
复制代码

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

RunTime

一、讲一下 OC 的消息机制

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

二、消息转发机制流程

  • 一、消息发送
  • 二、动态方法解析
  • 三、消息转发

消息发送阶段

消息发送流程是咱们平时最常用的流程,其余的像动态方法解析和消息转发实际上是补救措施。具体流程以下

消息发送1.png

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

动态方法解析

消息发送2.png

开发者能够实现如下方法,来动态添加方法实现

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

消息转发

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

消息发送6.png

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

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

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

具体应用

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

四、super的本质

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

具体实现请参考:

RunLoop

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

二、runloop内部实现逻辑

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

  • 一、通知观察者(observers)RunLoop即将启动
  • 二、通知观察者(observers)任何即将要开始的定时器
  • 三、通知观察者(observers)即将处理source0事件
  • 四、处理source0
  • 五、若是有source1,跳到第9步
  • 六、通知观察者(observers)线程即将进入休眠
  • 七、将线程置于休眠知道任一下面的事件发生
    • 一、source0事件触发
    • 二、定时器启动
    • 三、外部手动唤醒
  • 八、通知观察者(observers)线程即将唤醒
  • 九、处理唤醒时收到的时间,以后跳回2
    • 一、若是用户定义的定时器启动,处理定时器事件
    • 二、若是source0启动,传递相应的消息
  • 十、通知观察者RunLoop结束

RunLoop7.png

三、RunLoop与线程

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

四、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];
复制代码

五、RunLoop有几种状态

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

**六、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 影响
复制代码

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

多线程

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

  • 一、OSSpinLock
  • 二、os_unfair_lock
  • 三、pthread_mutex
  • 四、dispatch_semaphore
  • 五、dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • 六、NSLock
  • 七、NSRecursiveLock
  • 八、NSCondition
  • 九、NSConditionLock
  • 十、@synchronized
  • 十一、pthread_rwlock
  • 十二、dispatch_barrier_async
  • 1三、atomic

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

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

一、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);
 
复制代码

二、GCD

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

内存管理

一、使用CADisplayLink、NSTimer有什么注意点? CADisplayLink、NSTimer会对target产生强引用,若是target又对它们产生强引用,那么就会引起循环引用

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

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

三、讲一下你对 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);

四、ARC 都帮咱们作了什么 LLVM + Runtime

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

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

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

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

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

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

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

相关文章
相关标签/搜索