8、UI事件处理
iOS事件分类:触摸事件、定时器事件、传感器事件、远程控制事件
UIWindow:是一种特殊的View,可以作为试图容器协调VC,还可以处理事件分发
1、通常在一个程序中只会有一个UIWindow为keyWindow,但可以手动创建多个UIWindow
2、触摸事件传给当前window,其他事件只交给keyWindow
3、alert和键盘都是单独的window,每一个UIWindow之间是独立的
4、WindowLevel控制显示等级,默认值是 0,值越高就展示在越前面
触摸事件产生:
IOKit库捕获到事件,交给桌面app,再通过进程通讯交给当前app的runloop的source1任务,然后转成source0,封装成uievent添加到UIApplication事件队列,再交给当前UIWindow分发处理
UIWindow沿着视图树不断逆序遍历subviews,调用子视图的hitTest:withEvent:和pintInside:找到第一响应者,形成响应者链,将事件传递给第一响应者进行处理
触摸事件的传递
继承了UIResponder的UI对象能接受并处理事件,UIResponder内部提供了一系列的touch方法处理事件,提供nextResponder属性可以向上传递事件
hitTest:方法根据试图是否允许交互、是否隐藏、是否透明、触摸点是否在当前视图范围内pointInside方法等查找响应者
事件的响应
1、事件传递给第一响应者后开始处理响应,有三种事件响应方式:UIResponder的touches方法,UIGestureRecognzier和UIButton的UIControlEvent事件
2、UIWindow将事件优先交给手势处理,如果有手势处理直接调用响应者的touchcancel方法,事件不再传递处理结束
3、如无手势,系统会调用第一响应者的touches方法处理事件,第一响应者不能处理事件,会顺着响应链向上传递,直到UIApplication也不能处理将其丢弃
4、UIButton重写了UIResponder的touchs方法不调用super的touchs使事件不再往上传递
父试图加了手势,子button即时没添加target+action,手势也不响应,除非设置userEnabled=NO,UIControl没有重写touch方法,没有添加事件,父试图的手势也会响应
同时响应子按钮click和父试图的tap,设置tap的cancelsTouchesInView=no,参考:https://www.jianshu.com/p/617577ff4be1
响应链应用场景:
1、事件拦截和事件转发:有两种方案实现
(1)重写touchesBegan等系列方法进行拦截
(2)重写hitTest方法添加逻辑返回自己想要的响应者
2、扩大按钮点击范围:创建button分类重写pointInside方法使的系统调用hitTest时判断范围更大
3、回溯响应链生成埋点控件唯一id
4、基于UI响应链的事件传递方式,实现步骤:
(1)添加一个UIResponder的分类方法实现中调用nextResponder的该方法
- (void)routerEventWithSelectorName:(NSString *)selectorName object:(id)object userInfo:(NSDictionary *)userInfo {
[[self nextResponder] routerEventWithSelectorName:selectorName object:object userInfo:userInfo];
}
(2)当第一响应者发生事件后,调用该方法就会沿着响应链向上传递
(3)响应链上的控件可以实现该方法获取事件进行处理,或者添加内容让事件继续向上传递
手势和UI事件响应:
通过hittest找到第一响应者后,可能会有两条路线,手势识别和touchBegan系列方法同时执行,手势识别优先级高如果识别成功会调用touchCancel方法取消响应链上所有的touch方法调用
父View加了tap手势,子view也会相应手势,但是点击子button不会响应手势,不管button有没有添加事件
原因是UIButton,UISwitch,UISegmentedControl,UIStepper和UIPageControl等控件的是否接受触摸事件方法(gestureRecognizerShouldBegin:)返回值默认是NO,这样点击按钮时手势识别会失败,只处理button的touch事件和lick事件,button默认重写的touch事件默认不向上调用touch方法
如果想同时响应tap和button事件,设置设置tap的cancelsTouchesInView为NO,这样不再调用button的是否接受触摸事件方法,两个事件同时响应
aview上增加button,button添加点击事件,再添加一个子bview,点击bview时,bview和button都走了touchbegan和end系列方法,但是button事件没有执行,猜测只有button是第一响应者时才执行
如果aview加了tap手势,点击bview也会触发tap方法
父View加了tap手势,点击子视图uicollectionview的cell,发现cell没有被响应,响应的是tap手势事件
原因是手势识别时可以延时响应touch事件,只有手势识别完成才调用touch相关事件,cell的响应机制就是这样,第一响应者是cell,但是手势识别优先处理手势并没有处理touch事件
如果想要响应cell事件不响应tap,在tap的应接受触摸代理方法shouldReceiveTouch:判断点击的是tableview就返回NO
嵌套滚动试图冲突,如一个UICollectionView嵌套了一个UICollectionView,希望嵌套的UICollectionView在父视图达到一定高度时,父视图不再滚动,而是子视图滚动,有三种方案:
a、设置只有底层响应滚动上层不响应,底层scrollview可滚,上层tableview不可滚动,tableview.height = tableview.contentSize.height
b、创建底层滚动试图的子类,实现手势代理方法使两个滚动手势同时响应,监听两个viewdidscroll方法,在同一方法里分情况设置两个视图的滚动偏移量,如爱钱进产品列表页
c、上下试图同时响应滚动手势,在手势冲突的代理方法里根据偏移量判断响应哪个手势,https://www.jianshu.com/p/adc8d45f0fef
手势:
参考:https://www.jianshu.com/p/617577ff4be1
常见的手势冲突处理:https://www.jianshu.com/p/adc8d45f0fef
我们可以通过配置手势的属性来改变它的表现,下面介绍三个常用的属性:
cancelsTouchesInView:该属性默认是 true。顾名思义,如果设置成 false,当手势识别成功时,将不会发送 touchesCancelled 给目标视图,从而也不会打断视图本身方法的触发,最后的结果是手势和本身方法同时触发。有的时候我们不希望手势覆盖掉视图本身的方法,就可以更改这个属性来达到效果。
delaysTouchesBegan:该属性默认是 false。在上个例子中我们得知,在手指触摸屏幕之后,手势处于 .possible 状态时,视图的 touches 方法已经开始触发了,当手势识别成功之后,才会取消视图的 touches 方法。当该属性时 true 时,视图的 touches 方法会被延迟到手势识别成功或者失败之后才开始。也就是说,假如设置该属性为 true ,在整个过程中识别手势又是成功的话,视图的 touches 系列方法将不会被触发。
delaysTouchesEnded:该属性默认是 true。与上个属性类似,该属性为 true 时,视图的 touchesEnded 将会延迟大约 0.15s 触发。该属性常用于连击,比如我们需要触发一个双击手势,当我们手指离开屏幕时应当触发 touchesEnded,如果这时该属性为 false,那就不会延迟视图的 touchesEnded 方法,将会立马触发 ,那我们的双击就会被识别为两次单击。当该属性是 true 时,会延迟 touchesEnded 的触发,将两次单击连在一起,来正常识别这种双击手势。
9、分类Category
分类:
Category编译之后的底层结构是category_t,里面存储着分类的对象方法、类方法、属性声明、协议信息
在程序运行的时候,runtime会将Category的数据,合并到类信息class_rw_t的方法表头中(类对象、元类对象中)
rw_t->method是一个二维数据结构,原始类的只有一个指针指向类的方法list,runtime在运行时将数组扩容,将分类方法list插入二维数据前面,后编译的在前面
分类的属性:
Category可以添加属性,但是只有属性声明,不会自动生成set/get方法的实现,更不会添加成员变量
因为category_t结构体中并不存在成员变量,且成员变量列表class_ro_t是只读的,不可以在运行时添加,可以自己实现set/get方法通过关联对象间接实现
分类和延展的区别:
延展Class Extension在编译的时候包含在类信息中,Category是在运行时合并过去
如何做到分类方法不覆盖原方法:
可以在调用时runtime遍历方法list找到最后的方法调用
load、initialize方法
1、+load是启动时runtime加载类和分类的过程中根据函数地址调用,只调用一次;
runtime在调用+load方法之前,准备了两个数组分别存储类和分类列表,类数组是先根据编译先加入数组,加入子类时会先把父类加入;
分类只按编译顺序加入数据,调用时先遍历类数组调用+load,所有类都调用完再调用分类数组
2、+initialize方法会在类第一次接收到消息时调用,通过objc_msgSend进行调用
runtime的objc_msgSend内部实现,先判断类是否初始化,如未初始化会先调用父类的+initialize,再调用子类的+initialize,每个类只会初始化1次
3、因为initialize是通过objc_msgSend调用,存在两个问题:
分类实现了+initialize,就覆盖类本身的+initialize调用,如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)所以initialize实现代码要配合GCD ONCE函数
10、关联对象AssociatedObject:
关联对象:
一个对象可以通过key关联多个对象,使用key添加修改删除关联对象
对象走dealloc时会从全局HashMap中查找并删除他的关联对象
关联对象的key要保证全局唯一,有很多种方案实现,一般使用全局静态唯一地址值,建议使用@selecor(name)
存储结构:切套哈希表
外层HashMap以对象地址为key值为内层哈希表,内层哈希表以属性地址为key值为存储的value和内存管理变量
关联对象不会对传入的对象obj进行引用,底层实现支持根据obj生成一个唯一key,对应存储的value进行持有
关联对象如何实现weak修饰
关联对象默认修饰符只有retain, assgin copy没有weak
可以对象对象赋值给一个__weak变量,创建一个返回值为__weak变量的block,将block存储为关联对象值,取值时执行block
11、通知
注册通知有两种方式:selector和block
selecer需要传入接收者observer,selecter,name,和发送者anObject
Block不需要传入接收者,可以传指定队列中执行block,可以指定到主线程执行
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
anObject参数:
anObject:发送通知的对象,如果传nil表示不关心是谁发送的通知,观察者那边传入nil表示可以接受任何发送者的通知。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test:) name:@"test" object:@"1"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"test" object:@"2"];
object相同才会接收到通知方法调用,如果我有一个需求,用户点击某个按钮十次才会触发某个事件,那么我就可以把这个参数设置为 @10,每次点击将发送的 object 加1,这样当点击的数量等于10的时候就可以自动触发回调事件而不需要写
通知的存储结构:哈希表+链表(name和anObject是key,observer和seleter是链表节点值)
注册的通知存储在通知中心单利的NCTbl结构体中,以有无name和object分别储存在三个表结构中
1、有name和无论有无object,使用二维哈希表+链表Obs,外层key是name地址值为内层哈希表,内层哈希表key是object值时存储observer和select的链表
2、无name有anObject的,一维表+链表Obs,key为object值为链表
3、全无的,直接存链表Obs
通知的发送:
1、一条通知对象是由三个元素组成:通知名name,发送者object,附加参数userInfo,发送消息时选择性传入
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
2、发送通知时根据name和object查找到所有的链表节点obs对象(保存了observer和sel),放到数组中,依次调用performSelector同步发送,block形式注册的通知放到队列中执行
3、添加通知队列NSNotificationQueue的通知,可以根据添加的策略结合runloop运行时机将通知交给通知中心发送,并提供通知合并管理功能
从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop的时机来触发的
通知与多线程:
1、通知是线程安全的,通过runtime执行selecter,通知的发送和接收在同一线程且多个接收者顺序执行
2、想要接受时在指定线程执行可以在selecter方法内部切换线程,或使用block注册时指定队列,或使用NSMachPort实现线程通讯参考:https://juejin.cn/post/6844904147691503624
通知移除:
iOS9之后对接收者的持有由unsafe_unretained 变成了weak,不移除也安全
但是通过 addobserverForName :object: queue:usingBlock 方法注册的观察者是强引用的需要手动释放
12、KVO
KVO:键值监听,用于监听某个对象属性值的改变
使用NSObject分类实现监听的注册和移除方法,- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
一个KVO包含的数据有5个:调用add的对象为注册KVO的对象(被观察者)、实现监听代理的Obsever(观察者)、属性keyPath、新老值option,环境信息context(不同对象相同属性key时用于区分环境)
触发KVO的方式点语言self.age和KVC,直接访问成员变量不行self->age
KVC可以触发KVO,不管KVC是通过set赋值还是直接查找成员变量赋值,也就是KVC内部实现有调用KVO方法的逻辑,不是通过调用KVO派生子类的set方法实现的,应该是在setValue:forKey前后调用了willchangevalue和didchangevalue
KVO的原理:
1、注册KVO的类在运行时自动生成一个子类即派生类,派生会被缓存下来,将实例对象的isa指向派生子类,并重写set/class/delloc,添加isKVO方法
set方法调用fundation框架的_NSSetXXXValueAndNotify函数,函数内部调用willChangeValueForKey、父类原来的setter、didChange…方法,向监听者发消息调用代理方法
2、存储:这些数据存储在全局的嵌套hashMap+数组中,外层以被观察者为key,内层以被观察属性keyPath为key,数组里的元素存储观察者obsver和option等信息
3、KVO是线程同步的,回调函数执行与KVO发生在同一线程
https://www.jianshu.com/p/56baca325824
手动触发KVO:
automaticallyNotifiesObserversForName函数默认返回YES自动触发KVO,可以重写该函数返回NO,手动触发KVO
手动触发需要成对调用willChangeValueForKey和didChangeValueForKey
KVO 监听数组变化
1、对可变数组属性添加KVO默认数组内容变化不会触发KVO,可以通过mutableArrayValueForKey:获取可变数组再添加元素可以实现监听,如:[[self.selectedsArr mutableArrayValueForKey:@"selecteds"] addObject:]];
2、原理猜测是通过mutableArrayValueForKey函数获取一个重写了add和remove方法的数组,添加了willChange和didChange
KVO异常防护:
KVO异常原因:KVO的注册和移除必须成对出现,否则会出现崩溃,观察者已销毁未移除KVO、多次移除、未注册就移除都会出现崩溃、KVO使用期间keypath销毁、为实现代理方法等等
防护原理:
1、先建立NSObject分类,拦截add、remode、和delloc方法
2、创建一个KVODeleage类充当观察者,将原始的被观察者、观察者、keypath、options、context等信息全部存储在一个字典中,存储结构嵌套哈希表+数组,key是被观察者地址+属性keypath,值为观察者等
3、添加和移除观察者时可以结合字典排重
4、拦截dealloc后可以进行移除kvo避免漏移
https://www.imgeek.org/article/825358174
13、KVC:
键值编码:通过key和keypath存储访问实例变量的方式
KVC的属性赋值是以NSObject分类方式NSObject(NSKeyValueCoding)实现的
分类中方法+ (BOOL)accessInstanceVariablesDirectly;默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
KVC赋值流程:取值流程类似
KVC对容器类也实现了分类,添加了setValue: forKey:方法
如果是字典的话,则修改key对应的value,如果是数组或者集合,则会向每个对象发送此消息,去修改元素的key对应的property
对于字典value为nil时不会crash,对于字典相当于删除key-value对,相当于调用 -removeObjectForKey,调用字典的setObject: forKey:方法会crash
KVC可以触发KVO,不管KVC是通过set赋值还是直接查找成员变量赋值,也就是KVC内部实现有调用KVO方法的逻辑,不是通过调用KVO派生子类的set方法实现的,应该是在setValue:forKey前后调用了willchangevalue和didchangevalue方法
KVC引用场景:
1、解析数据时字典转模型,路由参数解析赋值
2、可视化无痕埋点时附加字段抓取
14、数组
数组:是一种顺序存储的线性表,所有元素的内存地址是连续的
OC数组本质:
NSArray和NSMutableArray底层使用的是隐藏类__NSArrayI、__NSArrayM和__NSSingleObjectArrayI
数据存储在id *list数组指针中,可变数组的list是循环数组,_offset标识起始插入位置
可变数组在超限时自动创建大小为原来二倍的新数组并复制过去
数组遍历方式:
OC中数组有4类:for、for in、枚举器enumerate、dispatch_apply函数等
for in性能最好,基于快速枚举,直接从c数组中取值
枚举器enumerate包含一种子线程遍历,耗时较大的遍历可用
为什么数组遍历删除元素crash
for in删除会Crash,因为底层遍历时会做检查,原数组发生改变就crash
for删除不会crash,因为底层调用n遍objectAtIndex:,for()中做了越界保护
数组遍历删除方案:
遍历数组取出待删除元素,调用removeObjectsInArray:
直接调用for删除会发生元素跳位,删除元素时可配合使用i--
15、NSDictionary和哈希表
NSDictionary底层实现:
1、NSDictionary是对NSMapTable的封装,底层采用哈希表存储,开放定制法解决哈希冲突
2、哈希表本质是一个数组,字典在存值时,key和value都不能为空,会根据key的哈希值经过位运算(取余%)计算得到存储的index
3、key可以是字符串或NSobject类型,但是必须遵守copying协议实现copyWithZone方法,继承NSObject作为key需要重载hash:和isEqual:方法,如不重载默认NSObject的hash方法返回内存地址,isEqual:只是判断内存地址是否相等==
4、NSMapTable是可变的,可以设置对key和value的weak引用,当key和value被释放时自动清除实体,也可以设置添加value时复制,一般用于实现弱引用表如SD内存缓存表
5、KVC通过字典分类添加setValue:forKey:方法进行字典赋值,key必须是字符串类型不能是对象,key不能为nil,value可以为空,为nil时删除key元素
6、NSSet底层封装NSHashTable,NSHashTable同样可以更灵活的定制规则
关于==、isEqual、hash
==基础类型比较值是否相等,对象类型判断两个对象的内存地址是否相等
isEqual:方法NSObject的默认实现是==,即比较内存地址,子类可以重载,可实现更高级别的比较,如地址不同但两个对象属性相同可认为是同一个对象
hash:方法NSObject默认返回对象地址,字典存值时根据哈希值计算位置,为了避免存值的哈希冲突在使用非字符串作为key是尽量重写该方法减少不同哈希冲突,NSString类型的hash函数经过重载已经不是简单的内存地址,不容易出现哈希冲突建议使用字符串作为key。https://juejin.cn/post/6844903717578211341
哈希表(hash table,也叫散列表)
1、哈希表根据键key直接访问Value的数据结构,哈希表的key和value封装成节点,实际存储在链表节点(拉链法)或数组中(开发定址法)
2、哈希函数:字符串key经过hash方法得到一个整数值x,根据除留余数法,将x值余哈希表大小M能得到最初的index,由于不同的key会得到相同的index所以需要处理哈希冲突
3、哈希表扩容2倍?(数组扩容一般1.5倍)
哈希表存值时根据key的哈希值对数组进行取余XXX%9,可转换成进行&运算XXX&(9-1),例如原始长度是16,扩容后的长度是32,这样做是为了&运算的结果尽量保持一致,数组扩容是2的n次幂时就可以尽量避免hash冲突的发生。https://blog.csdn.net/pk_sir/article/details/107858439
哈希冲突:
1、拉链法:数组 + 链表,最终的数据存储在链表节点中
字典的key通过哈希函数得到数组index,数组的每一个元素指向一个链表,链表的节点存储字典的key和value,出现哈希冲突时使用链表连接所有节点
链表过长时会转成红黑树存储
2、开放定址线性探测法:使用多个数组完成,最终的数据存储在数组中
数组keys存储所有key,values存储所有值,得到index即可访问数据
创建一个处理哈希冲突的数组存index,当哈希函数得到相同的index时直接将下标索引加一
哈希冲突方案对比:
拉链法链表动态申请,适合节点不固定情况,链表方便删除节点,但拉链法的内存使用较多,拉链法处理冲突简单不会产生堆积问题,适用于数据量大的
开发定制法删除节点时只能标记删除,不能真删,容易产生堆积问题,适用于数据量小或临时缓存表等如__weak表
iOS中常用哈希表的地方
16、SDWebimage
SD通过OperationQueue子线程下载任务、内存+磁盘双重缓存、子线程解码
下载并发数是6,超时15秒,缓存过期默认一周,50M
SD的缓存:
1、SD请求图片前先根据url的md5分别请求内存缓存和磁盘缓存,都没有再去下载
2、内存缓存:使用NSCache的子类存uiimage+弱引用表NSMapTable管理UIImage
使用若引用表获取图片的好处是当cache被清除时image还可能被页面强持有,仍可以通过内存访问,避免不必要的磁盘访问
3、磁盘缓存:使用url的md5拼接路径存储编码后的data
4、缓存清理:内存警告时内存缓存全部清除,App进入后台和退出时先清理磁盘过期图片,然后判断缓存是否超限如仍超限折半清除磁盘
NSCache做缓存比NSDictionary区别:
1、资源耗尽时自动删减,2、收到低内存告警时先删除最久未使用内存,3、多线程安全,4、可设置大小,5、默认不拷贝键key
url未变,但图片资源变化如何及时更新:
1、SD设置Option忽略缓存直接下载
2、设置HTTP1.1的If-Modified判断客户端缓存与服务器资源是否更新,如缓存是最新的HTTP会返回304,否则正常下载返回200
子线程解码:
JPEG/PNG不是位图,是经过编码压缩后的数据,需要将其解码成位图才能渲染到屏幕上,磁盘缓存存的是编码压缩的PNG图片
通常使用的imageNamed:默认会在主线程解码,消耗CPU,大量使用会卡顿,
imageNamed:有内存缓存,加载大图时可直接使用imageWithContentsOfFile:
SD的子线程解码步骤:在子线程创建绘图上下文,绘图,成图
17、CocoaPods原理:
CocoaPods实现原理:
1、使用Ruby编写的iOS库管理工具,包含若干个gems包,命令解析器、脚本解析器、下载器、工程集成器、pods插件管理器等
2、提供依赖库版本管理,下载,pod工程创建,workspace集成,预编译等功能
3、有三个主要目录:索引库、缓存库、每个工程的pods代码库
pod install和pod update区别:
1、install只对podfile中不符合.lock中条件的pod库更新,update会忽略.lock直接更新
2、这两个命令默认都会自动更新索引库repo,可以使用--no-repo-update忽略更新
pod install执行流程:
1、命令解析器解析pod命令,2、检查更新索引库,3、脚本解析器分析Podfile和.lock生成待下载列表,4、下载器下载pod库,5、集成器生成Pods工程集成进workspace,6、保存依赖到.lock,7、执行pod插件
Pod插件编译优化:
利用pod插件在pod install过程将库编译成二进制静态库,Xcode编译时只需要链接静态即可
18、Git原理:
Git是一个分布式版本控制系统,每个开发者本地保存一个完整的文件版本镜像,可以离线版本管理,然后再同步到中心版本库
git分4个区:本地工作区,本地暂存区,本地仓库,远程仓库
.git文件用于版本跟踪,存储本地仓库和暂存区,包含各文件版本的快照、分支、log等
Git底层实现:git是一套内容寻址的文件系统
git管理的内容经过加密算法生成三种对象
1、blob对象只跟文本文件的内容;2、tree对象记录文本文件内容和名称、目录等信息;3、commit对象记录本次提交的所有信息,包括提交人、提交时间,本次提交包含的tree及blob
代码回滚:如A之后提交了B
1、B未push到远端:git reset A
2、push到远端:1、反转提交Bgit revert B,2、git reset —hard A 然后git reset B 然后commit加push,3、切分支处理
合并代码:
1、git merge 合并分支,把一个分支合并进当前的分支,产生合并提交
2、git rebase 打补丁提交,先保存提交代码,拉取新分支内容,然后提交代码补丁,不会产生合并提交