UIView和CALayer
UIView
UIView表示屏幕上的一块矩形区域,它是基本上iOS中所有可视化控件的父类。UIView可以管理矩形区域里的内容,处理矩形区域的事件,包括子视图的管理以及动画的实现。
UIKit相关类的继承关系
UIView继承自UIResponder
,所以UIView可以做事件响应,它也是所有视图(控件)直接或间接的父类
CALayer
CALayer
继承于NSObject
,我们称之为层,CALayer类的概念与UIView非常类似,可以包含图片、文本、背景色等。CALayer直接继承自NSObject,没有事件响应的功能。CALayer中包含API,可判断某点是否在图层范围内,但是没有响应链的存在
UIView和CALayer的关系
在每一个UIView
实例当中,都有一个默认的支持图层layer
,UIView负责创建并且管理这个图层。UIView因为里面有layer层,才具有显示的功能。UIView仅仅是对layer的一层封装,实现了CALayer的delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级API。可以说,CALayer是UIView的内部实现细节。
UIView和CALayer的区别
1,UIView
可以响应事件,CALayer
不可以响应事件
2,一个 Layer 的 frame 是由它的 anchorPoint,position,bounds,和 transform 共同决定的,而一个 View 的 frame 只是简单的返回 Layer的 frame
3,UIView
主要对显示的内容的管理而CALayer
主要侧重显示内容的绘制
4,在做 iOS 动画的时候,修改非 RootLayer
的属性(譬如位置、背景色等)会默认产生隐式动画,而修改UIView则不会。
UIView的CALayer类似于UIView的子view树状结构,也可以向它的layer上添加子layer,来完成某些特殊的表示
UIView *firstView = [[UIView alloc] init];
firstView.frame = CGRectMake(200, 200, 200, 200);
firstView.backgroundColor = [UIColor redColor];
[self.view addSubview:firstView];
CALayer *layer = [[CALayer alloc] init];
layer.backgroundColor = [[UIColor greenColor] CGColor];
layer.position = CGPointMake(100,100); //中心点
layer.bounds = CGRectMake(100,100,80,80);
[firstView.layer addSublayer:layer];
可以看出并没有在父视图添加新的图层,而是在view上改变了颜色,这和addSubView并不一样。可以看出layer是对View显示内容的绘制。
- CAlayer视图结构类似于UIView的子View树形结构,可以在layer上添加子layer,类似于view添加view来实现一些特殊的表示。不同之处CALayer在添加的时候不会添加新视图,类似于修改原来的layer。
- UIVIew的layer树形在系统内部,被系统维护三份copy
- 第一份,逻辑树,代码可以在里面操作,例如通过代码更改layer的属性(比如frame\bounds)就在这一份进行操作
- 第二份,动画树,这是一个中间层,系统在这一层更改属性,进行各种渲染操作
- 第三份,显示树,这棵树的内容就是当前正被显示在屏幕上的内容
这三棵树的逻辑结构都是一样的,区别只是有各自的属性
UITableView
UITableView代理需要遵循的两个协议以及必须实现的方法
主要是通过2个协议:UITableViewDataSource和UITableViewDelegate
- UITableView需要一个数据源代理(
dataSource
)来显示数据,UITableView会向数据源查询一共有多少行数据以及每一行显示生命数据。没有设置数据源的UITableView只是一个空壳。凡是遵循UITableViewDataSource协议的OC对象,都可以是UITableView的数据源 - 我们也需要为UITableView设置代理对象(
delegate
),以便在UITableView触发某些事件时做出相应的处理。 - 凡是遵循了UITableViewDelegate协议的OC对象,都可以是UITableView的代理对象,一般会让控制器充当充当UITableView的dataSource和delegate,我们可以手动实现协议中的某些方法来完成UITableView的实现。
@protocol UITableViewDataSource的方法
//返回组数
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
//返回每组里的行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
//cell的实现
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
// section头的title(例如,通讯录不同姓名标识)
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
// section尾段的title
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
// section索引的title集合(例如,通讯录索引,帮助快速找到姓名)
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView{
}
@protocol UITableViewDelegate<NSObject, UIScrollViewDelegate>的方法
//点击cell事件
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
// 设置cell行高(因为参数是indexPath,所以可以设置不同section的行高,也能设置同一section不容row的行高)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
// section头部的height
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
// section尾部的height
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
// section头部的view
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
// section尾部的view
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
其中代理对象必须实现的方法是DataSource协议的方法
//每组的cell
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
//实现cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
几个主要的方法在TableView加载时的执行顺序
- numberOfSectionsInTableView
- numberOfRowsInSection
- cellForRowAtIndexPath
- heightForRowAtIndexPath
TbaleView的cell的复用
在滑动tableView时为了避免cell的重复的销毁创建而消耗性能就出现了cell的复用
简单来说:当创建了多个cell且屏幕显示不了所有的cell,系统会将已经创建不在显示的cell放入复用池中,当需要新建cell并且标识符与复用池中的标识符存在相同的情况,就会调用复用池中的cell,并且使用该cell的所有cell控件
cell的两种复用机制
自定义cell
cell复用会出现的问题
对于不同种类的自定义cell,有时复用会出现问题。如果自定义cell上的控件不同,会出现复用到控件不同的cell,这时就会出现界面的错乱。
解决方法:
- 弃用重用机制 - 从indexpath每次获取新的cell
- 对于不同种类的cell设置不同的标识符,通过不同的标识符去复用相应类型的cell
- 在prepareForReuses中重置所有的subView的显示属性为nil。(当前已经被分配的cell如果被重用了,会调用cell的prepareForReuse通知cell)
tableViewCell的行高计算
动态计算-缓存高度
动态计算
实际开发中,使用最多的应该是动态计算cell高度。例如标题高度不固定,标签不固定,这样就需要更具model里的内容计算行高
使用的时候在tableView的代理设置
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
WMSearchResultQAModel *model = self.dataArray[indexPath.row];
return [WMSearchResultQAModel calutWholeCellHeightWithModel:model];
}
这样就可以达到每个cell根据内容展示不同高度的要求了。 这种方法很繁琐,但是也是最精确的,最可控的
缓存行高
当tableView滚动的时会不停的调用heightForAtIndexPath这个代理方法,当cell的高度需要自适应时,就意味着每次回调这个方法都要计算高度,而计算是非常消耗时间的,就会产生卡顿。
为了避免重复且无意义的计算cell高度,缓存高度就释放重要。
缓存高度机制
缓存高度需要一个容器来保存高度数值,可以是model ,一个可变数组,一个可变字典,以达到每当回调 heightForRowAtIndexPath 这个方法时,我们先去这个缓存里去取,如果有,就直接拿出来,如果没有,就计算高度,并且缓存起来。
以model为例
在model里声明个cellheight属性,用于保存Model对应的cell高度,然后在heightForRowAtIndexPath 方法中,如果当前model的cellHeight为0,说明这个cell没有缓存过高度,则计算Cell的高度,并把这个高度记录在model的cellHeight。如果当前model的cellHeight不为0,则直接使用。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
WMSearchResultQAModel *model = self.dataArray[indexPath.row];
if (model.cellHeight > 0) {
// 有缓存的高度,取出缓存高度
return model.cellHeight;
}
// 没有缓存时,计算高度并缓存起来
CGFloat cellHeight; = [WMSearchResultQAModel calutWholeCellHeightWithModel:model];
// 缓存给model
model.cellHeight = cellHeight;
return cellHeight;
}
手动计算高度的两个方法
- sizeToFit
- boundingRectWithSize
sizeToFit
UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(10,100, 350, 0)];
label.numberOfLines = 0;
label.text =@"当前视图的边界和边界大小的变化123123213213213123123123123123213213123213213213123123213";
NSLog(@"the label bounds : %@",NSStringFromCGRect(label.frame));
[label sizeToFit];
NSLog(@"%f",label.frame.size.height);
[self.view addSubview:label];
我们不设置高度,设置label.numberOfLines = 0;和[label sizeToFit];可以实现label的最多展开,并可获得相应的高度。
注意事项
- 调整大小:sizeToFit 调整视图的大小,以适应内容。
- 不改变位置:sizeToFit 不会更改视图的位置。
- 确保内容已设置:调用 sizeToFit 之前,应设置好视图的内容。
doundingRectWithSize
返回文本会在所占据的矩形空间
CGRect rect=[(NSString *)obj boundingRectWithSize:CGSizeMake(1000, FONTHEIGHT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]} context:nil].size.width;
里面的参数如下:
- obj 是指要计算显示的字符串
- boundingRectWithSize 表示计算的宽高限制
- 计算高度时,需要宽度固定:CGSizeMake(1000, CGFLOAT_MAX)
这里的1000也可以用已经确定的控件的宽度替代self.label.width
计算结果表示在宽度最多为1000高度不限时,显示完全字符串需要的高度
计算宽度时,需要高度固定:CGSizeMake(CGFLOAT_MAX, 200)
同理200也可以用已知高度替换:self.label.height
计算结果表示在高度不超过200时,将给定的字符串现实完全需要的宽度 - options是文本绘制的附加选项
- NSStringDrawingUsesLineFragmentOrigin 是默认基线
- attributes字典格式,限定字符串显示的样式,一般限制字体较多,比如:@{NSFontAttributeName:[UIFont systemFontOfSize:16]}
context包括一些信息,例如如何调整字间距以及缩放。最终,该对象包含的信息将用于文本绘制。一般写nil。
UILabel * labelSecond = [[UILabel alloc] init];
labelSecond.numberOfLines = 0;
labelSecond.text =@"当前视图的边界和边界大小的变化123123213213213123123123123123213213123213213213123123213";
labelSecond.font = [UIFont systemFontOfSize:16];
//用我们预设的width来算label应该显示的size
CGRect rectTest = [labelSecond.text boundingRectWithSize:CGSizeMake(350, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]} context:nil];
//从刚才算的size中取出heigth
//加1是怕四舍五入完之后少了一点导致显示的label少了一行
CGFloat titleHeight = ceilf(rectTest.size.height) + 1;
//用算出来的结果显示label
labelSecond.frame = CGRectMake(10,200, 350, titleHeight);
//将label作为子视图添加到父视图
[self.view addSubview:labelSecond];
自适应行高-缓存高度
在 iOS8 之后,系统结合autolayout
提供了动态结算行高的方法 UITableViewAutomaticDimension
,做好约束,我们都不用去实现 heightForRowAtIndexPath
这个代理方法了。
实现步骤
1,tableView设置
// 预设行高
self.tableView.estimatedRowHeight = xxx;
// 自动计算行高模式
self.tableView.rowHeight = UITableViewAutomaticDimension;
2,在自定义cell,masonry布局
- (void)layoutSubviews {
[super layoutSubviews];
[self.headImgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.offset(kSpace15);
make.size.mas_equalTo(CGSizeMake(50.f, 50.f));
// 在自动计算行高模式下 要加上的
make.bottom.equalTo(self.contentView.mas_bottom).offset(-kSpace15);
}];
[self.nickNameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.headImgView.mas_right).offset(12.f);
make.top.offset(17.f);
}];
[self.jobWorkLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.nickNameLabel.mas_right).offset(8.f);
make.right.lessThanOrEqualTo(self.contentView.mas_right).offset(-kSpace15);
make.top.offset(21.f);
}];
[self.hospitalLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.headImgView.mas_right).offset(12.f);
make.top.equalTo(self.jobWorkLabel.mas_bottom).offset(6.f);
}];
[self.line mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.offset(0);
make.height.mas_equalTo(0.5f);
}];
}
- 所有子控件都要依赖self.contentView作为约束父控件
- 关键控件要做buttom约束,确定好控件的上下边界,根据控件的动态内容将cell纵向撑开。
3,最关键的一步: [cell layoutIfNeeded]
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
WMDoctorEvaluateDescribeInputCell *cell = [tableView dequeueReusableCellWithIdentifier:[WMDoctorEvaluateDescribeInputCell reuseIdentifier] forIndexPath:indexPath];
kWeakSelf
cell.describeInputBlock = ^(NSString * _Nonnull describeText) {
weakSelf.inputDescribeText = describeText;
};
//关键的一步,解决不正常显示问题
[cell layoutIfNeeded];
return cell;
}
缓存高度机制
首先获取cell实际显示的高度
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *key = [NSString stringWithFormat:@"%ld", (long)indexPath.row];
[self.heightDict setObject:@(cell.height) forKey:key];
NSLOG(@"第%@行的计算的最终高度是%f",key,cell.height);
}
didEndDisplayingCell当cell已经被正真的显示到屏幕上时会调用这个方法,此时的高度是cell的正真高度。根据indexPath.row作为key,将高度缓存进字典。
然后在 heightForRowAtIndexPath 方法里判断,如果字典里有值,则使用缓存高度,否则自动计算:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *key = [NSString stringWithFormat:@"%ld",indexPath.row];
if (self.heightDict[key] != nil) {
NSNumber *value = _heightDict[key];
return value.floatValue;
}
return UITableViewAutomaticDimension;
}
ViewController的生命周期
ViewController相关函数以及执行顺序
当一个视图被创建,并且在屏幕上显示的时候
1.alloc 创建对象,分配空间
2. init; 初始化对象,初始化数据
3. loadview 在UIViewController对象的view被访问且为空的时候调用
4. viewDidLoad 控制器载入完成,可以进行自定义数据,以及动态创建其他控件
5. viewWillAppear 视图出现在屏幕之前,马上这个视图就会被展现在屏幕上了
6. viewWillLayoutSubviews 该方法在通知控制器将要布局 view 的子控件时调用
7. viewDidLayoutSubviews 该方法在通知控制器已经布局 view 的子控件时调用
8. viewDidAppear 视图已在屏幕上渲染完成
当一个视图被移除屏幕并且销毁的时候,代码执行的顺序
1,viewWillDisappear 视图将被从屏幕上移除之前执行
2,viewDidDisappear 视图已经被从屏幕上移除,用户看不见这个视图了
3,dealloc 视图被销毁,释放在init和viewDidLoad中创建的对象
离屏渲染
图片渲染和显示的流程
- 在Application阶段,CPU会创建我们的视图,计算视图的一些数据,进行编解码,绘制纹理等操作然后交给GPU
- GPU先通过顶点着色器去确定图像在硬件上的上的具体显示位置,然后通过片源着色器计算每个像素点的颜色值,最后通过光栅化找到像素点的位置,并把颜色显示上去。最终转化为一个个屏幕像素。
- 然后把渲染后的数据放到帧缓存区(FrameBuffer),然后视图控制器就会读取数据交给显示器显示
掉帧
通过屏幕扫描的方式,会通过CRT电子枪从上到下逐行扫描,这个扫描的过程就是读取帧缓存区里的数据,当它扫描完的时候,它就会显示一帧的画面, 当显示完一帧画面后,CRT电子枪又回到原来的位置继续重新扫描显示下一帧。当一个垂直同步信号过来的时候,如果说CPU
和GPU
还没有完成渲染的结果去做提交,也就是没有把数据放到FrameBuffer
里面,这种情况,未过来提交过来这一帧的画面就会被丢弃,然后等待下一次垂直同步信号过来,再来显示新的画面,这个过程被称为掉帧。
离屏渲染
当视图比较复杂的时候,GPU无法扫描视图的全部内容并把渲染数据反在FrameBuffer
中,无法显示画面的全部内容,就需要开辟一块二外的内存缓存区,把剩下的渲染数据放入其中,再合并到FrameBuffer
中,最后进行视图的显示。
导致离屏渲染的操作
- 添加光栅化
- 添加遮罩
- 添加阴影
- 抗锯齿
- 设置背景颜色和圆角
- 不透明