5

As explained here, WKWebView has a bug whereby apps that bundle a local webpage have to copy the bundle into the tmp directory. My code for copying the bundle into tmp is:

// Clear tmp directory
NSArray* temporaryDirectory = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:NSTemporaryDirectory() error:NULL];
for (NSString *file in temporaryDirectory) {
    [[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), file] error:NULL];
}

NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *sourcePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"build"];
NSString *temporaryPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"build"];
NSError *error = nil;

// Copy directory
if(![fileManager copyItemAtPath:sourcePath toPath:temporaryPath error:&error]) {
    [self logError:@"Could not copy directory" :error];
}

I have timed this particular fragment of code, and it takes up 50% of my app's startup time! (~0.5s of a total of ~1s.)

Is there a way I can accelerate (or avoid completely) this particular fragment of code under iOS 8? Could threading help?

Community
  • 1
  • 1
Randomblue
  • 112,777
  • 145
  • 353
  • 547
  • Have you tried putting that code into a background queue? Right now it looks to be executing on the main thread which would certainly slow the launch down. The shared `NSFileManager` is thread safe so you shouldn't run into any problems there. – Sam Apr 18 '15 at 23:58
  • The copying of those files is in the critical path of my application. My application is effectively a `WKWebView` that needs to be loaded as fast as possible. – Randomblue Apr 19 '15 at 00:06
  • How big is the bundle that you're trying to copy? – pteofil Apr 28 '15 at 13:01
  • You could probably work on the images as being the ones taking most of space, and make them smaller. But I would say that a 0.5s it's not bad, and I don't think it would be noticeable if you would reduce that to 0.2 or 0.1s – pteofil Apr 28 '15 at 13:03

4 Answers4

6

Given that your assets are large enough to take 0.5s to get copied, my suggestion would be to take a slightly different approach to quicken the (apparent) startup time:

  1. Create a plain old HTML page with placeholder divs for loading the assets (if you like, you can include small assets (e.g. load indicator images) in base64)
  2. Once that HTML loads (maybe on webView:didFinishNavigation:), start the copy to the build folder
  3. Once the copy completes, populate the placeholder divs with your assets using javascript (with evaluateJavaScript:completionHandler:)

While I realize this doesn't directly address the question, I think a slight change of approach would be best in this case.

roop
  • 1,291
  • 8
  • 10
2

NOTE: This technique works well with UIKit's UIWebView and AppKit's WebView, but does not work for the new WKWebView, which appears to ignore the URL loading system. See comments.

Use NSURLProtocol to handle local file reads as though they were remote requests. For a full example, see the one in PandoraBoy called ResourceURLProtocol. I'll walk through a slightly simplified version of it here. We'll read http://.RESOURCE./path/to/file as if it were <resources>/path/to/file.

Every NSURLProtocol will be asked if it can handle every request that comes up in the system. It need to answer whether it can in +canInitWithRequest:. We'll say if the host is .RESOURCE., then we can handle it. .RESOURCE. is an illegal DNS name, so it can't conflict with any real host (but it's a legal hostname for URL-purposes).

NSString *ResourceHost = @".RESOURCE.";

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    return ( [[[request URL] scheme] isEqualToString:@"http"] &&
             [[[request URL] host] isEqualToString:ResourceHost] );
}

Then we need a couple of bookkeeping methods. Nothing much to see here.

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

-(void)stopLoading {
    return;
}

Now we get to the meat of it. startLoading is where you're going to do whatever you want to do with the request.

-(void)startLoading {
    NSBundle *thisBundle = [NSBundle bundleForClass:[self class]];
    NSString *notifierPath = [[thisBundle resourcePath] stringByAppendingPathComponent:[[[self request] URL] path]];
    NSError *err;
    NSData *data = [NSData dataWithContentsOfFile:notifierPath
                                          options:NSUncachedRead // Assuming you only need to read this once
                                            error:&err];
    if( data != nil ) {
        // Assuming you're only reading HTML. 
        // If you need other things, you'll need to work out the correct MIME type
        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[[self request] URL]
                                                            MIMEType:@"text/html"
                                               expectedContentLength:[data length]
                                                    textEncodingName:nil];

        // And we just pass it to the caller
        [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
        [[self client] URLProtocol:self didLoadData:data];
        [[self client] URLProtocolDidFinishLoading:self];
    } else {
        NSLog(@"BUG:Unable to load resource:%@:%@", notifierPath, [err description]);
        [[self client] URLProtocol:self didFailWithError:err];
    }
}

I also find a little ResourceURL wrapper helpful:

@implementation ResourceURL
+ (ResourceURL*) resourceURLWithPath:(NSString *)path {
    return [[[NSURL alloc] initWithScheme:@"http"
                                     host:ResourceHost
                                     path:path] autorelease];
}    
@end

To use it, you just need to first register your protocol handler:

[NSURLProtocol registerClass:[ResourceURLProtocol class]];

Then you can create a "resource URL" and load it:

ResourceURL *resource = [ResourceURL resourceURLWithPath:...];
[webView loadRequest:[NSURLRequest requestWithURL:resource]];

For more details on NSURLProtocol, as well as a more complicated caching example, see Drop-in Offline Caching for UIWebView (and NSURLProtocol).

PandoraBoy is full of NSURLProtocol examples (look for all the classes with Protocol in their names). You can hijack, spy on, redirect, or manipulate just about anything coming through the URL loading system this way.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
1

Try XWebView without copying files. It includes a tiny http server for you. You just need to call [webview loadFileURL:allowingReadAccessToURL:]

soflare
  • 751
  • 5
  • 16
1

Making this parallel with the rest of the 0.5s of startup time will definitely help. (This would only be possible if whatever you're doing in the rest of the 0.5s of startup does not depend on the WebView being loaded or the files being read)

The best optimization you'll get is by reducing the size of the files inside the "build" directory. Try these -

  1. Minify your pages and javascript files if you have any. https://developers.google.com/speed/docs/insights/MinifyResources
  2. If the directory size is significantly large, consider zipping it up, add zip to app, copying zip into "tmp" and unzipping it inside "tmp"
Manan
  • 93
  • 6