MacDeveloper面试题技术面试题收集

iOS知识汇总

2021-08-05  本文已影响0人  _我和你一样

1.网络

1.网络七层协议有哪些?

物理层:

主要功能:传输比特流;

典型设备:集线器、中继器;

典型协议标准和应用:V.35、EIA/TIA-232

数据链路层:

主要功能:保证无差错的疏忽链路的

典型设备:交换机、网桥、网卡

典型协议标准和应用:802.2、802.3ATM、HDLC、FRAME RELAY

网络层:

主要功能:路由、寻址

典型设备:路由器

典型协议标准和应用:IP、IPX、APPLETALK、ICMP

传输层:

主要功能:端到端控制

典型设备:网关

典型协议标准和应用:TCP、UDP、SPX

会话层:

主要功能:会话的建立和结束

典型设备:网关

典型协议标准和应用:RPC、SQL、NFS、X WINDOWS、ASP

表示层:

表示层相当于一个东西的表示,表示的一些协议,比如图片声音视频MPEG等

主要功能:数据表示、压缩和加密

典型设备:网关

典型协议标准和应用:ASCLL、PICT、TIFF、JPEG|MPEG

应用层:

主要功能:应用接口、应用程序

典型设备:网关

典型协议标准和应用:TELNET、FTP、HTTP

2.Http 和 Https 的区别?Https为什么更加安全?

区别:

  1. HTTP是明文传输, https是基于SSL进行的加密传输、有身份验证环节,更加安全
  2. HTTP默认端口号是80,HTTPS默认端口号是443
  3. HTTPS需要CA证书,极少免费。

安全的原因:HTTPS是基于SSL(安全套接字)和TLS(传输层安全),对网络进行加密处理,保障数据的完整性,更加安全。

3.HTTPS的连接建立流程

HTTPS为了兼顾安全与效率,同时使用了对称加密和非对称加密。在传输中涉及到3个密钥:服务端到公钥和私钥用于非对称加密,客户端生成的随机密钥用来进行对称加密

image.png

如图HTTPS连接过程大致分为8步

  1. 客户端访问HTTPS连接:客户端把安全协议版本号、客户端支持的加密算法列表、随机数C发给服务端。
  2. 服务端发送证书给客户端:服务端接受密钥算法配件后,会和自己支持的加密算法列表进行对比,如果不符合则断开连接。否则会在该算法列表中选择一种对称算法(如AES)、一种公钥算法(如具有特定密钥长度的RSA)和一种MAC算法发给客户端。服务端有一个密钥对(公钥和私钥)用来进行非对称加密使用,服务端保存着私钥,公钥可以发送给任何人。在发送加密算法的同时还会把数字证书和随机数S发送给客户端
  3. 客户端验证server证书:客户端会对server公钥进行检查,验证其合法性,如果公钥有问题,那么HTTPS传输就无法继续。
  4. 客户端组装会话密钥:如果公钥合格,那么客户端就会用服务端公钥来生成一个前主密钥(Pre-Master Secret,PMS),并通过该前主密钥和随机数C、S来组装成会话密钥
  5. 客户端将前主密钥发送给服务端:通过服务端端公钥对前主密钥进行非对称加密,发送给服务端
  6. 服务端通过私钥解密得到前主密钥:服务端接受到加密信息后,用私钥解密得到前主密钥。
  7. 服务端组装会话密钥:服务端通过前主密钥和随机数C、S来组装会话密钥。至此,服务端和客户端都已经知道了用于此次会话都都主密钥。
  8. 数据传输:客户端收到服务器发送来的秘文,用客户端密钥对其进行对称解密,得到服务器发送的数据。同理,服务端收到客户端发送的秘文,用服务端密钥进行对称解密得到客户端发送的数据。

4.解释一下 三次握手 和 四次挥手

5.TCP 和 UDP的区别

TCP:面向连接、传输可靠(保证数据正确性、保证数据顺序),用于传输大量数据(流模式),速度慢、建立连接需要开销较多(时间、系统资源)

UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快

6.Cookie和Session

Cookie:cookie主要用来记录用户状态,区分用户,状态保存在客户端。cookie功能需要浏览器支持。如果浏览器不支持cookie(如大部分手机中的浏览器)或者把cookie禁用了,cookie功能就会失效。

  • cookie的使用:首次访问某网站时,客户端会发送一个HTTP请求到服务器,服务器发送一个HTTP响应到客户端,其中包含set-cookie头部;客户端再发送HTTP请求时,包含cookie头部,服务端返回HTTP响应到客户端;隔断时间再访问时,客户端会直接发包含cookie头部的http请求,服务端响应客户端

  • cookie的修改和删除:在修改cookie的时候,只需要新cookie覆盖旧cookie即可,在覆盖的时候,由于cookie具有不可跨域名性,主要name、path、domain需与原cookie一致。删除cookie也一样,设置cookie的过期时间为过去的一个时间点或者maxage=0即可。

  • cookie的安全:cookie的使用存在争议,因为它被认为是对用户隐私的侵害,并且cookie并不安全。http协议不仅是无状态的,而且是不安全的,使用http协议的数据使用明文在网络传播有被截获的可能。如果不希望cookie在http非安全协议中传输可以设置cookie的Secure属性为true,浏览器只会在https和ssl等安全协议中传输此类cookie。Secure属性并不能对cookie内容加密,因而不能保证却对安全。如果需要提高安全性,需要在程序中对cookie进行加解密。除了Secure属性,也可设置cookie为httponly,如果设置此属性,那么通过js脚本将无法读取到cookie信息,能有效防止XSS攻击(跨站脚本攻击)

Session:是服务端使用的 一种记录客户端状态的机制。使用上比cookie简单,响应的增加了服务器的存储压力。不同于cookie保存在客户端浏览器中,session保存在服务器上,客户端访问浏览器时,服务端把客户端信息以某种形式记录在服务器上,这就是session。客户端浏览器再次访问时只需要从该session 中查找该客户的状态就可以了。

当程序需要为某个客户端的请求创建session时,会先检索客户端的请求里是否包含session表示(称为sessionid)如果包含则说明之前已经为此客户端创建过会话,服务器就按标识把这个session检索出来使用,检索不到会新建一个。如果客户端请求不包含会话标识,服务端会创建一个session并生成关联的会话id,这个sessionid将在本次响应中返回给客户端保存。

  • cookie和session的区别
    • cookie存储在客户端浏览器上,session数据存在服务器上
    • cookie相比session不是很安全
    • session会在一定时间内保存在服务器上,当访问增多会占用服务器性能,考虑减轻服务器性能可以使用cookie
    • 单个cookie保存数据不能超过4k,很多浏览器都限制一个站点最多保存20个。而session没有限制
    • 所以可以将登陆等重要信息存放session,其他如果需要保留可以放在cookie中

7.DNS是什么?

域名系统:domain name system, 用于 主机名到ip地址到转换。

因特网上的主机可以用多种方式标识,比如主机名或ip地址。主机名比如www.baidu.com 这种方式便于人们记忆和接受,但这种长度不一没有规律的字符串不方便路由器处理。路由器比较热衷于 定长的有清晰层次结构的ip地址。为了这种这两种方式,我们需要一种能进行主机名到ip地址转换到服务,这就是域名系统DNS。

DNS是一个由分层的DNS服务器实现的分布式数据库。一个使得主机能够查询分布式数据库的应用层协议: DNS服务器通常是运行BIND软件的UNIX机器,DNS协议运行在UDP上使用53号端口。DNS通常是由其他应用层协议所使用的,包括HTTP、SMTP等。其作用是将用户提供的主机名解析为ip地址。DNS的一种简单设计就是在因特网上只使用一个DNS服务器,该服务器包含所有的映射。很明显这种设计有很大问题:单点故障:如果该DNS服务器崩溃全世界网络随之瘫痪;通信容量:单个DNS服务器必须处理所有的DNS查询;远距离的集中式数据库:单个DNS服务器必须面对所有用户,距离过远会有严重时延。维护:该数据库过于庞大,还需要对新添加的主机频繁更新。所以DNS被设计成了一个分布式、层次数据库。

8.DNS解析过程

www.163.com为例:

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

2.多线程

1.进程与线程分别是什么意思?

进程是系统中正在运行的程序,就是一段程序的执行过程,我们可以理解为手机上一个正在运行的app。进程是操作系统分配资源的基本单元。每个进程之间相互独立,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源。

线程是程序执行的最小单元,是进程中的一个实体。一个进程想要执行任务必须至少有一条线程。应用程序启动时,系统会默认开启一条线程,也就是主线程。

进程和线程的关系:线程是进程的执行单元,进程中的所有任务都是在线程中执行的;线程是CPU分配资源和调度的最小单元;一个程序可以对应多个进程,一个进程至少有一条线程;同一个进程内的线程共享进程资源。

2.什么是多线程?

一个进程至少有一个线程,如果有多个线程在执行就是多线程。多线程的实现原理,如果是单核CPU,则是CPU在多个线程之间快速切换,造成多个线程同时执行的假象。如果是多核CPU,就真的可以实现多个线程同时执行。多线程的目的是为了同步完成多项任务,通过提高系统资源的利用率来提高工作效率。

3.多线程的优点和缺点有哪些?

优点:能适当的提高程序的执行效率;能适当提高资源的利用率(CPU、内存利用率)

缺点:每个线程都有一定的开销,从而影响性能。不仅有创建时的时间开销,还有消耗内存。每个线程大约消耗1kb的内核内存空间,用于存储与线程有关的数据结构和属性,这块儿内存是联动内存,无法被分页。此外还占用一定的栈空间,默认主线程占1M,子线程占512k,注意完整的栈不会被立即创建,实际消耗的栈空间随着使用而增加,如果开启大量线程,会占用大量内存空间降低程序性能;线程越多CPU调度线程的开销就越大。程序设计更加复杂比如线程之间的通信多线程的数据共享等。

4.多线程的 并行 和 并发 有什么区别?

并行:充分利用计算机的多核,在多个线程上同步进行

并发:CPU快速切换线程,让人感觉在同步进行

5.iOS中实现多线程的几种方案,各自有什么特点?

NSThread 面向对象,需手动创建线程,但不需要手动销毁。自线程间通信很难;

GCD C语言,充分利用了设备的多核,自动管理线程生命周期。比NSOperation高效;

NSOPeration 基于GCD封装,更加面向对象。比GCD多了一些功能,比如设置最大并发数,可以监控任务状态,轻松设置任务依赖等。

6.多个网络请求完成后如何执行?

  • 使用GCD的任务组

创建一个任务组 dispatch_group_t,每次网络请求前先进入组 dispatch_group_enter,请求回调后再离开组 dispatch_group_leave。进入和离开必须成对使用,否则组会一直存在。当所有enter都leave之后,会执行组通知dispatch_group_notify的block。

  • 使用GCD的信号量

信号量是基于计数器的一种多线程同步机制。如果信号计数大于1,计数-1返回,程序继续执行。如果计数为0则等待。

增加信号量 dispatch_semaphore_signal(semaphore)为计数+1操作。dispatch_semaphore_wait(sema,DISPATCH_TIME_FOREVER)为设置等待的时间,这里设置的是一直等待。

创建信号量为0,等待。等10个网络请求都完成了,信号量dispatch_semaphore_signal计数为1,然后计数-1返回。

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {
    
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%d---%d",i,i);
        count++;
        if (count==10) {
            dispatch_semaphore_signal(sem);
            count = 0;
        }
    }];
    [task resume];
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"end");
});

7.多个网络请求顺序执行后如何执行下一步?

使用信号量,每一次遍历,都让信号量等待,阻塞当前线程,直到信号增加调用之后

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {
    
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        NSLog(@"%d---%d",i,i);
        dispatch_semaphore_signal(sem);
    }];
    
    [task resume];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"end");
});

8.如何理解多线程中的死锁?

死锁是由于多个线程(进程)在执行过程中,因为争夺资源而造成的相互等待的现象。可以理解为卡住了。产生死锁的必要条件有4个:

  • 互斥条件:指进程对分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用,如果此时还有其他进程请求资源,则请求者只能等待,直到占有资源的进程用毕释放。
  • 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有,此时请求阻塞,但又对自己已获得的其他资源保持不放。
  • 不可剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程资源的环形链,即进程集合P{P0,P1,P2,...,Pn}中的P0正在等待一个P1占用的资源,P1正在等待P2占用的资源...Pn正在等待已被P0占用的资源
  • 最常见的就是同步函数+主队列的组合,本质是队列阻塞

9.如何去理解GCD执行原理?

GCD 维护着一个线程池,这个池中存放着一个个线程。这个池中线程是可以重用的,当一个段时间后这个线程没有调用的话,这个线程就会被销毁。注意开多少条线程是由底层线程池决定的(线程建议控制在3-5条),池是系统自动维护,不需要程序员干预。程序员需要关心的是向队列中添加任务,队列调度即可。

如果队列中存放的是同步任务,则任务出队之后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们调用current Thread的时候,就是同一条线程。

如果队列中存放的是异步任务(注意异步可以开线程),当任务出队之后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需要等待当前任务执行完毕就可以调度下一个任务,这时线程池会再次提供一个线程供第二个任务执行,执行完毕后再回到线程池中。

这样就对线程完成一个复用,而不需要每一个任务都开启新的线程。也就节约了系统开销,提高了效率。

3.架构 + 设计模式

一,项目架构

1.MVC、MVP、MVVM模式

MVC是比较直观的架构模式,最核心的就是通过Controller层来进行调控,首先看一下官方提供的MVC示意图:

image.png

Controller可以直接与View对话,通过IBoutlet直接操作View,IBoutlet直接对应View的控件(例如创建一个Button:需声明一个 IBOutlet UIButton * btn),View通过action向Controller报告时间的发生(用户点击了按钮)。Controller是View的直接数据源

MVP(Model、View、Presenter)

MVP模式是MVC模式的一个演化版本,其中Model与MVC模式中Model层没有太大区别,主要提供数据存储功能,一般都是用来封装网络获取的json数据;View与MVC中的View层有一些差别,MVP中的View层可以是viewController、view等控件;Presenter层则是作为Model和View的中介,从Model层获取数据之后传给View。


image.png

从上图可以看出,从MVC模式中增加了Presenter层,将UIViewController中复杂的业务逻辑、网络请求等剥离出来。

2.关于RAC你有怎样运用到解决不同API依赖关系

3.@weakify和我们宏定义的WeakSelf有什么区别?

  1. 微服务的架构设想

MVVM(Model、Controller/View、ViewModel)

在MVVM中,view和ViewCOntroller联系在一起,我们把它们视为一个组件,view和ViewController都不能直接引用model,而是引用是视图模型即ViewModel。viewModel是一个用来放置用户输入验证逻辑、视图显示逻辑、网络请求等业务逻辑的地方,这样的设计模式,会轻微增加代码量,但是会减少代码的复杂性

合理的运用架构模式有利于项目、团队开发工作,但是到底选择哪个设计模式,哪种设计模式更好,就像本文开头所说,不同的设计模式,只是让不同的场景有了更多的选择方案。根据项目场景和开发需求,选择最合适的解决方案。

关于RAC你有怎样运用到解决不同API依赖关系

@weakify和我们宏定义的WeakSelf有什么区别?

二,设计模式
  1. iOS有哪些常见的设计模式?

    单例模式:单例保证了应用程序生命周期内仅有一个该类的实例对象,而且易于外界访问。在iOS SDK中,UIApplication, NSBundle, NSNotificationCenter, NSFileManager, NSUserDefault, NSURLCache等都是单例.

    委托模式:委托代理是协议的一种,通过protocol方式实现,常见的有tableview及textFiled都采用了委托代理模式。

    观察者模式:观察者模式定义了一种一对多的依赖关系,让多个观察者同时监听某一个主题对象。在iOS中,观察者模式的具体实现有两种:通知机制 和 KVO

  2. 单例会有什么弊端?

优点:提供了对唯一实例的受控访问。系统内存中只存在一个对象,可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统性能。允许可变数目的实例。

缺点:单例模式中没有抽象层,因此但李磊的扩展有很大困难。单例类的职责过重,在一定程度上违背了单一职责原则。滥用单例会有负面问题,比如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象过多而出现连接池溢出。

  1. 编程中的六大设计原则?

    • 单一职责原则:一个类只做一件事,CALayer负责动画和视图的显示。UIView只负责事件的传递和响应

    • 开闭原则:对修改关闭,对扩展开放。要考虑到后续的扩展性,而不是在原有的基础上来回修改

    • 接口隔离原则:使用多个专门的协议,而不是一个庞大臃肿的协议,如UITableviewDelegate + UITableViewDataSource

    • 依赖倒置原则:抽象不应该依赖于具体实现,具体实现可以依赖于抽象。调用接口感觉不到内部是如何操作的

    • 里式替换原则:父类可以被子类无缝替换,且原有功能不受任何影响。如kvo

    • 迪米特法则:一个对象应当对其他对象尽可能少的了解,实现高聚合、低耦合

  2. 如何设计一个图片缓存框架?

    可以模仿sdwebimage。图片的存储是以图片的单向hash值为key。内存设计需要考虑存储的大小。因为内存空间有限,我们针对不同的图片给出不同的方案。比如 10k以下的50个,100k以下的20个,100k以上的10个。

    内存淘汰策略可以采用LRU,最近最少使用算法。触发淘汰策略的时机有三种:定期检查(不建议,耗性能)前后台切换时、每次读写时。

    磁盘设计需要考虑的问题:存储方式,移除策略可以设置为7天或者15天,图片请求的最大并发量,请求超时策略,请求优先级。

    图片解码:应用策略模式,针对jpg,png,gif等不同格式的图片进行解码。图片解码的时机,在子线程图片刚下载完时,在子线程刚从磁盘读取完时,避免在主线程压缩、解码,避免卡顿。

  3. 如何设计一个时长统计框架?

    记录器:页面式记录器、流式记录器、自定义式

    记录管理者:内存记录缓存、磁盘存储、上传器

    如何降低数据丢失率:定期写入磁盘、每当达到某个值时写入磁盘

    记录上传时机:前后台切换的时候可以上传、从无网络切换到有网络的时候可以上传

    上传时机的选择:立即上传、定时上传、延时上传

4.组件化+性能优化

组件化有什么好处?

你是如何组件化解耦的?

为什么CTMediator方案优于Router方案?

Router的缺点:

CTMediator的优点:

基于CTMediator的组件化方案,有哪些核心组成?

性能优化:

造成tableview卡顿的原因有哪些?

如何提升tableview的流畅度?

iOS保持界面流畅的技巧

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

启动时间可以通过xcode提供的工具来度量,在xcode的product-scheme-edit scheme - run -auguments中,将环境变量DYLD_PRINT_STATISTICS设置为YES,优化需以下方面入手:

如何降低app包的大小?

降低包大小,需要从两方面着手:

怎么检测图文混合?

1、模拟器debug中color blended layers红色区域表示图层发生了混合

2、Instrument-选中Core Animation-勾选Color Blended Layers

避免图层混合:

UILabel图层混合解决方法:

iOS8以后设置背景色为非透明色并且设置label.layer.masksToBounds=YES让label只会渲染她的实际size区域,就能解决UILabel的图层混合问题

iOS8 之前只要设置背景色为非透明的就行

为什么设置了背景色但是在iOS8上仍然出现了图层混合呢?

UILabel在iOS8前后的变化,在iOS8以前,UILabel使用的是CALayer作为底图层,而在iOS8开始,UILabel的底图层变成了_UILabelLayer,绘制文本也有所改变。

在背景色的四周多了一圈透明的边,而这一圈透明的边明显超出了图层的矩形区域,设置图层的masksToBounds为YES时,图层将会沿着Bounds进行裁剪 图层混合问题解决了

日常如何检查内存泄漏?

目前我知道的方式有以下几种

泄露的内存主要有以下两种:

如何优化app电量?

CPU 处理
定位
网络
图像

尽可能降低 CPU、GPU 的功耗。
尽量少用 定时器。
优化 I/O 操作。

不要频繁写入小数据,而是积攒到一定数量再写入
读写大量的数据可以使用 Dispatch_io ,GCD 内部已经做了优化。
数据量比较大时,建议使用数据库

减少压缩网络数据 (XML -> JSON -> ProtoBuf),如果可能建议使用 ProtoBuf。
如果请求的返回数据相同,可以使用 NSCache 进行缓存
使用断点续传,避免因网络失败后要重新下载。

网络不可用的时候,不尝试进行网络请求
长时间的网络请求,要提供可以取消的操作
采取批量传输。下载视频流的时候,尽量一大块一大块的进行下载,广告可以一次下载多个

如果只是需要快速确定用户位置,最好用 CLLocationManager 的 requestLocation 方法。定位完成后,会自动让定位硬件断电
如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
尽量降低定位精度,比如尽量不要使用精度最高的 kCLLocationAccuracyBest

需要后台定位时,尽量设置 pausesLocationUpdatesAutomatically 为 YES,如果用户不太可能移动的时候系统会自动暂停位置更新
尽量不要使用 startMonitoringSignificantLocationChanges,优先考虑 startMonitoringForRegion:

用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件

5.Runloop

1.Runloop 和线程的关系?

一个线程对应一个runloop,主线程的runloop时默认开启的,子线程的runloop以懒加载的形式创建。runloop存储在一个全局的可变字典中,线程是key,runloop是value。

2.RunLoop的运行模式

runloop的运行模式一共有5种,runloop只会运行在一个模式下,要切换模式,就要暂停当前的模式,重新启动一个运行模式

- kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行

- UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)

- kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式

- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用

-GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到

3.runloop内部逻辑?

实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

image.png

内部逻辑:

4.autoreleasePool 在何时被释放?

  1. GCD 在Runloop中的使用?

6.AFNetworking 中如何运用 Runloop?

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。

为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

runloop启动前内部必须至少有一个 timer、observer、source。所以AFNetworking在runloop run 之前先创建了一个新的NSMachPort添加进去了。

通常情况下,调用者需要持有这个machport并在外部线程通过这个port发送消息到loop内,但此处添加port只是为了让runloop不至于退出,并没有用于实际的发送消息。

当需要这个后台线程执行任务时,AFNetworking通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
  1. PerformSelector 的实现原理?

8.PerformSelector:afterDelay:这个方法在子线程中是否起作用?

9.事件响应的过程?

10.手势识别的过程?

11.CADispalyTimer和Timer哪个更精确?

6.Runtime

1.Category 的实现原理?

2.isa指针的理解,对象的isa指针指向哪里?isa指针有哪两种类型?

实例对象的 isa 指向类对象

类对象的 isa 指向元类对象

元类对象的 isa 指向元类的基类

纯指针,指向内存地址

NON_POINTER_ISA,除了内存地址,还存有一些其他信息

3.Objective-C 如何实现多重继承?

Object-c的类没有多继承,只支持单继承,如果要实现多继承的话,可使用如下几种方式间接实现

A和B组合,作为C类的组件

C类实现A和B类的协议方法

forwardInvocation:方法

4.runtime 如何实现 weak 属性?

weak 此特质表明该属性定义了一种「非拥有关系」(nonowning relationship)。

为这种属性设置新值时,设置方法既不持有新值(新指向的对象),也不释放旧值(原来指向的对象)。

runtime 对注册的类,会进行内存布局,从一个粗粒度的概念上来讲,这时候会有一个 hash 表,这是一个全局表,表中是用 weak 指向的对象内存地址作为 key,用所有指向该对象的 weak 指针表作为 value。

当此对象的引用计数为 0 的时候会 dealloc,假如该对象内存地址是 a,那么就会以 a 为 key,在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil。

runtime 如何实现 weak 属性具体流程大致分为 3 步:

5.讲一下 OC 的消息机制

6.runtime具体应用

7.runtime如何通过selector找到对应的IMP地址?

每一个类对象中都一个对象方法列表(对象方法缓存)

8.简述下Objective-C中调用方法的过程

Objective-C是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:

9.load和initialize的区别?

两者都会自动调用父类的,不需要super操作,且仅会调用一次(不包括外部显示调用).

10.怎么理解Objective-C是动态运行时语言?

也就是不同的对象以自己的方式响应了相同的消 息(响应了eat这个选择器)。因此也可以说,运行时机制是多态的基础.

KVO怎么用? 实现方式是什么。可不可以多次监控(addObserve)? 可不可以多次移除?? 线程 A 监控,线程 B 改变值, 最终体现在哪个线程

KVO怎么用:对被观察的对象添加观察者,同时被观察者要实现 改变方法的回调。

KVO的实现原理:Apple利用isa-swizzling来实现KVO,当某个类的对象属性第一次被观察时,系统会在运行期间动态的创建该类的派生类,在这个派生类中重写任何被观察属性的setter方法。每个类对象中都有一个isa指针,指向当前类。当一个类对象第一次被观察时,系统就会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时,实际执行的是派生类的setter方法。键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:,在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而observeValueForKey:ofObject:change:context:也会被调用

可不可以多次监控:可以多次监控,能收到多次回调。

可不可以多次移除:不可以多次移除,添加和移除需要成对出现,只有已经注册了的观察者才可以被移除,否则会报错。

线程 A 监控,线程 B 改变值, 最终体现在哪个线程:体现在线程B,这是由派生类的setter方法来决定的,在哪个线程改变就体现在哪个线程。

7. 内存管理

1.什么情况使用weak关键字,相比assign有什么不同?

在ARC中,在有可能出现循环引用的时候,往往需要让其中一端使用weak来打破循环引用。比如delegate代理属性;当自身已经对某对象进行一次强引用,没必要再强引用一次,此时也会使用weak,比如从图形化界面引入到代码中 IBOutlet控件一般使用weak,当然也可以使用strong。

与assign不同之处在于,weak表明该属性定义了一种非拥有关系 nonowning relationship.为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质与assign类似,不同之处在于 属性所指的对象销毁时,属性值会设置为nil。

assign的设置方法只会执行针对 纯量类型(scalar type,比如 CGFloat或NSInteger等 )的简单赋值操作。assign可以用非OC对象,而weak必须用于OC对象。

2.如何让自己的类用copy修饰符?如何重写带copy关键字的setter?

如果想让自定义对象具有拷贝功能,需要实现nscoping协议

如果自定义对象分为可变版本与不可变版本,那么需要同时实现NSCopying与NSMutableCopying协议。

首先,声明该类遵守NSCopying协议,实现协议方法:- (id)copyWithZone:(NSZone *)zone;

重写带copy关键字的setter方法

- (void)setName:(NSString *)name {
    //[_name release];
    _name = [name copy];
}

3.深拷贝与浅拷贝分别是什么?

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝是对指针指向的内容进行拷贝,经深拷贝后的指针指向的是两个不同地址的指针。当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:

  • 当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过构造函数实现
  • 当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用户返回函数调用处
  • 一般来说我们倾向于 copy方法是浅拷贝,mutablecopy是深拷贝。但也不必然,这取决于copy协议及可变copy的具体实现的方法

4.@property的本质是什么?ivar、getter、setter是如何生成并添加到这个类中的?

属性的本质是 实例变量 ivar + 存取方法 getter + setter

属性作为oc的一项特性,主要作用在于封装对象中的数据,OC对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过存取方法来访问,getter用于读取变量的值,而setter用于写入变量的值

ivar、getter、setter是自动合成这个类中,完成属性定义后,编译器会自动编写访问这些属性所需的方法,这个过程叫做自动合成autosynthesis,需要强调的是,这个过程由编译器在编译器执行,所以编辑器里看不到这些合成方法synthesized method的源代码。除了生成方法 getter和setter之外,编译器还要自动向类中添加适当类型的实例变量,并在属性名前加下划线,以此作为实例变量的名字。也可以在类的实现代码里,通过@synthesize语法来制定实例变量的名字。

5.@protocol和category中如何使用@property

6.使用CADisplayLink、NSTimer有什么注意点?BAD_ACCESS在什么情况下出现?

CADisplayLink、NSTimer可能会造成循环引用,可以使用YYWeakProxy或者为CADisplayLink、NSTimer添加block来解决循环引用,或者使用GCD的定时器

BAD_ACCESS会在访问野指针时出现,就是说访问了一个已经被释放了的对象会出现。死循环。

7.iOS内存分区情况

注意:在iOS中,堆区的内存是应用程序共享的,堆中的内存分配是系统负责的。系统使用一个链表来维护所有已经分配的内存空间(系统紧紧记录,并不管理具体的内容);变量使用结束后,需要释放内存,OC中是判断引用计数是否为0,如果是就说明没有任何变量使用该空间,那么系统将其回收;当一个app启动后,代码区、常量区、全局区大小就已经固定,因此指向这些区的指针不会产生崩溃性错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这个区域里面的内存时,一定要注意内存是否已经被释放,否则会出现野指针报错使程序崩溃。

8.iOS内存管理方式

9.循环引用

循环引用的实质:多个对象相互之间有强引用,不能释放让系统回收。

如何解决循环引用?

1、避免产生循环引用,通常是将 strong 引用改为 weak 引用。比如在修饰属性时用weak 在block内调用对象方法时,使用其弱引用,这里可以使用两个宏

#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self; // 弱引用
#define ST(strongSelf) __strong __typeof(&*self)strongSelf = weakSelf;

//使用这个要先声明weakSelf 还可以使用__block来修饰变量 在MRC下,__block不会增加其引用计数,避免了循环引用 在ARC下,__block修饰对象会被强引用,无法避免循环引用,需要手动解除。

2、在合适时机去手动断开循环引用。通常我们使用第一种。

delegate 是iOS中开发中比较常遇到的循环引用,一般在声明delegate的时候都要使用弱引用 weak,或者assign,当然怎么选择使用assign还是weak,MRC的话只能用assign,在ARC的情况下最好使用weak,因为weak修饰的变量在释放后自动指向nil,防止野指针存在

在控制器内,创建NSTimer作为其属性,由于定时器创建后也会强引用该控制器对象,那么该对象和定时器就相互循环引用了。如何解决呢?这里我们可以使用手动断开循环引用:如果是不重复定时器,在回调方法里将定时器invalidate并置为nil即可。如果是重复定时器,在合适的位置将其invalidate并置为nil即可

3、block循环引用

一个简单的例子:

@property (copy, nonatomic) dispatch_block_t myBlock;
@property (copy, nonatomic) NSString *blockString;

- (void)testBlock {
    self.myBlock = ^() {
        NSLog(@"%@",self.blockString);
    };
}

由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。解决方案就是使用__weak修饰self即可

__weak typeof(self) weakSelf = self;

self.myBlock = ^() {
        NSLog(@"%@",weakSelf.blockString);
 };

并不是所有block都会造成循环引用。只有被强引用了的block才会产生循环引用 而比如dispatch_async(dispatch_get_main_queue(), ^{}),[UIView animateWithDuration:1 animations:^{}]这些系统方法等 或者block并不是其属性而是临时变量,即栈block

[self testWithBlock:^{
    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
    block();
}

还有一种场景,在block执行开始时self对象还未被释放,而执行过程中,self被释放了,由于是用weak修饰的,那么weakSelf也被释放了,此时在block里访问weakSelf时,就可能会发生错误(向nil对象发消息并不会崩溃,但也没任何效果)。

对于这种场景,应该在block中对 对象使用__strong修饰,使得在block期间对 对象持有,block执行结束后,解除其持有。

__weak typeof(self) weakSelf = self;

self.myBlock = ^() {

        __strong __typeof(self) strongSelf = weakSelf;

        [strongSelf test];
 };

10.ARC 的 retainCount 怎么存储的?

存在64张哈希表中,根据哈希算法去查找所在的位置,无需遍历,十分快捷

散列表(引用计数表、weak表)

提升执行效率

11.ARC 在编译时做了哪些工作?

根据代码执行的上下文语境,在适当的位置插入 retain,release

8.数据结构

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

2.集合结构 线性结构 树形结构 图形结构

3.单向链表 双向链表 循环链表 分别为什么?

image.png image.png

4.数组和链表区别有哪些?

5.堆、栈和队列 分别是什么?

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

二叉树的节点定义如下
struct BinaryTreeNode{
  int m_nValue;
  BinaryTreeNode* m_pLeft;
  BinaryTreeNode* m_pRight;
}

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

int TreeDepth(TreeNode *pRoot){
  if(pRoot == nullptr) return 0;
  int left = TreeDepth(pRoot->left);
  int right = TreeDepth(pRoot->right);
  return left > right ? left + 1 : right + 1 
}

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

8.字符匹配 & 字符去重的方法

给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)

1.什么叫字典序:26个英文字母的顺序

2.什么叫字符的相对位置,就是你只能删除重复的但是不能左右移动元素

我们来模拟下从第一个元素到最后一个元素的过程算法就出来了:

step1:c

step2:cb

step3:cba

step4:bac

step5:bacd

step6:acdb

按照我们的思维来写算法:定义一个结果集,依次遍历每个元素,结果集不存在就加入,如果存在就判断两个元素之间的元素有没有比当前元素小的,有的话就删掉之前的,比如bacdb,两个b之间有a比b小,所以要删掉之前的b,改成acdb

这里用另一个方法栈+标志来实现

说明:栈中存的元素满足两个条件之一就可以:1.元素都是递增的,2,当前元素在后续不在出现(过了这个村就没这个店了)

public static String removeDuplicateLetters(String s) {
        if (s == null || s.equals("")) {
            return "";
        }
        char[] strArr = s.toCharArray();
        return getResult(strArr);
    }
public static String getResult(char[] strArr) {
    String result = "";
    //记录所有几点的最后一个出现的位置
    Map<Character, Integer> memory = new HashMap<>();
    //记录哪些节点已经放入了栈中
    Map<Character, Boolean> memory2 = new HashMap<>();
    Stack<Character> stack = new Stack();
 
    for (int i = 0; i < strArr.length; i++) {
        memory.put(strArr[i], i);
        memory2.put(strArr[i], false);
    }
    //第一个元素特殊处理下便于理解
    stack.add(strArr[0]);
    memory2.put(strArr[0], true);
 
    for (int i = 1; i < strArr.length; i++) {
        //如果节点没有出现在栈中
        if (!memory2.get(strArr[i])) {
            //如果栈顶节点大于当前节点并且该节点后续还会出现,取出栈顶节点然后继续取栈第二节点继续比较
            while (!stack.isEmpty() && strArr[i] < stack.peek()) {
                if (memory.get(stack.peek()) > i) {
                    memory2.put(stack.peek(), false);
                    stack.pop();
                } else {
                    break;
                }
            }
            //当循环完了后,当前节点肯定大于栈顶元素,入栈,记录该节点已经入栈了
            stack.push(strArr[i]);
            memory2.put(strArr[i], true);
        }
    }
    StringBuilder stringBuilder = new StringBuilder();
    while (!stack.empty()) {
        stringBuilder.insert(0, stack.pop());
    }
    return stringBuilder.toString();
}
 
public static void main(String[] args) {
    String result = removeDuplicateLetters("bacacca");
    System.out.println(result);
}

9.算法

1.时间复杂度 / 空间复杂度

2.常用的排序算法有哪些?

常用的排序算法有:选择排序、冒泡排序、插入排序

都将数组分为已排序部分和未排序部分。

选择排序:已排序部分定义在左端,然后选择未排序的最小元素和未排序的第一个元素交互。

冒泡排序:已排序部分定义在右端,在遍历未排序部分的过程中进行交换,将最大元素交换到最右端。

插入排序:已排序部分定义在左端,将未排序部分的第一个元素插入到已排序部分合适的位置。

//选择排序:最值出现在起始端,
void selectSort(int * arr,int length){
  for (int i = 0; i < length -1 ;i++){//趟数
    for (int j = i+1;j < length; j++)//比较次数
    if (arr[i]>arr[j]){
      int temp = arr[i];
      arr[i] = arr[j];
      arr[j] = temp;
    }
  }
}
//冒泡排序:最值出现在末尾,比较的是相邻的元素
void bulletSort(int* arr,int length){
  for(int i = 0; i < length -1; i++){
    for(int j = 0; j< length - i - 1;j++){
      if (arr[j]>arr[j+1]){
        int temp = arr[j];
        arr[j] = arr[j+1];
        arr[j+1] = temp;
      }
    }
  }
}

/**
 *  折半查找:优化查找时间(不用遍历全部数据)
 *
 *  折半查找的原理:
 *   1> 数组必须是有序的
 *   2> 必须已知min和max(知道范围)
 *   3> 动态计算mid的值,取出mid对应的值进行比较
 *   4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
 *   5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
 *
 */ 
 
// 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置 
int findKey(int *arr, int length, int key) {
    int min = 0, max = length - 1, mid;
    while (min <= max) {
        mid = (min + max) / 2; //计算中间值
        if (key > arr[mid]) {
            min = mid + 1;
        } else if (key < arr[mid]) {
            max = mid - 1;
        } else {
            return mid;
        }
    }
    return -1;
}

3.字符串反转

void char_reverse (char *cha) {

    // 定义头部指针
    char *begin = cha;
    // 定义尾部指针
    char *end = cha + strlen(cha) -1;
    
    
    while (begin < end) {
        
        char temp = *begin;
        *(begin++) = *end;
        *(end--) = temp;
    }
}

4.链表反转(头差法)

struct Node* reverseList(struct Node *head)
{
    // 定义遍历指针,初始化为头结点
    struct Node *p = head;
    
    // 反转后的链表头部
    struct Node *newH = NULL;
    
    // 遍历链表
    while (p != NULL) {
        
        // 记录下一个结点
        struct Node *temp = p->next;
        // 当前结点的next指向新链表头部
        p->next = newH;
        // 更改新链表头部为当前结点
        newH = p;
        // 移动p指针
        p = temp;
    }
    
    // 返回反转后的链表头结点
    return newH;
}

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

char findFirstChar(char* cha)
{
    char result = '\0';
    
    // 定义一个数组 用来存储各个字母出现次数
    int array[256];
    
    // 对数组进行初始化操作
    for (int i=0; i<256; i++) {
        array[i] =0;
    }
    // 定义一个指针 指向当前字符串头部
    char* p = cha;
    // 遍历每个字符
    while (*p != '\0') {
        // 在字母对应存储位置 进行出现次数+1操作
        array[*(p++)]++;
    }
    
    // 将P指针重新指向字符串头部
    p = cha;
    // 遍历每个字母的出现次数
    while (*p != '\0') {
        // 遇到第一个出现次数为1的字符,打印结果
        if (array[*p] == 1)
        {
            result = *p;
            break;
        }
        // 反之继续向后遍历
        p++;
    }
    
    return result;
}

6.如何查找两个子视图的共同父视图?

- (NSArray <UIView *> *)findCommonSuperView:(UIView *)viewOne other:(UIView *)viewOther
{
    NSMutableArray *result = [NSMutableArray array];
    
    // 查找第一个视图的所有父视图
    NSArray *arrayOne = [self findSuperViews:viewOne];
    // 查找第二个视图的所有父视图
    NSArray *arrayOther = [self findSuperViews:viewOther];
    
    int i = 0;
    // 越界限制条件
    while (i < MIN((int)arrayOne.count, (int)arrayOther.count)) {
        // 倒序方式获取各个视图的父视图
        UIView *superOne = [arrayOne objectAtIndex:arrayOne.count - i - 1];
        UIView *superOther = [arrayOther objectAtIndex:arrayOther.count - i - 1];
        
        // 比较如果相等 则为共同父视图
        if (superOne == superOther) {
            [result addObject:superOne];
            i++;
        }
        // 如果不相等,则结束遍历
        else{
            break;
        }
    }
    
    return result;
}

- (NSArray <UIView *> *)findSuperViews:(UIView *)view
{
    // 初始化为第一父视图
    UIView *temp = view.superview;
    // 保存结果的数组
    NSMutableArray *result = [NSMutableArray array];
    while (temp) {
        [result addObject:temp];
        // 顺着superview指针一直向上查找
        temp = temp.superview;
    }
    return result;
}

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

https://blog.csdn.net/weixin_42109012/article/details/91645051

//求一个无序数组的中位数
int findMedian(int a[], int aLen)
{
    int low = 0;
    int high = aLen - 1;
    
    int mid = (aLen - 1) / 2;
    int div = PartSort(a, low, high);
    
    while (div != mid)
    {
        if (mid < div)
        {
            //左半区间找
            div = PartSort(a, low, div - 1);
        }
        else
        {
            //右半区间找
            div = PartSort(a, div + 1, high);
        }
    }
    //找到了
    return a[mid];
}

int PartSort(int a[], int start, int end)
{
    int low = start;
    int high = end;
    
    //选取关键字
    int key = a[end];
    
    while (low < high)
    {
        //左边找比key大的值
        while (low < high && a[low] <= key)
        {
            ++low;
        }
        
        //右边找比key小的值
        while (low < high && a[high] >= key)
        {
            --high;
        }
        
        if (low < high)
        {
            //找到之后交换左右的值
            int temp = a[low];
            a[low] = a[high];
            a[high] = temp;
        }
    }
    
    int temp = a[high];
    a[high] = a[end];
    a[end] = temp;
    
    return low;
}

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

- (void)viewDidLoad {

    [super viewDidLoad];

    NSArray *oriArray = @[@(2),@(3),@(6),@(7),@(22),@(12)];

    BOOL isHaveNums =  [self twoNumSumWithTarget:9 Array:oriArray];

    NSLog(@"%d",isHaveNums);
}


- (BOOL)twoNumSumWithTarget:(int)target Array:(NSArray<NSNumber *> *)array {
    
    NSMutableArray *finalArray = [NSMutableArray array];
    
    for (int i = 0; i < array.count; i++) {
        
        for (int j = i + 1; j < array.count; j++) {
            
            if ([array[i] intValue] + [array[j] intValue] == target) {
                
                [finalArray addObject:array[i]];
                [finalArray addObject:array[j]];
                NSLog(@"%@",finalArray);
                
                return YES;
            }
        }
    }
    return NO;
}
上一篇 下一篇

猜你喜欢

热点阅读