美文网首页
UIGraphics需要注意的点

UIGraphics需要注意的点

作者: 小小棒棒糖 | 来源:发表于2022-05-14 11:34 被阅读0次

    背景

    UIGraphicsBeginImageContextWithOptions / UIGraphicsEndImageContext是一对老组合,我们通常使用它来创建画布,进行自定义绘制,它都有哪些需要注意的点呢?

    // 例:绘制平铺图
    - (void)drawTileImage:(UIImage *)inputImage {
        CGSize size = CGSizeMake(5000, 5000);
        UIGraphicsBeginImageContextWithOptions(size, YES, 1);
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        CGContextDrawTiledImage(ctx, CGRectMake(1000, 1000, 3000, 3000), inputImage.CGImage);
        UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    }
    

    1. 内存占用

    何时分配?

    UIGraphicsBeginImageContextWithOptions此句调用时,就会分配。

    占用大小?

    内存占用大小 = size.width * size.height * 4 * pow(scale, 2)。
    如5000*5000画布:5000 * 5000 * 4 * 1 = 95.37m,相当恐怖。
    进行完绘制后,一定要及时清理,否则易导致内存达峰OOM

    UIGraphicsBeginImageContextWithOptions可靠吗?

    如果当前可用内存不足,将导致UIGraphicsBeginImageContextWithOptions申请画布失败,后续绘制都不再可靠。
    不要在模拟器测试,模拟器可分配的内存非常大

    2. 嵌套使用场景

    - (void)testMemoryAlloc {
        CGSize size = CGSizeMake(1000, 9000);
        // 外层画布绘制
        UIGraphicsBeginImageContextWithOptions(size, NO, 1);
    
        // 子元素1绘制
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 200), YES, 1);
        CGContextRef ctx1 = UIGraphicsGetCurrentContext();
        UIGraphicsEndImageContext();
    
        // 子元素2绘制
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 200), YES, 1);
        CGContextRef ctx2 = UIGraphicsGetCurrentContext();
        UIGraphicsEndImageContext();
        
        UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    }
    

    1. 内存占用

    可以想像成一个二叉树,子级画布为叶子节点。
    -> 内存占用为当前节点到根节点路径中,所有节点内存之和。需要非常警惕内存峰值

    2. 绘制错乱问题

    继续看上图,当子元素1绘制创建画布失败时,会发生什么呢?

    答案:ctx1获取拿到了总画布,what the fuck?

    此时,若无查觉问题,基于ctx1的绘制,都是错误的绘制在了总画布上。绘制结果图,也拿到了总画布的5000*5000图。

    元素1绘制完成时,调用UIGraphicsEndImageContext,也将关闭总画布,导致总画布出图结果result为nil。

    这个元素1,不仅占了别人的媳妇,临走还烧了他的房子。

    为什么会如此?

    UIGraphicsBeginImageContextWithOptions / UIGraphicsEndImageContext,是一一对应的关系,对应入栈与出栈。对应每个栈元素,系统对应创建有一个画布,互不干扰。


    错误发生

    当元素1创建失败时,ctx1拿到了总画布的家门钥匙,为所欲为。

    什么场景会发生?

    1. 创建子元素时,恰巧内存不足
    2. 创建画布传入了错误尺寸。如size宽度为0

    如何避免?

    1. 避免错误发生

    控制整体App良好内存占用。这当然是一句没用的话,编辑器内你控制的再好,扛不住别的地方疯狂占用啊

    2. 错误发生时,控制影响面

    UIGraphicsGetCurrentContext随处都可调用,绘制代码虽然没外露,但仍不能阻止别处得到自身绘制的ctx。

    解决办法:不再使用UIAPI创建画布,使用Quartz的CGBitmapContextCreate/CGContextRelease这对组合。

            CGColorSpaceRef colorSpace;
            if (@available(iOS 9.0, tvOS 9.0, *)) {
                colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
            } else {
                colorSpace = CGColorSpaceCreateDeviceRGB();
            }
            int bytesPerPixel = 4;
            size_t bytesPerRow = ceil(bytesPerPixel * size.width / 4.0) * 4;
            size_t bitsPerComponent = 8;
            CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
            bitmapInfo |= !opaque ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
            _context = CGBitmapContextCreate(NULL,
                                           size.width,
                                           size.height,
                                           bitsPerComponent,
                                           bytesPerRow,
                                           colorSpace,
                                           bitmapInfo);
            CGColorSpaceRelease(colorSpace);
            if (_context == NULL) {
                NSAssert(_context != NULL, @"创建bitmap失败!!!");
                return nil;
            }
            // Quartz的Context坐标系转化为UIAPI下
            CGContextTranslateCTM(_context, 0, size.height);
            CGContextScaleCTM(_context, 1.0, -1.0);
    

    此种方式创建的bitmap,外界使用UIGraphicsGetCurrentContext无法获取其ctx。由于每个绘制者仅能获取自身ctx,当错误发生时,仅影响当前错误本身,不会影响扩散。
    如果上面例子中元素1申请画布失败,元素1绘制失败,元素2及总画布绘制不受影响。

    3. UIGraphicsGetCurrentContext与线程

    1. UIGraphicsGetCurrentContext只能获取当前线程下的ctx

    而不能获取其它线程创建的画布。

    - (void)testMemoryAlloc {
        CGSize size = CGSizeMake(5000, 5000);
        UIGraphicsBeginImageContextWithOptions(size, NO, 1);
        CGContextRef ctx1 = UIGraphicsGetCurrentContext();
        NSLog(@"ctx1:%p",ctx1);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            UIGraphicsBeginImageContextWithOptions(size, NO, 1);
            CGContextRef ctx2 = UIGraphicsGetCurrentContext();
            NSLog(@"ctx2:%p",ctx2);
            
            dispatch_async(dispatch_get_main_queue(), ^{
                CGContextRef ctx3 = UIGraphicsGetCurrentContext();
                NSLog(@"ctx3:%p",ctx3);
            });
        });
    
    // 输出为:
    2022-05-14 16:19:41.203680+0800 ZZTest[92817:3151952] ctx1:0x600000365500
    2022-05-14 16:19:41.255299+0800 ZZTest[92817:3152064] ctx2:0x600000374b40
    2022-05-14 16:19:41.255596+0800 ZZTest[92817:3151952] ctx3:0x600000365500
    

    使用非整数size创建画布,会发生什么?

    例如:创建一个宽高为99.2的绿色背景UIView,再生成宽高为99.2的红色图片,能完全盖住吗?

    - (void)viewDidLoad {
        CGRect rect = CGRectMake(100, 100, 99.2, 99.2);
        
        UIView *view = [[UIView alloc] initWithFrame:rect];
        view.backgroundColor = [UIColor redColor];
        [self.view addSubview:view];
        
        UIImageView *imageView = [[UIImageView alloc] initWithFrame:rect];
        imageView.image = [self testDrawImage:rect.size];
        [self.view addSubview:imageView];
    }
    
    - (UIImage *)testDrawImage:(CGSize)size {
        UIGraphicsBeginImageContextWithOptions(size, NO, 1);
        CGContextRef ctx1 = UIGraphicsGetCurrentContext();
        CGContextSetFillColorWithColor(ctx1, UIColor.blackColor.CGColor);
        CGContextFillRect(ctx1, CGRectMake(0, 0, size.width, size.height));
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
    

    生成效果


    结果图

    明显绿色透了出来。

    为什么边缘透光?

    使用CG绘制时,创建画布尺寸必须为整,如果使用非整size,会被强制向上取整。
    前面的例子,打印图片宽高

    image.png
    以上代码创建图片的过程,相当于画布创建100*100,而使用99.2*99.2进行颜色填充,所以边缘露光。

    相关文章

      网友评论

          本文标题:UIGraphics需要注意的点

          本文链接:https://www.haomeiwen.com/subject/ecmpurtx.html