于海老师的《资深大牛带你深度剖析ios面试》算是看完了,于海老师思路清晰、语言流畅、把握要点,把很多难点问题可以很好的讲通,还有很多偏角旮旯自己不知道或没有掌握的知识也是从这上面学习到的,看的人有种拍案叫绝的感觉。整体来看,这套课程是难得的精品课程。
当然,也有不足之处,所讲知识都是直戳要害,没有进行发散,对新手不是很友好,不过也是,这门课本身就是面向中高级iOS程序员的。
有些地方自己知道的,可以加深了解,串联知识点。
有些地方自己并没听说过,自己想弄明白需要下功夫查些资料。
看完,不等于学完,不做笔记,俩月、半年后不看,还是会忘记。
短短15小时课程,做笔记的话,每一节课的知识量都是很多,因此,自己花时间对视频课内容进行整理,与君共勉。
系统的UI传递机制是怎样的?
KVO的实现原理是怎样的?
简单说说消息传递机制和消息转发流程
当一个obj废弃的时候,指向它的weak指针为何会自动置位nil?
iOS如何进行内存管理的?
Block的实质是怎样的?使用Block为何容易产生循环引用?
简单说说怎样利用GCD实现高效的多读单写逻辑?
RunLoop为何能做到有事做事,没事休息?
怎样解决DNS劫持?
分别说说什么是桥接模式、责任链模式?
怎样设计一个图片缓存框架?
怎样设计一个网络框架?AFNetworing
请编写一个算法,查找一个字符串中,第一个只出现一次的字符。
AFNetworing大致是怎样实现的?
屏幕向上滑动,A1消失,A7即将出现,则A7可利用A1所在的内存地址,从而达到重用机制。
在一个tableView列表中,在主线程进行了删除操作,而在子线程进行了加载更多操作。如此一来,可能会造成数据源对照不上问题。
问题是,我觉得loadMore并不会造成数据源问题。
比如原有数据10条,存储在一个SourceMuttableArray里面
删除第2条数据,是对原有数据进行操作。
loadMore是加载更多数据,假入加载了新的10条数据,加载完毕后会放入SourceMuttableArray里面。
虽然是对同一个数组进行操作,但是老数据与新数据并没有相互影响。
此处如若改成reloadData刷新数据比较恰当。
如果是刷新操作,老数据删除,然而新的请求并不知道,刷新后被删除的数据又出现了,从而出现问题。
解决方案一:并发访问、数据拷贝
在主线程删除操作的时候,进行记录删除操作,然后在子线程数据返回的时候,再同步删除操作。从而达到数据同步。
这种,相当于手动将从服务器数据加载到本地的数据进行了修改。
大致相当于这种操作:
- (void)DeleteData
{
BOOL isHaveDeleteData = YES;
int c = indexPath.row;
}
- (void)reloadData
{
//数据请求下来了,存储在临时数组tempDataArray
if(isHaveDeleteData){
[self.tempDataArray removeObjectAtIndex:tempDataArray];
self.sourceDataArray = [NSMuttableArray arrayWithArray:self.tempDataArray];
}
}
利用GCD,创建串行队列,将子线程的返回的数据与主线程的删除数据操作,都放在串行队列中。
UIView里面的layer属性,其实就是CALayer类型。
CALayer里面有一个contents,负责显示
contents里面有一个backing store是一个bitmap位图,最终进行显示。
UIView为CALayer提供显示的内容,以及负责处理触摸等事件,参与响应链
CALayer通过contents负责显示内容
该机制体现了设计原则中的:单一设计原则
//作用:判断哪一个视图响应,就返回哪一个UIView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
1.若当前视图无法响应事件,则返回nil
2.若当前视图可以响应事件,同时有子视图可以响应,则返回子视图层次中的事件响应者
3.若当前视图可以响应事件,但无子视图可以响应事件,则返回自身作为当前视图层次中的事件响应者
}
//作用:判断触摸点是否在当前View上
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
}
– (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event的内部实现图
用代码表现出来大致是:
在UIButton中,重写-pointInside:withEvent:方法
老师程序中还重写了- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法,感觉没必要,只重新pointInside即可满足
在这个基础上,被问过扩展型的问题:
答:不可以
点击后,点击从上往下找合适的View,当点击是在UIView(A)上,最后由UIButton接收事件,响应。
当点击点是在UIButton的外部,其实也是UIView(A)的外部,此时UIView(A)里面的pointInside:withEvent:返回NO,即,触摸点没有在UIView(A)上面,也就不会UIView(A)里面的子类了,也就是不会走UIButton的重新后的pointInside:withEvent:。
也就是,触摸点不在父类上,则不会执行到子类。因此,重新子类pointInside:withEvent:方法也没有用。
2020-11-30更新
孟浪了,以上答案错误
正确答案是: 可以实现将UIButton扩大点击区域
做了两个实验:
首先,UIView大小为(100, 100, 100, 100),UIButton加到UIView上,大小为(0, 0, 50, 50),通过重写UIButton的-(BOOL)pointInside:withEvent:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *redView = [[UIView alloc] init];
redView.frame = CGRectMake(100, 100, 100, 100);
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
YZButton *btn = [[YZButton alloc] init];
btn.frame = CGRectMake(0, 0, 50, 50);
btn.backgroundColor = [UIColor yellowColor];
[btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[redView addSubview:btn];
}
#import "YZButton.h"
@implementation YZButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
//获取自己的bounds
CGRect bounds = self.bounds;
//扩大原热区直径至100,可以暴露个接口,用来设置需要扩大的半径。
CGFloat widthDelta = MAX(100, 0);
CGFloat heightDelta = MAX(100, 0);
//CGRectInset,负数是向外,正数是向里
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
//某一点是否在某一区域上
return CGRectContainsPoint(bounds, point);
}
@end
点击红色区域,有打印:
2020-11-30 10:09:32.393002+0800 test001[1807:50672] -[ViewController btnClick]
这个容易理解,UIButton的点击区域扩大了,扩大到了父控件的宽高
现在,将UIButton的frame变为UIView的宽高,也就是都是(100, 100, 100, 100),此时,按照题意,要将UIButton的点击区域扩大2倍,也就是(200, 200)
- (void)viewDidLoad {
[super viewDidLoad];
UIView *redView = [[UIView alloc] init];
redView.frame = CGRectMake(100, 100, 100, 100);
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
YZButton *btn = [[YZButton alloc] init];
btn.frame = CGRectMake(0, 0, 100, 100);
btn.backgroundColor = [UIColor yellowColor];
[btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[redView addSubview:btn];
}
#import "YZButton.h"
@implementation YZButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect bounds = self.bounds;
//扩大原热区直径至200,可以暴露个接口,用来设置需要扩大的半径。
CGFloat widthDelta = MAX(200, 0);
CGFloat heightDelta = MAX(200, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
@end
此时,点击UIButton外部的区域,是没有点击事件的,原因就是那个错误答案,点击point超出其父控件的点击区域,因此,不能响应事件。
然而,既然UIButton可以扩大点击区域,UIView也可以扩大点击区域,因此,重写UIView的-(BOOL)pointInside:withEvent:方法:
#import "YZView.h"
@implementation YZView
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect bounds = self.bounds;
//扩大原热区直径至200,可以暴露个接口,用来设置需要扩大的半径。
CGFloat widthDelta = MAX(200, 0);
CGFloat heightDelta = MAX(200, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
@end
神奇的一幕出现了,点击UIButton的外部区域,也可以打印
之前的错误答案在于,只想到了父控件不能响应事件,没有想到可以同时扩大父控件的点击区域。因此,可以通过-(BOOL)pointInside:withEvent:方法来扩大UIButton的点击区域,只需要将其父控件也扩大即可。
学习触摸事件首先要了解一个比较重要的概念-响应者对象(UIResponder)。
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。
UIView继承UIResponder,UIResponder里面有四个响应方法
//手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
UIView的继承关系
UIView : UIResponder : NSObject
UIButton的继承关系
UIButton : UIControl : UIView : UIResponder : NSObject
UIWindow : UIView
UIApplication : UIResponder
UIViewController : UIResponder(查代码可知)
UIControl里面有四个方法:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event; // touch is sometimes nil if cancelTracking calls through to this.
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;
事件响应链
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
这道题一开始想的是,调用view.superView,最终可以找到UIController所属的view,现在的问题就是,如何通过UIController的所属view,找到其控制器。因为view是UIController的一个属性,因此,也可以理解为:如何通过属性,找到类对象
但是,结合响应链的传递,以及事件处理传递,我们可以知道,通过响应链,可以根据view找到所属控制器,然后调用控制器的某些方法,去处理事情。
也就是UIView继承的UIResponder跟控制器是否存在某种关系?
响应链中的事件传递规则:
每一个响应者对象(UIResponder对象)都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的最佳响应者确定了,这个事件所处的响应链就确定了。
对于响应者对象,默认的 nextResponder 实现如下:
根据第一条,UIView的nextResponder要不是控制器,要不是superView
因此,可以得出以下代码:
- (UIViewController *)findViewController:(UIView *)sourceView
{
id target=sourceView;
while (target) {
target = ((UIResponder *)target).nextResponder;
if ([target isKindOfClass:[UIViewController class]]) {
break;
}
}
return target;
}
参考:
通过UIView获取UIViewController
iOS 触摸事件 hitTest touches nextResponder
该事件会被抛弃,什么也不会发生。
也就是UIGestureRecognizer、UIResponder、UIControl三者的关系
首先,我们先了解几个知识点:
系统类的UIControl(UIButton、UISwitch等)的响应优先级比其父视图上的手势识别器高
而自定义的UIControl,响应优先级比其父视图上的手势识别器低。
意思就是,如果一个UIView有一个手势识别器事件,UIView上有一个UIButton,UIButton有一个addTarget事件,点击UIButton,UIButton会先响应addTarget事件。
自定义YZButton,在里面做如下操作:
#import "YZButton.h"
@implementation YZButton
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
NSLog(@"%s", __func__);
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
NSLog(@"%s", __func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
NSLog(@"%s", __func__);
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
NSLog(@"%s", __func__);
}
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event
{
NSLog(@"%s", __func__);
return [super beginTrackingWithTouch:touch withEvent:event];
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event
{
NSLog(@"%s", __func__);
return [super continueTrackingWithTouch:touch withEvent:event];
}
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event
{
[super endTrackingWithTouch:touch withEvent:event];
NSLog(@"%s", __func__);
}
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event
{
[super cancelTrackingWithEvent:event];
NSLog(@"%s", __func__);
}
@end
然后
YZButton *btn = [[YZButton alloc] init];
btn.frame = CGRectMake(0, 0, 200, 200);
btn.backgroundColor = [UIColor redColor];
[self.view addSubview:btn];
[btn addTarget:self action:@selector(actionClick) forControlEvents:UIControlEventTouchUpInside];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick)];
[self.view addGestureRecognizer:tap];
打印结果:
-[YZButton beginTrackingWithTouch:withEvent:]
-[YZButton touchesBegan:withEvent:]
-[YZButton endTrackingWithTouch:withEvent:]
actionClick
-[YZButton touchesEnded:withEvent:]
手势识别器UIGestureRecognizer比UIResponder具有更高的事件响应优先级,也就是
手势识别器UIGestureRecognizer > UIResponder(touchBegin)
YZButton *btn = [[YZButton alloc] init];
btn.frame = CGRectMake(0, 0, 200, 200);
btn.backgroundColor = [UIColor redColor];
[self.view addSubview:btn];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick)];
[btn addGestureRecognizer:tap];
打印结果:
-[YZButton beginTrackingWithTouch:withEvent:]
-[YZButton touchesBegan:withEvent:]
tapClick
-[YZButton cancelTrackingWithEvent:]
-[YZButton touchesCancelled:withEvent:]
怎么从结果发现,好像是UIResponder比UIGestureRecognizer先调用呢???
我们搞一个自定义的YZTapGestureRecognizer,其继承UITapGestureRecognizer
#import "YZTapGestureRecognizer.h"
@implementation YZTapGestureRecognizer
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
NSLog(@"%s", __func__);
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
NSLog(@"%s", __func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
NSLog(@"%s", __func__);
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
NSLog(@"%s", __func__);
}
@end
然后,执行
YZButton *btn = [[YZButton alloc] init];
btn.frame = CGRectMake(0, 0, 200, 200);
btn.backgroundColor = [UIColor redColor];
[self.view addSubview:btn];
YZTapGestureRecognizer *tap = [[YZTapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick)];
[btn addGestureRecognizer:tap];
打印结果:
-[YZTapGestureRecognizer touchesBegan:withEvent:]
-[YZButton beginTrackingWithTouch:withEvent:]
-[YZButton touchesBegan:withEvent:]
-[YZTapGestureRecognizer touchesEnded:withEvent:]
tapClick
-[YZButton cancelTrackingWithEvent:]
-[YZButton touchesCancelled:withEvent:]
从结果可以看出,确实是
手势识别器UIGestureRecognizer > UIResponder(touchBegin)
三个方法全部打开
YZButton *btn = [[YZButton alloc] init];
btn.frame = CGRectMake(0, 0, 200, 200);
btn.backgroundColor = [UIColor redColor];
[self.view addSubview:btn];
[btn addTarget:self action:@selector(actionClick) forControlEvents:UIControlEventTouchUpInside];
YZTapGestureRecognizer *tap = [[YZTapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick)];
[btn addGestureRecognizer:tap];
打印结果:
-[YZTapGestureRecognizer touchesBegan:withEvent:]
-[YZButton beginTrackingWithTouch:withEvent:]
-[YZButton touchesBegan:withEvent:]
-[YZTapGestureRecognizer touchesEnded:withEvent:]
tapClick
-[YZButton cancelTrackingWithEvent:]
-[YZButton touchesCancelled:withEvent:]
结果分析:
首先是UIGestureRecognizer的手势识别器先接收事件
然后是UIResponder的touchBegin
由于[YZButton cancelTrackingWithEvent:]和[YZButton touchesCancelled:withEvent:]被调用,等于YZButton已经取消事件的接收,也就不会触发UIController的actionClick方法
也就是说:手势识别器调用完毕后,直接调用了touchesCancelled,使得actionClick不会触发
手势识别器UIGestureRecognizer > UIResponder(touchBegin)>UIController
更多学习有关事件传递与响应链
iOS触摸事件全家桶
Layout:UI布局、文本计算
Display:绘制,drawRect方法
Prepare:图片编解码
Commit:提交位图
顶点着色器就是处理顶点相关的信息,片段着色器就是处理画面的颜色信息。
在规定时间内(16.7ms)内,CPU和GPU没有做好准备,在VSync来临时,图片不能显示,造成卡顿、掉帧
人的大脑眼睛在1秒钟内接收至少60帧的画面,就是流畅的效果,也就是60FPS
1s/60 = 1000ms/60 约等于16.7ms
可以分别从CPU和GPU两方面着手
CPU
对象创建、跳转、销毁。放在子线程
预排版(布局计算、文本计算)放在子线程
预渲染(文本等异步绘制、图片编解码等)
GPU:
纹理渲染:减少离屏渲染
视图混合:尽可能的减少多层的View混合
[UIView setNeedsDisplay]这行代码执行调用,并不会立马进行UI绘制
[UIView setNeedsDisplay]会立刻调用layer的同名函数setNeedsDisplay
在当前runloop即将进行休眠的时候(kCFRunLoopBeforeWaiting),setNeedsDisplay才真正调用[CALayer display],然后进行UIView的绘制
异步绘制流程
异步绘制,就是异步在画布上绘制内容。
在子线程中绘制Core Graphic对象,最后再回到主线程中设置layer.contents内容。
1圆角:
view.layer.cornerRadius = 5.0;和view.layer.masksToBounds = YES;必须同时执行才会触发。
2图层蒙版
3阴影
4光栅化
光栅化概念:将图转化为一个个栅格组成的图象。
光栅化特点:每个元素对应帧缓冲区中的一像素。
离屏渲染会增加GPU的工作量,使得GPU处理时间变长,从而使得在屏幕显示中16.7ms中没有完成工作,造成UI卡顿、掉帧现象。
增加的工作量包括:
创建新的渲染缓冲区
上下文切换
一般baidu出来的答案如下,然而说明并不够透彻,在此补充说明:
1、init初始化不会触发layoutSubviews
2、addSubview会触发layoutSubviews
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化,xy改动不管,wh改动管
4、滚动一个UIScrollView会触发layoutSubviews
5、旋转Screen会触发父UIView上的layoutSubviews事件
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件
首先:layoutSubviews 字面意思是: 布局子控件,也就是说改变子控件会调用父类该方法;
1、init初始化不会触发layoutSubviews,
这点确实不会调用;
2、addSubview会触发layoutSubviews,
如果添加的子控件没有Frame,不会调用;
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化;
还有一个前提是该View 已经被添加到父控件, 此时View和其父控件的layoutSubviews都会调用;
也就包含了6 的情况
4、滚动一个UIScrollView会触发layoutSubviews ,因滚动UIScrollView,其子控件肯定对应会刷新,也就肯定会被调用;
这点会调用;
5、旋转Screen会触发控制器对应UIView上的layoutSubviews事件
做一点更正;
总结:改变子控件就会调用父类的方法;
参考:
layoutSubviews 调用时机