欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS面试题必考问题

程序员文章站 2022-06-10 10:06:51
...

iOS面试必考问题

1、多态、继承、封装

封装:就是对类中的一些字段,方法进行保护,不被外界所访问到,有一种权限的控制功能,四种访问权限修饰符:public,default,protected,private,访问权限一次递减的,这样我们在定义类的时候,哪些字段和方法不想暴露出去,哪些字段和方法可以暴露,可以通过修饰符来完成,这就是封装;

继承:使得我们没必要别写重复的代码,可重用性很高。缺点:继承提高了代码的耦合性.优点: 提高复用性,维护性

多态:定义类型和实际类型,一般是基于接口的形式实现的

2、代理模式、单例模式、观察者模式、工厂模式

代理这是iOS中一种消息传递的方式,也可以通过这种方式来传递一些参数
协议:用来指定代理双方可以做什么,必须做什么。
代理:根据指定的协议,完成委托方需要实现的功能。
委托:根据指定的协议,指定代理去完成什么功能。

协议是什么?有什么作用?
协议:声明一系列的方法,可由任何类实施,即使遵守该协议的类没有共同的超类。协议方法定义了独立于任何特定类的行为。简单的说,协议就是定义了一个接口,其他类负责来实现这些接口。如果你的类实现了一个协议的方法时,则说该类遵循此协议。
协议的作用:
1.定义一套公用的接口(Public)
@required:必须实现的方法
@optional:可选实现的方法(可以全部都不实现)
2.委托代理(Delegate)传值:
它本身是一个设计模式,它的意思是委托别人去做某事。
比如:两个类之间的传值,类A调用类B的方法,类B在执行过程中遇到问题通知类A,这时候我们需要用到代理(Delegate)。

单例:确保某一个类只有一个实例,而且自行实例化并向整个系统提供整个实例。
优点:
1、减少内存开支和系统性能开销;
2、避免对资源的多重占用;
3、优化和共享资源访问。
缺点:
1、单例模式没有接口,扩展很困难;
2、单例模式与单一职责有冲突。
通过GCD实现单例方法:

 (.h文件中)
+(DBManager *)sharedManager;   
.m文件中的实现:
+(DBManager *)sharedManager{
 Static DBManager *manager = nil;
  static dispatch_once_t token;
  dispatch_once(&token,^{
         if(manager == nil){
           manager = [[DBManager alloc]init];
   }
} );
return manager;
}

3、事件响应连

hit-Testing:找出这个触摸点下面的hit-test view的过程,HitTest会检测这个点击的点是不是发生在这个View上,如果是的话,就会去遍历这个View的subviews,直到找到最小的能够处理事件的view,如果整了一圈没找到能够处理的view,则返回自身。

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
 //该方法的处理过程:
 //首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
 //YES在,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:;
 // NO不在,当前视图的hitTest:withEvent:返回nil
 //若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束
 //若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self)

//判断触摸点是否在当前视图内,可以用来实现扩大View的相应区域
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

1.事件的产生
发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中为什么是队列而不是栈?因为队列的特定是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
2.事件的传递
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view

4、NSThread、NSOperation、GCD

什么是进程
a、是指在系统中正在运行的一个应用程序;
b、每个进程之间是独立的,每个进程运行在其专用且受保护的内存空间;
什么是线程
a、一个进程想要执行任务,必须得有线程(每个进程至少要有一条线程);
b、线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行;

5、MVC、MVVM等 View层架构模式

6、NSUserDefault、Plist、NSCode、Sqlite、CoreData、File

plist文件(属性列表)

preference(偏好设置)

NSKeyedArchiver(归档)

SQLite 3

CoreData (icloud 无主键)

7、delegate、Notification、block

block本质是一个数据类型,多用于参数传递,代替代理方法, (有多个参数需要传递或者多个代理方法需要实现还是推荐使用代理方法),少用于当做返回值传递. block是一个OC对象,它的功能是保存代码片段,预先准备好代码,并在需要的时候执行.

8、内存管理、内存优化、内存泄漏

自动释放池@autoreleasepool

自动释放池底层怎么实现

自动释放池以栈的形式实现:当你创建一个新的自动释放池时,它将被添加到栈顶。当一个对象收到发送autorelease消息时,它被添加到当前线程的处于栈顶的自动释放池中,当自动释放池被回收时,它们从栈中被删除,并且会给池子里面所有的对象都会做一次release操作.

OC对象的生命周期取决于引用计数,我们有两种方式可以释放对象:一种是直接调用release释放;另一种是调用autorelease将对象加入自动释放池中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。

我们的Mac以及iOS系统会自动创建一些线程,例如主线程和GCD中的线程,都默认拥有自动释放池。每次执行 “事件循环”(event loop)时,就会将其清空,这一点非常重要,请务必牢记!

9、堆和栈

栈,是由系统编译器自动管理,不需要程序员手动管理,存放方法(函数)的参数值,局部变量的值等,栈是向低地址扩展的数据结构,是一块连续的内存区域
堆,释放工作由程序员手动管理,不及时回收容易产生内存泄露,堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

栈区存放局部变量,先进后出,当程序执行出了作用于的范围,栈区局部变量就会被销毁,所以我们也不需要管理栈区的内存。
1.在iOS中,堆区的内存是所有应用程序共享的

2.堆区内存分配是由系统来负责的

3.系统通过链表来维护所有已经分配过的内存空间

4.系统只是记录分配了多少字节给应用程序,但是并不管理具体分配给的对象类型

5.变量使用结束后,需要释放内存。在OC中当retainCount == 0时,就说明没有任何变量使用该空间,那么系统就会直接回收

6.如果我们使用某一个变量之后,不释放内存,那么该内存就会被永远占用,造成内存泄漏

7.当对象已经释放,但是程序中的变量仍然指向该内存地址,这个时候,如果向该对象发送消息,就会出现经典的野指针错误

10、const、static、extern

11、readwrite ,readonly strong,weak,retain,assign,copy , nonatomic , natomic

readwrite : 是可读可写属性,需要生成setter 和 getter 方法;
readonly: 是可读属性,只生成getter方法,不生成setter方法,不希望属性再类外改变;
strong:
weak:
retain: 表示持有特性,setter方法将传入的参数保留,再赋值,传入参数引用计数retaincount + 1;
assign: 是赋值特性,setter方法将传入的参数赋值给实体变量,仅设置变量时,assign用于简单数据类型 如 NSInterger ,double ,float ,bool
copy: 表示赋值特性,setter方式将传入的对象复制一份,需完全复制一份新的变量时:
noatomic: 非原子性操作,决定编译器生成的setter和getter 方法是否是原子性的操作;
atomic: 表示多线程安全

12、documents,tmp,app,Library

14、category、extension(类扩展)

类别:Category 是表示一个指向分类的结构体的指针。
1.分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。所以< 原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性> ;
2.分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量(编译时会报警告);
3.可以在分类中访问原有类中.h中的属性;
4.如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法。所以同名方法调用的优先级为 分类 > 本类 > 父类。因此在开发中尽量不要覆盖原有类;
5.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。


扩展:

  1. 能为某个类添加成员变量,属性,方法;
  2. 一般的类扩展写到.m文件中;
  3. 一般的私有属性写到类扩展中

区别:

①类别中原则上只能增加方法(能添加属性的的原因只是通过runtime解决无setter/getter的问题而已);
②类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的(
用范围只能在自身类,而不是子类或其他地方);
③类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
④类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
⑤定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

15、KVO、KVC

KVC的本质就是 (键值编码)

定义: 在对象创建完成之后,动态(牵扯到运行时)的给对象的属性赋值
KVO 的本质就是(键值监听)

定义::Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。

KVO是基于KVC实现的,只有我们调用KVC去访问key值的时候KVO才会起作用。

KVO只用来监听属性值的变化,这个发送监听的操作是系统控制的,我们控制不了,我们只能控制监听操作
通知需要一个发送notification的对象,一般是notificationCenter,来通知观察者。KVO是直接通知到观察对象,并且逻辑非常清晰,实现步骤简单。
通知需要一个发送notification的对象,一般是notificationCenter,来通知观察者。KVO是直接通知到观察对象,并且逻辑非常清晰,实现步骤简单。

16、runtime

17、runloop

指导文章:http://www.swiftyper.com/2017/01/04/runloop-learning-note/

RunLoop 是一种事件驱动(Event Driven)模型,
但是一个 iOS 应用一旦启动起来后就会一直处于等待用户事件(类似点击或者触摸事件)的状态,除非用户手动关闭它,
当我们使用 Xcode 创建一个新的 iOS 应用时,它会自动帮我们生成 main 方法(这个方法在 main.m 文件中),而在 main 方法里面会调用 UIApplicationMain 方法。UIApplicationMain 会将 AppDelegate 设置为应用程序的代码,同时会为主线程创建一个 RunLoop 并启动,因此任何一个 iOS 应用一旦启动了,就至少存在一个 RunLoop,并且这个 RunLoop 是属于主线程的。之所以说属于主线程,是因为 RunLoop 与线程的关系是一一对应的。

其实事件驱动模型的本质就是一个死循环,然后在循环中不断等待消息的到来,然后对其进行处理,
RunLoop 实际上就是一个对象,它为我们提供了一个进入事件循环的入口函数,线程执行这个函数后就会处于消息循环中,直到循环结束(比如接收到退出消息)。

在 iOS 中提供了两个 RunLoop 对象:NSRunLoop 与 CFRunLoopRef。

其中,NSRunLoop 是基于 CFRunLoopRef 的简单封装。NSRunLoop 的 API 是非线程安全的,而 CFRunLoopRef 的 API 是线程安全的。更重要的一点是 CFRunLoopRef 的代码是开源的。

特点:1、使程序一直运行接收用户输入
2、决定程序何时处理哪些Event
3、调用解耦
4、节省cpu

NSDefaulRunLoopMode (默认状态,空闲状态)
NSTrackingRunLoopMode (滑动scrollView)
NSRunLoopCommonMode (上方两着都可执行)
列子: 当列表滑动的时候,NSTime 不会动。
解答: 因NSTime处于DefaulRunLoopMode,当滑动工程被切换到了TrackingRunLoopMode去关注滑动事件了,所以我们要将time设置为NSRunLoopCommonMode 

一个TableView延时加载图片的新思路
在cell里面把设置图片在 NSDefaulRunLoopMode中做,其滑动过程就不会设置图片,

18、View层架构、网络层架构、持久层架构、组件化架构、动态部署方案

19、UITableView的优化

20、二叉树、时间复杂度

21、TCP,UDP,HTTP

TCP也就是传输控制协议。它是一种面向连接的、可靠的、基于字节流的[传输层]通信协议。主要解决数据如何在网络中如何传输的,是一种长连接。建立一个TCP连接需要有三次握手,而终止一个TCP连接需要有4次握手,这是由TCP的半关闭(half-close)造成的。TCP包头的最小长度为20字节。大小不受限制,是可靠协议,安全送达

UDP也就是用户数据报协议。它与TCP相对应,是一种面向无连接的、不可靠的数据报传输协议,即不与对方建立连接,就直接就把数据包发送过去,因此缺乏可靠性。由于缺乏可靠性,UDP应用一般必须允许一定量的丢包、出错和复制。大小限制在64k之内无需连接,所以不可靠协议,但速度快

HTTP也就是超文本传输协议。HTTP是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

TCP与UDP的区别

(1)TCP是需要连接的传输协议(长连接),UDP是不需要连接的传输协议;
(2)TCP对系统资源要求较多,UDP要求较少;
(3)TCP的程序结构较复杂,UDP程序结构较简单;
(4)TCP是流模式,UDP是数据报模式 ;
(5)TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。

第三方库实现原理解析:

SDWebImage 如何为 UIImageView 添加图片(面试回答)

SDWebImage 中为 UIView 提供了一个分类叫做 WebCache, 这个分类中有一个最常用的接口, sd_setImageWithURL:placeholderImage:, 这个分类同时提供了很多类似的方法, 这些方法最终会调用一个同时具有 option progressBlock completionBlock 的方法, 而在这个类最终被调用的方法首先会检查是否传入了 placeholderImage 以及对应的参数, 并设置 placeholderImage.

然后会获取 SDWebImageManager 中的单例调用一个 downloadImageWithURL:... 的方法来获取图片, 而这个 manager 获取图片的过程有大体上分为两部分, 它首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以 url 作为数据的索引先在内存中寻找是否有对应的缓存, 如果缓存未命中就会在磁盘中利用 MD5 处理过的 key 来继续查询对应的数据, 如果找到了, 就会把磁盘中的缓存备份到内存中.

然而, 假设我们在内存和磁盘缓存中都没有命中, 那么 manager 就会调用它持有的一个 SDWebImageDownloader 对象的方法 downloadImageWithURL:... 来下载图片, 这个方法会在执行的过程中调用另一个方法 addProgressCallback:andCompletedBlock:forURL:createCallback: 来存储下载过程中和下载完成的回调, 当回调块是第一次添加的时候, 方法会实例化一个 NSMutableURLRequest 和 SDWebImageDownloaderOperation, 并将后者加入 downloader 持有的下载队列开始图片的异步下载.

而在图片下载完成之后, 就会在主线程设置 image 属性, 完成整个图像的异步下载和配置.

总结SDWebImage 的图片加载过程其实很符合我们的直觉:
查看缓存
缓存命中
返回图片
更新 UIImageView
缓存未命中
异步下载图片
加入缓存
更新 UIImageView

UITableView重用cell

UITableViewCell的复用机制是,在tableview中存在一个复用池.这个复用池是一个队列或一个链表.然后通过dequeueReusableCellWithIdentifier:获取一个cell,如果当前cell不存在,即新建一个cell,并将当前cell添加进复用池中.如果当前的cell数量已经到过tableview所能容纳的个数,则会在滚动到下一个cell时,自动取出之前的cell并设置内容.

排序算法

冒泡排序

NSMutableArray *arr_M = [NSMutableArray arrayWithObjects:@1,@4,@2,@3,@5,nil];
    //遍历`数组的个数`次
    /*
     i = 0 的时候,j的相邻两个位置都要比较排一下位置:
         j = 0 的时候:arr_M = 41235
         j = 1 的时候:arr_M = 42135
         j = 2 的时候:arr_M = 42315
         j = 3 的时候:arr_M = 42351
     i = 1;
      ……  ……
     */
    for (int i = 0; i < arr_M.count; ++i) {

        //遍历数组的每一个`索引`(不包括最后一个,因为比较的是j+1)
        for (int j = 0; j < arr_M.count-1; ++j) {
            //根据索引的`相邻两位`进行`比较`
            if (arr_M[j] < arr_M[j+1]) {
                [arr_M exchangeObjectAtIndex:j withObjectAtIndex:j+1];
            }
        }
    }
    NSLog(@"最终结果:%@",arr_M);

选择排序

NSMutableArray *arr_M = [NSMutableArray arrayWithObjects:@1,@4,@2,@3,@5,nil];

    for (int i=0; i<arr_M.count; i++) {

        for (int j=i+1; j<arr_M.count; j++) {

            if (arr_M[i]<arr_M[j]) {

                [arr_M exchangeObjectAtIndex:i withObjectAtIndex:j];

            }

        }
    }
    NSLog(@"%@",arr_M);

copy与mutableCopy 深复制和浅复制

copy 拷贝的对象为不可修改的,而mutableCopy拷贝的对象为可改变的对象,即使被复制的对象本身为不可改变的,调用mutableCopy方法复制出来的副本也是可修改的,当程序调用对象的copy方法来复制自身时,底层需要调用copyWithZone:方法来完成实际的复制工作,copy返回实际上就是copyWithZone:方法的返回值;mutableCopy与mutableCopyWithZone:方法也是同样的道理。

浅复制和深复制:顾名思义,浅复制,并不拷贝对象本身,仅仅是拷贝指向对象的指针;深复制是直接拷贝整个对象内容到另一块内存中。再简单些说:浅复制就是指针拷贝;深复制就是内容拷贝。

AsyncSocket

考虑到数据量比较大的问题,所以数据大的时候我会使用分包和组包的功能去实现。TCP/IP在传输数据的时候,一般不会大于1500字节,所以我每512字节分了

一个包。然后当一次性数据包接收太多的时候,就出现了粘包的问题。因为我在数据传输的时候使用的是json,每一个分包都是由{}括起来的,所以我就想着在包头上加上一段基本不会重复

的分割字符串,然后服务器接收到分包的时候每次都根据这个字符串分割一下,第一次分割的时候第一行绝对是空字符串 例如:@Hinagiku{“Name”=“桂雏菊”}, 我分割出来结果是:

“”,“桂雏菊”,所以说第一行我就可以直接跳过,每次取分包的时候从第二行开始取。然后后台根据包的ID号,序号进行组包。如果当前分包在5分钟内没有接收完毕,就代表当前分包接收失败

了,要求客户端或服务器重新发送。粘包问题解决完毕之后,我开始实现心跳包功能,当时想的是,每隔1分钟发一次心跳包,服务器放一个线程。每隔几秒钟判断一次,当前的所有TCP连接的
最后一次访问时间是多少号,如果超过了这个时间则断开当前连接

tag参数是为了在回调方法中匹配发起调用的方法的,不会加在传输数据中。
需注意的一点是:发送时的tag和接收时的tag是无关的。
以read为例分析:

  • (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag
    上面的方法会生成一个数据类:AsyncReadPacket,此类中包含tag,并把此对象放入数组theReadQueue中。
    在CFStream中的回调方法中,会取theReadQueue最新的一个,在回调方法中取得tag,并将tag传
    给回调方法:
  • (void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag;
    如此而已