UIGraphics需要注意的点

背景

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进行颜色填充,所以边缘露光。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容

  • 绘制像素到屏幕上 answer-huang22 Mar 2014 分享文章 一个像素是如何绘制到屏幕上去的?有很多...
    阿狸旅途T恤阅读 1,633评论 0 7
  • 一、UIView和CALayer是什么关系? 1,UIView能够显示在屏幕上归功于CALayer,通过调用dra...
    闪电迷阅读 793评论 0 1
  • 1.UIImageView尽量设置为不透明 opque尽量设置为YES 当UIImageView的opque设置为...
    奔跑的喔汼阅读 607评论 0 0
  • 设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的...
    iOS菜鸟大大阅读 707评论 0 1
  • 一个像素是如何绘制到屏幕上去的?有很多种方式将一些东西映射到显示屏上,他们需要调用不同的框架、许多功能和方法的结合...
    skogt阅读 453评论 0 0