iOS底层面试题

底层面试概要

  • 对象的本质
  • Category
  • Block
  • Runtime
  • RunLoop
  • 多线程
  • 内存管理
  • 性能优化

对象的本质

Objective-C对象的本质

  1. 一个NSObject对象占用多少内存
    系统分配了16个字节;64位环境下,NSObject内部只使用了8个字节(64位环境下)存储isa。
  2. 对象的isa指向哪里?
    instance对象的isa->类对象
    类对象的isa->metaClass对象
    metaClass对象的isa->基类的metaClass对象
    基类的metaClass对象->基类的metaClass对象
  3. OC的类信息存放在哪里?
    实例对象的方法,属性,成员变量,协议存在类对象中
    类对象的方法存在元类对象中

Category

  1. Category的实现原理
    • Category编译之后,底层结构是struct category_t,里面存储着分类的对象方法,类方法,属性,协议信息
    • 在程序运行的时候,Runtime会将Category的数据,合并到类信息中(类对象,元类对象)
  2. Category和Extension的区别?
    • Extension发生在编译的时候,他的信息已经包含在类信息中
    • Category是在运行时,将信息同步到类信息中
  3. Category中有load方法吗?load方法什么时候调用?能继承吗?
    • 有load方法
    • load方法是在Runtime加载类/分类的时候调用(静态调用-函数地址调用)。
    • load方法可以继承,但是开发中强烈不建议主动调用load方法
  4. load和initialize方法的区别是什么?
    • 调用方式:load是函数地址直接调用;initialize是通过objc_msgSend调用
    • 调用时机:load方法是程序启动Runtime加载类/分类的时候调用;initialize是该类第一次收到消息的时候调用。
    • 调用次数:load方法,一般系统自动执行,每个类只会执行一次。initialize如果子类没有实现,会调用父类的initialize,所以initialize会调用多次。
  5. 他们在Category中的调用顺序?以及出现继承时他们之间的调用过程?
    • load:先调用类的load,按照编译顺序加载,但是如果有父类,先处理父类(递归);其次再调用分类的load,按照编译的顺序加载。
    • initialize:调用子类的+initialize前,先调用父类+initialize(递归)
  6. Category能否添加成员变量?如果可以,如何给Category添加成员变量
    • 可以,通过关联对象添加。

Block

  1. block原理是怎样的?本质是什么?
    • block是封装函数及其上下文OC对象
  2. __block的作用是什么?有什么使用注意点?
    • 编译器会将__block修饰的变量封装成对象,解决block可以修改auto变量
    • MRC环境,block不会对修饰的OC对象产生强引用
  3. block的属性修饰词为什么是copy?使用block有哪些注意点?
    • 如果block不进行copy操作,就不会在堆上。就无法控制block生命周期
    • 注意点:循环引用的问题
  4. block修改NSMutableArray内部数据的时候,需不需要加__block?
    • 不需要

Runtime

  1. 如果需要交换UIControl的didMoveToSuperview方法,用于捕获控件的点击事件,需要的注意点是什么?

    • 由于didMoveToSuperview方法是UIView的方法,UIControl继承自UIView,所以在方法交换之后,UIView及继承UIView的子类,都需要增加交换的方法,否则会因为找不到方法而崩溃
  2. 讲一下OC的消息机制

    • OC的方法调用都是转换成objc_msgSend函数调用,给receiver方法调用者发送一个消息@selector方法名
    • objc_msgSend底层有3大阶段
      • 消息发送
      • 动态方法解析
      • 消息转发
  3. 消息转发机制流程

    • forwardTargetForSelector 返回target对象
    • methodSignatureForSelector 返回方法签名;如果返回签名不为空,进行第3步
    • forwardInvocation: 参数传入一个方法调用对象,可以自定义调用机制
  4. 什么是Runtime?平时项目中有用过吗?

RunLoop

  1. 讲讲 RunLoop,项目中有用到吗?

    • 定时器(Timer)、PerformSelector
    • GCD Async Main Queue
    • 事件响应、手势识别、界面刷新
    • 网络请求
    • AutoreleasePool
  2. RunLoop的基本作用

    • 保持程序的持续运行
    • 处理App中的各种事件(比如触摸事件、定时器事件等)
    • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
  3. Runloop内部实现逻辑?

    1. 通知Observers:进入Loop
    2. 通知Observers:即将处理Timers
    3. 通知Observers:即将处理Sources
    4. 处理Blocks
    5. 处理Source0(可能会再次处理Blocks)
    6. 如果存在Source1,就跳转到第8步
    7. 通知Observers:开始休眠(等待消息唤醒)
    8. 通知Observers:结束休眠(被某个消息唤醒)
      1. 处理Timer
      2. 处理GCD Async To Main Queue
      3. 处理Source1
    9. 处理Blocks
    10. 根据前面的执行结果,决定如何操作
      1. 回到第02步
      2. 退出Loop
    11. 通知Observers:退出Loop
  4. Runloop和线程的关系?

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

    • 结构上来说:Timer里面有模式,模式里面有Timer
    • 流程上来说:Timer是在Runloop运行流程中工作的
  6. 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?

    • 将Timer添加到Runloop的CommonMode里
  7. runloop 是怎么响应用户操作的, 具体流程是什么样的?

    1. Source1来捕获系统事件
    2. Source1把事件包装成事件队列(EventQueue)
    3. Source0处理事件队列
  8. 说说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
    };
  9. runloop的mode作用是什么?

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

多线程

  1. 你理解的多线程?

  2. iOS的多线程方案有哪几种?你更倾向于哪一种?
    pthread NSThread GCD NSOperation
    更倾向于GCD;但是也要看场景

  3. GCD 的队列类型
    同步队列``并发队列

  4. 说一下 OperationQueue 和 GCD 的区别,以及各自的优势
    Operation Queue 是基于GCD封装的抽象类,目的是为了提高灵活度,以满足多线程操作频繁、灵活度要求高的复杂场景。

  5. 线程安全的处理手段有哪些?
    加锁GCD同步队列信号量

  6. OC你了解的锁有哪些?在你回答基础上进行二次提问;自旋和互斥对比?使用以上锁需要注意哪些?
    路由地址

  7. 以下代码是在主线程执行的,会不会产生死锁?

    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仍然想让代码块 立即 在主线程(主队列)中同步执行。

  8. 问下面代码的打印结果是什么?

    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]];
  9. 请问下面代码的打印结果是什么?

    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

内存管理

  1. 使用CADisplayLink、NSTimer有什么注意点?
    CADisplayLinkNSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。解决方案:1:使用Block处理。2.使用代理对象NSProxy

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

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

  4. ARC 都帮我们做了什么?
    答:LLVM+Runtime相互协作;ARC利用LLVM编译器,自动帮我们生成release,retain,autorelease代码。像弱引用会在运行时做操作。

  5. weak指针的实现原理
    答:将弱引用存到哈希表里面,对象将要销毁的时候,取出弱引用表,将指向当前对象的弱指针置为nil。

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

  7. 方法里有局部对象, 出了方法后会立即释放吗

  8. 一下两段代码能发生什么事?有什么区别?

    1
    2
    3
    4
    5
    6
    dispatch_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
    6
    dispatch_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,涉及不到对象的内存管理,而上面一段代码因为多线程操作一个对象,有可能会造成对象的过度释放,导致崩溃。