iOS面试知识点整理
整理一下最近面试问到的知识点。
一、iOS的设计模式有哪些,简单的介绍一下:
MVC模式:Model 设置对应的属性及构造方法。View 主要负责 UI 的实现,而 UIView 所产生的事件都可以采用委托的方式,交给 UIViewController 实现。 Controller 控制器创建视图控件,并将模型数据传递给视图控件。
1)Model 和 View 永远不能相互通信,只能通过 Controller 传递。
2)Controller 可以直接与 Model 对话(读写调用 Model),Model 通过 Notification 和 KVO 机制与 Controller 间接通信。
MVVM模式:viewmodel层,对封装的network进行处理,将请求的网络数据存储在model模型里面。然后使用block带出去,方便在VC中使用处理。
主要思想:
1 由于展示逻辑被抽取到viewmodel中,所以view中的代码变得非常轻量级。
2 由于viewmodel中的代码与UI无关,所以它具有良好的测试性。
3 对于一个封装大量业务逻辑的model来说,改变它可能会比较困难,并且存在一定风险,在这种场景下,viewmodel可以作为model的适配器使用。从而避免对model进行较大的改动。
MVP模式:自己没有用过,有需要的可以了解一下。
工厂模式:工厂方法是类方法的一种应用,工厂方法用于生成对象。工厂方法让一个类的实例化到子类中去进行。优点:极大优化代码,不用一直调用alloc方法去创建,提高代码复用性。同时可以将大量操作放到工厂类中去处理。缺点:必须存在继承关系。当所有的类不是继承自同一个父类的时候扩展比较困难。
单例模式:是一种特殊的工厂方法,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,节约系统资源。(使用场合) 在整个应用程序中,共享一份资源(这份资源只需要创建初始化一次). 例如一些网络工具类/沙盒工具类等等。单例的生命周期和操作的生命周期一样长,跟随着AppDelegate的销毁而销毁。(有的面试官也会让手写一下单例)。
二、内存管理的一些知识:
野指针:只要有一个对象释放了,我们就称这个对象为“僵尸对象”。当一个指针指向一个僵尸对象,我们就称这个指针为野指针。给野指针发送消息会报错。为了避免报错,一般情况下我们会在这个对象释放后将对象指针设为空指针。 =nil;
空指针:没有指向存储空间的指针(里面存的是nil)。给空指针发消息没有任何反应。
ARC的内存管理:ARC判断一个对象是否需要释放不是通过引用计数来判断的,而是通过强指针来判断的。只要有一个强指针变量指向对象,对象就会保持在内存中。
强指针:默认所有对象的指针都是强指针,被__strong修饰的指针。
弱指针:被__weak修饰的指针。用弱指针保存新创建的对象对象会被立即释放掉。(问题:如果我想用__weak来创建一个对象怎么实现?)
强引用:如果一个指针变量指向了某个对象,那么这个对象就多了一个拥有者,并且不会被程序释放,这种指针特性称为强引用。
弱引用:指针变量不影响其指向对象的拥有者个数。这种不会改变该对象拥有者个数的指针特性称为弱引用。弱引用适合解决强引用循环的内存管理问题。
weak:weak指针不会增加其指向对象的引用计数。当其指向对象销毁后,weak指针自动设置为nil,避免出现野指针。
copy:分为深拷贝和浅拷贝
浅拷贝:也称指针拷贝,副本和源对象是一个对象,源对象的引用计数+1,本质是增加了一个指向源对象的指针。
深拷贝:源对象的引用计数不变,本质是产生新对象。此外复制还有copy和mutableCopy之分。
copy:能接收copy消息的类必须遵循NSCopying协议。对系统的非容器对象,copy是浅拷贝。对系统的容器类,如果是不可变容器,copy是浅拷贝。对系统的容器类,如果是可变容器,copy是深拷贝,但其返回的是不可变对象。
mutableCopy:必须遵循NSMutableCopying协议。对所有的类,NSMutable都是深拷贝。
1.字符串的声明用 copy 还是 strong?
当原字符串是NSString类型时,由于它是不可变类型的,不管是使用strong特性,还是copy特性的对象,它们所指向的地址都跟原字符串是一样的,都指向原字符串对象。也就是说当原字符串是NSString类型时,copy特性的操作,只是做了一次浅拷贝,只是增加了指针指向原字符串所指向的地址。
当原字符串是NSMutableString类型时,strong特性对象只是增加了原字符串的引用计数,但是copy特性对象则是对原字符串进行了深拷贝,创建了一个新对象,并且指向了这个新对象。此时,copy特性对象是NSString类型的不可变的,strong特性对象是NSMutableString类型的可变的。
但是我们在大多数情况下,在声明NSString属性时,都是希望其不被改变,防止数据出错。所以大多数情况下还是选择copy特性,从而来避免一些无法预估的bug。
2.代理的声明为什么用weak ?
delegate 之所以用weak来修饰,是防止循环引用,weak属性的变量是不为其所属对象持有的,并且在该变量被销毁之后,此weak变量的值会自动被赋值为nil。而assign属性一般是对C基本数据类型成员变量的声明,当然也可以用在对象类型成员变量上,只是其代表的意义只是单纯地拷贝所赋值变量的值。即如果对某assign成员变量B赋值某对象A的指针,则此B只是简单地保存此指针的值,且并不持有对象A,也就意味着如果A被销毁,则B就指向了一个已经被销毁的对象,如果再对其发送消息会引发崩溃。
那关于delegate 既然用weak这么好用,用assign会出现野指针,为什么还会用呢?
weak和strong属性是ARC才引入的,而在MRC情况下,只能使用assign修饰了。weak之所以强大的地方,是当引用的对象被销毁时,它的值也会变为nil,所以推荐使用ARC。
当自己在使用前人写好的MRC代码,那这些delegate怎么来使用呢?
像这样写在ViewController被销毁时,将delegate制空。在ARC模式下,我们不需要继承父类的dealloc方法(不用写[super dealloc])
-(void)dealloc {
self.XXX.delegate = nil;
}
关于MRC的一些知识可以自己去学习。
三、iOS runtime消息机制方法调用流程:
OC 在向一个对象发送消息时,runtime库会根据对象的ISA指针找到改对象的对应的类或其父类中查找方法。如果在顶层父类中依然找不到对应的方法时,程勋会在运行时抛出异常并挂掉。但在这之前,runtime会给出三次拯救机会。(具体ISA指针怎么查找方法,三次拯救机会可自己去学习)。
runtime常用的方法:
1、动态交换两个方法的实现(系统方法拦截)。
2、给分类动态扩展属性。我们知道在分类中不能添加成员属性,虽然用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现的生成。可以用runtime来实现。
3、runtime字典转模型。
4、动态添加方法。处理一个未实现的方法和去除报错。
5、实现NSCoding的自动归档和解归档。
四、简单说一下KVC、KVO
KVC:键值编码,通过键值路径为对象的属性赋值。
KVO:键值监听,当指定的对象属性被修改后,则对象会收到通知。KVO的底层实现原理是系统给当前的类创建子类,在子类setter方法中调用父类的setter方法,通过修改ISA指针指向系统创建的子类实现当前属性值改变的监听。
KVO使用场景:监听模型属性实时更新UI。
如果赋值没有通过setter方法或者KVC ,而是直接修改属性对应的成员变量,例如仅调用 _name = @"newName"; 这时是不触发KVO机制,更不会调用回调方法。
五、block
定义block时,用copy修饰。因为block创建时默认是创建在栈上的, 超过作用域后就会被销毁, 只有使用copy才会生成一个堆block, 在作用域外被访问。
block循环引用问题:由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。
解决办法:使用__weak
修饰self,使其在block中不被持有,打破循环引用。
block访问外部变量时,block会对内部变量进行一次临时的拷贝,把栈区的地址拷贝到堆区,block在内部操作的是副本,对block外部变量的真实值不会造成影响。
当在Block内部”修改“外部变量,不被允许。如果非要在Block内部修改外部变量,需要使用__block修饰外部变量。__block修饰外部变量作用:使外部变量可以在Block内部修改。被__block标记的外部变量,一旦在Block内部”使用过“,那么Block对外部变量的拷贝就不是临时的了;Block外部变量的真实值就会发生变化,那么这个变量的地址在后续的使用中都是堆区地址。
六、多线程
进程:正在进行中的程序被称为进程,负责程序运行的内存分配;每一个进程都有自己独立的虚拟内存空间。
线程:线程是进程中一个独立的执行路径(控制单元);一个进程中至少包含一条线程,即主线程,线程是进程的最小执行单位。
队列:dispatch_queue_t,一种先进先出的数据结构,线程的创建和回收不需要程序员操作,由队列负责。
串行:队列中的任务只会顺序执行。
并行:队列中的任务通常会并发执行。
全局队列:是系统开发的,直接拿过来(get)用就可以;与并行队列类似,但调试时,无法确认操作所在队列
dispatch_queue_t q = dispatch_get_global_queue(dispatch_queue_priority_default, 0)。
主队列:每一个应用程序对应唯一一个主队列,直接get即可;在多线程开发中,使用主队列更新UI
dispatch_queue_t q = dispatch_get_main_queue();
操作:
异步:dispatch_async 异步操作,会并发执行,无法确定任务的执行顺序;
同步:dispatch_sync 同步操作,会依次顺序执行,能够决定任务的执行顺序;
GCD
GCD 的创建 :
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
对于串行队列,GCD 提供了的一种特殊的串行队列:主队列(Main Dispatch Queue)。所有放在主队列中的任务,都会放到主线程中执行。可使用dispatch_get_main_queue()
获得主队列。
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();
对于并发队列,GCD 默认提供了全局并发队列(Global Dispatch Queue)。可以使用dispatch_get_global_queue
来获取。需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT
。第二个参数暂时没用,用0
即可。
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
GCD任务的创建:
GCD 提供了同步执行任务的创建方法dispatch_sync
和异步执行任务创建方法dispatch_async
。
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});
GCD线程间的通讯:
在iOS开发过程中,我们一般在主线程里边进行UI刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
/**
* 线程间通信
*/
- (void)communication {
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 异步追加任务
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
// 回到主线程
dispatch_async(mainQueue, ^{
// 追加在主线程中执行的任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
});
}
GCD栅栏方法 dispatch_barrier_async :
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏
一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async
方法在两个操作组间形成栅栏。dispatch_barrier_async
函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async
函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。
GCD的延迟执行方法 dispatch_after :
我们经常会遇到这样的需求:在指定时间(例如3秒)之后执行某个任务。可以用 GCD 的dispatch_after
函数来实现。
需要注意的是:dispatch_after
函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after
函数是很有效的。
GCD 只执行一次 dispatch_once :
我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once
函数。使用dispatch_once
函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once
也可以保证线程安全。
GCD快速迭代方法 dispatch_apply:
通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数dispatch_apply
。dispatch_apply
按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
如果是在串行队列中使用 dispatch_apply
,那么就和 for 循环一样,按顺序同步执行。可这样就体现不出快速迭代的意义了。
我们可以利用并发队列进行异步执行。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply
可以 在多个线程中同时(异步)遍历多个数字。
还有一点,无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait
方法
GCD任务组 dispatch_group
:
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。(比如同时下载多张图片,最后一张图片下载完成时合并这些图片。)
调用队列组的 dispatch_group_async
先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enter、dispatch_group_leave
组合 来实现dispatch_group_async
。
调用队列组的 dispatch_group_notify
回到指定线程执行任务。或者使用 dispatch_group_wait
回到当前线程继续向下执行(会阻塞当前线程)。
dispatch_group_notify :
监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
//下载2张图片,下载完之后合并图片
- (void)downloadImgAndEdit {
// 创建一个组
dispatch_group_t group = dispatch_group_create();
// 开启一个任务下载图片1
__block UIImage *image1 = nil;
dispatch_group_async(group, global_queue, ^{
//下载操作
});
// 开启一个任务下载图片2
__block UIImage *image2 = nil;
dispatch_group_async(group, global_queue, ^{
//下载操作
});
// 同时执行下载图片1和下载图片2操作
// 等group中的所有任务都执行完毕, 再回到主线程执行其他操作
dispatch_group_notify(group, main_queue, ^{
self.imageView1.image = image1;
self.imageView2.image = image2;
// 合并
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0);
[image1 drawInRect:CGRectMake(0, 0, 100, 100)];
[image2 drawInRect:CGRectMake(100, 0, 100, 100)];
self.bigImageView.image = UIGraphicsGetImageFromCurrentImageContext();
// 关闭上下文
UIGraphicsEndImageContext();
});
}
dispatch_group_wait : 暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。
NSOperation
操作(Operation):
执行操作的意思,换句话说就是你在线程中执行的那段代码。
在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
操作队列(Operation Queues):
这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
操作队列通过设置 最大并发操作数(maxConcurrentOperationCount) 来控制并发、串行。
NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
NSOperation 实现多线程的使用步骤分为三步:
创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
创建队列:创建 NSOperationQueue 对象。
将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中。
之后呢,系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作。
NSOperation 操作依赖
NSOperation、NSOperationQueue 最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。NSOperation 提供了3个接口供我们管理和查看依赖。
- (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
- (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
@property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。
NSOperation 优先级
NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。
NSOperation与GCD比较的优点:
1、在NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码);
2、NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;
3、我们能将KVO应用在NSOperation中,可以监听一个Operation是否完成或取消,这样子能比GCD更加有效地掌控我们执行的后台任务;
4、在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;
5、我们能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有*度,能够在其之上添加更多自定制的功能。
多线程的死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
向一个串行队列中同步添加任务就一定会导致死锁。
多线程数据竞争
是指多个线程在没有正确加锁的情况下,同时访问同一块数据,并且至少有一个线程是写操作,对数据的读取和修改产生了竞争,从而导致各种不可预计的问题。
解决办法:使用dispatch_barrier_async 将write操作和前后的read操作屏蔽开。
七、RunLoop
Runloop是一个对象,这个对象在循环中用来处理程序在运行过程中出现的各种事件(比如触摸,UI刷新,定时器事件,selector事件)从而保证程序的持续运行;而且在程序没有事件处理的时候处于休眠状态,从而节省CPU资源,提高程序性能。
苹果不允许直接创建RunLoop,它提供了两个自动获取的函数: CFRunLoopGetMain()和CFRunLoopGetCurrent(). 线程和RunLoop之间是一一对应的。线程创建时并没有RunLoop,且如果不去获取,那么RunLoop一直都不存在. RunLoop的创建发生在第一次获取的时候( 除了主线程的RunLoop,你只能在一个线程的内部获取自己对应的RunLoop ) ;RunLoop的销毁发生在线程结束后;主线程的RunLoop是程序开始就创建的;子线程的RunLoop在第一次获取RunLoop对象时创建的;
关于定时器与页面滑动:
很多人在不懂之前,写的定时器与页面滑动的事件相冲突,即页面滑动时,定时器不工作: 主要原因是页面滑动时,主线程的RunLoop会停止,并且以 UITrackingRunLoopMode形式启动,这个时候schedule方式生成的NSTimer处在KCFRunLoopDefaultMode下,所以不会被运行;解决办法时,将NSTimer注册到kCFRunLoopCommonModes下,则NSTimer在Mode切换时仍然可以运行;
八、响应者链 (UIResponder)
在iOS中UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events,如插入耳机调节音量触发的事件)。我们知道UIApplication、UIView、UIViewController这几个类是直接继承自UIResponder,UIWindow是直接继承自UIView的一个特殊的View,所以这些类都可以响应事件。当然我们自定义的继承自UIView的View以及自定义的继承自UIViewController的控制器都可以响应事件。iOS里面通常将这些能响应事件的对象称之为响应者。
响应者链条的传递过程是:由第一响应者(对于触摸事件来说是hist-test view)开始向上传递。如果该视图是控制器的根视图,先传递给控制器,再传递给父视图,如果不是控制器的根视图,直接传递给父视图。
只要在响应者的处理方法里面调用父类的方法,就可以让多个视图和控制器响应同一个事件,响应者链条的根本目的是:共享事件,让多个视图和控制器可以对同一事件做不同的处理。
九、socket TCP于UDP的区别 网络的七层协议
谈到任何联网的协议,我们就必须要谈到OSI(网络七层协议模型),必须遵循这个协议模型,我们的手机和电脑才可以联网通信,首先来看一下OSI。
OSI是一个开放性的通信系统互连参考模型,他是一个定义得非常好的协议规范。OSI模型有7层结构,每层都可以有几个子层。
网络的七层协议:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
http协议对应于应用层
tcp协议对应于传输层
ip协议对应于网络层
三者本质上没有可比性。 何况HTTP协议是基于TCP连接的。
简单了解OSI之后我们来看一下我们手机与电脑通信,所能够使用的两种数据通信,一种是HTTP请求,一种是Socket通信,HTTP是属于短连接,适合新闻,订票信息等客户端发起请求,每一次请求结束,自动断开连接。而Socket是属于长连接,适合游戏,聊天等实时数据。
建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务器端,称为ServerSocket。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
建立起一个TCP连接需要经过“三次握手”。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”。具体的自己去看。
TCP 和 UDP 的区别
TCP | UDP | |
是否连接 | 面向连接 | 面向非连接 |
传输可靠性 | 可靠 | 不可靠 |
应用场合 | 传输大量数据 | 传输少量数据 |
速度 | 快 | 慢 |
十、XML跟json的区别,json的底层原理
XML底层原理:XML解析常用的解析方法有两种:DOM解析和SAX解析。DOM采用建立树形结构的方式访问XML文档,而SAX采用的事件模型。DOM解析把XML文档转化为一个包含其内容的树,并可以对树进行遍历。使用DOM解析器的时候需要处理整个XML文档转化为一个包含其内容的树,并可以对树进行遍历。使用DOM解析器的是时候需要处理整个XML文档,所以对性能和内存的要求比较高。SAX在解析xml文档的时候可以出发一系列的事件爱你,当发现给定的tag的时候,他可以**一个回调方法,告诉该方法制定的标签已经找到。SAX对内存的要求通常会比较低,因为它让开发人员自己来决定要处理的tag。特别是当开发人员只需要处理文档中所包含的部分数据时,SAX这种扩展功能得到了更好的体现。
JSON底层原理:遍历字符串中的字符,最终根据格式规定的特殊字符,比如{}号,[]号,:号等进行区分,{}号是一个字典的开始,[]号是一个数组的开始,:号是字典的键和值的分水岭,最终仍是将json数据转化为字典,字典中值可能是字典,数组,或字符串而已。
1)可读性方面:基本相同,xml的可读性比较好
2)可扩展性方面:都具有很好的扩展性
3)编码难度方面:相对而言:JSON的编码比较容易
4)解码难度:json解码难度基本为零,xml需要考虑子节点和父节点
5)数据体积方面:json相对于xml来讲,数据体积小,传递的速度更快些
6)数据交互方面:json与JavaScrpit的交互更加方便,更容易解析处理,更好的数据交互
7)数据描述方面:xml对数据描述性比较好
8)传输速度方面:json的速度远远快于xml。
十一、APP安全相关的
1、不要 在plist文件、项目中的静态文件中 存储关键的信息,如果要保存,记得 对称加密。
2、NSUserdefault 中不要保存关键信息,如果要保存,还是加密吧。sqlite也是这样子的。
3、接口如何保证安全呢?首先用HTTPS,虽然HTTPS已经很安全了,但是数据也是有可能被**的。所以 接口一定要自己加密。接口如果用对称加密,** 放到代码里 是能被反编译出来的。如果你的 APP的安全性很高,就不要把** 写到代码里。
4、**要定期更换。比如 3个月 或半年换一次,如果**是从接口通过非对称加密 获取的,直接修改服务端就可以了。
5、类名方法名混淆,程序代码混淆。
十二、NSObject 中 load 于 initialize 方法的区别
十三、开发中对线上bug的处理
在xcode中菜单的window下选择organizer,在打开的窗口中选择Crashes,这样Xcode会开始下载相关的崩溃信息到本地中(网络环境不好时可能要等待一些时间)。在崩溃信息这一栏苹果会按照崩溃数量排序,将崩溃数量最多的排在最前。右侧的详细信息会显示是崩溃时的调用堆栈,可以看到是哪行代码导致的崩溃。选中要解决的崩溃后,可以在窗口右侧选择open in project。在打开的项目中,会直接定位到崩溃的那行代码。在解决完这个crash后可以标记为已经解决。
十四、使用autolayout布局获取frame 注意什么:
要调用super view的 layoutIfNeeded 方法。或者在 viewDidLayout方法里获取最好。
十五、WKWebView UIWebView相关知识及与JS的交互
UIWebView
JS调OC方法:
1、需要引入系统库JavaScriptCore.framework。2、创建webView。3、创建JSContext桥梁.4、在webViewDidFinishLoad方法中调与后台规定名称的js方法
WKWebView
iOS8之后我们使用WebKit
框架中的WKWebView来加载网页。相比UIWebView,在性能、稳定性、占用内存方面有很大提升;允许JavaScript的Nitro库加载并使用(UIWebView中限制);增加加载进度属性:estimatedProgress,不用在自己写假进度条了;支持了更多的HTML的属性。
WKWebView
中的WKUIDelegate
实现UI弹出框的一些处理(警告面板、确认面板、输入框)。
JS调iOS-JS端必须使用window.webkit.messageHandlers.JS_Function_Name.postMessage(null)
,其中JS_Function_Name
是iOS端提供个JS交互的Name。
如果message.body
中没有参数,JS代码中需要传null
防止iOS端不会接收到JS的交互。
十六、tableViewCell自适应高度
当我们在cell
中进行高度计算然后return
高度,再在控制器中调用是不可取的,原因在于tableView
的代理方法heightForRowAtIndexPath
会在cellForRowAtIndexPath
之前调用。
方法一:在model中计算cell的高度,然后在heightForRowAtIndexPath
返回高度。
方法二:iOS 8之后 设置预估行高,开启cell的自适应,在cell里设置好约束(注意竖向约束要设置好,并且label的左右约束也要设置好),并且注释掉heightForRowAtIndexPath代理方法
。
十七、本地存储的一些方法,SQLit的使用
NSUserDefaults
优点:不需要关心文件名;快速进行键值对存储;直接存储基本数据类型。
缺点:不能存储自定义数据;取出的数据都是不可变的。
NSKeyedArchiver、NSKeyedUnarchiver: 存储一些自定义数据对象。归档需要遵循NSCoding
协议。
plist存储: plist
存储的不是数组就是字典。plist
不能存储自定义对象。
CoreData :
NSManagedObjectContext 管理对象,上下文,持久性存储模型对象,处理数据与应用的交互。
NSManagedObjectModel 被管理的数据模型,数据结构。
NSManagedObject 被管理的数据记录
NSFetchRequest 数据请求
NSEntityDescription 表格实体结构
十八、webView中如果网页内有异步请求或者重定向时,就会多次调用webViewDidFinishLoad方法,怎么解决?
解决方法是使用webView.isLoading属性。
十九、tableView delegate调用顺序
1. 有多少个分区。
- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
2. 估算header和Footer视图高度。(若没有,执行11,14)
- (CGFloat)tableView:(UITableView*)tableView
estimatedHeightForHeaderInSection:(NSInteger)section
- (CGFloat)tableView:(UITableView*)tableView
estimatedHeightForFooterInSection:(NSInteger)section
3. 每个分区有多少行
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section
4. 估算每个cell的高度(若没有,执行8)
- (CGFloat)tableView:(UITableView*)tableView
estimatedHeightForRowAtIndexPath:(NSIndexPath*)indexPath
-------------------------------------------------------------
5. 右边索引的titles。
- (nullableNSArray<NSString*> *)sectionIndexTitlesFor
TableView:(UITableView*)tableView
—————————————多少行重复执行—————————-———
6. 每个cell的内容。
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
7. 设置indexPath上的每一行的cell的缩进
- (NSInteger)tableView:(UITableView*)tableView indentationLevelForRowAtIndexPath:(NSIndexPath*)indexPath
8. 依次计算可视区域里每个cell的高度
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath
9. 某一行是否可以编辑(删除)
- (BOOL)tableView:(UITableView*)tableView canEditRowAtIndexPath:(NSIndexPath*)indexPath
10.将要展示Cell视图
- (void)tableView:(UITableView*)tableView willDisplayCell:(UITableViewCell*)cell forRowAtIndexPath:(NSIndexPath*)indexPath
------------------------------------------------------------
11.header视图高度(若没有估算,即2,此方法已执行,不再执行)
- (CGFloat)tableView:(UITableView*)tableView
heightForHeaderInSection:(NSInteger)section
12.header视图内容(若没有视图,将显示header标题)
- (nullableUIView*)tableView:(UITableView*)tableView
viewForHeaderInSection:(NSInteger)section
- (NSString*)tableView:(UITableView*)tableView
titleForHeaderInSection:(NSInteger)section
13.将要展示header视图
- (void)tableView:(UITableView*)tableView
willDisplayHeaderView:(UIView*)view
forSection:(NSInteger)section
14.footer视图高度(若没有估算,即2,此方法已执行,不再执行)
- (CGFloat)tableView:(UITableView*)tableView
heightForFooterInSection:(NSInteger)section
15.footer视图内容(若没有视图,将显示footer标题)
- (nullableUIView*)tableView:(UITableView*)tableView
viewForFooterInSection:(NSInteger)section
- (NSString*)tableView:(UITableView*)tableView
titleForFooterInSection:(NSInteger)section
16.将要展示footer视图
- (void)tableView:(UITableView*)tableView willDisplayFooterView:(nonnullUIView*)view forSection:(NSInteger)section
上一篇: Shell脚本面试题及答案