iOS 经典面试题<记录篇>
1、APP的启动过程、main函数?
内核初始化空间创建进程-》加载解析执行文件 - 》载入动态链接器(加载依赖库)
程序启动分为两类:
一、
有storyboard
情况下:
1.main函数
2.UIApplicationMain
创建UIApplication对象
创建UIApplication的delagate对象
3。根据info.plist获得Main.storyboard文件名,加载Main.storyboard
创建UIWindow
设置window 的 rootViewController
显示窗口
二、没有storyboard
情况下:
1.main函数
2.UIApplicationMain
创建UIApplication对象
创建UIApplication的delagate对象
3。delagate监听处理系统事件调用application:didFinishLaunchingWithOptions:方法
在application:didFinishLaunchingWithOptions:方法创建UIWindow
设置window 的 rootViewController
显示窗口
2、讲一下__block?
1.
Block
: 用copy
修饰,定义实在栈内存的,需要拷贝到堆内存来由程序员自己管理内存。
2.__block
作为修饰被捕获的对象,使对象在block
中可以被修改,在执行完业务代码之后手动把对象置为nil来打破循环引用
但是这样做有很大的限制:
block
必须执行!!!不执行的话就循环引用了!!!!!- 需要保证被捕获的对象在
block
执行之后不再使用!!!因为block
执行之后对象变为nil了。
3、如何理解iOS组件化路由?
4、如何理解iOS组件化?
当一个业务和产品越来越多,越来越复杂的时候,就要考虑
组件化
了
我们肯定会想到CocoaPods
工具,因为它基本都是组件化管理,面向对象就好,并没有互相通信,相互依赖
优点:避免冲突,项目维护,模块间耦合性低容易分离
;
缺点:引起组建之间版本维护等问题
;
如何实现(概念):
分离模块与模块之间的耦合关系,避免模块与模块直接直接联系
。
5、你是怎么理解RunLoop的?
- 字面意思就是“跑圈”,翻译的雅一点就是“运行的循环”;
- 内部就是
do-while
循环,在这个循环内部不断地处理各种任务。点击了屏幕(TouchEvent)、UI界面的刷新事件、定时器事件(Timer)、Selector、Observer等等
- 当我们将我们的程序退出到后台,注意只是退出到后台,并没有terminate,这时
RunLoop
不会处理任何事件,此时为了节省CPU
的资源,RunLoop
会自动进入休眠模式。
4.一个线程对应唯一一个
RunLoop
对象,这点很重要。但是往往,RunLoop
并不能保证我们线程安全;
- 主线程中默认开启一个
RunLoop
,在main函数
,只要程序不关闭,他就会一直运行循环。退出后台才会被休眠或者销毁;子线程的Runloop需要手动启动(调用run 方法)
WeChat654c9c0e3ac10c7c0084018f5b1aa1ef.png
runloop的mode
是什么?作用是什么?
1). 用来控制一些特殊操作只能在指定模式下运行,一般可以通过指定操作的运行。
mode 来控制执行时机,以提高用户体验
2). 系统默认注册了 5 个 Mode,其作用
1.kCFRunLoopDefaultMode
:App 的默认 Mode,通常主线程是在这个 Mode下运行,对应 OC 中的:NSDefaultRunLoopMode
2.UITrackingRunLoopMode
:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3.kCFRunLoopCommonModes
:这是一个标记 Mode,不是一种真正的 Mode,事件可 以 运 行 在 所 有 标 有 common modes 标 记 的 模 式 中 , 对 应 OC 中 的NSRunLoopCommonModes , 带 有 common modes 标 记 的 模 式 有 :UITrackingRunLoopMode 和 kCFRunLoopDefaultMode
4.UIInitializationRunLoopMode
:在启动 App 时进入的第一个 Mode,启动完成后就不再使用
5.GSEventReceiveRunLoopMode
:接受系统事件的内部 Mode,通常用不到
6. 说说 多线程 ?
优点:多线程同时(并发执行),提高程序的执行效率,提高资源利用率,包括CPU、内存等;
缺点:占用一定的内存,线程过多反而会降低性能,程序设计更加复杂,比如线程之间的通信或者数据共享。
在iOS中有3种多线程方案,分别为:NSThread、NSOperation、GCD
• 一个进程可有多个线程。
• 一个线程可有多个队列。
• 队列可分并发和串行队列。
NSThread
:1.轻量级最低,相对简单,可直接操作线程对象;
2.需要手动管理线程活动(生命周期、睡眠、同步等);
NSOperation
:1.自带线程管理的抽象类,操作上可更注重自己逻辑;
2.面向对象的抽象类,只能实现它或者使用它定义好的两个子类:NSInvocationOperation
和NSBlockOperation
GCD
:Grand Central Dispatch是由苹果开发的一个多核编程的解决方案。iOS4.0+才能使用。优点
1.最高效,避开并发陷阱。
2.官方推荐,更为简单。
3.性能更好 。
4。不需要编写任何线程管理代码。
缺点:基于C语言实
参考文章:https://www.jianshu.com/p/9a470a6dfcc9
7. 理解flutter
?
Flutter是由谷歌开发的一个使用 一套编程语言 实现 跨平台(iOS,Android)原生开发的“工具”。
目前还不是很成熟,很多公司都再试水,或者直接再原生代码嵌入Flutter混合开发。
参考官网:https://flutter.dev/
参考mac、Windows、等系统安装教程:https://www.jianshu.com/p/4ed03ee3516f
8. 理解swiftUI
?
写更少的代码,打造更出色的 app。
在 UI 开发领域,描述性语言是最佳方式
SwiftUI 是一种创新、简洁的编程方式,通过 Swift 的强大功能,在所有 Apple 平台上构建用户界面。借助它,您只需一套工具和 API,即可创建面向任何 Apple 设备的用户界面。SwiftUI 采用简单易懂、编写方式自然的声明式 Swift 语法,可无缝支持新的 Xcode 设计工具,让您的代码与设计保持高度同步。SwiftUI 原生支持“动态字体”、“深色模式”、本地化和辅助功能——第一行您写出的 SwiftUI 代码,就已经是您编写过的、功能最强大的 UI 代码。
9.给一个UIView获取所属的UIViewController?
通过响应链查找视图控制器,nextResponder获取下一个响应者
OC 版本
- (UIViewController *)getControllerFromView:(UIView *)view {
// 遍历响应者链。返回第一个找到视图控制器
UIResponder *responder = view;
while ((responder = [responder nextResponder])){
if ([responder isKindOfClass: [UIViewController class]]){
return (UIViewController *)responder;
}
}
// 如果没有找到则返回nil
return nil;
或者
id responder = self.nextResponder;
while (![responder isKindOfClass: [UIViewController class]] && ![responder isKindOfClass: [UIWindow class]])
{
responder = [responder nextResponder];
}
if ([responder isKindOfClass: [UIViewController class]])
{
// responder就是view所在的控制器
// do something
}
Swift 版本
func viewController(aClass: AnyClass) -> UIViewController?{
for(var next=self.superview;(next != nil);next=next?.superview){
let nextResponder = next?.nextResponder()
if((nextResponder?.isKindOfClass(aClass)) != nil){
return nextResponder as? UIViewController
}
}
return nil
}
10、Get与Post的区别?
可以从提交数据模式、数据限制、安全性来细说。
提交的数据
GET
提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连, 操作简单, 不过容易让外界看到, 安全性不高。
POST
请求, 将参数放到 body 里面, POST请求的操作相对复杂, 需要将参数和地址分开, 不过安全性高,参数放在body里面, 不容易被捕获抓包。
传输数据限制
GET
提交的数据大小有限制(因为浏览器对URL的长度有限制)
POST
方法提交的数据没有限制。
一般用
GET
来进行获取数据,因为有缓存,所以可以降低服务器的压力;
POST
一般用来进行操作数据,如增删改。
GET
方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值。
安全性
GET&&Post
POST
的安全性要比GET
的安全性高。比如:通过GET
提交数据,用户名和密码将明文出现在URL上。
11、swift 常用的第三方有哪些?
参考文章:https://www.jianshu.com/p/2638f79be33f
12、说说IM开发的过程,遇到的问题,以及你所了解的im知识?
12-1.场景
im社交软件
的开发,如果是一般需求,项目里面只是陪衬(客服之类)的情况下,可以借助第三方IMSDK(环信、融云之类)接口来配合使用,但是如果业务需求比较大的社交软件,一般会是自己公司里面团队进行开发IM系统。
12-2.客户端连接过程
1.客户端调用 socket(...) 创建socket;
2.绑定IP地址、端口等信息到socket上,用函数bind()
3.客户端调用 connect(...) 向服务器发起连接请求以建立连接;
4.客户端与服务器建立连接之后,就可以通过send(...)/receive(...)向客户端发送或从客户端接收数据;
5.客户端调用 close 关闭 socket;
static NSString *Khost = @"127.0.0.1";
static uint16_t Kport = 6969;
static NSInteger KPingPongOutTime = 3;
static NSInteger KPingPongInterval = 5;
12-3.说明
CocoaAsyncSocket
Protobuf
的 优势:1)跨语言,跨平台 2)性能优越 3)兼容性好
使用基于Socket原生
CocoaAsyncSocket配合Protobuf
开发IM系统,Protobuf 是由 Google 开发的一种语言无关,平台无关,可扩展的序列化结构数据的方法,可用于通信和数据存储,可以理解为比Json还好的数据加密方式。
12-4. 建立长链接逻辑
用
socket
与后台连接时需要完成:3次握手消息,成功握手后开启心跳包
,届时就可以和后台相互收发。收发的内容受长度限制。取决于后台配置,也可能与移动端有关。长链接的scoket 理论上不会断开,因此还会涉及到粘包拆包解包
的问题,目前接收到的那些通过Protobuf
序列化后的数据需要通过base128
编码的解码方式才可以解析,头部占用长度1-4个字节
。
12-5. 举例:竞技场-解包处理
enum{
socketBouldIdByxt = 0, //心跳包
socketBouldIdByaq = 1, //安全认证
socketBouldIdByStop = 2, //停止链接
socketBouldIdBylist = 100, //竞技场列表
socketBouldIdByOutJJC = 1001, //退出竞技场
socketBouldIdByInJJC = 1002, //加入竞技场
socketBouldIdByInfoChange = 1003, //用户状态变更
};
-(void) didReadData:(NSData *)data {
//将接收到的数据保存到缓存数据中
[self.cacheData appendData:data];;
_receData = data.length + _receData;
if (self.cacheData.length < 12) {//不完整的包
[self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
return;
}
//总的包都长度
int totalLenght = [BytesUtils getIntFromData:self.cacheData withOffset:8] + 12;
if (_cacheData.length >= totalLenght) {
//截取完整数据包
NSData *dataOne = [_cacheData subdataWithRange:NSMakeRange(0, totalLenght)];
//接收到的每个包的头尾长度都是不固定的,是需要开发的时候协商的。data 取内容的时候,是需要去掉头尾的字节。再去转
NSString *string = [BytesUtils getAppStrDatafromData:dataOne];
int bould = [BytesUtils getId:dataOne];
if (bould == socketBouldIdByaq) {
// NSLog(@"安全验证通过");
}else if (bould == socketBouldIdByxt){
// NSLog(@"心跳验证通过");
}else if (bould == socketBouldIdByStop){
// NSLog(@"心跳验证通过");
self.socket.userData = SocketOfflineByStopConnect;
[self.socket disconnect];
}else{
if (bould == socketBouldIdBylist) {//竞技场列表
NSArray *arr = [ATGeneralFuncUtil JsonToDict:string];
if ([self.delegate respondsToSelector:@selector(jjcListArr:)]) {
[self.delegate jjcListArr:arr];
}
}else if (bould == socketBouldIdByOutJJC || bould == socketBouldIdByInJJC) {//退出或者加入竞技场
NSDictionary *dict = [ATGeneralFuncUtil JsonToDict:string];
if ([self.delegate respondsToSelector:@selector(listDidChange:isIn:)]) {
[self.delegate listDidChange:dict isIn:bould == socketBouldIdByOutJJC?false:true];
}
}else if (bould == socketBouldIdByInfoChange) {//竞技场用户状态发生变更
NSDictionary *dict = [ATGeneralFuncUtil JsonToDict:string];
if ([self.delegate respondsToSelector:@selector(listInfoDidChange:)]) {
[self.delegate listInfoDidChange:dict];
}
}
}
self.count ++ ;
[_cacheData replaceBytesInRange:NSMakeRange(0, totalLenght) withBytes:nil length:0];
[self didReadData:nil];
}else{
[self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
}
}
写在最后:实际上每个消息发过来的时候都有一定的大小 ,正常情况就是收到消息进行解包 , 就是一个消息体能够正常接受,但是再收到消息巨大的情况下 , 下个包的部分消息会拼到前一个包上,这是导致丢包的一个问题,所以要对收到的消息进行拼接再解包,这时候就是粘包 ,其他还要了解 心跳机制、PingPong机制、断线重连、离线消息等技术点。
IM的其他实现方式
(1)基于WebSocket最具代表性的一个第三方框架-SocketRocket
实现的思路和基于CocoaAsyncSocket框架类似,需要编写遵守webSocket协议的服务端,感兴趣的也可以参照实现一下。
(2)基于MQTT协议的框架-MQTTKit
MQTT是一个聊天协议,它比webSocket更上层,属于应用层,它的基本模式是简单的发布订阅,也就是说当一条消息发出去的时候,谁订阅了谁就会收到消息。其实它并不适合IM的场景,例如用来实现有些简单IM场景,却需要很大量的、复杂的处理。这个框架是c来写的,把一些方法公开在MQTTKit类中,对外用OC来调用,这个库有4年没有更新了。
(3)基于XMPP协议的框架-XMPPFramework
XMPP是较早的聊天协议(2000年发布第一个公开版本),当时主要是用来打通 ICQ、MSN 等 PC 端的聊天软件而设计的,技术比较成熟,它本身有很多优点,如开放、标准、可扩展,并且客户端和服务器端都有很多开源的实现,但是相对于移动端它也有很明显的缺点,譬如数据负载过重、不支持二进制,在交互中有50% 以上的流量是协议本身消耗的,需要做深度的二次开发。
Protobuf参考文章:https://www.jianshu.com/p/bb1c8adc62c7
CocoaAsyncSocket参考文章:https://www.jianshu.com/p/8800328f60bf
13、开发过程中遇到什么难题,如何解决的?
不回答一些比较基础的难题,比如控件不好写,或者数组越界之类的话题。
可以说说比较高大上的 内存、数据储存、缓存、图片处理、视频处理。
比如:我在开发我们项目的时候, 涉及到图像处理的问题,就比如说现在网络上比较火的用
SDWebImage
下载超清大图的时候出现的崩溃问题,因为decodeImageWithImage
这个方法用于对图片进行压缩并且缓存起来,以保证tableview/collectionview
交互更加流畅,但是如果用此方法加载超清大图的时候, 会适得其反,有可能导致上G的内存消耗, 解决办法是对于高清图片,应该放在图片解压之后,禁止缓存解压后的数据。 代码如下”
- SD4.X的解决办法
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
- SD5.0 及以上的解决办法
SDWebImageAvoidDecodeImage添加了这个枚举,意思是在子线程程解压缩图片
[self.imageView sd_setImageWithURL:self.url placeholderImage:[UIImage imageNamed:@"logo"] options:SDWebImageAvoidDecodeImage];
再比如:我在开发大数据列表的时候,公司产品要求进入界面展示缓存数据,由于数据模式以及展示UI类型比较多,数据处理起来比较麻烦,于是想到使用的一个第三方
FMDB
封装工具来实现,实现过程中,出现有些数据未保存。造成UI显示混乱。
解决方案:输出检查缓存的数据是否正常,发现数组。字典等类型无法转换,考虑到这个需求是后期提出的,无法马上更换FMDB
数据库的工具,只能在原基础上把数组的数据转换成一个字符串,用某一个符号,比如“&”不重复的符号来拼接储存数据,在进入界面的时候再把字符串转换成数组,即可。
再比如:当一个动态界面发布多种数据时候,比如1个视频或者多个视频+9张图片这种,由于公司需求需要一个点发布,自己马上就能看到但是不能操作的一条动态,在测试阶段,6以下的低内存机型会出现崩溃的问题。
解决方案:由于图片高清导致内存暴涨,部分机型会崩溃, 在点发布的时候,要对图片进行一个压缩处理后展示,这样可以避免一个崩溃问题,然后在提供给后台一个预发布的功能,图片异步上传,先传文本等字符串数据,传递后台预发布接口后返回一个id,在等图片上传完成后,请求发布成功接口,传入id,这样就可以做到先展示图片+视频数据,又能同时在默默的上传数据了。
14、RAC框架 你了解多少?
参考文章:https://www.jianshu.com/p/1837209e68d5
15、 讲一下 iOS 如何管理内存的 ?
可以稍微说下mrc+arc:
1.MRC 手动内存管理
2.ARC 自动引用计数,arc的判断准则,只要没有强指针指向对象,对象就会被释放。
然后最好回答工作中是如何处理内存管理的。
1.block的内存管理:
由于使用block很容易造成循环引用,因此一定要小心内存管理问题。最好在基类controller下重写以下dealloc,加一句打印日志,表示类已经得到释放。如果出现无法打印信息,说明这个类一直得不到释放,表明很有可能是使用block的地方出现循环引用了。对于block中需要引用外部controller的属性或者成员变量的时候,一定要使用弱引用,特别是成员变量像_textId这样的,导致内存得不到释放。
2.对于普通项目来说,因为现在都是ARC项目,记住ARC下的黄金内存管理原则:
只要还有人使用这个对象,那么这个对象就不会被回收
只要你想使用这个对象,那么就应该让这个对象的引用计数+1
当你不像使用这个对象的时,应该让对象引用计数-1
谁创建,就由谁来release
如果你通过alloc、new 、copy来创建一个对象时,当你不想用这个对象的时候就必须调用release 或者 autorelease 让引用计数-1
不是你创建的就不用负责 release
总结:有加就有减,曾让某个计数+1.就应该让其最后-1
拓展
内存管理研究的对象
- 野指针:指针变量没有进行初始化或者指向的空间已经被释放了。
使用野指针调用调用对象方法,会报异常 ,程序崩溃。
通常再调用完release方法后,把保存对象指针的地址清空,赋值为nil,所以【nil retain】 不会有异常。- 内存泄漏。
3.僵尸对象:堆中已经被释放的对象(retainCount = 0)- 空指针:指针赋值为空,nil
16、 开发项目时你是怎么检查内存泄漏?
- 静态分析 analyze
- instruments工具里面有一个leak 可以动态分析
如果再block中多次使用weakSelf的话,可以再block中先使用strongSelf,防止block执行时weakSelf被意外释放。
ARC下,将weak改用为block 即可。
17、 常见的出现内存循环引用场景有哪些?
1.定时器(NSTimer):初始化要指定self为target,容易造成循环引用(self->timer->self);
2.block:成员变量再block内部使用,self.变量。互相引用;
3.delegate:使用assign(MRC),或者weak(ARC)
18、 KVC底层实现?
KVC
:键值编码 使用字符串直接访问对象属性。
- 当一个对象调用
setValue
方法时,方法内部会做以下操作:
1.检查是否存在相应的key
的set
方法,如果存在,就调用set方法;
2.如果set
方法不存在,就会查找与key
相同名称并且带下划线的成员属性,如果有,则直接给成员变量赋值。
3.如果没有找到_key
,就会查找相同名称的属性Key
,如果有直接赋值。
4.如果还没有找到,则调用valueForUndefinedKey
:和setValue:forUndefinedKey
:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写他们。
19、 KVO底层实现?
KVO
:键值观察机制,它提供了观察某一属性的变化方法。
- kvo 基于runtime 机制实现。当一个对象的属性值发生改变时,系统会自动生成一个类,继承
NSKVONotifying_MYPerson
isa=(Class)NSKVONotifying_MYPerson.
最后 MYPerson 改变成NSKVONotifying_MYPerson
,说的不清楚,建议代码输出断点测试下。
使用完记得要移除观察者。
20、 谈谈你是怎么封装View的?
1.先添加子控件
2,接收模型数据根据模型数据来控制控件的位置
3.自己的事情自己做,把不需要暴露的都封装起来。
21、 什么情况下会死锁?如何避免死锁?
死锁:两个或两个以上的线程,互相等待彼此停止以获得某种资源,但是没有一方会提前退出的情况。
“同步”且在“当前队列【主线程】” “同步”且在“同一个串行队列”1、“同步”且在“当前队列【主线程】”中执行任务,必定死锁
2、“同步”且在“同一个串行队列”中执行任务,必定死锁
避免死锁
1.避免在串行队列中执行同步任务。
2.避免Operation相互依赖。
23、了解哪些新技术点和方向?
1、iOS 14 轻应用(App Clips)开发调试: https://www.jianshu.com/p/21af85107a34
24、UITableView 卡顿的情况如何优化?
1). 正确的复用cell。
2). 设计统一规格的Cell
3). 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法;
4). 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
4). 滑动时按需加载,这个在大量图片展示,网络加载的时候很管用!
5). 减少子视图的层级关系
6). 尽量使所有的视图不透明化以及做切圆操作。
7). 不要动态的add 或者 remove 子控件。最好在初始化时就添加完,然后通过hidden来控制是否显示。
8). 使用调试工具分析问题。
9). cell的展示数据最好在展示之前处理,避免过多的逻辑处理
10).使用正确的数据结构。
11).减少动画效果。
25、 ios 内存几大区域?
1). 栈区(stack): 栈区主要存放局部变量和函数参数等相关变量,超出作用域之后自动
释放。由编译器自动分配和释放。
2). 堆区(heap): 堆区存放alloc,new等关键字生成的对象。由程序员分配和释放,若
不释放,则程序结束由操作系统回收。
3). 全局静态区(global): 全局静态区主要存放静态数据,全局数据和常量,程序运行之后一直存在。由编译器管理(分配和释放)。
4). 文字常量区: 由编译器管理(分配释放),程序结束由系统释放;存放常量字
符。
5). 程序代码区: 存放函数的二进制代码。
注:我们平时关注最多的部分都是堆区的内存。
26、 数据存储的优劣?
1).
NSUserDefaults
优点:轻量级,易操作,保密性好
缺点:
1.不支持自定义对象的存储。
2.存储的数据都是不可变的,想将可变数据存入需要先转为不可变才可以存储。
3.定时把缓存中的数据写入磁盘的,而不是即时写入。
场景:用于存储用户的偏好设置和用户信息,如用户名,是否自动登录,字体大小等。
2).plist(属性列表文件)
优点:轻量级,易上手
缺点:
1.不支持自定义对象的存储。
2.对数据的操作比较繁琐。需要全部读写。
场景:用于存储程序中经常用到且数据量小而不经常改动的数据。
3). 归档(属性列表文件)
优点:可以创建自己想要的数据模型,然后统一以模型方式存储
缺点:
1.速度相对较慢
2.保存和读取都需要一次性读取和写入。不支持检索。
3.需要对对象进行归档,结档操作,相对麻烦。
场景:需要对自定义模型进行保存。比如通讯录分类排序。保存对象
4). FMDB
优点:支持大批量的数据存储,支持检索,操作方便
缺点:
1.原生的支持类型不多。
2.需要有sql基础。
场景:即时聊天,社交。
5). CoreData
优点:支持对象存储。无需任何sql语句。支持可视化管理
缺点:
1.操作繁琐
场景:即时聊天,社交。
27、静态库和动态库的区别?
1). 静态库和动态库的区别
1. 系统创建的库为动态库,即在一次加载中,针对手机上的所有应用,该库都是通用的。
2. 开发者创建的库为静态库。该库的作用域仅在该app范围内。多次使用则多次加载。
2)..framework
和.a
的区别
1. 两者都是静态库。
2. .a是一个纯二进制
文件,而.framework里面除了二进制文件还包括资源文件
。
3. .a文件不能直接使用,需要.h文件配合使用,而.framework可独立使用。
4. .a无需暴露内部文件,而.framework 需要至少暴露一个头文件。
27、RunTime
image.png1). runtime简介
1. runtime简称运行时,OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。
2. 对于OC的函数,属于动态调用过程,在编译的时候不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
3. 在编译阶段:OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言调用未实现的函数就会报错。
2). runtime的五个功能
1. 发送消息
2. 交换方法
3. 动态添加方法
4. 给分类添加属性
5. 字典转模型
3). runtime的应用场景
1. 关联对象(Objective-C Associated Objects)给分类增加属性
2. 方法魔法(Method Swizzling)方法添加和替换和KVO实现
3. 消息转发(热更新)解决Bug(JSPatch)
4. 实现NSCoding的自动归档和自动解档
5. 实现字典和模型的自动转换(MJExtension)
4). 运行时阶段的消息发送的详细步骤
1. 检测selector 是不是需要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain,release 这些函数了。
2. 检测target 是不是nil 对象。ObjC 的特性是允许对一个 nil对象执行任何一个方法不会 Crash,因为会被忽略掉。
3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,若可以找得到就跳到对应的函数去执行。
4. 如果在cache里找不到就找一下方法列表methodLists。
5. 如果methodLists找不到,就到超类的方法列表里寻找,一直找,直到找到NSObject类为止。
6. 如果还找不到,Runtime就提供了如下三种方法来处理:动态方法解析、消息接受者重定向、消息重定向。三者关系见消息转发流程图
28、谈谈你对MVC的理解
1). 数据管理者(M)、数据展示者(V)、数据加工者(C)
2). M 应该做的事:
1. 给 ViewController 提供数据
2. 给 ViewController 存储数据提供接口
3. 提供经过抽象的业务基本组件,供 Controller 调度
3). C 应该做的事:
1. 管理 View Container 的生命周期
2. 负责生成所有的 View 实例,并放入 View Container
3. 监听来自 View 与业务有关的事件,通过与 Model 的合作,来完成对应事件的业务。
2). V 应该做的事:
1. 响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在 View 去做)等。
2. 界面元素表达。
29、TCP 和 UDP 有什么区别?
TCP 和 UDP1). TCP 是面向连接的,建立连接需要经历三次握手,保证数据正确性和数据顺序(
上面IM面试题里面有细说
)
2). UDP 是非连接的协议,传送数据受生成速度,传输带宽等限制,可能造成丢包
3). UDP 一台服务端可以同时向多个客户端传输信息
4). TCP 报头体积更大,对系统资源要求更多
30、 常用的设计模式有哪些?
1). 单例模式
2). 观察者模式
3). 代理模式
4). 享元模式
3). 工厂方法模式
3). 抽象工厂模式
31、 沙盒目录结构是怎样的?各自用于那些场景?
1).
Application
:存放程序源文件,上架前经过数字签名,上架后不可修改
2).Documents
:常用目录,iCloud 备份目录,存放数据
3).Library
:
3- 1.Caches
:存放体积大又不需要备份的数据
3-2.Preference
:设置目录,iCloud 会备份设置信息
4).tmp
:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能
32、 类方法和实例方法的区别?
1).
类方法
:
1. 类方法是属于类对象的
2. 类方法只能通过类对象调用
3. 类方法中的 self 是类对象
4. 类方法可以调用其他的类方法
5. 类方法中不能访问成员变量
6. 类方法中不能直接调用对象方法
7. 类方法是存储在元类对象的方法缓存中
2).实例方法
:
1. 实例方法是属于实例对象的
2. 实例方法只能通过实例对象调用
3. 实例方法中的 self 是实例对象
4. 实例方法中可以访问成员变量
5. 实例方法中直接调用实例方法
6. 实例方法中可以调用类方法(通过类名)
7. 实例方法是存放在类对象的方法缓存中
33、 区分nil 、Nil、NULL、NSNUll?
1).
nil
: 一般是指把一个对象置空,既完全是一个空对象,完全从内存中释放。
2).Nil
: 和nil基本没有任何区别,也可以说只要是可以使用nil的地方都可以使用Nil,
反之亦然。但是作为程序猿,我们应该更加严谨一些。nil和Nil的区别在于,
nil表示置空一个对象,而Nil表示置空一个类。
3).NULL
: 大家都知道oc 是基于c的,并且oc是完全兼容c的,NULL源于c,表示一个空指针. 即:int *p = NULL
4). NSNull
: 很有意思,大家一般都会觉得,NSNull也是空,但是看着这货又是“NS”开
头的很像一个对象,实质上NSNull的确是一个对象,它继承于NSObject。那它
和nil的区别在哪里呢?nil是把一对象完全释放,就是完全从内存中释放。但是
当我想把一个对象置空但是又想要一个容器的时候,我们就可以使用NSNull。
比如一瓶矿泉水,我们不想要里面的水,但是我们想保留瓶子一样。
34、 区分深拷贝与浅拷贝?
1).
浅拷贝
:指针拷贝
,不增加新的内存。只是新增加一个指针指向原来的内存区域
。
2).深拷贝
:内容拷贝
,同时拷贝指针和指针指向的内存。新增指针指向新增的内存
。
3). 拷贝条件:
iOS中并非所有的对象都支持copy和mutableCopy,只有遵循了NSCopy协议或者
NSMutableCoy协议的类才行。如果遵循着两个协议就必须分别实现
copyWithZone和mutableCopyZone方法
4). 拷贝原则:
1. 非容器类:像NSString、NSNumber这样的不能包含其他对象的系统类
不可变对象调用copy是浅拷贝;而调用muablecopy是深拷贝并得到可变对象
可变对象调用copy和mutablecopy都是深拷贝, 区别在于copy返回不可变对
象,mutablecopy返回可变对象
2. 容器类:像NSArray、NSMutableArray等系统类
不可变对象调用copy是浅拷贝,而调用muablecopy是深拷贝并得到可变对象。
可变对象调用copy和mutablecopy都是深拷贝,区别在于copy返回不可变对象,
mutablecopy返回可变对象
3. 容器类与非容器类的拷贝原则相似,但需要注意的是:所有的容器类的拷贝,
拷贝后新容器里的元素始终是浅拷贝,其指针都指向原来对象。
34、 Category(类别)、 Extension(扩展)和继承的区别?
1). 分类有名字,类扩展没有分类名字,是一种特殊的分类。
2).分类只能扩展方法
(属性仅仅是声明,并没真正实现),类扩展可以扩展属性、成员变量和方法
。
3).继承
可以增加,修改或者删除方法,并且可以增加属性
。
35、ViewController生命周期
1). initWithCoder:通过nib文件初始化时触发。
2). awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
3). loadView:开始加载视图控制器自带的view。
4). viewDidLoad:视图控制器的view被加载完成。
5). viewWillAppear:视图控制器的view将要显示在window上。
6). updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
7). viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
8). viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
9). viewDidAppear:视图控制器的view已经展示到window上。
10). viewWillDisappear:视图控制器的view将要从window上消失。
11). viewDidDisappear:视图控制器的view已经从window上消失。
36、如何保证线程安全?
1).
原子操作
(Atomic Operation):是指不会被线程调度机制打断的操作;这种操作
一旦开始,就一直运行到结束,中间不会有任何的上下文切换(context switch)。
2).内存屏障
(Memory Barrier):确保内存操作以正确的顺序发生,不阻塞线程(锁
的底层都会使用内存屏障,会减少编译器执行优化的次数,谨慎使用)。
3).锁
。
1.互斥锁
(pthread_mutex):原理与信号量类似,但其并非使用忙等,而是阻塞
线程和休眠,需切换上下文。
2.递归锁
(NSRecursiveLock):本质是封装了互斥锁的
PTHREAD_MUTEX_RECURSIVE类型的锁,允许同一个线程在未释放其拥有的锁时反
复对该锁进行加锁操作。
3.自旋锁
(OSSpinLock):通过全局变量,来判断当前锁是否可用,不可用就忙
等。
4.@synchronized(self)
:系统提供的面向OC的API,通过把对象hash当做锁来
用。
5.NSLock
:本质是封装了互斥锁的PTHREAD_MUTEX_ERRORCHECK类型的锁,
它会损失一定性能换来错误提示,因为面向对象的设计,其性能稍慢。
6.条件变量(NSConditionLock)
:底层通过(condition variable)pthread_cond_t
来实现的,类似信号量具有阻塞线程与信号机制,当某个等待的数据就绪后唤醒线
程,比如常见的生产者-消费者模式。
7.信号量(dispatch_semaphore)
37、 AFNetworking 断点续传原理是什么?
1.检查服务器文件信息
2.检查本地文件信息
3.如果比服务器文件小,开启断点续传,利用HTTP请求头的Range 来实现
4.如果比服务器文件大,重新下载;
4.如果和服务器文件一样大小,下载完成;
38、 iOS自带框架库有哪些?
分类:
音视频:CoreAudio、OpenAL、MediaLibrary、AVFoundation
数据管理: CoreData 、SQLite
图片和动画: CoreAnmiation、OpenGLES 、Quartz2D
网络:WebKit、Bonjour、BSDScokects
用户应用: AddressBook 、Cor Loaction 、MapKit 、 StoreKit、
39、im丢包?具体怎么拼接?用什么协议传输?
丢包实际场景中更多应该都是网络环境导致的。只能说怎么去验证包的完整性,用于判断包是否完整是否存在丢包。
tcp
40、你是如何内存优化的?性能?
41、你是怎么使用多线程,哪些场景?
42、如何使用一个多线程来请求多个接口,最后刷新UI?
异步并行队列,通过等待信号线程等待所有线程全部执行完毕再刷新
43、说一下runtime的交换方法,具体怎么实现?添加属性又要怎么做?
交换方法:https://www.jianshu.com/p/5f85a676326f,简单说,可以在系统启动后自动运行的load方法里面通过replace方法交换,交换后重写方法的实现。
添加属性:https://www.jianshu.com/p/3131b98b0420,简单说,一般会问分类里面能不能添加属性,正常你添加后没办法使用,但是我们可以使用runtime动态添加属性,在点m里面重写它的setter&&getter方法,例如下代码:
#import <objc/runtime.h>
@implementation People (P)
- (NSString *)address
{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAddress:(NSString *)address
{
objc_setAssociatedObject(self,
@selector(address),
address,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
44、 NSTimer定时器使用runloop,为何要使用?怎么管理内存泄漏?
当使用
NSTimer
的scheduledTimerWithTimeInterval
方法时。事实上此时Timer
会被加入到当前线程的Run Loop
中,且模式是默认的NSDefaultRunLoopMode
。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动
操作,会将Run Loop切换成NSEventTrackingRunLoopMode
模式,在这个过程中,默认的NSDefaultRunLoopMode
模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行。
所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:
NSRunLoopCommonModes
,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合
。
无论是单次执行的NSTimer还是重复执行的NSTimer都不是准时的,这与当前NSTimer所处的线程有很大的关系,如果NSTimer当前所处的线程正在进行大数据处理(假设为一个大循环),NSTimer本次执行会等到这个大数据处理完毕之后才会继续执行
这期间有可能会错过很多次NSTimer的循环周期,但是NSTimer并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环,也就是在一个循环周期内只会执行一次循环。
无论循环延迟的多离谱,循环间隔都不会发生变化,在进行完大数据处理之后,有可能会立即执行一次NSTimer循环,但是后面的循环间隔始终和第一次添加循环时的间隔相同。这个事件是怎么执行的?并且为什么有的时候会延迟?为什么子线程中创建的Timer并不执行?
首先,在进入循环开始以后,就要处理source0事件,处理后检测一下source1端口是否有消息,如果一个Timer的时间间隔刚好到了则此处有可能会得到一个消息,则runLoop直接跳转至端口激活处从而去处理Timer事件。
第二,为什么会延迟?我们知道,两次端口事件是在两个runLoop循环中分别执行的。比如Timer的时间间隔为1秒,在第一次Timer回调结束后,在很短时间内立即进入runLoop的下一次循环,这次并不是Timer回调并且是一个计算量非常大的任务,计算时间超过了1秒,那么runLoop的第二个循环就要执行很久,无法进入下一个循环等待有可能即将到来的Timer第二次回调的信号,所以Timer第二次回调就会推迟了。
第三,为什么在子线程中创建的Timer并且提交到当前runLoop中并不会运行?这还是要从runLoop的获取函数中看,当调用currentRunLoop的时候会取当前线程对应的runLoop,而首次是取不到的,则会创建一个新的runLoop。但是!这个runLoop并没有run。就是没有开启
- (void)applicationDidBecomeActive:(UIApplication*)application{
// NSThread 创建一个子线程
[NSThreaddetachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:selfwithObject:nil];
}
// 测试把timer加到不运行的runloop上的情况
- (void)testTimerSheduleToRunloop1
{
NSLog(@"Test timer shedult to a non-running runloop");
SvTestObject *testObject4 = [[SvTestObject alloc] init];
NSTimer*timer = [[NSTimeralloc] initWithFireDate:[NSDatedateWithTimeIntervalSinceNow:1] interval:1target:testObject4 selector:@selector(timerAction:) userInfo:nilrepeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 打开下面一行输出runloop的内容就可以看出,timer却是已经被添加进去//
NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]);
// 下面一行, 该线程的runloop就会运行起来,timer才会起作用
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
NSLog(@"invoke release to testObject4");
}
- (void)applicationWillResignActive:(UIApplication*)application
{
NSLog(@"SvTimerSample Will resign Avtive!");
}
45、.你平时怎么封装组件化?
移植性强,通用类型,满足多种需求,可以说下自己的库
46.怎么封装网络请求,具体怎么实现?
通用的GET+POST,还有下载,上传等等、
47、.GCD的信号量你是什么理解的?
A,B,C三个任务并发执行,但是C要等A,B执行完成之后再执行。
信号量:
// 创建一个信号,value:信号量
dispatch_semaphore_create(<#long value#>)
// 使某个信号的信号量+1
dispatch_semaphore_signal(<#dispatch_semaphore_t dsema#>)
// 某个信号进行等待或等待降低信号量 timeout:等待时间,永远等待为 DISPATCH_TIME_FOREVER
dispatch_semaphore_wait(<#dispatch_semaphore_t dsema#>, <#dispatch_time_t timeout#>)
正常的使用顺序是先降低然后再提高,这两个函数通常成对使用。
两种方法
-(void)dispatch_group_function1
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_queue_create("com.dispatch.test", DISPATCH_QUEUE_CONCURRENT), ^{
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
NSURLSessionDownloadTask *task = [[NSURLSession sharedSession] downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 请求完成,可以通知界面刷新界面等操作
NSLog(@"第一步网络请求完成");
// 使信号的信号量+1,这里的信号量本来为0,+1信号量为1(绿灯)
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// 以下还要进行一些其他的耗时操作
NSLog(@"耗时操作继续进行");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});
dispatch_group_async(group, dispatch_queue_create("com.dispatch.test", DISPATCH_QUEUE_CONCURRENT), ^{
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://www.github.com"]];
NSURLSessionDownloadTask *task = [[NSURLSession sharedSession] downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 请求完成,可以通知界面刷新界面等操作
NSLog(@"第二步网络请求完成");
// 使信号的信号量+1,这里的信号量本来为0,+1信号量为1(绿灯)
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// 以下还要进行一些其他的耗时操作
NSLog(@"耗时操作继续进行");
dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新界面等在主线程的操作");
});
}
2019-02-24 18:19:16.251067+0800 Semaphore[33094:12267734] 耗时操作继续进行
2019-02-24 18:19:16.251071+0800 Semaphore[33094:12267735] 耗时操作继续进行
2019-02-24 18:19:16.549563+0800 Semaphore[33094:12267748] 第一步网络请求完成
2019-02-24 18:19:18.091922+0800 Semaphore[33094:12267737] 第二步网络请求完成
2019-02-24 18:19:18.092222+0800 Semaphore[33094:12267662] 刷新界面等在主线程的操作
设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任务1
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 1");
sleep(1);
NSLog(@"complete task 1");
dispatch_semaphore_signal(semaphore);
});
//任务2
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 2");
sleep(1);
NSLog(@"complete task 2");
dispatch_semaphore_signal(semaphore);
});
//任务3
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 3");
sleep(1);
NSLog(@"complete task 3");
dispatch_semaphore_signal(semaphore);
});
2019-02-24 18:31:02.447769+0800 Semaphore[33286:12284312] run task 1
2019-02-24 18:31:02.447767+0800 Semaphore[33286:12284310] run task 2
2019-02-24 18:31:03.450756+0800 Semaphore[33286:12284312] complete task 1
2019-02-24 18:31:03.450756+0800 Semaphore[33286:12284310] complete task 2
2019-02-24 18:31:03.450997+0800 Semaphore[33286:12284311] run task 3
2019-02-24 18:31:04.454259+0800 Semaphore[33286:12284311] complete task 3
48、SDWebImage底层基本原理?
实现功能及思路:
- cell多图片展示:通过NSData网络下载小图片
- 重复下载问题:设置任务缓存,每次下载前先判断
- 内存暴涨问题:图片二级缓存机制。
- 卡住主线程:在子线程中下载,下载完毕后回主线程刷新UI
- 图片显示错误:通过reloadRowsAtIndexPaths定点刷新UI
49、 给出以下两个数组,block调用后分别输出结果是什么?
NSArray *arr= @[@"1"];
NSMutableArray *mutableArray = [[NSMutableArray alloc]initWithObjects:@"123", nil];
void(^block)(void) = ^{
NSLog(@"array %@",arr);
NSLog(@"mutableArray %@",mutableArray);
};
arr = @[@"2"];
[mutableArray addObject:@"456"];
block();
答案:1和123、456 ,为什么呢?我们通过测试可以知道,不可变数组在进行重新赋值后,系统会自动分配另一块内存给它,所以原来的值不会改变,可变数组的add方法是正常可以执行的,不会有内存地址改变的情况。具体看输出
NSArray *arr= @[@"1"];
NSLog(@"不可变数组指针地址%p===内存地址%p",arr,arr);
NSMutableArray *mutableArray = [[NSMutableArray alloc]initWithObjects:@"123", nil];
NSLog(@"可变数组指针地址%p===内存地址%p",mutableArray,mutableArray);
void(^block)(void) = ^{
NSLog(@"array %@",arr);
NSLog(@"mutableArray %@",mutableArray);
};
arr = @[@"2"];
NSLog(@"不可变数组指针地址%p===内存地址%p",arr,arr);
[mutableArray addObject:@"456"];
NSLog(@"可变数组指针地址%p===内存地址%p",mutableArray,mutableArray);
block();
2020-12-25 17:09:33.959913+0800 NativeFlutterDemo[66551:20771232] 指针地址0x600001dac1a0===内存地址0x600001dac1a0
2020-12-25 17:09:38.265963+0800 NativeFlutterDemo[66551:20771232] mutableArray指针地址0x6000011d8810===内存地址0x6000011d8810
2020-12-25 17:09:41.655014+0800 NativeFlutterDemo[66551:20771232] 指针地址0x600001da8350===内存地址0x600001da8350
2020-12-25 17:10:58.841020+0800 NativeFlutterDemo[66551:20771232] mutableArray指针地址0x6000011d8810===内存地址0x6000011d8810
50.TCP的三次握手是什么?
socket是对tcp的封装,tcp属于网络传输层,能保证数据的安全传输 ,那么tcp三次握手概念:
1.建立连接,客户端A向服务端B发送SYN
包,等待服务端B接收;
2.服务端B收到客户端A的请求,并向客户端A发送一个SYN+ACK
包,等待客户端A接收;
3.客户端A收到数据后确认包ACK = K+1
,完成三次握手;
51.开启一个定时器NStimer,如何在离开界面的释放这个对象?
关于NSTimer释放和内存泄漏的问题。
@(NSTimer)[内存管理,NSTimer释放,循环引用]
出现有些时候无法释放timer,导致dealloc不调用。
问题分析:
原因是 Timer 添加到 Runloop 的时候,会被 Runloop 强引用,然后 Timer 又会有一个对 Target 的强引用(也就是 self )也就是说 NSTimer 强引用了 self ,导致 self 一直不能被释放掉,所以 self 的 dealloc 方法也一直未被执行.
NSTimer还有一个规则:(在哪个线程创建就要在哪个线程停止,否则会导致资源不能被正确的释放。)
问题关键:
问题的关键就在于 self 被 NSTimer 强引用了,如果能打破这个强引用,问题应该就能决了。
来自YYKit的解决方案,把self偷梁换柱:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
return [NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];
}
52.滚动的cell 中都有一个定时器是倒计时,(抢购等倒计时)怎么让他滑动时候依然可以正常倒计时?
Nstimer的创建可以给一个mode,kCFRunLoopCommonModes 、UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
53.产品经理后期在项目已经完成的情况下插入一条需求,要求 监听按钮的点击,埋点统计。要怎么实现?
runtime来实现,在程序启动时候,+(void)load方法里面 替换按钮的系统方法,在【super 方法】之前拦截所点击的按钮tag 来进行标记,最终传给服务器
54.多张图片同时上传给用户好的体验怎么使用gcd来实现?
dispatch_get_global_queue 队列组
#pragma mark //加载 图片
-(void)case10{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
UIImage * img1 = [self myImageWithUrl:@"https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1464070575&di=3a13253ffa34b1dc5a15210527e1c3a5&src=http://img3.3lian.com/2014/f1/2/d/11.jpg"];
UIImage * image2=[self myImageWithUrl:@"http://img3.3lian.com/2014/f1/2/d/9.jpg"];
dispatch_async(dispatch_get_main_queue(), ^{
_image1.image =img1;
_image2.image =image2;
});
});
}
-(void)case11{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
//创建一个组
dispatch_group_t group = dispatch_group_create();
__block UIImage * image1 = nil;
__block UIImage * image2 = nil;
dispatch_group_async(group, queue, ^{
NSLog(@"1111111>>>>>>%@",[NSThread currentThread]);
image1 =[self myImageWithUrl:@"https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1464070575&di=3a13253ffa34b1dc5a15210527e1c3a5&src=http://img3.3lian.com/2014/f1/2/d/11.jpg"];
});
dispatch_group_async(group, queue, ^{
NSLog(@"22222222>>>>>>%@",[NSThread currentThread]);
image2 =[self myImageWithUrl:@"http://img3.3lian.com/2014/f1/2/d/9.jpg"];
});
//获取到图片后刷新ui
dispatch_group_notify(group, queue, ^{
_image1.image = image1;
_image2.image =image2;
});
});
}
55.给出一个UIView,我要操作可点击事件,某一块区域不让点击怎么实现?
1.理解事件传递过程,用这个来实现扩大点击范围
2.使用Runtime机制扩大点击范围
传递过程
当用户点击屏幕后:
UIApplication 先响应事件。
然后传递给UIWindow。
如果window可以响应。就开始遍历window的subviews。遍历原则是:从离用户最近的那个子视图去遍历
遍历的过程中,如果第一个遍历的view1可以响应,那就遍历这个view1的subviews(依次这样不停地查找,直至查找到合适的响应事件view)。
如果view1不可以响应,那就开始对view2进行判断和子视图的遍历。依次类推view3,view4……
如果最后没有找到合适的响应view,这个消息就会被抛弃。这个就是iOS中的事件链。
如图
事件的传递
为了方便,我们将
- (UIView *)hitTest:(CGPoint)point withEvent:(nullableUIEvent *)event
;称为方法A
- (BOOL)pointInside:(CGPoint)point withEvent:(nullableUIEvent *)event
;称为方法B
对view进行重写这两个方法后,就会发现,点击屏幕后,首先响应的是方法A;
如果方法A中,我们没有调用父类的这个方法,那就根据这个方法A的返回view,作为响应事件的view。(当然返回nil,就是这个view不响应).
如果方法A中,我们调用了父类的这个方法,也就是
[super hitTest:point withEvent:event]
;那这个时候系统就要调用方法B;通过这个方法的返回值,来判断当前这个view能不能响应消息。
如果方法B返回的是
NO
,那就不用再去遍历它的子视图。方法A返回的view就是可以响应事件的view。
如果方法B返回的是
YES
,那就去遍历它的子视图。(就是上图我们描述的那样,找到合适的view返回,如果找不到,那就由方法A返回的view去响应这个事件。)
55.为什么只有主线程的runloop是开启的?
mian()函数中调用UIApplicationMain,这里会创建一个主线程,用于UI处理,为了让程序可以一直运行并接收事件,所以在主线程中开启一个runloop,让主线程常驻
56.PerformSelector
和runloop
的关系?
当调用NSObect的 performSelector:相关的时候,内部会创建一个timer定时器添加到当前线程的runloop中,如果当前线程没有启动runloop,则该方法不会被调用.
开发中遇到最多的问题就是这个performSelector: 导致对象的延迟释放,这里开发过程中注意一下,可以用单次的NSTimer替代.
57.KVO
的实现原理
通过
runtime
派生子类的方式 复写相关需要KVO监听的属性,在该属性setter之前和之后调用NSObject的监听方法,这样KVO就实现了属性变换前后的回调.
KVO派生的子类具体格式应该是:
NSKVONotifying_
+类名的类 eg:NSKVONotifying_Person
58.如何手动关闭KVO?
被观察的对象复写如下方法 返回NO即可关闭KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
如果关闭后还想触发
KVO
的话 修改需要手动调用在变量setter
的前后 主动调用willChangeValueForKey
:和didChangeValueForKey
:
59.通过KVC修改属性会触发KVO么?
会,反之不会。
60.哪些情况下使用kvo会崩溃,怎么防护崩溃?
使用不当 会crash,比如:
添加和移出不是成对出现且存在多线程添加KVO的情况,经常遇到的crash是移出 - 内存 dealloc的时候 或者对象销毁前没有正确移出Observer
如何防护?
1.注意移出对象 匹配
2.内存野指针问题,一定要在对象销毁前移出观察者
3.可以使用第三方库BlockKit
添加KVO,blockkit
内部会自动移除Observer
避免crash
.