这次面试主要针对基础,暴露了一部分更底层的问题,题目就只写我回答不好的。

笔试题目

  1. assign 和 weak 的区别,延伸出来就是为什么 weak 可以自动置为 nil ,为什么 assign 不会?主要是 assign 为什么不会?
  2. 程序的存储区域有哪些,堆和栈的区别,什么存储在堆上,什么存储在栈上?
  3. 为什么 c 数组类型不可变,OC 数组可变,有什么区别?
  4. property 的本质是什么,@sychronize 和 @dynamic 又会做什么?
  5. load 方法 和 initialize 方法的调用时机,顺序,注意点分别是什么?
  6. 通知 跟 delegate 的区别,什么时候用 通知,什么时候用 delegate ,通知是同步的还是异步的?底层实现是什么?
  7. 页面之间传值的方式有哪些,block 和 delegate 的区别是什么,为什么 delegate 可以找到准确的方法传值?
  8. 快排的思想,和优化,时间和空间复杂度是多少,递归的时间复杂度和空间复杂度是多少?
  9. NSTimer 的使用注意点是什么,实际底层原理是什么?延时的多种实现方式和区别是什么?
  10. 多线程安全怎么保证,有哪些锁,各有什么特点,OC 属性的 atomic 线程安全吗,底层实现是什么?
  11. 进程和线程的理解。
  12. 组件化怎么实现模块间通信和解耦?
  13. 离屏渲染,什么时候会离屏渲染?什么是离屏渲染,主线程的渲染流程是什么?怎么优化离屏渲染?
  14. 多线程之间共享内存吗?

答案

  1. assign 可以修饰基本数据类型跟对象类型,但是 weak 只能修饰对象类型,一般来说用 assign 修饰基本数据类型,weak 指针指向的对象,被销毁时,weak 指针会被置为 nil,因为 Runtime 维护了一个 hash 表,key 是指向对象的地址,Value 是所有指向这个地址的 weak 指针的数组,当我们初始化一个 weak 指针的时候,在这个表里会记录下来,释放时,会根据释放对象的地址,找到对应的 weak 指针数组,遍历这个数组将其置为 nil,同时从表里删除。
    而 assign 对应的所有权修饰符是 __unsafe_unretain,不会对对象进行 retain 操作,假如这个对象销毁了,有可能会发生野指针错误。对象中存储所有的属性和 isa 指针,假如属性是 assign 修饰的基本数据类型,值被拷贝到了对象中,也就是堆中,假如属性是 assign 修饰的对象类型,拷贝到对象中的是指针,指向堆中的地址,这个地址有可能被释放掉。
  2. 代码区:编译时确定,存储着应用程序的代码。
    数据区:存放全局变量,静态变量,常量。
    BSS 段:存储未初始化的全局变量,静态变量。
    栈区:局部变量、基本数据类型,都存储在栈上是由系统进行内存管理的,函数执行完毕以后,栈会自动的释放该变量,栈比较小,当执行某个方法的时候,栈会为方法需要的内存开辟空间,结束后尾指针指向栈顶从而释放空间。每个线程都有自己的栈空间。
    堆区:只有堆上的对象类型才会有内存管理,需要手动去进行内存管理,堆是链表进行管理的,系统有一个存储空闲内存地址的链表,分配空间时,去查找这个链表,找到第一个满足的节点。多线程会共享堆空间。
  3. 因为 OC 数组中存储的是指针,指针指向的对象是可以类型不同的。
  4. property 是在编译期间,由 Xcode 帮助生成 _成员变量 getter 和 setter 方法,默认 @sychronize var = _var,@sychronize 表示由系统生成 getter 和 setter 方法,同时可以自定义 property 生成的成员变量名称,如 @sychronize var; 就会生成叫 var 的成员变量。@dynamic 表示不生成 getter 和 setter 方法,由我们自己生成。
  5. load 方法,当类被引入到项目的时候就会调用,main 函数执行之前,每个类的 load 方法只会调用一次,执行顺序是 superClass -> subClass -> category,load 函数是系统自动调用的,不需要调用父类。注意不要在其中使用其他类,因为不保证其他类已经执行了 load 方法。一般在里面实现 方法交换。
    initialize 方法,只有在给这个类发送消息的时候,才调用一次,当子类没有实现方法时,会调用两次父类的方法,子类会覆盖掉父类的实现,分类也会覆盖掉主类的实现。
  6. 通知是同步的,即发送通知后,会执行注册的方法,执行完才会继续向下执行。
  7. 从使用场景区分,当有多个回调方法的时候,写一个 delegate 就可以,而需要写多个 block,比如 self.delegate 可以执行多个方法,但是 self.block 只能执行一个代码块。
    从效率上看,block 消耗更多,delegate 消耗比较少。
  8. 快速排序时间复杂度是 O(nlogn) 空间复杂度是 O(logn),即递归的深度。递归的空间复杂度是 O(n)
  9. 循环引用,滑动时会被停止,使用 timerWith 方法创建的 NSTimer ,必须手动加到线程的 runLoop 中才会执行,或者执行点的时候,runLoop 还在运行其他的,会跳过执行点。CADisplayLink ,屏幕刷新的时候调用,依托于屏幕刷新频率,假如跳帧,会受到影响。GCD 定时器,不受 runloop mode 切换的影响,但是也有可能会被阻塞。
    延时操作有三种,NSTimer ,performSelector:afterDelay: ,GCD 的 dispatch_after,前两种都是创建一个 timer 到 runloop 中,主线程可以,但是子线程默认没有 runloop 的,需要手动开启一个 runloop ,GCD 的 dispatch_after 可以不受 runloop 的影响,将执行代码一段时间后加入到队列中。
  10. @sychronized 互斥锁,对象级别,atomic 就是用这个实现的对 getter 和 setter 加锁,但是也不能保证线程安全,可能会绕过这两个方法对属性进行改变。
    自旋锁,一个死循环,循环等待,效率最高,但是有线程安全问题。
    信号量,通过信号机制进行加锁,等待的线程会睡眠。
    互斥锁,加锁后,其他线程睡眠,直到解锁。
  11. 进程可以认为是一个 程序。
    线程是进程中执行运算的最小单位,每个进程都有一个主线程,每个线程肯定只属于一个进程。每个进程都是串行执行任务的,为了并发执行,要开启多个线程进行操作。
  12. 通过中介者
  13. 每个线程都有自己的栈,管理自己的状态和局部自动变量,共享堆区。