小灰学习 tableView 卡顿优化, 当然是用 Async View
小灰看下 gg 了的 Graver 框架
问题与解决: 在渲染上
复杂的 cell , 滚动起来,容易卡顿
复杂,即包含很多控件 ( 位图 ),形成一颗复杂的图层树
下图,是一个简单的 cell, 层次有一些
Graver 可,一个 cell, 变成一张位图,层次简单
而且 graver ,充分利用多线程
回顾下流程
1, 拿到网络请求的数据,大家都是这么做的
2, 业务数据的加工,方便渲染
AFN 的结果回调线程,不使用主线程,
开个子线程,去处理
上一篇,tableView 卡顿优化,除了缓存计算高度,还可以...
就是这么做的
3, 异步渲染
思路: 图片绘制,比较简单
核心是,只有主线程,才能操作 UI
要充分利用多线程,则不能停留在 UIKit, 基本不使用 addSubview:
CALayer 就得上场了
多线程绘制技术
UI 绘制,走 func draw(
, 这个是主线程
override func draw(_ rect: CGRect) {
}
复制代码
需求是,多线程
实现了,下面的方法,就不会跑 func draw(
override func draw(_ layer: CALayer, in ctx: CGContext) {
}
复制代码
如果走完
func draw(_ layer: CALayer, in
,
接着走
func draw(
,
需要调用父类
override func draw(_ layer: CALayer, in ctx: CGContext) {
super.draw(layer, in: ctx)
}
复制代码
不走 func draw(
,
效果类似
override func display(_ layer: CALayer) {
}
复制代码
graver 的实现:
首先是画布
WMGAsyncDrawView
作为基类,负责绘制的逻辑
WMGAsyncDrawLayer
配合 WMGAsyncDrawView
使用,状态控制
WMGCanvasView
继承自 WMGAsyncDrawLayer
,
于绘制的基础上,添加常见的能力:
圆角、边框、阴影
WMGCanvasControl
继承自 WMGCanvasView
添加事件处理
- 系统的,码控件,
一个控件,一张位图,一个 target - action
- 现在是一张大的位图,不同的位置,对应不同的事件
下文重点讲
一般就用到
WMGCanvasView
,
WMGMixedView
用的,少一点
WMGMixedView
继承自 WMGCanvasControl
添加了绘制基坐标的便利计算,四个角和中心点
添加了待绘制内容 WMMutableAttributedItem
,
来做辅助事件处理
有了新的画布,配合新的绘制方式 WMGTextDrawer
文本绘制工具类, 是框架核心类,混排图文的绘制、size 计算,
都依赖文本绘制工具类 WMGTextDrawer
实现
画布,提供图形上下文
CGContextRef
文本绘制工具类, 拿到图形上下文,
把数据 ( 文字 / 图片 ), 绘制在画布上
配合新的数据单元 WMGVisionObject
与 WMMutableAttributedItem
WMGVisionObject
, 视觉元素的抽象,
Graver 框架中,对所有视觉元素进行抽象,即每个视觉元素都由其位置、大小、内容唯一决定
WMGVisionObject
, 包含位置 frame ( CGRect
) 和展示内容 WMMutableAttributedItem
WMMutableAttributedItem
包含若干 WMGTextAttachment
WMGTextAttachment
由显示内容和位置,组成
流程: Graver
配合 MVVM 使用,分工明确
使用起来,弯弯绕绕
获取数据,通过 Engine
,一般就是网络数据,
一个界面对应的网络数据,来一个继承自 WMGBaseEngine
一个界面对应的 ViewModel, 持有这个 engine
ViewModel 把 engine 弄到的网络数据 JSON ( 业务数据 Model One), 转化为 Model Two
ViewModel 持有 arrayLayouts
, 包含很多 WMGBaseCellData
( 这个就是 Model Two )
...
渲染流程, 主要是异步渲染
该框架设计挺充分的,主线程渲染,也成
WMGAsyncDrawView
提供异步渲染能力
- 触发
//displayLayer : 异步绘制的关键, 创建位图,并赋值 layer.contents = bitmapIMage
- (void)displayLayer:(CALayer *)layer{
if (!layer) return;
NSAssert([layer isKindOfClass:[WMGAsyncDrawLayer class]], @"WMGAsyncDrawingView can only display WMGAsyncDrawLayer");
if (layer != self.layer) return;
[self _displayLayer:(WMGAsyncDrawLayer *)layer rect:self.bounds drawingStarted:^(BOOL drawInBackground) {
// 开始绘制
} drawingFinished:^(BOOL drawInBackground) {
[self drawingDidFinishAsynchronously:drawInBackground success:YES];
} drawingInterrupted:^(BOOL drawInBackground) {
[self drawingDidFinishAsynchronously:drawInBackground success:NO];
}];
}
复制代码
- 进行绘制任务
上文介绍的第 4 个队列,渲染队列,即 [self drawQueue]
绘制要点,就是 void (^drawBlock)(void) =
显示出来,即 layer.contents = (id)image.CGImage;
//异步线程当中操作的
- (void)_displayLayer:(WMGAsyncDrawLayer *)layer
rect:(CGRect)rectToDraw
drawingStarted:(WMGAsyncDrawCallback)startCallback
drawingFinished:(WMGAsyncDrawCallback)finishCallback
drawingInterrupted:(WMGAsyncDrawCallback)interruptCallback{
BOOL drawInBackground = layer.isAsyncDrawsCurrentContent && ![[self class] globalAsyncDrawingDisabled];
[layer increaseDrawingCount]; //计数器,标识当前的绘制任务
NSUInteger targetDrawingCount = layer.drawingCount;
NSDictionary *drawingUserInfo = [self currentDrawingUserInfo];
//Core Graphic & Core Text
void (^drawBlock)(void) = ^{
void (^failedBlock)(void) = ^{
if (interruptCallback)
{
interruptCallback(drawInBackground);
}
};
//不一致,进入下一个绘制任务
if (layer.drawingCount != targetDrawingCount)
{
failedBlock();
return;
}
CGSize contextSize = layer.bounds.size;
BOOL contextSizeValid = contextSize.width >= 1 && contextSize.height >= 1;
CGContextRef context = NULL;
BOOL drawingFinished = YES;
if (contextSizeValid) {
UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
context = UIGraphicsGetCurrentContext();
if (!context) {
WMGLog(@"may be memory warning");
}
CGContextSaveGState(context);
if (rectToDraw.origin.x || rectToDraw.origin.y){
CGContextTranslateCTM(context, rectToDraw.origin.x, -rectToDraw.origin.y);
}
if (layer.drawingCount != targetDrawingCount)
{
drawingFinished = NO;
}
else
{
//子类去完成啊~父类的基本行为来说~YES
drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];
}
CGContextRestoreGState(context);
}
// 所有耗时的操作都已完成,但仅在绘制过程中未发生重绘时,将结果显示出来
if (drawingFinished && targetDrawingCount == layer.drawingCount)
{
CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
{
// 让 UIImage 进行内存管理
UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
void (^finishBlock)(void) = ^{
// 由于block可能在下一runloop执行,再进行一次检查
if (targetDrawingCount != layer.drawingCount)
{
failedBlock();
return;
}
// 显示出来
// 赋值的操作
layer.contents = (id)image.CGImage;
[layer setContentsChangedAfterLastAsyncDrawing:NO];
[layer setReserveContentsBeforeNextDrawingComplete:NO];
if (finishCallback)
{
finishCallback(drawInBackground);
}
// 如果当前是异步绘制,且设置了有效fadeDuration,则执行动画
if (drawInBackground && layer.fadeDuration > 0.0001)
{
layer.opacity = 0.0;
[UIView animateWithDuration:layer.fadeDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
layer.opacity = 1.0;
} completion:NULL];
}
};
if (drawInBackground)
{
dispatch_async(dispatch_get_main_queue(), finishBlock);
}
else
{
finishBlock();
}
}
if (CGImage) {
CGImageRelease(CGImage);
}
}
else
{
failedBlock();
}
UIGraphicsEndImageContext();
};
if (startCallback)
{
startCallback(drawInBackground);
}
if (drawInBackground)
{
// 清空 layer 的显示
if (!layer.reserveContentsBeforeNextDrawingComplete)
{
layer.contents = nil;
}
//[self drawQueue] 异步绘制队列,绘制任务
dispatch_async([self drawQueue], drawBlock);
}
// else , 不使用异步渲染
}
复制代码
至于网络图片
- 放在绘制的后期
- (void)drawingDidFinishAsynchronously:(BOOL)asynchronously success:(BOOL)success
{
if (!success) {
return;
}
[_lock lock];
// 三个点: ( 递归 ) 锁的重入、for循环遍历移除元素、多线程同步访问共享数据区
for (__block int i = 0; i < _arrayAttachments.count; i++) {
if (i >= 0) {
WMGTextAttachment *att = [_arrayAttachments objectAtIndex:i];
if (att.type == WMGAttachmentTypeStaticImage){
WMGImage *ctImage = (WMGImage *)att.contents;
if ([ctImage isKindOfClass:[WMGImage class]]) {
// 图片下载流程
__weak typeof(self)weakSelf = self;
// 下载网络图片,基于对 SDWebImg 的封装
[ctImage wmg_loadImageWithUrl:ctImage.downloadUrl options:0 progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.lock lock];
if ([strongSelf.arrayAttachments containsObject:att]){
[strongSelf.arrayAttachments removeObject:att];
i--;
// 标记,当前需要改变
[strongSelf setNeedsDisplay];
}
[strongSelf.lock unlock];
}];
}
}
}
}
[_lock unlock];
}
复制代码
略,对 Core Text 的完整封装
系统推荐的,简洁明了,富文本 -> UILabel -> 添加子视图( addSubView )
现在是,所有的富文本,直接绘制在一张位图上,跳过了建立 ( 可能很多 )UILabel 的环节
带来新的问题: 动画
看了下,这方面,好像没涉及
UIView 的传统思路,很好
带来新的挑战:事件处理
小灰的思路
得到一张位图,知道位图上不同控件的位置,
给位图,添加一个手势,拿到手势的坐标 pt,
基于 pt, 与手势的位置,
判断一下,完了
graver 的处理,借鉴了系统的传递链
graver 涵盖了位置的判断,还做了更多的控制
事件的注册
即,事件的保存
这一层,在 ViewModel
ViewModel
中转下,交给 WMGTextAttachment
去保存
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents priority:(NSInteger)priority
{
if (target && action && [target respondsToSelector:action]) {
[self.arrayAttachments enumerateObjectsUsingBlock:^(WMGTextAttachment * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (priority <= obj.eventPriority) {
obj.eventPriority = priority;
[obj __addTarget:target action:action forControlEvents:controlEvents];
}
}];
}
}
复制代码
事件的调用
事件的激活
- 位图的点击开始,取位置
位图, @interface WMGListTextView : WMGCanvasControl
保存点击的位置
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{}
- 发起事件调用
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[_textDrawer touchesEnded:touches withEvent:event];
[super touchesEnded:touches withEvent:event];
}
复制代码
进入 WMGTextDrawer
方法中,
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self eventDelegateDidPressActiveRange:activeRange];
});
}
复制代码
进一步调用
- (void)eventDelegateDidPressActiveRange:(id<WMGActiveRange>)activeRange
{
if (_eventDelegateHas.didPressActiveRange) {
[_eventDelegate textDrawer:self didPressActiveRange:activeRange];
}
}
复制代码
返回到位图 @interface WMGListTextView : WMGCanvasControl
- (void)textDrawer:(WMGTextDrawer *)textDrawer didPressActiveRange:(id<WMGActiveRange>)activeRange
{
if (activeRange.type == WMGActiveRangeTypeAttachment) {
//文本附件(图片,事件响应)
WMGTextAttachment *att = (WMGTextAttachment *)activeRange.bindingData;
[att handleEvent:att.userInfo];
}
}
复制代码
把事件,消费掉
- (void)handleEvent:(id)sender{
if (_target && _selector) {
if ([_target respondsToSelector:_selector]) {
// 执行方法
[_target performSelector:_selector withObject:sender];
}
}
}
复制代码