《iOS面试题 - 老生常谈》之提示答案

7,731 阅读36分钟

数据结构、算法和网络结构

这两个类别的面试题请参考算法网络

面向对象的基础题

  • 面向对象的几个设计原则?

    • SOLID原则(更新:据朋友提醒应为七个)
      • Single Responsibility Principle(单一原则)
      • Open Close Principle(开闭原则)
      • Liskov Substitution Principle(里氏替换原则)
      • Interface Segregation Principle(接口分离原则)
      • Dependency Inversion Principle(依赖倒置原则)
      • Law of Demeter(Leaset Knowledge Principle)迪米特法则(最少知道原则)
      • Composite Reuse Principle(合成复用原则)
  • Hash表的实现?

    • 通过把关键码值(key)映射到表中的一个位置来访问记录,Hash实现的关键是散列函数和冲突解决(链地址法和开放定址法)。
  • 什么是进程和线程?有什么区别?

    • 进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
    • 线程:操作系统能够进行运算调度的最小单位
    • 区别:线程被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
  • 内存的几大区域?各自的职能?

    • 栈区:由编译器自动分配和释放,一般存放函数的参数值,局部变量等。
    • 堆区:由程序员分配和释放,若不释放,则程序结束由操作系统回收。
    • 全局区(static):由编译器管理(分配和释放),程序结束由系统释放。全局变量和静态变量(相邻两块区,初始化和未初始化)。
    • 文字常量区:由编译器管理(分配释放),程序结束由系统释放;存放常量字符。
    • 程序代码区:存放函数的二进制代码。
  • 架构、框架和设计模式的区别?

    • 架构:一种顶层概括性的设计概念,像是蓝图,将不同的需求抽象为具体组件并使组件互相通信。
    • 框架:软件框架是提取特定领域软件的共性部分形成的体系结构,不同领域的软件项目有不同的框架类型。
    • 设计模式:一套被反复使用、多数人知晓、经过分类编目的总结,代码设计的一个成果,可以用在不同软件框架中一种实际问题的解决方案。(在软件开发中总结出的一种适合解决某种问题的方法)
  • MVC、MVVM和MVP架构的不同?

    • MVC
      • 【优点】简单易上手、没有多层调用
      • 【缺点】耦合性强;不利于单元测试,Controller担负了太多的事件处理,处理用户操作,管理View的声明周期,API接口调用,处理错误,处理回调,监听通知,处理视图方向等,容易造成臃肿,维护不方便
    • MVP
      • 【优点】利于单元测试、明确责任分配
      • 【缺点】要写一些额外的代码(如绑定),若业务逻辑复杂,也会造成Presenter的臃肿
    • MVVM
      • 【优点】责任划分明确;逻辑清晰;可测性高;双向绑定,配合第三方开源库使用方便
      • 【缺点】(1)数据绑定使得Bug很难被定位;(2)对于过大的项目,数据绑定需要花费更多的内存;依赖于开源库实现双向绑定,学习起来比较复杂;

iOS基础面试题

UI

  • UIView和CALayer的区别?
    • UIView 和 CALayer 都是 UI 操作的对象。两者都是 NSObject 的子类,发生在 UIView 上的操作本质上也发生在对应的 CALayer 上。
    • UIView 是 CALayer 用于交互的抽象。UIView 是 UIResponder 的子类( UIResponder 是 NSObject 的子类),提供了很多 CALayer 所没有的交互上的接口,主要负责处理用户触发的种种操作。
    • CALayer 在图像和动画渲染上性能更好。这是因为 UIView 有冗余的交互接口,而且相比 CALayer 还有层级之分。CALayer 在无需处理交互时进行渲染可以节省大量时间。
    • CALayer的动画要通过逻辑树、动画树和显示树来实现
  • loadView是干嘛用的?
    • loadView用来自定义view,只要实现了这个方法,其他通过xib或storyboard创建的view都不会被加载 。
  • layoutIfNeeded、layoutSubviews和setNeedsLayout的区别?
    • layoutIfNeeded:方法调用后,在主线程对当前视图及其所有子视图立即强制更新布局。
    • layoutSubviews:方法只能重写,我们不能主动调用,在屏幕旋转、滑动或触摸界面、子视图修改时被系统自动调用,用来调整自定义视图的布局。
    • setNeedsLayout:方法与layoutIfNeeded相似,不同的是方法被调用后不会立即强制更新布局,而是在下一个布局周期进行更新。
  • iOS的响应链?什么情况会影响响应链?
    • 事件UIResponder:继承该类的对象才能响应
    • 事件处理:touchesBegain、touchesMoved、touchesEnded、touchesCancelled;
    • 事件响应过程:
      1. UIApplication(及delegate)接收事件传递给 keyWindow
      2. keyWindow遍历subViews的hitTest:withEvent:方法和pointInside方法找到点击区域内的视图并处理事件
      3. UIView的子视图也会遍历其hitTest:withEvent:方法,以此类推,直到找到点击区域内最上层的视图,将视图逐步返回给UIApplication
      4. 最后找到的视图成为第一响应者,若无法响应,调用其nextResponder方法,一直找到响应链中能处理该事件的对象(据朋友提醒:UIControl子类不适用)。
      5. 最后到Application依然没有能处理该事件的对象的话,就废弃该事件;
      • 关键是hitTest:withEvent:方法和pointInside方法
      • ⚠️ 以下几种情况会忽略,hidden为YES,alpha小于等于0.01,userInteractionEnabled为NO,
    • UIControl的子类和UIGestureRecognizer优先级较高,会打断响应链;
  • 说几种给UIImageView添加圆角的方式?
    • cornerRadius(iOS9之后不会导致离屏渲染)
    • CoreGraphic绘制
    • UIBezierPath(本质同上)
  • iOS有哪些实现动画的方式?
    • UIView Animation:系统提供的基于CALayer Animation的封装,可以实现平移、缩放、旋转等功能。
    • CALayer Animation:底层CALayer配合CAAnimation的子类,可以实现更复杂的动画
    • UIViewPropertyAnimator:iOS10后引入的用于处理交互的动画,可以实现UIView Animation的所有效果。
  • 使用drawRect有什么影响?
    • 处理touch事件时会调用setNeedsDisplay进行强制重绘,带来额外的CPU和内存开销。

OC基础

  • iOS的内存管理机制?
    • 引用计数,从MRC(Manuel Reference Count)到ARC(Automatic Reference Count)。(最好能说明对象的生命周期)
  • @property后的相关修饰词有哪些?
    • 原子性:
      1. nonatomic:非原子性,编译器不会对其进行加锁(同步锁的开销较大)
      2. atomic:原子性(默认),编译器合成的方法会通过锁定机制确保原子性(非线程安全)
    • 读写权限:
      1. readwrite:可读写(默认),若不用@dynamic修饰编译器会自动为其生成setter和getter方法,否则编译器不生成由用户自己实现
      2. readonly:只读,若不用dynamic修饰仅生成getter方法,否则编译器不生成getter方法 有用户来实现
    • 内存管理语义:
      1. strong:修饰引用类型(表示持有当前实例),保留新值释放旧值,若修饰IMMutable不可变类型建议用copy
      2. copy:指修饰不可变集合类型、AttributeString、Block(ARC下strong和copy一样,把栈中的Block拷贝到堆中,copy表示不需要copy操作了)(设置方法不保留新值而是将其copy,不可变类型的可变类型子类)
      3. weak:修饰引用类型(非持有关系),不保留新值,不释放旧值(属性所指对象被摧毁时,属性值也会清空置nil)
      4. assign:修饰基本数据类型(也可以修饰引用类型,但可能会产生野指针),执行简单赋值操作
      5. unsafe_unretained:修饰引用类型(也可以修饰基本类型),所指对象被摧毁时,属性值不会被清空(unsafe)
    • 方法名:
      1. getter=指定方法名
      2. setter(不常用)
  • dynamic和synthesis的区别?
    • dynamic:告诉编译器不要帮我自动合成setter和getter方法,自己来实现,若没有实现,当方法被调用时会导致消息转发。
    • synthesis:指定实例变量的名字,子类重载父类属性也需要synthesis(重新set和get、使用dynamic,在Protocol定义属性、在category定义属性,默认不会自动合成)。
  • array为何用copy修饰?mutableArray为何用strong修饰?
    • array:若用strong修饰,在其被赋值可变的子类后,内容可能会在不知不觉中修改,用copy防止被修改。
    • mutableArray若用copy修饰会返回一个NSArray类型,若调用可变类型的添加、删除、修改方法时会因为找不到对应的方法而crash。
  • 深拷贝和浅拷贝(注意NSString类型)?
    • NSString:strong和copy修饰的属性是同等的,指向同一个内存地址,mutableCopy才是内存拷贝;
    • NSMutableString:strong和copy不同,strong指向同一个内存地址,copy则会进行内存拷贝,mutableCopy也会进行内存拷贝;
    • NSArray:对于字符类型和自定义对象结果是不同的,strong和copy都指向相同内存地址,mutableCopy也仅仅是指针拷贝;但是mutableCopy可以把Array变成MutableArray
    • PS:copy产生一个不可变类型,mutableCopy产生一个可变类型;对于字符类型mutableCopy会拷贝字符,copy对于NSArray是不拷贝的,对于NSMutableArray 是拷贝的;对于对象类型,仅仅是指针拷贝;(建议手动试试)
  • Block的几种类型?
    • _NSConcreteGlobalBlock:全局Block也是默认的block类型,一种优化操作,私有和公开,保持私有可防止外部循环引用,存储在数据区,当捕获外部变量时会被copy到堆上
    • _NSConcreteStackBlock:栈Block,存储在栈区,待其作用域结束,由系统自动回收
    • _NSConcreteMallocBlock:堆Block,计数器为0 的时候销毁
  • isEqual和“==”的区别?
    • ==:比较两个指针本身,而不是其所指向的对象。
    • isEqual:当且仅当指针值也就是内存地址相等;若重写该方法则要保证isEqual相等则hash相等,hash相等isEqual不一定相等;若指针值相等则相等,值不相等就判断对象所属的类,不属于同一个类则不相等,同属一个类时再判断每个属性是否相等。
  • id和NSObject的区别?
    • id:指向对象的指针,编译时不做类型检查,运行时向其发送消息才会对其检查。
    • NSObject:NSObject类及其子类,编译时做类型检查,向其发送的消息无法处理时就会执行消息转发。
    • id<NSObject>:编译器不对其做类型检查,对象所属的类默认实现名为NSObject的Protocol,既能响应方法又不对其做类型检查.
  • 通知、代理、KVO和Block的不同(结合应用场景回答)?
    • 通知: 适用于毫无关联的页面之间或者系统消息的传递,属于一对多的信息传递关系。例如接收系统音量、系统状态、键盘等,应用模式的设置和改变,都比较适合用通知去传递信息。
    • 代理: 一对一的信息传递方式,适用于相互关联的页面之间的信息传递,例如push和present出来的页面和原页面之间的信息传递。
    • block: 一对一的信息传递方式,效率会比代理要高(毕竟是直接取IMP指针的操作方式)。适用的场景和代理差不多,都是相互关联页面之间的页面传值。
    • KVO:属性监听,监听对象的某一属性值的变化状况,当需要监听对象属性改变的时候使用。例如在UIScrollView中,监听contentOffset,既可以用KVO,也可以用代理。但是其他一些情况,比如说UIWebView的加载进度,AVPlayer的播放进度,就只能用KVO来监听了,否则获取不到对应的属性值。
  • 什么是循环引用?__weak、__strong和__block的区别?
    • 循环引用:两个对象互相持有对方,一个释放需要另外一个先释放,delegate的weak声明就是为了防止循环引用。(一般指双向强引用,单向强引用不需要考虑,如:UIView动画Block)
    • __weak:Block会捕获在Block中访问Block作用域外的实例,这样会有内存泄漏的风险,用__weak修饰表示在Block不会导致该实例引用计数器加1,也可以在Block执行结束后强制将Block置nil,这样Block捕获的实例也会跟着释放,如果捕获的仅是基本数据类型,Block只会对其值进行拷贝一份,此时值再怎么变化也不会影响Block内部的操作。
    • __strong:在Block中使用__weak修饰的实例很容易被释放,所以需要加锁判断是否释放,未释放则对其进行强引用持有,保证向该实例发送消息的时候不会导致崩溃
    • __block:Block默认不允许修改外部变量的值,以int类型为例,若在Block中访问变量就把该变量进行Copy一份保存到Block函数内,然后变量在Block外部无论怎么改变都不会影响Block中使用的变量的值,若在Block改变外部变量的值,变量必须要用__block修饰,Block是把该变量的栈内存地址拷贝到堆中,所以可以直接把改变的新值写入内存。(把栈中变量的指针地址拷贝到堆中)
  • 内存泄漏、野指针和僵尸对象的区别?
    • 内存泄露:在堆中申请的不再使用的内存没有释放,程序结束释放(Analyzer,Leaks)
    • 野指针:指向的内存已经被释放,或被系统标记为可回收(用Malloc Scribble调试)。
    • 僵尸对象:已经被释放的对象,指针指向的内存块认为你无权访问或它无法执行该消息。(EXC_Bad_Access,开启NSZombieEnabled检测)
  • nil、Nil、NULL、NSNull的区别?
    • nil:空实例对象(给对象赋空值)
    • Nil:空类对象(Class class = Nil)
    • NULL:指向C类型的空指针
    • NSNull:类,用于空对象的占位符(用于替代集合中的空对象,还有判断对象是否为空对象)
  • static和const的区别?
    • const:声明全局只读变量,(若前面没有static修饰,在另外一个文件中声明同名的常量会报错)
    • static:修饰变量的作用域(本文件内),被修饰的变量只会分配一份内存,在上一次修改的基础上进行修改。
    • 一般两者配合使用,如:static const NSTimeInterval kAnimationDuration = 1.0;不会创建外部符合,编译时预处理指令会把变量替换成常值。
  • iOS中有哪些设计模式?
    • 【单例】保证应用程序的生命周期内仅有一个该类的实力对象,易于外界访问。如:UIApplication、NSBundle、NSNotificationCenter、NSFileManager、NSUserDefault、NSURLCache等;
    • 【观察者】定义了一种一对多的依赖关系,可以让多个观察者同时监听一个主题对象,当主题对象状态或值发生改变,会通知所有的观察者;KVO当对象属性变化时,通知观察此属性的对象。案例代表:通知和KVO
    • 【类簇】(隐藏抽象基类背后的实现细节)如:UIButton、NSNumber、NSData、NSArray、NSDictionary、NSSting。用isMemberOfClass和isKindOfClass来判断。
    • 【命令模式】(运行时可以调用任意类的方法),代表:NSInvocation,封装一个请求或行为作为对象,包含选择器、方法名等。
    • 【委托模式】“我想知道列表中被选中的内容在第几行”?可以,接受我的委托就可以知道;只是接受我的委托就要帮我完成这几件事情,有必须要完成的,有不必要完成的,至于你怎么完成我就不关心了。
    • 【装饰器模式】:装饰器模式在不修改原来代码的情况下动态的给对象增加新的行为和职责,它通过一个对象包装被装饰对象的方法来修改类的行为,这种方法可以做为子类化的一种替代方法。 案例代表:Category和Delegation
  • 静态库和动态库的区别?
    • 静态库:.a(.h配合)和.framework(资源文件.bundle)文件,编译好的二进制代码,使用时link,编译时会拷贝一份到target程序中,容易增加target体积。
    • 动态库:.tbd和.dylib,程序运行时动态加载到内存,可共享使用,动态载入有性能损失且有依赖性(系统直接提供给的framework都是动态库!)
  • iOS中内省的几个方法?
    • 内省是指对象在运行时将其自身细节泄露为对象的能力。 这些细节包括对象在继承树中的位置,它是否符合特定的协议,以及它是否响应某个特定的消息。以及它是否响应某一消息。
    1. class和superclass方法
    2. isKindOfClass:和isMemberOfClass:
    3. respondsToSelector:
    4. conformsToProtocol:
    5. isEqual:

OC进阶

  • Foundation和CoreFoundation的转换?

    • __bridge:负责传递指针,在OC和CF之间转换,不转移管理权,若把OC桥接给CF,则OC释放后CF也无法使用。
    • __bridge_retained:将OC换成CF对象,并转移对象所有权,同时剥夺ARC的管理权,之后需要使用CFRelease释放。
    • __bridge_transfer:将CF转换成OC,并转移对象所有权,由ARC接管,所以不需要使用CFRelease释放。
  • array和set的区别?查找速度和遍历速度谁更快?

    • array:分配的是一片连续的存储单元,是有序的,查找时需要遍历整个数组查找,查找速度不如Hash。
    • set:不是连续的存储单元,且数据是无序的,通过Hash映射存储的位置,直接对数据hash即可判断对应的位置是否存在,查找速度较快。
    • 遍历速度:array的数据结构是一片连续的内存单元,读取速度较快,set是不连续的非线性的,读取速度较慢;
  • 什么是内联函数?为什么需要它?

    • 用inline修饰的函数,不一定是内联函数,而内联函数一定是inline修饰的。
    • 普通函数:编译会生成call指令(入栈、出栈),调用时将call指令地址入栈,并将子程序的起始地址送入,执行完毕后再返回原来函数执行的地址,所以会有一定的时间开销。
    • #define宏定义和inline修饰的函数代码被放入符号表中,使用时直接替换,没有调用的开销;#define宏定义的函数:使用时从符号表替换,有格式要求,不会对参数作有效性检查且返回值不能强转;inline修饰的内联函数:不需要预编译,使用时直接替换且作类型检查,是一段直接在函数中展开的代码(宏是直接文本替换)
    • PS:inline定义的内联函数代码被放入符号表中,在使用时直接替换(把代码嵌入调用代码),不像普通函数需要在函数表中查找,省去压栈和出栈,没有了调用的开销,提高效率;
    • PS:inline修饰的内联函数只是向编译器申请,最后不一定按照内联函数的方式执行,可能申请会失败,因为函数内不允许使用循环或开关语句,太大会被编译器还原成普通函数;inline关键词一定要与函数定义一起使用,声明的话不算内联函数;
  • 图片显示的过程?

    1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
    2. 然后将生成的 UIImage 赋值给 UIImageView ;
    3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
    4. 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
      • 分配内存缓冲区用于管理文件 IO 和解压缩操作;
      • 将文件数据从磁盘读到内存中;
      • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
      • 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。(必须要了解位图相关的知识点)
  • dispatch_once如何只保证只执行一次?

    • 多线程中,若有一个线程在访问其初始化操作,另外一个线程进来后会延迟空待,有内存屏障来保证程序指令的顺序执行
    void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){
        volatile long *vval = val;
        if (dispatch_atomic_cmpxchg(val, 0l, 1l)) {
            func(ctxt); // block真正执行
            dispatch_atomic_barrier();
            *val = ~0l;
        } 
        else 
        {
            do
            {
                 _dispatch_hardware_pause();
            } while (*vval != ~0l);
            dispatch_atomic_barrier();
        }
    }
    
  • NSThread、NSRunLoop和NSAutoreleasePool三者之间的关系?

    • 根据官方文档中对 NSRunLoop 的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建。
    • 根据官方文档中对NSAutoreleasePool的描述,我们可知,在主线程的 NSRunLoop 对象(在系统级别的其他线程中应该也是如此,比如通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 获取到的线程)的每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。
    • 需要手动添加autoreleasepool的情况:
      1. 如果你编写的循环中创建了大量的临时对象;
      2. 如果你创建了一个辅助线程
  • 分类可扩展的区别?(可从内存布局、加载顺序、分类方法和原类方法的执行顺序来回答)

    • extension:必须在.m中添加,编译时作为类的一部分参与内存布局,生命周期与类一样,添加私有实现,隐藏类的私有信息。
    • category:独立的.h和.m文件,独立编译动态加载dyld,为已有类扩展功能。
    • 分类局限:无法添加实例变量,可以添加属性但不会为其生成实例变量(在运行时,对象的内存布局已确定,若添加实例变量就会破坏类的内存布局,这对编译型语言来说是灾难性的)
    • 分类的加载:把实例方法、属性和协议添加到类上,若有协议和类方法则添加到元类, 运行时分类的方法列表被动态添加到类的方法列表中,且在类的原有方法前面;
    • 方法顺序:【load方法】先调用父类的load方法再调用分类的,【重写的方法】由于分类方法在原类方法的前面,所以优先调用分类中的方法。
  • OC对象释放的流程?runloop的循环周期会检查引用计数,释放流程:release->dealloc->dispose

    • release:引用计数器减一,直到为0时开始释放
    • dealloc:对象销毁的入口
    • dispose:销毁对象和释放内存
      • objc_destructInstance:调用C++的清理方法和移除关联引用
        • clearDeallocating:把weak置nil,销毁当前对象的表结构,通过以下两个方法执行(二选一)
          • sidetable_clearDeallocating:清理有指针isa的对象
          • clearDeallocating_slow:清理非指针isa的对象
      • free:释放内存
  • CDDisplayLink和NSTimer的区别?

    • CADisplayLink:以和屏幕刷新率相同的频率将内容画到屏幕上的定时器,需手动添加runloop
    • NSTimer:可自动添加到当前线程的runloop,默认defualt模式,也可以选择添加的loopMode;
  • 用runtime实现方法交换有什么风险?

    • 风险1:若不在load中交换是非原子性的,在initial方法中不安全
    • 风险2:重写父类方法时,大部分都是需要调用super方法的,swizzling交换方法,若不调用,可能会出现一些问题,如:命名冲突、改变参数、调用顺序、难以预测、难以调试。

runtime源码相关

  • 知道AutoreleasePoolPage吗?它是怎么工作的?

    • AutoreleasePool由AuthReleasePoolPage实现,对应AutoreleasePoolPage 的具体实现就是往AutoreleasePoolPage中的next位置插入一个POOL_SENTINEL,并且返回插入的POOL_SENTINEL的内存地址。这个地址也就是我们前面提到的 pool token,在执行pop操作的时候作为函数的入参。
    • 通过调用 autoreleaseFast函数来执行具体的插入操作;autoreleaseFast函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:
      1. 当前 page存在且没有满时,直接将对象添加到当前page中,即next指向的位置;
      2. 当前page存在且已满时,创建一个新的page,并将对象添加到新创建的 page中,然后关联child page。
      3. 当前page不存在时,即还没有page时,创建第一个page,并将对象添加到新创建的page中。
  • KVO的底层实现?(看过RAC源码的应该知道,RAC监听方法也是基于此原理,只是稍微有些不同)

    1. 当一个object有观察者时,动态创建这个object的类的子类
    2. 对每个被观察的property,重写其setter方法
    3. 在重写的setter方法中调用willChangeValueForKey:和didChangeValueForKey通知观察者
    4. 当一个property没有观察者时,重写方法不会被删除,直到移除所有的观察者才会删除且删除动态创建的子类。
    // 1)监听前的准备
    [human addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { /* 监听后的处理 */ }
    // 2)关闭系统观察者的自动通知
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            return NO;
        }
        return [super automaticallyNotifiesObserversForKey:key];
    }
    // 3)调用这两个方法
    [self willChangeValueForKey:@"name"];
    /* 在此对值进行修改 */
    [self didChangeValueForKey:@"name”];
    
  • 被weak修饰的对象是如何被置nil的?知道SideTable吗?

    • Runtime对注册的类会进行内存布局,有个SideTable结构体是负责管理类的引用计数表和weak表,weak修饰的对象地址作为key存放到全局的weak引用表中,value是所有指向这个weak指针的地址集合,调用release会导致引用计数器减一,当引用计数器为0时调用dealloc,在执行dealloc时将所有指向该对象的weak指针的值设为nil,避免悬空指针。
  • 什么是关联对象?可以用来干嘛?系统如何管理管理对象?支持KVO吗?

    • 关联对象:在运行时动态为指定对象关联一个有生命周期的变量(通过objc_setAssociatedObject和objc_getAssociatedObject),用于实现category中属性保存数据的能力和传值。(支持KVO,让关联的对象作为观察者)
    • 管理关联对象:系统通过管理一个全局哈希表,通过对象指针地址和传递的固定参数地址来获取关联对象。根据setter传入的参数策略,来管理对象的生命周期。通过一个全局的hash表管理对象的关联,通过对象指针地址获取对象关联表,再根据自定义key查找对应的值(外表:key(对象指针)-value(hash表),内表:key(自定义name)-value(管理的值))
  • isa、对象、类对象、元类和父类之间的关系?

    • 类:对象是类的一个实例,类也是另一个类的实例,这个类就是元类 (metaclass)。元类保存了类的类方法。当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,一直找到继承链的根部,找不到就转发。
    • 元类:元类 (metaclass) 也是一个实例,那么元类的 isa 指针又指向哪里呢?为了设计上的完整,所有的元类的 isa 指针都会指向一个根元类 (root metaclass)。根元类 (root metaclass) 本身的 isa 指针指向自己,这样就行成了一个闭环。上面提到,一个对象能够接收的消息列表是保存在它所对应的类中的。在实际编程中,我们几乎不会遇到向元类发消息的情况,那它的 isa 指针在实际上很少用到。不过这么设计保证了面向对象的干净,即所有事物都是对象,都有 isa 指针。
    • 继承:我们再来看看继承关系,由于类方法的定义是保存在元类 (metaclass) 中,而方法调用的规则是,如果该类没有一个方法的实现,则向它的父类继续查找。所以,为了保证父类的类方法可以在子类中可以被调用,所以子类的元类会继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。

  • 知道创建类的方法objc_allocateClassPair?方法里面具体做了什么事情?

    // objc-class-old.mm
    Class objc_allocateClassPair(Class supercls, const char *name, 
                             size_t extraBytes)
    {
        Class cls, meta;
        if (objc_getClass(name)) return nil;
        // fixme reserve class name against simultaneous allocation
        if (supercls  &&  (supercls->info & CLS_CONSTRUCTING)) {
            // Can't make subclass of an in-construction class 
            return nil;
        }
        // Allocate new classes. 
        if (supercls) { //  若父类存在,父类的内存空间+额外的空间 = 新类的内存大小
            cls = _calloc_class(supercls->ISA()->alignedInstanceSize() + extraBytes);
            meta = _calloc_class(supercls->ISA()->ISA()->alignedInstanceSize() + extraBytes);
        } else {        //  若父类不存在,基类的内存空间+额外的空间 = 新类的内存大小(objc_class是objc_object的子类)
            cls = _calloc_class(sizeof(objc_class) + extraBytes);
            meta = _calloc_class(sizeof(objc_class) + extraBytes);
        }
        // 初始化
        objc_initializeClassPair(supercls, name, cls, meta);
        return cls;
    }
    
    // objc-runtime-new.mm
    Class objc_allocateClassPair(Class superclass, const char *name, 
                             size_t extraBytes)
    {
        Class cls, meta;
        rwlock_writer_t lock(runtimeLock);
        // Fail if the class name is in use.
        // Fail if the superclass isn't kosher.
        if (getClass(name)  ||  !verifySuperclass(superclass, true/*rootOK*/)) {
            return nil;
        }
        // Allocate new classes.
        cls  = alloc_class_for_subclass(superclass, extraBytes);
        meta = alloc_class_for_subclass(superclass, extraBytes);
        // fixme mangle the name if it looks swift-y?
        objc_initializeClassPair_internal(superclass, name, cls, meta);
        return cls;
    }
  • class_ro_t 和 class_rw_t 的区别?
    • class_ro_t是readonly,class_rw_t是readwrite;
    • 在编译之后,class_ro_t的baseMethodList就已经确定。当镜像加载的时候,methodizeClass方法会将baseMethodList添加到class_rw_t的methods列表中,之后会遍历category_list,并将category的方法也添加到methods列表中。这里的category指的是分类,基于此,category能扩充一个类的方法。这是开发时经常需要使用到。
    • class_ro_t在内存中是不可变的。在运行期间,动态给类添加方法,实质上是更新class_rw_t的methods列表。
  • 除了objc_msgSend,还知不知道别的消息发送函数?
    • objc_msgSend_stret:当CPU的寄存器能够容纳下消息返回类型时,该函数才能处理此消息,若超过CPU寄存器则由另一个函数执行派发,原函数会通过分配在栈上的变量来处理消息所返回的结构体。
    • objc_msgSendSuper_stret:向父类实例发送一个返回结构体的消息
    • objc_msgSendSuper:向超类发送消息([super msg])
    • objc_msgSend_fpret:消息返回浮点数时,在某些结构的CPU中调用函数时需要对“浮点数寄存器”做特殊处理,不同的架构下浮点数表示范围不一样(寄存器是CPU的一部分,用于存储指令、数据和地址)
  • 什么是方法交换?怎么用的?
    • SEL方法地址和IMP函数指针是通过DispatchTable表来映射,可以通过runtime动态修改SEL,所以可以实现方法的交换(使用经验根据项目来谈即可)。

数据持久化

  • plist:XML文件,读写都是整个覆盖,需读取整个文件,适用于较少的数据存储,一般用于存储App设置相关信息。
  • NSUserDefault:通过UserDefault对plist文件进行读写操作,作用和应用场景同上。
  • NSKeyedArchiver:被序列化的对象必须支持NSCoding协议,可以指定任意数据存储位置和文件名。整个文件复写,读写大数据性能低。
  • CoreData:是官方推出的大规模数据持久化的方案,它的基本逻辑类似于 SQL 数据库,每个表为 Entity,然后我们可以添加、读取、修改、删除对象实例。它可以像 SQL 一样提供模糊搜索、过滤搜索、表关联等各种复杂操作。尽管功能强大,它的缺点是学习曲线高,操作复杂。
  • SQLite(FMDB、Realm)

多线程

  • 串行队列和并发队列的区别?同步和异步的区别?

    • 串行队列(Serial Queue):指队列中同一时间只能执行一个任务,当前任务执行完后才能执行下一个任务,在串行队列中只有一个线程。
    • 并发队列(Concurrent Queue):允许多个任务在同一个时间同时进行,在并发队列中有多个线程。串行队列的任务一定是按开始的顺序结束,而并发队列的任务并不一定会按照开始的顺序而结束。
    • 同步(Sync):会把当前的任务加入到队列中,除非等到任务执行完成,线程才会返回继续运行,也就是说同步会阻塞线程。
    • 异步(Async):也会把当前的任务加入到队列中,但它会立刻返回,无需等任务执行完成,也就是说异步不会阻塞线程。
    • PS:无论是串行还是并发队列都可以执行执行同步或异步操作。注意在串行队列上执行同步操作容易造成死锁,在并发队列上则不用担心。异步操作无论是在串行队列还是并发队列上都可能出现线程安全的问题。
  • GCD和NSOperation的区别?

    • NSOperation VS GCD
    1. 语法:面向对象(重量级) - 面向C(轻量级)
    2. 相比GCD的优点:
      1. 取消某个操作(未启动的任务)、
      2. 指定操作间的依赖关系(根据需求指定依赖关系)
      3. 通过键值观察机制监控NSOperation对象的属性(KVO)
      4. 指定操作的优先级
      5. 重用NSOperation对象
      6. 提供可选的完成block

线程安全

  • 如何保证线程安全?

    • 原子操作(Atomic Operation):是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何的上下文切换(context switch)。
    • 内存屏障(Memory Barrier):确保内存操作以正确的顺序发生,不阻塞线程(锁的底层都会使用内存屏障,会减少编译器执行优化的次数,谨慎使用)。
      • 互斥锁(pthread_mutex):原理与信号量类似,但其并非使用忙等,而是阻塞线程和休眠,需切换上下文。
      • 递归锁(NSRecursiveLock):本质是封装了互斥锁的PTHREAD_MUTEX_RECURSIVE类型的锁,允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。
      • 自旋锁(OSSpinLock):通过全局变量,来判断当前锁是否可用,不可用就忙等。
      • @synchronized(self):系统提供的面向OC的API,通过把对象hash当做锁来用。
      • NSLock:本质是封装了互斥锁的PTHREAD_MUTEX_ERRORCHECK类型的锁,它会损失一定性能换来错误提示,因为面向对象的设计,其性能稍慢。
      • 条件变量(NSConditionLock):底层通过(condition variable)pthread_cond_t来实现的,类似信号量具有阻塞线程与信号机制,当某个等待的数据就绪后唤醒线程,比如常见的生产者-消费者模式。
      • 信号量(dispatch_semaphore)
        // 传入值需>=0,若传0则阻塞线程并等待timeout
        dispatch_semaphore_create(1):
        // lock 资源已被锁,会使得signal值-1
        dispatch_semaphore_wait(signal, overTime):
        // unlock,会使得signal值+1
        dispatch_semaphore_signal(signal):
        
  • 什么是死锁?如何避免死锁?

    • 死锁:两个或两个以上的线程,互相等待彼此停止以获得某种资源,但是没有一方会提前退出的情况。
    • 避免在串行队列中执行同步任务;避免Operation相互依赖;
  • 什么是优先倒置?

    • 低优先级任务会先于高优先级任务执行,假设:任务优先级A>B>C,A等待访问被C正在使用的临界资源,同时B也要访问该资源,B的优先级高于C,同时,C优先级任务被B次高优先级的任务所抢先,从而无法及时地释放该临界资源。这种情况下,B次高优先级任务获得执行权,而高优先级任务A只能被阻塞。 可以设置相同的任务优先级避免优先倒置。

项目经验相关题

  • 什么时候重构?怎么重构的?(包括但不限于以下几点,仅供参考)
    • 当前架构支撑不了业务、当前设计模式弊端太多、项目急着上线没有来得及优化等等。
    1. 优化业务逻辑;删除没用的代码(包括第三方库、变量、方法等,注释除外);
    2. 保持类和方法的单一性原则,把不属于本类功能的部门移植到一个新类中
    3. 根据实际使用的需求、优化基类中的属性(像BaseObject等)
    4. 修改一点就测试一点,保证每一步的重构不影响原有功能
    5. 规范(命名、语法等)
    6. 编译优化
    • PS: 重构是一个长期的过程,每一次紧迫迭代的新功能都可能需要优化,特别是在多人开发的时候,如果来不及在项目发布前Code Review,在重构时Code Review就显得很有必要。
  • AppDelegate如何瘦身?
    • 主要是在保证功能和效率以及性能的前提下,把第三方的初始化代码拆解到别的文件中处理
    1. category
      • 优点:简单、直接,不需要任何第三方来协助;
      • 缺点:添加属性不太方便,需要借助关联对象来实现;
    2. FRDModuleManager
      • 优点:简单、易维护,耦合性低;
      • 缺点:模块增多需分配不少内存,对象都是长期持有的(若有依赖关系,优先级不明确)
  • 如何解决卡顿?
    • 卡顿主要是因为主线程执行了一些耗时操作导致
    • 耗时计算:尽可能放到子线程中执行
    • 图片解压缩:在子线程强制解压缩
    • 离屏渲染:避免导致离屏渲染(注意绘制方法)
    • 优化业务流程:减少中间层,业务逻辑
    • 合理分配线程:UI操作和数据源放到主线程,保证主线程尽可能处理少的非UI操作,同时控制App子线程数量。
    • 预加载和延时加载:平衡CPU和GPU使用率;优先加载可视内容,提升界面绘制速度。
  • 如何排查Crash?
    • 断点:一般的Crash
    • zombie object:僵尸对象
    • dSYM:iOS编译后保存16进制函数地址映射信息的文件 (根据Crash的内存地址,定位对应的Crash代码的范围)
  • 如何检测内存泄漏?有没有遇到内存警告?怎么解决的?
    • dealloc方法:手动检测的笨方法
    • 第三方库PLeakSniffer和MLeaksFinder:自动检测
  • 有何优化App启动速度?(main前和main后)
    • 设置环境变量:DYLD_PRINT_STATISTICS或DYLD_PRINT_STATISTICS_DETAILS为1(更详细),Xcode-> Edit Scheme-> Run-> Arguments,添加以上二选一 环境变量
        main之前:
            Total pre-main time:  94.33 milliseconds (100.0%) // main函数前总共需要94.33ms
            dylib loading time:  61.87 milliseconds (65.5%)    // 动态库加载
            rebase/binding time:   3.09 milliseconds (3.2%)    // 指针重定位
            ObjC setup time:  10.78 milliseconds (11.4%)    // Objc类初始化
            initializer time:  18.50 milliseconds (19.6%)    // 各种初始化
            slowest intializers :    // 在各种初始化中,最耗时的如下:
                libSystem.B.dylib :   3.59 milliseconds (3.8%)
                libBacktraceRecording.dylib :   3.65 milliseconds (3.8%)
                GTFreeWifi :   7.09 milliseconds (7.5%)
    
    • main()函数之前耗时的影响因素

      • 动态库加载越多,启动越慢。
      • ObjC类越多,启动越慢
      • C的constructor函数越多,启动越慢
      • C++静态对象越多,启动越慢
      • ObjC的+load越多,启动越慢
    • main()函数之后耗时的影响因素

      • 执行applicationWillFinishLaunching的耗时
      • rootViewController及其childViewController的加载、view及其subviews的加载
    • 具体优化内容:

      1. 移除不需要用到的动态库
      2. 移除不需要用到的类
      3. 合并功能类似的类和扩展(Category)
      4. 压缩资源图片
      5. 优化applicationWillFinishLaunching
      6. 优化rootViewController加载
      7. 挖掘最后一点性能优化
    • PS:应该在400ms内完成main()函数之前的加载,整体过程耗时不能超过20秒,否则系统会kill掉进程,App启动失败

开源库

这部分主要跟简历中提到的相关库有关,建议对简历中提到的开源库,一定要有所准备。

SDWebImage

SDWebImage几乎是每个iOS开发者都用过的开源库,也是在简历中曝光度比较高的开源库之一,同时也几乎是面试都会问到的,所以要准备充分再去。

  • 从调用到显示的过程?
    1. 根据请求的文件URL路径判断当前是否存在还没结束的操作,若有的话就取消并移除与URL映射的操作。
    2. 设置placeholder占位图,不管URL是否为空都会设置,且在当前队列同步执行。
    3. 判断URL是否为空,为空则执行completedBlock回调,并结束。
    4. URL非空,若是首次则开始相关类的初始化,如:加载进度及其相关的Block、SDWebImageManager(SDWebImageCache管理缓存、SDWebImageDownloader管理),若非首次还是使用最初初始化的实例,因为SDWebImageManager是以单例的形式存在。
    5. 开始进入SDWebImageManager的范围,URL容错处理、创建负责加载图片和取消加载图片的类,判断当前的URL是否在黑名单里,若存在则执行回调,返回当前的操作类,不再处理下载失败过的URL,并结束。
    6. 若URL不在黑名单里,则开始在内存缓存中查找,找到后执行doneBlock回调,在该回调方法中执行判断当前操作是否已取消,未取消则回调并异步设置图片,在回调的Block中,每次都会检查当前操作是否已取消,若取消则不处理,并结束。
    7. 内存缓存没找到,则在磁盘缓存中查找,一般是在串行队列异步执行,根据URL路径找到存储的数据并取出,然后缩放解压缩返回,执行回调设置图片,并结束。
    8. 磁盘没找到则下载图片,下载完成后缓存,设置图片;SDWebImage对图片作了哪些优化:子线程强制解压缩,从硬盘获取的图片和下载的图片都进行解压缩操作,提高渲染效率,节省主线程的工作量。

ReactiveCocoa

该库比较复杂,可问的问题也非常多,以下仅供参考,建议自己找答案(自己理解后才能从容面对)

  • 冷热信号的区别?
  • RAC如何监听方法?
  • bind方法做了什么?
  • RAC中的RACObserver和KVO有什么区别?
  • RAC的map和flattenMap的区别?

工具

难免会遇到一些有关常用工具的问题,提供几个仅供参考

  • Git、SVN?
    • 问题可深可浅,浅:基本用法或相关命令,深:Git的工作原理
  • CocoaPods
    • pod update和pod install的区别
  • CI(持续集成、持续部署)
    • 后期更新