以100道面试题构建自己的iOS开发体系 2

190 阅读27分钟

九、Category

31、Category相关
1、Category的使用场合是什么?
- 将一个类拆成很多模块(其实就是解耦,将相关的功能放到一起)


2、说说Category的实现原理
- 通过runtime动态将分类的方法合并到类对象、元类对象中
- Category编译之后的底层结构是 struct_category_t , 里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将 Category 的数据,合并到类信息中(类对象、元类对象)


3、category和extension区别
- Extension在编译时,就把信息合并到类信息中
- Category是在运行时,才会将分类信息合并到类信息中
- 分类声明的属性,只会生成getter/setter方法声明,不会自动生成成员变量和getter/setter方法实现,而扩展会
- 分类不可用为类添加实例变量,而扩展可以

分类的局限性:
- 无法为类添加实例变量,但可通过关联对象进行实现
- 分类的方法如果和类重名,会覆盖原来方法的实现
- 多个分类的方法重名,会调用最后编译的那个分类的实现


4、为什么category不能添加属性?使用Runtime就可以了?
- 分类没有自己的isa指针
- 类最开始生成了很多基本属性,比如IvarList,MethodList
- 分类只会将自己的method attach到主类,并不会影响到主类的IvarList
- 实例变量没有setter和getter方法。也没有自己的isa指针

- 关联对象都由AssociationsManager管理
- AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。
- 相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址
- 而这个map的value又是另外一个AssAssociationsHashMap,里面保存了关联对象的kv对


5、Category中有load方法么?load方法什么时候调用的?load方法能继承么?
- 有
- +load方法会在runtime加载类、分类时调用;
- 每个类、分类的+load,在程序运行过程中只调用一次

- 调用顺序
- 先调用类的+load,(按照编译先后顺序,先编译,先调用),调用子类的+load之前会调用父类的+load
- 再调用分类的+load按照编译先后顺序调用(先编译,先调用)


6、test方法和load方法的本质区别?(+load方法为什么不会被覆盖)
- test方法是通过消息机制调用 objc_msgSend([MJPerson class], @selector(test))
- +load方法调用,直接找到内存中的地址,进行方法调用


7、load调用顺序
- +load方法会在runtime加载类、分类时调用
- 每个类、分类的+load,在程序运行过程中只调用一次

调用顺序
- 先调用类的+load方法,之后按照编译先后顺序调用(先编译,先调用,调用子类的+load之前会先调用父类的+load)
- 再调用分类的+load,之后按照编译先后顺序调用(先编译,先调用)


8、不同Category中存在同一个方法,会执行哪个方法?如果是两个都执行,执行顺序是什么样的?
- 根据Build Phases->Compile Sources中添加的文件顺序,后面的会覆盖前面的


9、load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承时他们之间的调用过程?
区别:
调用方式不同
- load是根据函数地址直接调用
- initialize是荣光objc_msgSend调用

调用时刻
- load是runtime加载 类/分类 的时候调用(只会调用1次)
- initialize是类第一次接收消息时调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

调用顺序
- load:先调用类的load。先编译的类,优先调用load(调用子类的load之前,会先调用父类的load)
- 再调用分类的load(先编译的分类,优先调用load)
- initialize:先初始化父类,  再初始化子类(可能最终调用的是父类的initialize方法)


10、⚠️:
- category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
- category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面
- 这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。


11、为什么不能动态添加成员变量?
- 方法和属性并不“属于”类实例,而成员变量“属于”类实例
- “类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。
- 假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用

十、网络

32、TCP、UDP各自的优缺点及区别
TCP优点:( 可靠,稳定)
- 在传递数据之前,会有三次握手来建立连接,
- 在数据传递时,有确认、窗口、重传、拥塞控制机制,
- 在数据传完后,还会断开连接用来节约系统资源

TCP缺点:(慢,效率低,占用系统资源高)
- TCP在传递数据之前,要先建连接,这会消耗时间
- 在数据传递时(确认机制、重传机制、拥塞控制机制)等都会消耗大量的时间
- 因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击

UDP的优点:(快)
- UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制
- UDP是一个无状态的传输协议,所以它在传递数据时非常快

UDP的缺点:(不可靠,不稳定)
- 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包

小结TCP与UDP的区别:
- TCP面向连接(如打电话要先拨号建立连接); UDP是无连接的,即发送数据
- TCP提供可靠的服务。通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流; UDP是面向报文的
- 每一条TCP连接只能是点到点的; UDP支持一对一,一对多,多对一和多对多的交互通信
33、Scoket连接和HTTP连接的区别
- HTTP协议是基于TCP连接的,是应用层协议,主要解决如何包装数据。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。
- HTTP连接:短连接,客户端向服务器发送一次请求,服务器响应后连接断开,节省资源。服务器不能主动给客户端响应,iPhone主要使用类NSURLConnection
- Socket连接:长连接,客户端跟服务器端直接使用Socket进行连接,没有规定连接后断开,因此客户端和服务器段保持连接通道,双方可以主动发送数据
34、HTTP协议的特点,关于HTTP请求GET和POST的区别
特点:
- HTTP超文本传输协议,是短连接,是客户端主动发送请求,服务器做出响应,服务器响应之后,链接断开
- HTTP是一个属于应用层面向对象的协议,HTTP有两类报文:请求报文和响应报文
- HTTP请求报文:一个HTTP请求报文由请求行、请求头部、空行和请求数据4部分组成
- HTTP响应报文:由三部分组成:状态行、消息报头、响应正文

GET请求
- 参数在地址后拼接,不安全(因为所有参数都拼接在地址后面)
- 不适合传输大量数据(长度有限制,为1024个字节)

POST请求
- 参数在请求数据区放着,相对GET请求更安全
- 数据大小理论上没有限制
- 提交的数据放置在HTTP包的包体中
35、断点续传怎么实现的?
- 断点续传主要依赖于 HTTP 头部定义的 Range 来完成
- 有了 Range,应用可以通过 HTTP 请求获取失败的资源,从而来恢复下载该资源
- 当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小

36、网络层相关面试

37、DNS是什么?DNS解析过程

域名系统(Domain Name System,DNS)
因特网上的主机,可以使用多种方式标识:

1、区别:
- 主机名:方便人们记忆和接受,但长度不一、没有规律的字符串,路由器并不方便处理
- IP地址:路由器方便处理,不便于人们记忆

为了折衷这两种方式,需要一种能进行主机名到IP地址转换的目录服务,就是 域名系统(Domain Name System,DNS)


2、作用:
- 将用户提供的主机名解析为IP地址


3、DNS解析过程(以www.163.com为例:)
- 打开浏览器,输入一个域名(www.163.com)。客户端会发出一个DNS请求到本地DNS服务器(本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动)
- 本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,直接返回结果。如果没有,向DNS根服务器进行查询。
- 根DNS服务器没有记录具体的域名和IP地址的对应关系,而是给出域服务器的地址,告诉他可以到域服务器上去继续查询
- 本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。
- .com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
- 最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,
- 本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

过程:本地服务器->根服务器->域服务器->域名解析服务器
- 整合成流程图
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
37、TCP建立连接的三次握手中,第二次握手发送的包会包含的标记,最正确的描述是?
38、json转model实现
39、如何设计一个网络库
40、AFNetworking如何是https的,如何适配ipv6的?

十一、UI

41、Storyboard/Xib和纯代码UI相比,有哪些优缺点?
storyboard/xib优点
- 简单直接。直接通过拖拽和点选即可完成配置。
- 跳转关系清楚

缺点:
- 协作冲突(多人提交代码)
- 很难做到页面继承和重用
- 不便于进行模块化管理
- 影响性能(多图层渲染)
42、自动布局AutoLayout原理,性能如何
43、说明比较方法:layoutifNeeded、layoutSubviews、setNeedsLayout
44、如果页面 A 跳转到 页面 B,A 的 viewDidDisappear 方法和 B 的 viewDidAppear 方法哪个先调用?
- A -->viewWillDisappear
- B-->viewWillAppear
- A-->viewDidDisappear
- B-->viewDidAppear
45、离屏渲染,隐式动画和显式动画相关
  • iOS圆角的离屏渲染,你真的弄明白了吗

  • iOS开发-解决页面卡顿小技巧(很常用)

    ⚠️经常看到,圆角会触发离屏渲染。但其实这个说法是不准确的,因为圆角触发离屏渲染也是有条件的!

    1、离屏渲染触发条件:

    • 背景色、边框、背景色+边框,再加上圆角+裁剪,因为 contents = nil 没有需要裁剪处理的内容,所以不会造成离屏渲染。

    • 一旦为contents设置了内容,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染。

    2、在一个表内有很多cell,每个cell上有很多个视图,如何解决卡顿问题? 3、切圆角一定会触发离屏渲染吗?

    4、iOS 9及之后的系统版本,苹果进行了一些优化

    • 只设置contents或者UIImageView的image,并加上圆角+裁剪,是不会产生离屏渲染的。
    • 但如果加上了背景色、边框或其他有图像内容的图层,还是会产生离屏渲染。
    • 使用类似于UIButton的视图的时候需要注意
46、frame和bouns的区别。什么时候frame和bouns的高宽不相等
47、事件响应过程(响应链)
  • iOS开发---图解事件的产生和响应 ✨✨✨✨✨

  • iOS触摸事件全家桶

    1、事件的传递 (寻找最合适的view的过程)

    • 当一个事件发生后,事件会从父控件传给子控件 (UIApplication->UIWindow->UIView->initial view)

    2、事件的响应

    • 首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView)

    • 如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件

    • 如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递

    • 一直到window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

    3、⚠️注意

    • 事件的传递是从上到下(父控件到子控件)

    • 事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)

    4、重要方法: 4.1、hitTest:withEvent:

    • 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法

    • 寻找并返回最合适的view(能够响应事件的那个最合适的view)

    • (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { // 1.判断自己能否接收触摸事件 if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) { return nil; } // 2.判断触摸点在不在自己范围内 if (![self pointInside:point withEvent:event]) { return nil; } // 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件 for(NSInteger i = self.subviews.count; i >= 0; i --) { UIView *childView = self.subviews[i]; CGPoint childPoint = [self convertPoint:point toView:childView]; UIView *fitView = [childView hitTest:childPoint withEvent:event]; if (fitView) { return fitView; } } // 没有找到比自己更合适的view return self; }

    4.2、pointInside:withEvent:

    • 判断点在不在当前view上(方法调用者的坐标系上)

    • 如果返回YES,代表点在方法调用者的坐标系上;

    • 返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

    5、穿透

    • 假设有一个黄色控件和白色控件,白色空间覆盖在黄色控件上
    • 点击白色view想要黄色view来响应该事件,就是所谓的穿透

    方法一、

    • (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGPoint yellowPoint = [self convertPoint:point toView:_yellowView]; if ([_yellowView pointInside:yellowPoint withEvent:event]) { return _yellowView; } return [super hitTest:point withEvent:event]; }

    方法二、

    • (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { CGPoint yellowPoint =[_yellowView convertPoint:point fromView:self]; if ([_yellowView pointInside:yellowPoint withEvent:event]){ return NO; } else { return [super pointInside:point withEvent:event]; } }
48、drawRect
49、手势识别的过程
  • 手势识别的过程

    这里主要说的是关于runloop的概念点

    • 当_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用Cancel,将当前的 touchesBegin/Move/End 系列回调打断
    • 随后系统将对应的 UIGestureRecognizer 标记为待处理
    • 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件
    • 这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer 的回调。
    • 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理
50、IBOutlet连出来的视图属性为什么可以被设置成weak?

IBOutlet

UITableView相关优化
  • Cell异步图片加载优化缓存机制详解

    • 每个UITableViewCell都有一个UIImageView需要加载,如果没有缓存,有五个Cell请求一个url,同时请求肯定不实际,如何处理?( UITableViewCell 异步加载图片缓存机制优化)
    • 微信UITableView滑动的时候,动图是不动的,为什么?(Runloop的mode)
两个半透明控件重贴部分颜色加深问题

十二、其他

51、数组和链表的区别
- 数组在内存上给出了连续的空间
- 链表,内存地址上可以是不连续的,每个链表的节点包括原来的内存和下一个节点的信息(单向的一个,双向链表的话,会有两个)

数组: 
- 优点: 使用方便,查询效率比链表高,内存为一连续的区域 
- 缺点: 大小固定,不适合动态存储,不方便动态添加

 链表: 
- 优点: 可动态添加删除,大小可变   
- 缺点: 只能通过顺次指针访问,查询效率低
53、谈谈你对编译、链接的理解
54、leak工具使用
55、应用程序启动过程,启动优化
- 应用启动时是用怎样加载所有依赖的Mach-O文件的?
- 请列举你所知道main()函数之前耗时的因素都有哪些

App启动分为两种:
- 冷启动(Cold Launch):从零开始启动app
- 热启动(Warm Launch):app已在内存中,在后台存活,再次点击图标启动app


启动时间的优化,主要是针对冷启动进行优化
1、通过添加环境变量可以打印app的启动时间分析(详情请见下图)
- DYLD_PRINT_STATISTICS
- DYLD_PRINT_STATISTICS_DETAILS(比上一个详细)
- 一般400毫秒以内正常

打印结果:
Total pre-main time: 238.05 milliseconds (100.0%)              // main函数调用之前(pre-main)总耗时
         dylib loading time: 249.65 milliseconds (104.8%)      // 动态库耗时 
        rebase/binding time: 126687488.8 seconds (18128259.6%) 
            ObjC setup time:  10.67 milliseconds (4.4%)        // OC结构体准备耗时 
           initializer time:  52.83 milliseconds (22.1%)       // 初始化耗时 
           slowest intializers :                               // 比较慢的加载 
             libSystem.B.dylib :   6.63 milliseconds (2.7%)
   libBacktraceRecording.dylib :   6.61 milliseconds (2.7%)
    libMainThreadChecker.dylib :  31.82 milliseconds (13.3%)


2、冷启动可以概括为3大阶段
- dyld
- runtime
- main


3、dyld(dynamic link editor),Apple的动态连接器,可以装载Mach-O(可执行文件、动态库等)
- 装载app的可执行文件,同时递归加载所有依赖的动态库
- 当dyld把可执行文件、动态库都装载完成后,会通知runtime进行下一步处理


4、runtime所做的事情
- 调用map_images函数中调用call_load_methods,调用所有Class和Category的+load方法
- 进行各种objc结构的初始化(注册objc类、初始化类对象等等)
- 调用C++静态初始化器和__attribure__((constructor))修饰的函数(JSONKit中存在具体应用)
- 到此为止,可执行文件和动态库中所有的符号(Class, Protocol, Selector, IMP...)都已按格式成功加载到内存中,被runtime所管理


5、总结
- app的启动由dylb主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
- 并由runtime负责加载成objc定义的结构
- 所有初始化工作结束后,dyld就会调用main函数
- 接下来就是ApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法


6、按照不同的阶段优化
dyld
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少objc类、分类的数量、减少selector数量(定期清理不必要的类、分类)
- 减少C++虚构函数
- Swift尽量使用struct

runtime
- 使用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、Objc的+load方法

main
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
- 按需加载

DYLD_PRINT_STATISTICS设置为1

56、包体积优化
57、项目的优化、性能优化
  • 你一般是如何优化你的APP的?

    启动速度:

    • 启动过程中做的事情越少越好(尽可能将多个接口合并)
    • 不在UI线程上作耗时的操作(数据的处理在子线程进行,处理完通知主线程刷新节目)
    • 在合适的时机开始后台任务(例如在用户指引节目就可以开始准备加载的数据)
    • 尽量减小包的大小
    • 辅助工具(友盟,听云,Flurry)

    页面浏览速度

    • json的处理(iOS 自带的NSJSONSerialization,Jsonkit,SBJson)
    • 数据的分页(后端数据多的话,就要分页返回,例如网易新闻,或者 微博记录)
    • 数据压缩(大数据也可以压缩返回,减少流量,加快反应速度)
    • 内容缓存(例如网易新闻的最新新闻列表都是要缓存到本地,从本地加载,可以缓存到内存,或者数据库,根据情况而定)
    • 延时加载tab(比如app有5个tab,可以先加载第一个要显示的tab,其他的在显示时候加载,按需加载
    • 算法的优化(核心算法的优化,例如有些app 有个 联系人姓名用汉语拼音的首字母排序)

    操作流畅度优化

    • Tableview 优化(tableview cell的加载优化)
    • ViewController加载优化(不同view之间的跳转,可以提前准备好数据)
58、说说你自己吧
- 你在项目中技术亮点、难点
- 你的发展方向(职业规划)
- 你的优点、你的缺点
59、说说组件化,你是如何组件化解耦的
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
60、静态库、动态库相关
  • iOS 库.a 和 framework的区别和创建

    1、什么是库?

    • 共享代码,实现代码的复用,一般分为静态库和动态库。

    2、静态库和动态库的区别 静态库(.a和.framework 样式):

    • 链接时完整的拷贝到可执行文件,多次使用多次拷贝,造成冗余,使包变的更大
    • 但是代码装载速度快,执行速度略比动态库快

    动态库:(.dylib和.framework)

    • 链接时不复制,程序运行时由系统加在到内存中,供系统调用,系统加在一次,多次使用,共用节省内存。

    3、为什么framework既是静态又是动态?

    • 系统的framework是动态的,自己创建的是静态的。

    4、.a 和 .framework 的区别是什么?

    • .a 是单纯的二进制文件,需要 .h文件配合,不能直接使用
    • .framework是二进制文件+资源文件,可以直接使用。 .framework = .a + .h + sorrceFile(资源文件)

十三、OC对象相关

61、对 OC 中 Class 的源码理解?其中 cache 的理解?说说NSCache缓存策略
  • iOS开发-底层篇-Class详解

    struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY;

    #if !OBJC2 Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; #endif

    } OBJC2_UNAVAILABLE; /* Use Class instead of struct objc_class * */

62、protocol中能否添加属性
  • iOS开发 - protocol中定义属性?

    • OC语言的协议里面是支持声明属性的
    • 但在协议中声明属性其实和在其中定义方法一样,只是声明了getter和setter方法,并没有具体实现
63、OC内联函数 inline
  • iOS OC内联函数 inline

    作用:

    • 替代宏

    inline函数与宏有区别

    • 解决函数调用效率的问题

    • 函数之间调用,是内存地址之间的调用,当函数调用完毕之后还会返回原来函数执行的地址。

    • 函数调用有时间开销,内联函数就是为了解决这一问题

    inline相比于宏的优点

    • 避免了宏的缺点:需要预编译.因为inline内联函数也是函数,不需要预编译.

    • 编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。消除了它的隐患和局限性。

    • 可以使用所在类的保护成员及私有成员

    inline相比于函数的优点

    • inline函数避免了普通函数的,在汇编时必须调用call的缺点:取消了函数的参数压栈,减少了调用的开销,提高效率.所以执行速度确比一般函数的执行速度要快.
    • 集成了宏的优点,使用时直接用代码替换(像宏一样)
64、id和NSObject ,instancetype的区别?
  • iOS中的NSObject*、id和instancetype

    • id和instancetype都可以做方法的返回值。

    • id类型的返回值在编译期不能判断对象的真实类型,即非关联返回类型

    • instancetype类型的返回值在编译期可以判断对象的真实类型,即关联返回类型。

    • id可以用来定义变量, 可以作为返回值, 可以作为形参

    • instancetype只能用于作为返回值。

    非关联返回类型、关联返回类型 TODO(待填充);⌛️⌛️⌛️⌛️⌛️

65、方法签名有什么作用?
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
66、nil、Nil、NULL、NSNull的区别?
- nil:指向一个对象的空指针    
- Nil:指向一个类的空指针,   
- NULL:指向其他类型(如:基本类型C类型)的空指针, 用于对非对象指针赋空值.
- NSNull:在集合对象中,表示空值的对象.

NSNull在Objective-C中是一个类 .NSNull有 + (NSNull *)null; 单例方法.多用于集合(NSArray,NSDictionary)中值为空的对象.

NSArray *array = [NSArray arrayWithObjects: [[NSObject alloc] init], [NSNull null], @"aaa", nil, [[NSObject alloc] init], [[NSObject alloc] init], nil];
NSLog(@"%ld", array.count);// 输出 3,NSArray以nil结尾
67、NSDictionary底层实现原理
  • NSDictionary底层实现原理

    • 在OC中NSDictionary是使用hash表来实现key和value的映射和存储的。

    hash表存储过程简单介绍:

    • 根据key值计算出它的hash值h;
    • 假设箱子的个数是n,那么键值对应该放在第(h%n)个箱子中。
    • 如果该箱子中已经有了键值对,就是用开放寻址法或者拉链法解决冲突。使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。
68、父类的property是如何查找的?
  • 父类的property是如何查找的?

    • 子类中的propert_list、method_list、ivar_list并不包含父类
    • 子类对象的_IMPL包含父类的

    从以上几点回答 TODO(待填充);⌛️⌛️⌛️⌛️⌛️

69、+load与 +initialize
  • OC中load方法和initialize方法的异同

    共同点:

    • 方法只会执行一次
    • 在类使用之前,就自动调用了这两个方法

    区别:

    • 执行时机不同()

    • load方法:如果类自身没有定义,并不会调用其父类的load方法;

    • initialize方法:如果类自身没有定义,就会调用其父类的initialize方法;

    执行的前提条件:

    • load 只要类所在文件被引用,就会执行;
    • 如果类没有引用进项目,就不会有load的执行;
    • initialize 需要类或者其子类的第一个方法被调用,才会执行,而且是在第一个方法执行之前,先执行;
    • 即使类文件被引用进项目,但是没有使用,那么initialize就不会调用执行;
70、iOS如何实现多继承,代码书写一下
71、类与结构体的区别
- 结构体只能封装数据,而类还可以封装行为
- 赋值:结构体是拷贝,对象之间是地址
- 结构体变量分配在栈空间(如果是一个局部变量的情况下),而对象分配在堆空间
72、crash崩溃怎么解,崩溃到底层代码
NSSetUncaughtExceptionHandler可以统计闪退
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
73、属性、成员变量、set、get方法相关
- 属性可以与set方法和get方法 三者同时存在吗,如果不行,请说明原因?
换句话说就是:iOS中同时重写属性的setget方法时,为什么访问不了下划线属性?

原因:
- 属性的setter方法和getter方法是不能同时进行重写,
- 因为,一旦你同时重写了这两个方法,那么系统就不会帮你生成这个成员变量了

解决方式:
@synthesize authType = _authType;
- 意思是,将属性的setter,getter方法,作用于这个变量。
74、isa和superclass相关
1、对象的isa指针指向哪里?superclass指针呢?(⚠️图-总结图)
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基类的meta-class

- class的superclass指向父类的class(如果没有父类,superclass指针为nil)
- meta-class的superclass指向父类的meta-class
- ⚠️基类的meta-class的superclass指向基类的class



2、方法调用查找(⚠️⚠️⚠️图-instance调用对象的轨迹;图-类方法调用轨迹)
- 对象方法的调用:通过instance的isa找到class,最后找到对象方法的实现进行调用
- 类方法的调用:当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用

3、class对象的superclass指针
Student : Person : NSObject

当Student的instance对象要调用Personal的对象方法时:
- 先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用

4、meta-class对象的superclass指针
当Student的class要调用Person的类方法时
- 先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用

isa指针

superclass指针

meta-class

总结.jpg

instance调用对象的轨迹

类方法调用轨迹

75、OC的类信息存放在哪里?
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法,存放在meta-class对象中
- 成员变量的具体值,存放在instance对象中
76、class、meta-class的结构
struct objc_class : objc_object {
    Class ISA;
    Class superclass;
    cache_t cache;             // 方法缓存
    class_data_bits_t bits;    // 用于获取具体的类信息
}

& FAST_DATA_MASK

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;        // 
    method_array_t methods;      // 方法列表
    property_array_t properties; // 属性列表
    protocol_array_t protocols;  // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name; // 类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; // 成员变量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
}
77、共用体和结构体的区别

部分参考文章: