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