UIGraphics需要注意的點(diǎn)

背景

UIGraphicsBeginImageContextWithOptions / UIGraphicsEndImageContext是一對(duì)老組合,我們通常使用它來創(chuàng)建畫布,進(jìn)行自定義繪制,它都有哪些需要注意的點(diǎn)呢?

// 例:繪制平鋪圖
- (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. 內(nèi)存占用

何時(shí)分配?

UIGraphicsBeginImageContextWithOptions此句調(diào)用時(shí),就會(huì)分配。

占用大小?

內(nèi)存占用大小 = size.width * size.height * 4 * pow(scale, 2)。
如5000*5000畫布:5000 * 5000 * 4 * 1 = 95.37m,相當(dāng)恐怖。
進(jìn)行完繪制后,一定要及時(shí)清理,否則易導(dǎo)致內(nèi)存達(dá)峰OOM

UIGraphicsBeginImageContextWithOptions可靠嗎?

如果當(dāng)前可用內(nèi)存不足,將導(dǎo)致UIGraphicsBeginImageContextWithOptions申請(qǐng)畫布失敗,后續(xù)繪制都不再可靠。
不要在模擬器測(cè)試,模擬器可分配的內(nèi)存非常大

2. 嵌套使用場(chǎng)景

- (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. 內(nèi)存占用

可以想像成一個(gè)二叉樹,子級(jí)畫布為葉子節(jié)點(diǎn)。
-> 內(nèi)存占用為當(dāng)前節(jié)點(diǎn)到根節(jié)點(diǎn)路徑中,所有節(jié)點(diǎn)內(nèi)存之和。需要非常警惕內(nèi)存峰值

2. 繪制錯(cuò)亂問題

繼續(xù)看上圖,當(dāng)子元素1繪制創(chuàng)建畫布失敗時(shí),會(huì)發(fā)生什么呢?

答案:ctx1獲取拿到了總畫布,what the fuck?

此時(shí),若無查覺問題,基于ctx1的繪制,都是錯(cuò)誤的繪制在了總畫布上。繪制結(jié)果圖,也拿到了總畫布的5000*5000圖。

元素1繪制完成時(shí),調(diào)用UIGraphicsEndImageContext,也將關(guān)閉總畫布,導(dǎo)致總畫布出圖結(jié)果result為nil。

這個(gè)元素1,不僅占了別人的媳婦,臨走還燒了他的房子。

為什么會(huì)如此?

UIGraphicsBeginImageContextWithOptions / UIGraphicsEndImageContext,是一一對(duì)應(yīng)的關(guān)系,對(duì)應(yīng)入棧與出棧。對(duì)應(yīng)每個(gè)棧元素,系統(tǒng)對(duì)應(yīng)創(chuàng)建有一個(gè)畫布,互不干擾。


錯(cuò)誤發(fā)生

當(dāng)元素1創(chuàng)建失敗時(shí),ctx1拿到了總畫布的家門鑰匙,為所欲為。

什么場(chǎng)景會(huì)發(fā)生?

  1. 創(chuàng)建子元素時(shí),恰巧內(nèi)存不足
  2. 創(chuàng)建畫布傳入了錯(cuò)誤尺寸。如size寬度為0

如何避免?

1. 避免錯(cuò)誤發(fā)生

控制整體App良好內(nèi)存占用。這當(dāng)然是一句沒用的話,編輯器內(nèi)你控制的再好,扛不住別的地方瘋狂占用啊

2. 錯(cuò)誤發(fā)生時(shí),控制影響面

UIGraphicsGetCurrentContext隨處都可調(diào)用,繪制代碼雖然沒外露,但仍不能阻止別處得到自身繪制的ctx。

解決辦法:不再使用UIAPI創(chuàng)建畫布,使用Quartz的CGBitmapContextCreate/CGContextRelease這對(duì)組合。

        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, @"創(chuàng)建bitmap失敗?。?!");
            return nil;
        }
        // Quartz的Context坐標(biāo)系轉(zhuǎn)化為UIAPI下
        CGContextTranslateCTM(_context, 0, size.height);
        CGContextScaleCTM(_context, 1.0, -1.0);

此種方式創(chuàng)建的bitmap,外界使用UIGraphicsGetCurrentContext無法獲取其ctx。由于每個(gè)繪制者僅能獲取自身ctx,當(dāng)錯(cuò)誤發(fā)生時(shí),僅影響當(dāng)前錯(cuò)誤本身,不會(huì)影響擴(kuò)散。
如果上面例子中元素1申請(qǐng)畫布失敗,元素1繪制失敗,元素2及總畫布繪制不受影響。

3. UIGraphicsGetCurrentContext與線程

1. UIGraphicsGetCurrentContext只能獲取當(dāng)前線程下的ctx

而不能獲取其它線程創(chuàng)建的畫布。

- (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

使用非整數(shù)size創(chuàng)建畫布,會(huì)發(fā)生什么?

例如:創(chuàng)建一個(gè)寬高為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;
}

生成效果


結(jié)果圖

明顯綠色透了出來。

為什么邊緣透光?

使用CG繪制時(shí),創(chuàng)建畫布尺寸必須為整,如果使用非整size,會(huì)被強(qiáng)制向上取整。
前面的例子,打印圖片寬高

image.png

以上代碼創(chuàng)建圖片的過程,相當(dāng)于畫布創(chuàng)建100*100,而使用99.2*99.2進(jìn)行顏色填充,所以邊緣露光。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 繪制像素到屏幕上 answer-huang22 Mar 2014 分享文章 一個(gè)像素是如何繪制到屏幕上去的?有很多...
    阿貍旅途T恤閱讀 1,756評(píng)論 0 7
  • 一、UIView和CALayer是什么關(guān)系? 1,UIView能夠顯示在屏幕上歸功于CALayer,通過調(diào)用dra...
    閃電迷閱讀 885評(píng)論 0 1
  • 1.UIImageView盡量設(shè)置為不透明 opque盡量設(shè)置為YES 當(dāng)UIImageView的opque設(shè)置為...
    奔跑的喔汼閱讀 794評(píng)論 0 0
  • 設(shè)計(jì)模式是什么? 你知道哪些設(shè)計(jì)模式,并簡(jiǎn)要敘述? 設(shè)計(jì)模式是一種編碼經(jīng)驗(yàn),就是用比較成熟的邏輯去處理某一種類型的...
    iOS菜鳥大大閱讀 808評(píng)論 0 1
  • 一個(gè)像素是如何繪制到屏幕上去的?有很多種方式將一些東西映射到顯示屏上,他們需要調(diào)用不同的框架、許多功能和方法的結(jié)合...
    skogt閱讀 483評(píng)論 0 0

友情鏈接更多精彩內(nèi)容