1

I have a custom UIView that I want to render as an UIImage. The custom UIView is a subclass of UIImageView.

Inside this view, I am rendering some UI elements (drawing a bunch of circles over the image). The number of circles added can go up to the order of thousands.

I am using this simple code snippet to render the view as an UIImage:

// Create the UIImage (code runs on the main thread inside an @autorelease pool)
UIGraphicsBeginImageContextWithOptions(viewToSave.image.size, viewToSave.opaque, 1.0);
[viewToSave.layer renderInContext:UIGraphicsGetCurrentContext()];
imageToSave = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// Since I'm rendering some stuff inside the .layer of the UIView,
// I don't think I can use "drawViewHierarchyInRect: afterScreenUpdates:"

Here is the memory allocations taken from Instruments, in an example with ~3000 circles added as sub-views:

MEMORY ALLOCATIONS (HEAP + ANONYMOUS VM)

Now here's the strange part... This runs fine and I can render the image multiple times (consecutive) and save it in the image gallery on devices like iPhone 5, iPhone 5s, iPhone 6s, iPad Air 2, iPad Mini 4... But the same code triggers a memory warning on iPhone X and eventually crashes the application...

Unfortunately, I do not have access to an iPhone X and the person who reported this doesn't have access to a Mac, so I cannot investigate deeper.

I really don't know if I am doing anything wrong... Are you aware if there is something different about the iPhone X? I've been struggling this issue for quite a while...

rhcpfan
  • 557
  • 7
  • 19
  • 2
    Can't tell you why it's crashing on one iPhone X, but... you have *three thousand* subviews???? Sounds like a recipe for problems... If they are "circles", you may (or likely) would be better off drawing them directly rather than adding them as subviews. – DonMag Jan 25 '18 at 15:50
  • Where is the image coming from? Photo Library? The .image part? – agibson007 Jan 25 '18 at 15:52
  • How do you add the circles? – LGP Jan 25 '18 at 16:04
  • @DonMag The image is coming from the photo library. The reason that I’m not drawing them directly is that they are user-interactive (you can move them, delete them, etc.) – rhcpfan Jan 25 '18 at 16:55
  • @LGP I’m creating the ‘UIView’ instance and add it using addSubview on the main ‘UIView’ – rhcpfan Jan 25 '18 at 16:57
  • Is viewToSave of type `UIImageView`? You use it to size the image context. Is it different in size from the viewToSave.bounds? – LGP Jan 25 '18 at 17:03
  • @LGP viewToSave is of type UIImageView. It’s size property is equal to the original resolution of the image – rhcpfan Jan 26 '18 at 06:30
  • I just ran into the same issue. But I only draw one small view of 52x26 pt. On all other phones, it works fine. But on the iPhone X (regardless if on the device or the simulator) my app freezes, the CPU goes to 100% and the memory is growing until my app is killed. Same with the new UIGraphicsImageRenderer API. And I use raw CG drawing code. No UIView involved. – simonseyer Jan 26 '18 at 14:07
  • @simonseyer In my case it was because of the display scale (for the iPhone X is @3x). If this happens also in the simmulator, or if you can test on device, you may try using Instruments (memory allocations) and get a better understanding of the issue. – rhcpfan Jan 26 '18 at 16:29
  • @rhcpfan Yeah, I actually did a test that also revealed that the problem is related to the scale factor. Assigning `[UIImage imageWithData:UIImagePNGRepresentation(oldImage)]` works (`oldImage` is the CG-drawn image), `[UIImage imageWithData:UIImagePNGRepresentation(oldImage) scale:3.0]` doesn't. But by making the image one pixel larger it also works with the correct scale factor. – simonseyer Jan 26 '18 at 16:48

4 Answers4

1

I guess that the problem has to do with how CALayer:renderInContext: handles drawing of thousands of views in a context that requires them to be scaled up. Would it be possible to try render the sub-views yourself? Then compare and verify if it works better by using instrumentation.

UIImage *imageToSave = [self imageFromSubLayer:viewToSave];

- (UIImage *)imageFromSubLayers:(UIImageView *)imageView {
    CGSize size = imageView.image.size;
    UIGraphicsBeginImageContextWithOptions(size, YES, .0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    for (CALayer *layer in imageView.layer.sublayers)
        [layer renderInContext:context];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return image;
}
LGP
  • 4,135
  • 1
  • 22
  • 34
1

As allways, the answer lays in the smallest (and not included in the question) detail...

What I really didn't consider (and found out after quite a while), is that the iPhone X has a scale factor equal to @3x. This is the difference between the iPhone X and all other devices that were running the code just fine...

At some point in my code, I was setting the .contentScaleFactor of the subviews to be equal to [UIScreen mainScreen].scale. This means that on higher-end devices, the image quality should be better.

For the iPhone X, [UIScreen mainScreen].scale returns 3. For all of the other devices I have tested with, [UIScreen mainScreen].scale returns 2. This means that on the iPhone X, the ammount of memory used to render the image is way higher.

Fun fact number two: From another useful SO post, I found out that on the iPhone X, if you try to allocate more than 50% of it's total amount of memory (1392 MB), it crashes. On the other hand, for example, in the case of the iPhone 6s, the percentage is higher: 68% (1396 MB). This means that for some older devices you have more meory to work with than on the iPhone X.

Sorry for missleading, it was a honest mistake from my part. Thank you all for your answers!

rhcpfan
  • 557
  • 7
  • 19
0

I recently wrote a method into an app I'm making to convert UIView's into UIImages (so i could display gradients on progress views/tab bars). I ended up settling with the following code, I'm using this code to render tab bar buttons, it works on all devices including the X.

Objective C:

UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:gradientView.bounds.size];
    UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
        [gradientView drawViewHierarchyInRect:gradientView.bounds afterScreenUpdates:true];
    }];

Swift 4:

let renderer = UIGraphicsImageRenderer(size: gradientView.bounds.size)
        let image = renderer.image { ctx in
            gradientView.drawHierarchy(in: gradientView.bounds, afterScreenUpdates: true)
        }

I used this code in a sample project I wrote up, here is a link to the project files:

Swift, Objective C

as you will see both projects will run on iPhone X perfectly!

Axemasta
  • 763
  • 1
  • 9
  • 24
0

I know, the following sounds weird. But try to make the target image one pixel larger than the one that you are drawing. This solved it for me (my particular problem: "[CALayer renderInContext]" crashes on iPhone X).

In code:

UIGraphicsBeginImageContextWithOptions(
    CGSizeMake(viewToSave.image.size.width + 1, 
               viewToSave.image.size.height + 1), 
    viewToSave.opaque, 
    1.0
);
simonseyer
  • 650
  • 1
  • 7
  • 14