面试题知识点梳理
重点
- KVC、KVO
- GCD
- Runtime iOS开发之Runtime——面试解析
- runloop
- Block iOS开发之Block
- 内存管理、堆栈
- 通知、代理delegate
- Net、Https双向认证
- 数据持久化、数据库
- MVVM、MVP、MVC
- 第三方库
- 性能优化: TableView
- 响应式编程 RxSwift
- 与Js交互、WKWebView
拓展知识点
- TCP/UDP
- Socket iOS底层原理--socket详解
- 设计模式、六大设置原则
- 数据结构
- 算法 十大经典排序算法-动图演示
面试要求
1、熟悉iOS开发常用设计模式、多线程、数据持久化、网络通信、动画效果、界面布局、json解析、自定义控件等
2、对runtime、GCD、KVO、Block等有一定了解,熟悉iOS内存管理机制,对程序性能优化、内存优化有一定经验。
1.分类(category)的作用
-
作用:可以在不修改原来类的基础上,为一个类扩展方法。
-
最主要的用法:给系统自带的类扩展方法。
130、多线程
1、多线程概念
多条线程是同步完成多项任务,提高资源的使用效率。多核的CPU运行多线程更为出色;在iOS应用中,对多线程最初的理解为并发。
2、多线程的作用
实现负载均衡问题,提高cpu利用效率。
3、使用场景
数据请求框架、多张图片下载、定时器,视频图像的采集、处理、保存等耗时操作的方法。
133、进程和线程的区别与联系是什么?
一个程序至少有个一进程,一个进程至少有一个线程:
进程:拥有独立的内存单元,而多个线程共享一块内存
线程:线程是进程内的一个执行单元
联系:线程是进程的基本组成单位。
136、对比iOS中的多线程技术
-
NSThread
NSThread需要手动管理线程生命周期
-
GCD
- GCD仅仅支持FIFO队列,只可以设置队列的优先级。而NSOperationQueue中的每个任务都可以被重新设置优先级(setQueuePriority:),从而实现不同操作的执行顺序调整。
- GCD的执行速度比NSOperationQueue快
- GCD不支持异步操作之间的依赖关系设置。如果某个操作依赖另一个操作的数据,使用NSOperationQueue能够设置依赖按照正确的顺序执行操作(addDependency:)。
-
NSOperationQueue
- 方便停止队列中的任务(cancelAllOpeations, suspended);GCD不方便停止队列中的任务
- 支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)
- NSOperationQueue可设置最大并发数量(节电), GCD具有dispath_one(只执行一次、单例)和dispatch_after(延时执行)功能
-
NSObject分类
NSObject分类(perform)和NSThread遇到对象分配需要手动管理内存和线程生命周期
NSObject分类线程通信
137、多线程优缺点
优点:
- 是应用程序的响应速度更快,用户界面在进行其他工作的同时仍始终保持活动状态;
- 优化任务执行,适当提高资源利用率(CPU,内存)
缺点:
- 线程占用内存空间,管理线程需要额外的CPU开销,开启大量线程,降低性能;
- 增加程序复杂度,如线程间通信,多线程资源共享等。
134、 异步执行两个耗时操作,等两次耗时操作都执行完毕后,再回到主线程执行操作。
dispatch_queue_t queue = dispatch_get_global_queue(0, 0); // 全局并发队列
dispatch_queue_t group = dispatch_queue_create("并行队列", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{ // 异步执行操作1
// longTime1
});
dispatch_group_async(group, queue, ^{ // 异步执行操作2
// longTime2
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 主线程刷新UI
// reload Data
});
面试题陷阱
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
//奔溃原因:
// 同步线程不会去创建新的线程。
// 在同步线程里面执行dispatch_get_main_queue()时会发送线程卡死的现象
/* 正确做法:异步切换主线程
NSLog(@"1");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
*/
GCD
1、创建队列
//OBJECTIVE-C
//串行队列
dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", NULL);
dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL);
//并行队列
dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT);
//SWIFT
//串行队列
let queue = dispatch_queue_create("tk.bourne.testQueue", nil);
let queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL)
//并行队列
let queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT)
2、全局队列
//OBJECTIVE-C
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//SWIFT
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
3、创建任务
// 创建同步任务
// OBJECTIVE-C
dispatch_sync(<#queue#>, ^{
//code here
NSLog(@"%@", [NSThread currentThread]);
});
// SWIFT
dispatch_sync(<#queue#>, { () -> Void in
//code here
println(NSThread.currentThread())
})
// 创建异步任务
// OBJECTIVE-C
dispatch_async(<#queue#>, ^{
//code here
NSLog(@"%@", [NSThread currentThread]);
});
// SWIFT
dispatch_async(<#queue#>, { () -> Void in
//code here
println(NSThread.currentThread())
})
4、延时
// 创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 设置延时,单位秒
double delay = 3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), queue, ^{
// 3秒后需要执行的任务
});
NSOprationQueue
NSOperation
有一个非常实用的功能,那就是添加依赖。比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。这时就可以用到依赖了:
//1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上传图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//4.设置依赖
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二
//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
面试题:NSoperationQueue处理A,B,C三个线程,要求执行完A,B后才能执行C。
参考地址:https://blog.csdn.net/qq_30932479/article/details/79790646
- NSOperation添加依赖关系实现
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *A = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"A----%@",[NSThread currentThread]);
}];
NSBlockOperation *B = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"B---%@",[NSThread currentThread]);
}];
NSBlockOperation *C = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"C---%@",[NSThread currentThread]);
}];
[C addDependency:A];
[C addDependency:B];
[queue addOperation:A];
[queue addOperation:B];
[queue addOperation:C];
- 使用GCD的栅栏函数或者队列组
/*
1,开启两个线程处理A和B
2,通过任务组执行A,B之后执行C
*/
dispatch_queue_t queue = dispatch_queue_create("dealWith", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"A----%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"B---%@",[NSThread currentThread]);
});
dispatch_group_notify(group, queue, ^{
NSLog(@"C---%@",[NSThread currentThread]);
});
- GCD添加栅栏函数
//栅栏函数(栅栏函数不能用全局并发队列)
//等执行完栅栏函数中的代码才继续执行下面的代码
dispatch_queue_t queue = dispatch_queue_create("dealWith", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_async(queue, ^{
NSLog(@"A----%@",[NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"B---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"C---%@",[NSThread currentThread]);
});
142、Core Data
Core Data是iOS 5之后才出现的一个基于Sqlite进行封装的数据持久化框架。它提供了对象-关系映射(ORM)的功能,即能够将OC对象转换为数据,保存到sqlite数据库文件中,也能够将保存在数据库中的数据还原为OC对象。在数据操作期间,不需要编写任何SQL语句。
- Core Data是对sqlite数据库的封装
- Core Data中的NSManagedObjectContext在多线程中不安全
- 多线程访问Core Data,最好的方法是一个线程一个NSManagedObjectContext
- NSpersistentStoreCoordinator持久化存储调度器,NSpersistentStore对象会将MOC提交的改变同步到数据库中
如何解决Core Data线程数据同步问题?
监听通知NSManagedObjectContextDidSaveNotification,在耗时操作处理完之后告诉主上下文哪些改变了。我们可以通过主线程执行合并操作来实现。
152、UITableView
-
UITableView最核心的思想
Cell的重用机制。简单理解:UITableView只会创建一屏幕(或者一屏幕多一点)Cell,其他都是从中取出来重用的。每当Cell滑出屏幕时,就会收到一个Cell集合(复用池)中。当要显示某一位置的Cell时,会先从复用集合中取,如果有则直接拿来显示;如何没有,才会创建新的Cell。这样极大减少了内存的开销。
tableView:cellForRowAtIndexPath: 方法只负责赋值
tableView:heightForRowAtIndexPath: 方法只负责计算高度
-
自定义高度
-
UITableView性能优化
- 缓存行高
- 异步绘制
- 异步加载图片以及缓存
- 滑动时按需加载,特别是加载大量的图片的列表
- 不要动态创建子视图:所有子视图都预先创建,如果不需要显示设置隐藏
- 所有子视图都应该添加到contentView上
- 尽量少用或者不用透明图层
- cell栅格化
-
离屏渲染的问题
下面的情况或操作会引发离屏渲染问题:
- 为图层设置遮罩(layer.mask)
- 将图层的layer.masksToBounds/vew.clipsToBounds属性设置为true
- 将图层的layer.allowsGroupOpacity属性设置为true、layer.opacity小于1.0
- 设置阴影layer.shadow
- layar.shouldRasterize属性为true
- layar.cornerRadius,
- 使用CGContext在drawRect:方法中绘制大部分都会导致离屏渲染
-
离屏渲染优化方案
-
圆角优化
渲染机制是GPU在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会给我们带来额外的性能损耗。如果圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的频繁切换,性能的代价会宏观地表现在用户体验上—掉帧。
-
方案一:使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角图片
UIImageView *imageView = [[UIImageView alloc] initWithFrame: CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed: @"myImg"];
// 开始对imagewView进行画图
UIGraphicsBeginImageContextWith±Options(imageView.bounds.size, NO, 1.0);
// 使用贝塞尔曲线画出一个圆形路径
[UIBezierPath bezier±PathWithRoundedRect:imageView.bounds cornerRadius: imageView.frame.size.width] addClip];
[imageView drawRect: imageView.bounds];
// 重新设置圆角图片
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
// 结束画图
UIGraphicsEndImageContext±();
[self.view addSubview: imageView];
方案二:使用CAShapeLayer和UIBezierPath设置圆角遮罩层
UIImageView *imageView = [[UIImageView alloc] initWithFrame: CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed: @"myImg"];
// 使用贝塞尔曲线画出一个圆形路径
UIBezierPath *maskPath = UIBezierPath bezierPathWithRoundRect: imagewView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii: imageView.bounds.size];
// 创建CAShapeLayer
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = imageView.bounds;
// 设置绘制路径
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
[self.view addSubView: imageView];
-
shadow优化
- 通过设置shadowPath来优化性能,能大幅度提升性能。
imageView.layer.shadowColor = [UIColor grayColor].CGColor; imageView.layer.shadowOpacity = 1.0 imageView.layer.shadowRadius = 2.0 UIBezierPath *path = [UIBezierPath bezieerPathWithRect: imageView.frame]; imageView.layer.shadowPath = path.CGPath;
-
异步进行layer渲染 AsyncDisplayKit
-
Core Animation工具检测离屏渲染: Xcode->Open Develeper Tools-> Instruments -> Core Animation
162、Objective-C堆和栈的区别?
管理方式:栈是由编译器自动管理,无需我们手动控制;堆释放工作由程序员控制,容易产生memory leak。
分配方式:堆只有动态分配。栈分为静态分配和动态分配。
分配效率:栈分配效率高。堆的分配效率相对低。
栈:
- 栈向低地址拓展的数据结构,是一块连续的内存区域。先进后出的数据结构。
堆:
- 堆向高地址拓展的数据结构,是不连续的内存区域。树形结构。
- 系统是用链表来存储空闲内存地址,自然是不连续,而链表的遍历方向是由低地址向高地址。堆的大小受限于手机系统中有效的虚拟内存。由此可见,堆获取得空间比较灵活,也比较大。
145、说说关于UDP/TCP的区别?
UDP
- 是不可靠传输协议,不需要建立连接,速度快
- 将数据及源和目的封装成数据包中
- 每个数据报的大小限制在64k之内
TCP
- 建立连接,形成传输数据通道
- 通过三次握手完成连接,是可靠协议,安全送达
- 必须建立连接,效率稍低
- 连接中进行大数据传输
TCP与UDP的区别:
- 基于连接与无连接
- 对系统资源的要求(TCP较多,UDP较少)
- 流模式与数据报模式
- UDP程序结构较简单
- TCP保证数据正确性、确保顺序;UDP可能丢包,不确保顺序
TCP传输原理
-
TCP如何防止乱序和丢包
-
描述一下三次握手
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,并向服务器发送确认标包ACK(ack=k+1),此时发送完毕,客户端和服务器端进入ESTABLISHED状态,完成三次握手。完成三次握手,客户端与服务器端开始传输数据。
-
三次握手过程
第一次握手:建立连接时,客户端发送同步序列编号到服务器,并进入发送状态,等待服务器确认。
第二次握手:服务器收到同步序列编号,并确认同时自己也发送一个同步序列编号+确认标志,此时服务器进入接收状态
第三次握手:客户端收到服务器发送的包,并向服务器发送确认标志,随后连接成功。
148、Block
-
Block定义格式:
typedef void(^completion)(BOOL finnished)
-
使用block时什么情况会发生循环引用,如何解决?
一个对象中强引用了block,在block中又使用了该对象,就会发生循环引用。解决方法:将该对象使用__weak或者__block修饰符修饰之后再在block中使用。
__weak typeof(self) weakSelf = self; __weak typeof(self) weakSelf = self; [self doSomeBlockJob:^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (strongSelf) { ... } }];
iOS中block的详解weakSelf、strongSelf https://blog.csdn.net/xgb742951920/article/details/69258372
-
在block内如何修改block外部变量?
在block中访问的外部变量是复制过去的,即:写操作不对原变量生效。
// __block修饰局部变量,这个变量在block内外属于同一个地址上的变量,可以被block内部修改。 __block int a = 0; void (^foo)(void) = ^ { a = 1; } foo()
120、KVO、NSNotification、delegate以及block区别?
122、runtime/消息转发机制
-
runtime原理
1.1、runtime基本概念
runtime是一套OC底层纯C语言编写的库。我们平时编写的OC代码中,程序运行过程,其实最终都是转成了runtime的C语言代码,runtime是OC的幕后工作者。
1.2、runtime工作原理
在程序运行过程中,动态创建类:
objc_allocateClassPair
,class_addIvar
,objc_registerClassPair
.动态为某个类添加属性/方法,修改属性/方法(修改封装的框架)
objc_setAssociatedObject
,objc_setIvar
遍历一个类的所有成员变量(属性)/方法(字典转模型,归档解析)
class_copyIvarList
,class_copyPropertyList
,class_copyMethodList
-
消息机制
2.1 消息转发的原理
当向一个对象发送消息时,
objc_msgSend
方法根据对象的isa指针找到对象的类,然后在类型的调度表查(dispath table)找selector方法。一旦找到selector,objc_msgSend
根据调度表的内存地址调用改实现方法。
消息转发调用方法顺序:
objc_msgSend
-->CacheLookup
-->objc_msgSend_uncache
-->±MethodTableLookup
-->class_lookupMethodAnd±LoadCache3
-->lookUpImpOrForward
-
动态绑定
动态绑定—在运行时确定要调用的方法
链表
1、删除单链表节点
r = p->pNext; // p后面的结点
p->pNext = r->pNext; // 修改p的Next结点
free(r); // 释放内存
2、插入单链表结点
r = p->pNext;
p->pNext = q; // p的Next结点指向新结点q
q->pNext = r; // 新插入结点的Next结点指向r
3、创建单链表
#include <stdio.h>
#include <malloc.h>
typedef struct Node
{
int data;
struct Node *pNext;
}NODE, *PNODE; // NODE等价于struct Node、 PNODE等价于struct NODE *
// 函数声明
PNODE create_list(void);
// 遍历链表
void traverse_list(PNODE pHead);
// 判断链表是否为空
bool is_empty(PNODE pHead);
// 链表的长度
int length_list(PNODE);
// 在某个位置上插入结点
bool insert_list(PNODE, int, int *);
// 删除链表
bool delete_list(PNODE, int, int *);
// 链表排序
void sort_list(PNODE);
int main(id)
{
PNODE pHead = NUll;
pHead = create_list();
traverse_list(pHead);
sort_list(pHead);
return 0;
}
// 创建链表
PNODE create_list(void)
{
int len; // 链表长度
int i;
int val; // 临时存放用户输入的结点的值
PNODE pHead = (PNODE)malloc(sizeof(NODE));
printf("请输入您需要生产的链表结点个数: len = ");
scanf("%d",&len);
for (i =0; i<len; ++i)
{
printf("请输入第%d个结点的值:", i+1);
scanf("%d", &val);
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL != pNew)
{
printf("分配失败,程序终止!")
exit(-1);
}
// 将pNew挂到链表尾部
pNex->data = val;
pHead->pNext = pNew;
pNew->pNext = NULL:
}
return pHead;
}
// 判断链表是否为空
bool is_empty(PNODE pHead)
{
if (NULL == pHead->pNext)
{
return true;
}else{
return false;
}
}
// 链表长度
int length_list(PNODE pHead)
{
PNODE p = pHead->pNext;
int len = 0;
while(NULL ! = p)
{
++len;
p = p->pNext;
}
return len;
}
// 遍历
void traverse_list(PNODE pHead)
{
PNODE p = pHead->pNext;
while(NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
}
// 链表排序
void sort_list(PNODE pHead)
{
int i, j, t;
PNODE p, q;
int len = length_list(pHead);
// 冒泡排序
for (i=0,p=pHead->pNext; i< len - 1; ++i, p p->pNext)
{
for(j=i+1, q=p->pNext; j < len; ++j,q=q->pNext)
{
// 后面的结点比前面的大,则交换位置
if (p->data > q->data)
{
t = p->data;
p->data = q->data;
q->data = t;
}
}
}
return;
}
// 在某个位置上插入结点
bool insert_list(PNODE pHead, int pos, int val)
{
int i = 0;
PNODE p = pHead;
while(NULL ! = p && i < pos -1)
{
p = p->pNext;
++i;
}
if (i > pos - 1 || NULL == p)
return false;
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("动态分配内存失败!")
}
pNew->data = val;
PNODE q = p->pNext;
p->pNext = pNew;
pNew->pNext = q;
return true;
}
// 删除链表中某个下标的结点并返回删除元素pVal
bool delete_list(PNODE pHead, int pos, int *pVal)
{
int i = 0;
PNODE p = pHead;
while(NULL ! = p->pNext && i < pos -1)
{
p = p->pNext;
++i;
}
if (i > pos - 1 || NULL == p->pNext)
return false;
PNODE q = p->pNext;
// 删除的值
*pVal = q->data;
// 删除结点后面的结点
p->pNext = p->pNext->pNext;
free(q);
q = NULL:
return true;
}
数据结构概念
狭义:
- 数据结构是专门研究数据存储的问题
- 数据的存储包含两方面:个体的存储+个体关系的存储
广义:
- 数据结构既包含数据的存储也包含数据的操作
- 对存储数据的操作就是算法
算法概念
狭义:算法是和数据的存储方式密切相关
广义:算法和数据的存储方式无关
数据的存储结构:
- 线性:
- 连续存储【数组】
- 优点:存取元素的效率非常高
- 缺点:
- 事先必须知道数组的长度
- 需要大块连续的内存块
- 插入、删除元素的效率极低
- 离散存储【链表】
- 优点:
- 空间没有限制
- 插入、删除元素很快
- 缺点:
- 存取速度很慢
- 优点:
- 线性结构的应用—栈
- 线性结构的应用—队列
- 连续存储【数组】
- 非线性
- 树
栈
队列
170、UIKit、CoreAnimation和CoreGraphics的关系是什么?在开发过程中是否使用到CoreAnimation 和CoreGraphics?
178、动画
CAAnimation 动画基类
- CAPropertyAnimation 属性动画抽象类
- CABasicAnimation 基础动画
- CAKeyframeAnimation 关键帧动画
- CAAnimationGroup 组动画
- CATransition 转场动画