tableView 卡顿优化的标准解决: Async View

小灰学习 tableView 卡顿优化, 当然是用 Async View

小灰看下 gg 了的 Graver 框架

问题与解决: 在渲染上

复杂的 cell , 滚动起来,容易卡顿

复杂,即包含很多控件 ( 位图 ),形成一颗复杂的图层树

下图,是一个简单的 cell, 层次有一些

截屏2021-11-01 下午3.51.08.png

Graver 可,一个 cell, 变成一张位图,层次简单

截屏2021-11-01 下午3.55.37.png

而且 graver ,充分利用多线程

回顾下流程

1, 拿到网络请求的数据,大家都是这么做的

截屏2021-11-01 下午4.01.35.png

2, 业务数据的加工,方便渲染

AFN 的结果回调线程,不使用主线程,

开个子线程,去处理

截屏2021-11-01 下午4.03.01.png

上一篇,tableView 卡顿优化,除了缓存计算高度,还可以...

就是这么做的

3, 异步渲染

three.png

思路: 图片绘制,比较简单

核心是,只有主线程,才能操作 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

文本绘制工具类, 拿到图形上下文,

把数据 ( 文字 / 图片 ), 绘制在画布上

配合新的数据单元 WMGVisionObjectWMMutableAttributedItem

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];

        }
    }

}
复制代码

github repo

Guess you like

Origin juejin.im/post/7025675059096191006