iOS底层面试题
底层面试概要
- 对象的本质
- Category
- Block
- Runtime
- RunLoop
- 多线程
- 内存管理
- 性能优化
对象的本质
Objective-C对象的本质
- 一个NSObject对象占用多少内存
系统分配了16个字节;64位环境下,NSObject内部只使用了8个字节(64位环境下)存储isa。 - 对象的isa指向哪里?
instance对象的isa->类对象
类对象的isa->metaClass对象
metaClass对象的isa->基类的metaClass对象
基类的metaClass对象->基类的metaClass对象 - OC的类信息存放在哪里?
实例对象的方法,属性,成员变量,协议存在类对象中
类对象的方法存在元类对象中
Category
- Category的实现原理
- Category编译之后,底层结构是struct category_t,里面存储着分类的对象方法,类方法,属性,协议信息
- 在程序运行的时候,Runtime会将Category的数据,合并到类信息中(类对象,元类对象)
- Category和Extension的区别?
- Extension发生在编译的时候,他的信息已经包含在类信息中
- Category是在运行时,将信息同步到类信息中
- Category中有load方法吗?load方法什么时候调用?能继承吗?
- 有load方法
- load方法是在Runtime加载类/分类的时候调用(静态调用-函数地址调用)。
- load方法可以继承,但是开发中强烈不建议主动调用load方法
- load和initialize方法的区别是什么?
- 调用方式:load是函数地址直接调用;initialize是通过objc_msgSend调用
- 调用时机:load方法是程序启动Runtime加载类/分类的时候调用;initialize是该类第一次收到消息的时候调用。
- 调用次数:load方法,一般系统自动执行,每个类只会执行一次。initialize如果子类没有实现,会调用父类的initialize,所以initialize会调用多次。
- 他们在Category中的调用顺序?以及出现继承时他们之间的调用过程?
- load:先调用类的load,按照编译顺序加载,但是如果有父类,先处理父类(递归);其次再调用分类的load,按照编译的顺序加载。
- initialize:调用子类的+initialize前,先调用父类+initialize(递归)
- Category能否添加成员变量?如果可以,如何给Category添加成员变量
- 可以,通过关联对象添加。
Block
- block原理是怎样的?本质是什么?
- block是封装
函数
及其上下文
的OC对象
- block是封装
- __block的作用是什么?有什么使用注意点?
- 编译器会将__block修饰的变量封装成对象,解决block可以修改auto变量
- MRC环境,block不会对修饰的OC对象产生强引用
- block的属性修饰词为什么是copy?使用block有哪些注意点?
- 如果block不进行copy操作,就不会在堆上。就无法控制block生命周期
- 注意点:循环引用的问题
- block修改NSMutableArray内部数据的时候,需不需要加__block?
- 不需要
Runtime
如果需要交换UIControl的didMoveToSuperview方法,用于捕获控件的点击事件,需要的注意点是什么?
- 由于didMoveToSuperview方法是UIView的方法,UIControl继承自UIView,所以在方法交换之后,UIView及继承UIView的子类,都需要增加交换的方法,否则会因为找不到方法而崩溃
讲一下OC的消息机制
- OC的方法调用都是转换成
objc_msgSend
函数调用,给receiver
方法调用者发送一个消息@selector方法名
objc_msgSend
底层有3大阶段- 消息发送
- 动态方法解析
- 消息转发
- OC的方法调用都是转换成
消息转发机制流程
forwardTargetForSelector
返回target对象methodSignatureForSelector
返回方法签名;如果返回签名不为空,进行第3步forwardInvocation:
参数传入一个方法调用对象,可以自定义调用机制
什么是Runtime?平时项目中有用过吗?
RunLoop
讲讲 RunLoop,项目中有用到吗?
- 定时器(Timer)、PerformSelector
- GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool
RunLoop的基本作用
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
Runloop内部实现逻辑?
- 通知Observers:进入Loop
- 通知Observers:即将处理Timers
- 通知Observers:即将处理Sources
- 处理Blocks
- 处理Source0(可能会再次处理Blocks)
- 如果存在Source1,就跳转到第8步
- 通知Observers:开始休眠(等待消息唤醒)
- 通知Observers:结束休眠(被某个消息唤醒)
- 处理Timer
- 处理GCD Async To Main Queue
- 处理Source1
- 处理Blocks
- 根据前面的执行结果,决定如何操作
- 回到第02步
- 退出Loop
- 通知Observers:退出Loop
Runloop和线程的关系?
- 每条线程都有唯一的一个与之对应的RunLoop对象
RunLoop
保存在一个全局的Dictionary里,线程作为key,RunLoop
作为value;{thread: Runloop}- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
RunLoop
会在线程结束时销毁- 主线程的
RunLoop
已经自动获取(创建),子线程默认没有开启RunLoop
Timer 与 Runloop 的关系?
- 结构上来说:Timer里面有模式,模式里面有Timer
- 流程上来说:Timer是在Runloop运行流程中工作的
程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
- 将Timer添加到Runloop的CommonMode里
runloop 是怎么响应用户操作的, 具体流程是什么样的?
- Source1来捕获系统事件
- Source1把事件包装成事件队列(EventQueue)
- Source0处理事件队列
说说runLoop的几种状态
1
2
3
4
5
6
7
8
9
10/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将推出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};runloop的mode作用是什么?
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
多线程
你理解的多线程?
iOS的多线程方案有哪几种?你更倾向于哪一种?
pthread
NSThread
GCD
NSOperation
更倾向于GCD;但是也要看场景GCD 的队列类型
同步队列``并发队列
说一下 OperationQueue 和 GCD 的区别,以及各自的优势
Operation Queue
是基于GCD
封装的抽象类,目的是为了提高灵活度,以满足多线程操作频繁、灵活度要求高的复杂场景。线程安全的处理手段有哪些?
加锁
,GCD同步队列
,信号量
等OC你了解的锁有哪些?在你回答基础上进行二次提问;自旋和互斥对比?使用以上锁需要注意哪些?
路由地址以下代码是在主线程执行的,会不会产生死锁?
1
2
3
4
5
6
7
8
9
10
11
12
13
14@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
@end答:会产生死锁,因为
viewDidLoad
方法就是在主线程(主队列)中进行的,而line-9
仍然想让代码块 立即 在主线程(主队列)中同步执行。问下面代码的打印结果是什么?
1
2
3
4
5
6
7
8
9
10
11
12
13@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}
- (void)test {
NSLog(@"2");
}答:打印结果是:1, 3。
原因:performSelector:withObject:afterDelay:
的本质是往Runloop中添加定时器,子线程默认没有启动Runloop
。解决方法:在line-8
后插入1
2//[[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];请问下面代码的打印结果是什么?
1
2
3
4
5
6
7
8
9
10- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test {
NSLog(@"2");
}答:只会打印1。因为在调用test的时候,线程已经销毁了。会报错
target thread exited while waiting for the perform
内存管理
使用CADisplayLink、NSTimer有什么注意点?
CADisplayLink
、NSTimer
会对target
产生强引用,如果target
又对它们产生强引用,那么就会引发循环引用。解决方案:1:使用Block处理。2.使用代理对象NSProxy
介绍下内存的几大区域
- 代码段:编译之后的代码
- 数据段
- 字符串常量:比如NSString *str = @”123”
- 已初始化数据:已初始化的全局变量、静态变量等
- 未初始化数据:未初始化的全局变量、静态变量等
- 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
- 堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
讲一下你对 iOS 内存管理的理解
ARC 都帮我们做了什么?
答:LLVM+Runtime相互协作;ARC利用LLVM编译器,自动帮我们生成release,retain,autorelease代码。像弱引用会在运行时做操作。weak指针的实现原理
答:将弱引用存到哈希表里面,对象将要销毁的时候,取出弱引用表,将指向当前对象的弱指针置为nil。autorelease对象在什么时机会被调用release
方法里有局部对象, 出了方法后会立即释放吗
一下两段代码能发生什么事?有什么区别?
1
2
3
4
5
6dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghijklmn"];
});
}1
2
3
4
5
6dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}答:上面一段执行,会出现坏内存的崩溃。下面一段代码name变量存储的是
Tagged Pointer
,涉及不到对象的内存管理,而上面一段代码因为多线程操作一个对象,有可能会造成对象的过度释放,导致崩溃。