给iOS中高级求职者的一份面试题解答

12,276 阅读31分钟

前段时间更新了一篇 给iOS中高级面试官的一份招聘要求 收到很多小伙伴的点赞与关注。可能有很多小伙伴已经带着我在那篇文章给大家提供的一些面试技巧 & 其中的面试题 已经开始招聘或者应聘了!这里应大家要求,对里面的面试题提供相关答案!相信无论是面试官还是求职者都是有所收获的~~

PS:篇幅有点长,大家可以关注或者点赞收藏以备不时之需!!!

iOS基础

1:讲讲你对atomic & nonatomic的理解

  • 1、原子操作对线程安全并无任何安全保证。被 atomic 修饰的属性(不重载设置器和访问器)只保证了对数据读写的完整性,也就是原子性,但是与对象的线程安全无关。
  • 2、线程安全有保障、对性能有要求的情况下可使用 nonatomic替代atomic,当然也可以一直使用atomic
  • 3:详细参考

2:被 weak 修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable 么?里面的结构可以画出来么?

被weak修饰的对象在被释放时候会置为nil,不同于assign;

Runtime 维护了一个 weak表,用于存储指向某个对象的所有weak指针weak表 其实是一个 hash(哈希)表Key 是所指对象的地址,Valueweak指针 的地址(这个地址的值是所指对象指针的地址)数组。

  • 1、初始化时:runtime 会调用 objc_initWeak函数,初始化一个新的 weak指针 指向对象的地址。
  • 2、添加引用时:objc_initWeak函数 会调用 objc_storeWeak() 函数objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
  • 3、释放时,调用 clearDeallocating函数clearDeallocating函数首先根据对象地址获取所有 weak指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entryweak表中删除,最后清理对象的记录。
  • 4:详细参考
struct SideTable {
    // 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
}

struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

3:block 用什么修饰?strong 可以?

  • block 本身是像对象一样可以 retain,和 release。但是,block 在创建的时候,它的内存是分配在栈(stack)上,而不是在堆(heap)上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。
  • 使用 retain 也可以,但是block的retain行为默认是用copy的行为实现的
  • 因为 block 变量默认是声明为栈变量的,为了能够在block的声明域外使用,所以要把 block 拷贝(copy)到堆,所以说为了 block 属性声明和实际的操作一致,最好声明为 copy
  • 详细参考

4:block 为什么能够捕获外界变量? __block做了什么事?

研究Block的捕获外部变量就要除去函数参数这一项,下面一一根据这4种变量类型的捕获情况进行分析。

  • 自动变量
  • 静态变量
  • 静态全局变量
  • 全局变量

首先 全局变量global_i静态全局变量static_global_j 的值增加,以及它们被Block 捕获进去,这一点很好理解,因为是全局的,作用域很广,所以Block捕获了它们进去之后,在Block里面进行++操作,Block结束之后,它们的值依旧可以得以保存下来。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0结构体 就是这样把自动变量捕获进来的。也就是说,在执行 Block 语法的时候,Block 语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是 Block 自身中。

这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量,等等,这些变量在Block里面并不会被使用到。那么这些变量并不会被Block捕获进来,也就是说并不会在构造函数里面传入它们的值。

Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。

5:谈谈你对事件的传递链和响应链的理解

  • 一:响应者链 UIResponser包括了各种Touch message 的处理,比如开始,移动,停止等等。常见的 UIResponserUIView及子类UIViController,APPDelegateUIApplication等等。

回到响应链,响应链是由UIResponser组成的,那么是按照哪种规则形成的。

  • A: 程序启动 UIApplication会生成一个单例,并会关联一个APPDelegateAPPDelegate作为整个响应链的根建立起来,而``UIApplication会将自己与这个单例链接,即UIApplicationnextResponser(下一个事件处理者)为APPDelegate`。

  • B:创建UIWindow 程序启动后,任何的UIWindow被创建时,UIWindow内部都会把nextResponser设置为UIApplication单例UIWindow初始化rootViewController,rootViewControllernextResponser会设置为UIWindow

  • C:UIViewController初始化 loadView, VCviewnextResponser会被设置为VC.

  • D:addSubView addSubView操作过程中,如果子subView不是VC的View,那么subViewnextResponser会被设置为superView。如果是VCView,那就是 subView -> subView.VC ->superView如果在中途,subView.VC被释放,就会变成subView.nextResponser = superView

我们使用一个现实场景来解释这个问题:当一个用点击屏幕上的一个按钮,这个过程具体发生了什么。

    1. 用户触摸屏幕,系统硬件进程会获取到这个点击事件,将事件简单处理封装后存到系统中,由于硬件检测进程和当前App进程是两个进程,所以进程两者之间传递事件用的是端口通信。硬件检测进程会将事件放到APP检测的那个端口。
  • 2.APP启动主线程RunLoop会注册一个端口事件,来检测触摸事件的发生。当事件到达,系统会唤起当前APP主线程的RunLoop。来源就是App主线程事件,主线程会分析这个事件。

  • 3.最后,系统判断该次触摸是否导致了一个新的事件, 也就是说是否是第一个手指开始触碰,如果是,系统会先从响应网中 寻找响应链。如果不是,说明该事件是当前正在进行中的事件产生的一个Touch message, 也就是说已经有保存好的响应链

  • 二:事件传递链

通过两种方法来做这个事情。

// 先判断点是否在View内部,然后遍历subViews
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  
//判断点是否在这个View内部
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds
  • A: 流程

  • 1:先判断该层级是否能够响应(1.alpha>0.01 2.userInteractionEnabled == YES 3.hidden = NO)

  • 2:判断改点是否在view内部,

  • 3:如果在那么遍历子view继续返回可响应的view,直到没有。

  • B:常见问题

  • 父view设置为不可点击,子view可以点击吗

  • 不可以,hit test 到父view就截止了

  • 子view设置view不可点击不影响父类点击

  • 同父view覆盖不影响点击

  • 手势对responder方法的影响

  • C:实际用法

  • 点一一个圆形控件,如何实现只点击圆形区域有效,重载pointInside。此时可将外部的点也判断为内部的点,反之也可以。

  • 事件响应链在复杂功能界面进行不同控件间的通信,简便某些场景下优于代理和block

6:谈谈 KVC 以及 KVO 的理解?

7:RunLoop 的作用是什么?它的内部工作机制了解么?

字面意思是“消息循环、运行循环”,runloop内部实际上就是一个do-while循环,它在循环监听着各种事件源、消息,对他们进行管理并分发给线程来执行。

  • 1.通知观察者将要进入运行循环。 线程和 RunLoop 之间是一一对应的

  • 2.通知观察者将要处理计时器。

  • 3.通知观察者任何非基于端口的输入源即将触发。

  • 4.触发任何准备触发的基于非端口的输入源。

  • 5.如果基于端口的输入源准备就绪并等待触发,请立即处理该事件。转到第9步。

  • 6.通知观察者线程即将睡眠。

  • 7.将线程置于睡眠状态,直到发生以下事件之一:

    • 事件到达基于端口的输入源。
    • 计时器运行。
    • 为运行循环设置的超时值到期。
    • 运行循环被明确唤醒。
  • 8.通知观察者线程被唤醒。

  • 9.处理待处理事件。

    • 如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步。
    • 如果输入源被触发,则传递事件。
    • 如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。
  • 10.通知观察者运行循环已退出。

8:苹果是如何实现 autoreleasepool的?

arc下编译器会优化成

void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
  • 向一个结构AutoreleasePoolPage,中写入需要自动释放的对象,类似一种标记,调用objc_autoreleasePoolPop(context)后,就会把这中间的对象release一下。
  • 这里要注意的是,方法返回值是怎么做到自动释放的?
  • 其使用Thread Local Storage(TLS)线程局部存储,每次存入线程或者从线程取出来。
  • 我们没有卸载{}中的自动释放对象,会在每个runloop结束时候去释放,相当于一个大的autoreleasepool中。
  • 参考文章
  • 苹果是如何实现autoreleasepool的

9:谈谈你对 FRP (函数响应式) 的理解,延伸一下 RxSwift 或者 RAC

参考文章:RxSwift(1)— 初探 看这一篇文章也就够了!然后结合 RxSwift 映射到 RAC!函数响应式的思想是不变的!至于内部的封装有所不同,但是最终却是殊途同归!

10:平时开发有没有玩过 Instrument

分析:这里的内容非常有意思,对于一个iOS高级开发人员,我觉得还有很有必要掌握的!尤其开发3-5年,如果没有掌握这些内容我觉得是不合格的

我个人建议在掌握面试题的同时还需要求职者更多的去分析和拓展!比如你的探索思路,你在这个知识点意外的延伸。还有你再实际开发过程的落地!而这些都是加分项!

Runtime

1:什么是 isa,isa 的作用是什么?

2:一个实例对象的isa 指向什么?类对象指向什么?元类isa 指向什么?

  • 参考文章
  • 实例对象的isa 指向类
  • 类对象的isa指向元类
  • 元类isa 指向根元类

  • 3:objc 中类方法和实例方法有什么本质区别和联系?

  • 参考文章

类方法:

  • 1.类方法是属于类对象的
  • 2.类方法只能通过类对象调用
  • 3.类方法中的self是类对象
  • 4.类方法可以调用其他的类方法
  • 5.类方法中不能访问成员变量
  • 6.类方法中不能直接调用对象方法

实例方法:

  • 1.实例方法是属于实例对象的
  • 2.实例方法只能通过实例对象调用
  • 3.实例方法中的self是实例对象
  • 4.实例方法中可以访问成员变量
  • 5.实例方法中直接调用实例方法
  • 6.实例方法中也可以调用类方法(通过类名)

4:loadinitialize 的区别?

+load

  • 1、只要程序启动就会将所有类的代码加载到内存中(在main函数执行之前), 放到代码区(无论该类有没有被使用到都会被调用)
  • 2、+load方法会在当前类被加载到内存的时候调用, 有且仅会调用一次
  • 3、当父类和子类都实现+load方法时, 会先调用父类的+load方法, 再调用子类的+load方法
  • 4、先加载原始类,再加载分类的+load方法
  • 5、当子类未实现+load方法时,不会调用父类的+load方法
  • 6、多个类都实现+load方法,+load方法的调用顺序,与Compile Sources中出现的顺序一致

load方法在Apple官方文档中的描述

+initialize

  • 1、当类第一次被使用的时候就会调用(创建类对象的时候)
  • 2、initialize方法在整个程序的运行过程中只会被调用一次, 无论你使用多少次这个类都只会调用一次
  • 3、initialize用于对某一个类进行一次性的初始化
  • 4、先调用父类的initialize再调用子类的initialize
  • 5、当子类未实现initialize方法时,会把父类的实现继承过来调用一遍,再次之前父类的initialize方法会被优先调用一次
  • 6、当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)

initialize方法在Apple官方文档中的描述

5:_objc_msgForward 函数是做什么的?直接调用会发生什么问题?

当对象没有实现某个方法 ,会调用这个函数进行方法转发。 (某方法对应的IMP没找到,会返回这个函数的IMP去执行)

  • 1.调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。
  • 2.调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。
  • 3.调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。
  • 4.调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。

如果直接调用这个方法,就算实现了想调用的方法,也不会被调用,会直接走消息转发步骤。

6:简述下 Objective-C 中调用方法的过程

  • Objective-C是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:
  • objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类
  • 然后在该类中的方法列表以及其父类方法列表中寻找方法运行
  • 如果,在最顶层的父类(一般也就NSObject)中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常 unrecognized selector sent to XXX
  • 但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会,这三次拯救程序奔溃的说明见问题《什么时候会报unrecognized selector的异常》中的说明

PS:Runtime 铸就了Objective-C 是动态语言的特性,使得C语言具备了面向对象的特性,在程序运行期创建,检查,修改类、对象及其对应的方法,这些操作都可以使用runtime中的对应方法实现。

7:能否想向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

  • 1.不能向编译后得到的类增加实例变量
  • 2.能向运行时创建的类中添加实例变量

解释:

  • 1.编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定, runtime会调用 class_setvarlayoutclass_setWeaklvarLayout来处理 strong weak引用.所以不能向存在的类中添加实例变量
  • 2.运行时创建的类是可以添加实例变量,调用class_addIvar函数.但是的在调用objc_allocateClassPair之后, objc_registerClassPair之前,原因同上.

8:谈谈你对切面编程的理解

维基百科对于切面编程(AOP)的解释是这样的:面向切面的程序设计(aspect-oriented programming,AOP,又译作面向侧面的程序设计、观点导向编程、剖面导向程序设计)是计算机科学中的一个术语,指一种程序设计范型。该范型以一种称为切面的语言构造为基础,切面是一种新的模块化机制,用来描述分散在对象、类、函数)中的横切关注点。参考文章

分析:Runtime 这个模块iOS面试无论初中高都会面试。我觉得这个模块不光只是仅仅问问关于知识点内容,我更新想要听到求职者在这里面的爬坑探索辛历路程!Runtime这个模块是刷开页面开发的关键点!

网络&多线程

1:HTTP的缺陷是什么?

HTTP 主要有这些不足,例举如下。

  • 通信使用明文(不加密),内容可能会被窃听
  • 不验证通信方的身份,因此有可能遭遇伪装
  • 无法证明报文的完整性,所以有可能已遭篡改

这些问题不仅在HTTP上出现,其他未加密的协议中也会存在这类问题。

2:谈谈三次握手,四次挥手!为什么是三次握手,四次挥手?

参考文章 我觉得这个地方还是需要自我理解,用自己的话去表达出来!

3:socket 连接和 Http 连接的区别

http 是基于 socket 之上的。socket 是一套完整的 tcp,udp协议的接口。

  • HTTP协议:简单对象访问协议,对应于应用层,HTTP协议是基于TCP连接的。
    • tcp协议:对应于传输层。
    • ip协议:对应于网络层。

TCP/IP是传输层协议,主要解决数据如何在网络中传输,而HTTP协议是应用层协议,主要解决如何包装数据。

Socket是对TCP/IP 协议的封装,它本身不是协议,而是一个调用接口,通过Socket,我们才能使用TCP/IP协议

  • http连接:就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断掉。
  • socket连接:就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉,但是由于各种环境因素可能会使连接断开。

http是客户端用http协议进行请求,发送请求时候需要封装http请求头,并绑定请求的数据,服务器一般有web服务器配合。http请求方式为客户端主动发起请求,服务器才能给响应,一次请求完毕后则断开连接以节省资源。服务器不能主动给客户端响应。iPhone主要使用的类是NSUrlConnectionsocket是客户端跟服务器直接使用socket“套接字”进行拼接,并没有规定连接后断开,所以客户端和服务器可以保持连接,双方都可以主动发送数据。一般在游戏开发或者股票开发这种即时性很强的并且保持发送数据量比较大的场合使用。主要类是CFSocketRef。

  • UDP:是用户数据报协议:主要用在实时性要求高以及对质量相对较弱的地方,但面对现在高质量的线路容易丢包。
  • TCP:是传输控制协议,是面向连接的,,运行环境必然要求其可靠性不可丢失包有良好的拥塞控制机制。

4:HTTPS,安全层除了SSL还有,最新的? 参数握手时首先客户端要发什么额外参数

5:什么时候POP网络,有了 Alamofire 封装网络 URLSession为什么还要用Moya

POP网络:面向协议编程的网络能够大大降低耦合度!网络层下沉,业务层上浮。中间利用 POP网络Moya 隔开。如果你的项目是 RxSwift 函数响应式的也没有关系!因为有 RxMoya

参考文章:

6:如何实现 dispatch_once

+ (instancetype)sharedInstance
{
    /*定义相应类实例的静态变量;
    意义:函数内定义静态变量,无论该函数被调用多少次,
         在内存中只初始化一次,并且能保存最后一次赋的值
    */
    static ClassName *instance = nil;
    /*定义一个dispatch_once_t(其实也就是整型)静态变量,
    意义:作为标识下面dispatch_once的block是否已执行过。
         static修饰会默认将其初始化为0,当值为0时才会执行block。
         当block执行完成,底层会将onceToken设置为1,这也就是为什
         么要传onceToken的地址(static修饰的变量可以通过地址修改
         onceToken的值),同时底层会加锁来保证这个方法是线程安全的
    */
    static dispatch_once_t onceToken;
    /*只要当onceToken == 0时才会执行block,否则直接返回静态变量instance*/
    dispatch_once(&onceToken, ^{
        instance = [[ClassName alloc] init];
        //...
    });
    return instance;
}

iOS原理之CGD-dispatch_once的底层实现

7:能否写一个读写锁?谈谈具体的分析 8:什么时候会出现死锁?如何避免? 9:有哪几种锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说

分析:这个模块可能是一般开发人员的盲区。对于这一块一定要有自己的理解!学习的方向就是查漏补缺,一步一个吃掉!如果你一整块去啃,你会发现很枯燥!虽然开发过程中你可能用不到,但是面试这一块是你必须要掌握的!

数据结构

1.数据结构的存储一般常用的有几种?各有什么特点?

数据的存储结构是数据结构的一个重要内容。在计算机中,数据的存储结构可以采取如下四中方法来表现。

  • 顺序存储方式

简单的说,顺序存储方式就是在一块连续的存储区域 一个接着一个的存放数据。顺序存储方式把逻辑上相连的结点存储在物理位置上相邻的存储单元里,结点间的逻辑关系由存储单元的邻接挂安息来体现。顺序存储方式也称为顺序存储结构(sequentialstorage structure),一般采用数组或者结构数组来描述。 线性存储方式主要用于线性逻辑结构的数据存放,而对于图和树等非线性逻辑结构则不适用。

  • 链接存储方式

链接存储方式比较灵活,其不要求逻辑上相邻的结点在物理位置上相邻,结点间的逻辑关系由附加的引用字段表示。一个结点的引用字段往往指导下一个结点的存放位置。 链接存储方式也称为链接式存储结构(LinkedStorage Structure),一般在原数据项中增加应用类型来表示结点之间的位置关系。

  • 索引存储方式

索引存储方式是采用附加索引表的方式来存储结点信息的一种存储方式。索引表由若干个索引项组成。索引存储方式中索引项的一般形式为:(关键字、地址)。其中,关键字是能够唯一标识一个结点的数据项。

  • 索引存储方式还可以细分为如下两类:

    • 稠密索引(Dense Index):这种方式中每个结点在索引表中都有一个索引项。其中,索引项的地址指示结点所在的的存储位置;

    • 稀疏索引(Spare Index):这种方式中一组结点在索引表中只对应一个索引项。其中,索引项的地址指示一组结点的起始存储位置。

  • 散列存储方式

散列存储方式是根据结点的关键字直接计算出该结点的存储地址的一种存储的方式。 在实际应用中,往往需要根据具体数据结构来决定采用哪一种存储方式。同一逻辑结构采用不同额存储方法,可以得到不同的存储结构。而且这四种节本存储方法,既可以单独使用,也可以组合起来对数据结构进行存储描述。

2.集合结构 线性结构 树形结构 图形结构 3.单向链表 双向链表 循环链表 4.数组和链表区别 5.堆、栈和队列

6.输入一棵二叉树的根结点,求该树的深度?

如果一棵树只有一个结点,它的深度为1。 如果根结点只有左子树而没有右子树, 那么树的深度应该是其左子树的深度加1,同样如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1. 如果既有右子树又有左子树, 那该树的深度就是其左、右子树深度的较大值再加1。

public static int treeDepth(BinaryTreeNode root) {
    if (root == null) {
        return 0;
    }
    int left = treeDepth(root.left);
    int right = treeDepth(root.right);
    return left > right ? (left + 1) : (right + 1);
}

7.输入一课二叉树的根结点,判断该树是不是平衡二叉树?

  • (1)需要重复遍历节点多次的解法
  • (2)每个节点只需遍历一次的解法
  • 参考文章:

算法

1.时间复杂度

计算机科学中,时间复杂性,又称时间复杂度算法时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。 时间复杂性

2.空间复杂度

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。比如直接插入排序时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量时间复杂度&空间复杂度

3.常用的排序算法

  • 1、冒泡排序
  • 2、选择排序
  • 3、插入排序
  • 4、希尔排序
  • 5、快速排序
  • 6、归并排序
  • 7、堆排序
  • 常见的7种排序算法

4.字符串反转

- (NSString *)reversalString:(NSString *)originString{
    NSString *resultStr = @"";
    for (NSInteger i = originString.length -1; i >= 0; i--) {
      NSString *indexStr = [originString substringWithRange:NSMakeRange(i, 1)];
      resultStr = [resultStr stringByAppendingString:indexStr];
    }
  return resultStr;
}

5.链表反转(头差法)

public Node reverseList(){
        Node cur = head;
        Node prev = null;
        Node curNext = head.next;
        Node reverHead = null;
        while(cur!=null){
            cur.next = prev;
            cur = curNext;
            prev = cur;
            curNext = curNext.next;
        }
        reverHead = cur;
        return reverHead;
}

> 6.有序数组合并

```objc
- (void)merge {
    /*
     有序数组A:1、4、5、8、10...1000000,有序数组B:2、3、6、7、9...999998,A、B两个数组不相互重复,请合并成一个有序数组C,写出代码和时间复杂度。
     */
    //(1).
    NSMutableArray *A = [NSMutableArray arrayWithObjects:@4,@5,@8,@10,@15, nil];
//    NSMutableArray *B = [NSMutableArray arrayWithObjects:@2,@6,@7,@9,@11,@17,@18, nil];
    NSMutableArray *B = [NSMutableArray arrayWithObjects:@2,@6,@7,@9,@11,@12,@13, nil];
    NSMutableArray *C = [NSMutableArray array];
    int count = (int)A.count+(int)B.count;
    int index = 0;
    for (int i = 0; i < count; i++) {
        if (A[0]<B[0]) {
            [C addObject:A[0]];
            [A removeObject:A[0]];
        }
        else if (B[0]<A[0]) {
            [C addObject:B[0]];
            [B removeObject:B[0]];
        }
        if (A.count==0) {
            [C addObjectsFromArray:B];
            NSLog(@"C = %@",C);
            index = i+1;
            NSLog(@"index = %d",index);
            return;
        }
        else if (B.count==0) {
            [C addObjectsFromArray:A];
            NSLog(@"C = %@",C);
            index = i+1;
            NSLog(@"index = %d",index);
            return;
        }
    }
    //(2).
    //时间复杂度
    //T(n) = O(f(n)):用"T(n)"表示,"O"为数学符号,f(n)为同数量级,一般是算法中频度最大的语句频度。
    //时间复杂度:T(n) = O(index);
}

7.查找第一个只出现一次的字符(Hash查找)

两个思路:

  • 1:hash�不同编译器对字符数据的处理不一样,所以hash之前先把字符类型转成无符号类型;
  • 2,空间换时间,用buffer数组记录当前只找到一次的字符,避免二次遍历。
# define SIZE 256
char GetChar(char str[])
{
  if(!str)
    return 0;
  char* p = NULL;
  unsigned count[SIZE] = {0};
  char buffer[SIZE];
  char* q = buffer;
  for(p=str; *p!=0; p++)
  {
    if(++count[(unsigned char)*p] == 1)
      *q++ = *p;
  }
  
  for (p=buffer; p<q; p++)
  {
    if(count[(unsigned char)*p] == 1)
    return *p;
  }
return 0;
}

8.查找两个子视图的共同父视图

这个问的其实是数据结构中的二叉树,查找一个普通二叉树中两个节点最近的公共祖先问题 假设两个视图为UIViewAUIViewC,其中 UIViewA继承于UIViewBUIViewB继承于UIViewDUIViewC也继承于UIViewD;即 A->B->D,C->D

- (void)viewDidLoad {
    [super viewDidLoad];
    Class commonClass1 = [self commonClass1:[ViewA class] andClass:[ViewC class]];
    NSLog(@"%@",commonClass1);
    // 输出:2018-03-22 17:36:01.868966+0800 两个UIView的最近公共父类[84288:2458900] ViewD
}
// 获取所有父类
- (NSArray *)superClasses:(Class)class {
    if (class == nil) {
        return @[];
    }
    NSMutableArray *result = [NSMutableArray array];
    while (class != nil) {
        [result addObject:class];
        class = [class superclass];
    }
    return [result copy];
}

- (Class)commonClass1:(Class)classA andClass:(Class)classB {
    NSArray *arr1 = [self superClasses:classA];
    NSArray *arr2 = [self superClasses:classB];
    for (NSUInteger i = 0; i < arr1.count; ++i) {
        Class targetClass = arr1[i];
        for (NSUInteger j = 0; j < arr2.count; ++j) {
            if (targetClass == arr2[j]) {
                return targetClass;
            }
        }
    }
    return nil;
}
  • 方法一明显的是两层for循环,时间复杂度为 O(N^2) 一个改进的办法:我们将一个路径中的所有点先放进NSSet中.因为NSSet的内部实现是一个hash表,所以查询元素的时间的复杂度变成 O(1),我们一共有N个节点,所以总时间复杂度优化到了O(N)
- (Class)commonClass2:(Class)classA andClass:(Class)classB{
    NSArray *arr1 = [self superClasses:classA];
    NSArray *arr2 = [self superClasses:classB];
    NSSet *set = [NSSet setWithArray:arr2];
    for (NSUInteger i =0; i<arr1.count; ++i) {
        Class targetClass = arr1[i];
        if ([set containsObject:targetClass]) {
            return targetClass;
        }
    }
    return nil;
}

9.无序数组中的中位数(快排思想)

参考:求无序数组中的中位数

10.给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。

你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。 示例:给定nums = [2, 7, 11, 15], target = 9 --- 返回 [0, 1] 思路:

  • 第一层for循环从索引0到倒数第二个索引拿到每个数组元素,
  • 第二个for循环遍历上一层for循环拿到的元素的后面的所有元素。
  • 参考文章
class Solution {
    public int[] twoSum(int[] nums, int target) {
       int len = nums.length;
        int[] result = new int[2];
        for(int i = 0; i < len; i++){
            for(int j = i+1; j < len; j++){
                if(nums[i] + nums[j] == target){
                    result[0] = i;
                    result[1] = j; 
                    return result;
                }
            }
        }
        return result;
    }
}

分析:这个模块是绝大部分开发人员的软肋!这个模块是最能测试求职者思维能力的!但是我不建议面试官直接让求职者手写 在那样的面试紧张环境,手写数据结构或者一些算法代码,是非常有挑战的!思维到我觉得差不多!

架构设计

1:设计模式是为了解决什么问题的?

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

设计模式最主要解决的问题是通过封装和隔离变化点来处理软件的各种变化问题。 隔离变化的好处在于,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。很多设计模式的意图中都明显地指出了其对问题的解决方案,学习设计模式的要点是发现其解决方案中封装的变化点。

三本经典书籍:《GOF设计模式》《设计模式解析》,《Head First Design Pattern》

设计模式是软件开发领域的精髓之一。学好设计模式是目前每一个开发人员的必修课,

2:看过哪些第三方框架的源码,它们是怎么设计的?

这个题目就看你个人的感触,考量你平时的功底! 大家可以针对性一些常见的框架:RxSwiftAlamofireMoyaAFNetworingYYKit.... 掌握会用的同时,必须要掌握底层的核心思想!

3:可以说几个重构的技巧么?你觉得重构适合什么时候来做?

  • 重复代码的提炼
  • 冗长方法的分割
  • 嵌套条件分支的优化
  • 去掉一次性的临时变量
  • 消除过长参数列表
  • 提取类或继承体系中的常量
  • 让类提供应该提供的方法
  • 拆分冗长的类
  • 提取继承体系中重复的属性与方法到父类

在新功能增加时候,在扩展不再简单的时候。重构是一个不断的过程。

4:开发中常用架构设计模式你怎么选型?

这里也是一道开放性题目!并不是说某一种架构就是最优秀的~只有最合适的!根据公司情况,项目现状,以及开发者水平及时调整,设计!

5:你是如何组件化解耦的?

iOS 解藕、组件化最常用的是使用统跳路由的方式,目前比较常用的 iOS 开源路由框架主要是JLRoutesMGJRouterHHRouter等,这些路由框架各有优点和缺点,基本可以满足大部分需求。目前最常用来作路由跳转,以实现基本的组件化开发,实现各模块之间的解藕。但是,在实际中开发中会发现,无法彻底使用它们完成所有模块间通信,比如模块间的同步、异步通信等。再比如,我们在配置了相关路由跳转的 URL 后,如何在上线之后动态修改相关跳转逻辑?在模块间通信时,如何在上线后动态修改相关参数?APP 能否实现类似 Web 的302跳转学习参考

分析:架构设计这一层对于一个iOS中高级开发人员来说。这一块那是他必须要去思考和感受总结的!如果这位求职者开发4-5年了,一直都在做应用层界面开发,那么想必他未来的职业晋升是已经落后了的!面试官不妨在这一个模块单独设计成一面,就和求职者一起交流讨论。毕竟这些思维的设计,也许能够给面试官带来一些不一样的东西!😊

性能优化

1:tableView 有什么好的性能优化方案?

2: 界面卡顿和检测你都是怎么处理?

3:谈谈你对离屏渲染的理解?

4:如何降低APP包的大小

5:日常如何检查内存泄露?

6:APP启动时间应从哪些方面优化?

分析:现在APP性能优化已经成为iOS中高级开发人员必须要去关系的东西!这一块我个人建议结合实际开发去和求职者交流。而不是仅仅停留在知识点问答,因为没有实际开发能力的性能优化都只是纸上谈兵!

总结

这一套面试题还是有一定的水平和难度的!但是对于要应聘一份iOS中高级开发岗位,还是比较中肯的!希望大家能够在接下来的跳槽涨薪有自己的思想。

文章有长,建议关注备份,不管是正在面试还是即将面试,应该对你有帮助!既然看到这里:麻烦点个赞吧!👍

PS:对本文内容存在疑问还望指出,谢谢!加油,静候你的佳音