TextureGroup / Texture

Smooth asynchronous user interfaces for iOS apps.
https://texturegroup.org/
Other
8.02k stars 1.29k forks source link

Texture's ASImageNode using 6x more memory to display large image than UIImageView #1529

Open christianselig opened 5 years ago

christianselig commented 5 years ago

Using Texture 2.8.1. (Also mentioned in Slack)

I’m having a heck of a time figuring out how to get Texture (specifically ASImageNode) to work well with a very large image download. Specifically this image from Reddit.

I haven’t had any trouble with UIKit (UIImageView) which uses 250MB of RAM to render the image, but Texture peaks at about 1.5GB before levelling back down to around 20MB. Unfortunately this means it crashes most devices due to excessive memory usage, and even my iPhone X gets multiple memory warnings (though it doesn't crash).

The image is being shown in a UIScrollView and the image is about 6MB and 12,000 x 12,000. I created a sample project with just a small bit of code that maps the UIKit implementation to Texture. (Note the scroll view isn't inherently necessary and the issue is visible without it.)

I'm sure I'm just doing something silly, and to be fair I'm not understating the complex nature of image rendering! I've just found in the past Texture always performs very favorably to UIKit so something seemed odd. It might be possible that UIImageView loads the image into memory only in visible tiles?

Code:

import UIKit
import AsyncDisplayKit
import PINRemoteImage

class ViewController: UIViewController, UIScrollViewDelegate {
    let scrollView = UIScrollView()

    var imageView: UIImageView?
    var imageNode: ASImageNode?

    override func viewDidLoad() {
        super.viewDidLoad()

        scrollView.backgroundColor = .black
        scrollView.frame = view.bounds
        scrollView.delegate = self
        view.addSubview(scrollView)

        // Note: large image (6MB / 12K x 12K / 144 megapixels)
        let url = URL(string: "https://preview.redd.it/jlwba6iohkt21.jpg?auto=webp&s=2da20d8e4e24fbbd654a3c002de93d8521062648")!

        // For this example we know the size already so for simplicity…
        let imageSize = CGSize(width: 12_283, height: 12_283)

        ASPINRemoteImageDownloader.shared().sharedPINRemoteImageManager().downloadImage(with: url, options: [.downloadOptionsSkipDecode]) { (result) in
            print("Download complete!")

            DispatchQueue.main.async {
                guard let image = result.image else { return }

                // Toggle this if you want to use UIKit instead of AsyncDisplayKit
                let useAsyncDisplayKit = true

                if useAsyncDisplayKit {
                    let imageNode = ASImageNode()
                    imageNode.frame = CGRect(origin: .zero, size: imageSize) // <-- This is what causes the memory spike
                    imageNode.image = image
                    self.imageNode = imageNode
                    self.scrollView.addSubview(imageNode.view)
                } else {
                    let imageView = UIImageView()
                    imageView.frame = CGRect(origin: .zero, size: imageSize)
                    imageView.image = image
                    self.imageView = imageView
                    self.scrollView.addSubview(imageView)
                }

                let scaleToFit = min(self.scrollView.bounds.width / imageSize.width, self.scrollView.bounds.height / imageSize.height);

                self.scrollView.contentSize = imageSize
                self.scrollView.minimumZoomScale = scaleToFit
                self.scrollView.maximumZoomScale = 1.0
                self.scrollView.zoomScale = scaleToFit
            }
        }
    }

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageNode?.view ?? imageView
    }
}

Sample Project: LargeImageTextureTest.zip

maicki commented 5 years ago

@christianselig Hey - would be interesting to see if this one could maybe help you: https://github.com/TextureGroup/Texture/pull/1469. It's on master only yet, but if you would point to that. Also if you run it within Instruments what is taking up the memory?

christianselig commented 5 years ago

@maicki I assume it's not implicitly activated, right? (It's an experimental feature, ASExperimentalDrawingGlobal?) If so, how exactly do I activate that, haha? I'm seeing some stuff around ASConfigurationManager but having trouble finding anything exact…

maicki commented 5 years ago

@christianselig Do something like that in your AppDelegate:

@implementation ASConfiguration (UserProvided)
+ (ASConfiguration *)textureConfiguration {
  ASConfiguration *cfg = [[ASConfiguration alloc] init];
  cfg.experimentalFeatures |= ASExperimentalDrawingGlobal;
  return cfg;
}
@end
christianselig commented 5 years ago

@maicki Sorry I'm still a little lost and trying to put this into Swift, is that just a category on ASConfiguration, and it internally looks for a function called textureConfiguration? It's not a protocol or anything?

EDIT: Just a sec actually I'll just rewrite this in Objective-C haha, my Objective-C to Swift conversion isn't the best.

christianselig commented 5 years ago

@maicki

Okay, pointed my Podfile to the master branch, rewrote it in Objective-C (see below) and added that code to the AppDelegate.

The result was the same, crashed with the following message:

Message from debugger: Terminated due to memory issue

Whereas the UIKit version worked.

Objective-C version:

#import "ViewController.h"
#import "AsyncDisplayKit/AsyncDisplayKit.h"
#import "PINRemoteImage/PINRemoteImage.h"

@interface ViewController ()

@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) ASImageNode *imageNode;
@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.scrollView = [[UIScrollView alloc] init];
    self.scrollView.frame = self.view.bounds;
    self.scrollView.backgroundColor = [UIColor blackColor];
    self.scrollView.delegate = self;
    [self.view addSubview:self.scrollView];

    NSURL *url = [NSURL URLWithString:@"https://preview.redd.it/jlwba6iohkt21.jpg?auto=webp&s=2da20d8e4e24fbbd654a3c002de93d8521062648"];

    CGSize imageSize = CGSizeMake(12283.0, 12283.0);

    [[[ASPINRemoteImageDownloader sharedDownloader] sharedPINRemoteImageManager] downloadImageWithURL:url options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult * _Nonnull result) {
        NSLog(@"Download complete!");

        if (!result.image) {
            return;
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            // Toggle this if you want to use UIKit instead of AsyncDisplayKit
            BOOL useAsyncDisplayKit = YES;

            if (useAsyncDisplayKit) {
                ASImageNode *imageNode = [[ASImageNode alloc] init];
                imageNode.frame = CGRectMake(0.0, 0.0, imageSize.width, imageSize.height);
                imageNode.image = result.image;
                self.imageNode = imageNode;
                [self.scrollView addSubview:imageNode.view];
            } else {
                UIImageView *imageView = [[UIImageView alloc] init];
                imageView.frame = CGRectMake(0.0, 0.0, imageSize.width, imageSize.height);
                imageView.image = result.image;
                self.imageView = imageView;
                [self.scrollView addSubview:imageView];
            }

            CGFloat scaleToFit = fminf(self.scrollView.bounds.size.width / imageSize.width, self.scrollView.bounds.size.height / imageSize.height);

            self.scrollView.contentSize = imageSize;
            self.scrollView.minimumZoomScale = scaleToFit;
            self.scrollView.maximumZoomScale = 1.0;
            self.scrollView.zoomScale = scaleToFit;
        });
    }];
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    if (self.imageNode) {
        return self.imageNode.view;
    }

    return self.imageView;
}

@end

And the AppDelegate.m:

#import "AppDelegate.h"
#import "AsyncDisplayKit/AsyncDisplayKit.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    return YES;
}

@end

@implementation ASConfiguration (UserProvided)
+ (ASConfiguration *)textureConfiguration {
    ASConfiguration *cfg = [[ASConfiguration alloc] init];
    cfg.experimentalFeatures |= ASExperimentalDrawingGlobal;
    return cfg;
}
@end

I profiled it in Instruments in both the Swift version (without the experimental feature) and the Objective-C version with the experimental feature and both seemed the same with createContentsForKey seemingly taking the lions share of memory usage in ASImageNode.mm.

Full Swift (non-experiment) call stack/memory used:

Screen Shot 2019-05-30 at 6 19 18 PM

Full Objective-C (experiment) stack:

Screen Shot 2019-05-30 at 6 44 48 PM
tuchangwei commented 5 years ago

I have 10000 records, the memory was 40M if I didn't add ASImageNode, but it was 150M if I added ASImageNode(didn't set an image to it) and if I scrolled the ASCollectionView, the memory was up and up.