背景
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è)畫布,互不干擾。

當(dāng)元素1創(chuàng)建失敗時(shí),ctx1拿到了總畫布的家門鑰匙,為所欲為。
什么場(chǎng)景會(huì)發(fā)生?
- 創(chuàng)建子元素時(shí),恰巧內(nèi)存不足
- 創(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;
}
生成效果

明顯綠色透了出來。
為什么邊緣透光?
使用CG繪制時(shí),創(chuàng)建畫布尺寸必須為整,如果使用非整size,會(huì)被強(qiáng)制向上取整。
前面的例子,打印圖片寬高

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