47

以前也有人问过类似的问题,但我永远找不到解决方案。

这是我的情况 - 我的 UIWebView 加载了一个远程 html 页面。网页中使用的图像在构建时是已知的。为了让页面加载更快,我想在iOS应用中打包图片文件,在运行时替换掉。

[请注意,html 是远程的。我总是得到从本地加载 html 和图像文件的答案 - 我已经这样做了]

我得到的最接近的建议是在 html 页面和 iOS 应用程序中使用自定义 url 方案,例如 myapp://images/img.png,用 NSURLProtocol 子类拦截 myapp:// URL,并用本地替换图像图片。理论上听起来不错,但我还没有遇到一个完整的代码示例来证明这一点。

我有Java背景。我可以使用自定义内容提供程序轻松地为 Android 做到这一点。我确信 iOS/Objective-C 必须存在类似的解决方案。我在 Objective-C 方面没有足够的经验来在短时间内自己解决它。

任何帮助将不胜感激。

4

5 回答 5

87

好的,这里是一个示例,如何子类化 NSURLProtocol并提供已经在包中的图像( image1.png )下面是子类的标题、实现以及如何在 viewController(不完整的代码)和本地 html 文件(可以很容易地与远程文件交换)中使用它的示例。我已经调用了自定义协议:myapp://正如您在底部的 html 文件中看到的那样。

感谢您的提问!我自己问了很长时间,弄清楚这一点所花费的时间每一秒都是值得的。

编辑: 如果有人在使我的代码在当前 iOS 版本下运行时遇到困难,请查看 sjs 的答案。当我回答这个问题时,它正在工作。他指出了一些有用的补充并纠正了一些问题,所以也给他一些支持。

这是它在我的模拟器中的样子:

在此处输入图像描述

MyCustomURLProtocol.h

@interface MyCustomURLProtocol : NSURLProtocol
{
    NSURLRequest *request;
}

@property (nonatomic, retain) NSURLRequest *request;

@end

MyCustomURLProtocol.m

#import "MyCustomURLProtocol.h"

@implementation MyCustomURLProtocol

@synthesize request;

+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
    if ([theRequest.URL.scheme caseInsensitiveCompare:@"myapp"] == NSOrderedSame) {
        return YES;
    }
    return NO;
}

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

- (void)startLoading
{
    NSLog(@"%@", request.URL);
    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[request URL] 
                                                        MIMEType:@"image/png" 
                                           expectedContentLength:-1 
                                                textEncodingName:nil];

    NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"image1" ofType:@"png"];  
    NSData *data = [NSData dataWithContentsOfFile:imagePath];

    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [[self client] URLProtocol:self didLoadData:data];
    [[self client] URLProtocolDidFinishLoading:self];
    [response release];
}

- (void)stopLoading
{
    NSLog(@"something went wrong!");
}

@end

MyCustomProtocolViewController.h

@interface MyCustomProtocolViewController : UIViewController {
    UIWebView *webView;
}

@property (nonatomic, retain) UIWebView *webView;

@end

MyCustomProtocolViewController.m

...

@implementation MyCustomProtocolViewController

@synthesize webView;

- (void)awakeFromNib
{
    self.webView = [[[UIWebView alloc] initWithFrame:CGRectMake(20, 20, 280, 420)] autorelease];
    [self.view addSubview:webView];
}

- (void)viewDidLoad
{   
    // ----> IMPORTANT!!! :) <----
    [NSURLProtocol registerClass:[MyCustomURLProtocol class]];

    NSString * localHtmlFilePath = [[NSBundle mainBundle] pathForResource:@"file" ofType:@"html"];

    NSString * localHtmlFileURL = [NSString stringWithFormat:@"file://%@", localHtmlFilePath];

    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:localHtmlFileURL]]];

    NSString *html = [NSString stringWithContentsOfFile:localHtmlFilePath encoding:NSUTF8StringEncoding error:nil]; 

    [webView loadHTMLString:html baseURL:nil];
}

文件.html

<html>
<body>
    <h1>we are loading a custom protocol</h1>
    <b>image?</b><br/>
    <img src="myapp://image1.png" />
<body>
</html>
于 2011-04-06T21:26:31.353 回答
39

尼克韦弗的想法是正确的,但他的答案中的代码不起作用。它也打破了一些命名约定,永远不要用NS前缀命名你自己的类,并遵循首字母缩写词的约定,例如标识符名称中的 URL。为了便于理解,我将坚持使用他的名字。

这些更改是微妙但重要的:丢失未分配的requestivar,而是参考由提供的实际请求NSURLProtocol,它工作正常。

NSURLProtocolCustom.h

@interface NSURLProtocolCustom : NSURLProtocol
@end

NSURLProtocolCustom.m

#import "NSURLProtocolCustom.h"

@implementation NSURLProtocolCustom

+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
    if ([theRequest.URL.scheme caseInsensitiveCompare:@"myapp"] == NSOrderedSame) {
        return YES;
    }
    return NO;
}

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

- (void)startLoading
{
    NSLog(@"%@", self.request.URL);
    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL 
                                                        MIMEType:@"image/png" 
                                           expectedContentLength:-1 
                                                textEncodingName:nil];

    NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"image1" ofType:@"png"];  
    NSData *data = [NSData dataWithContentsOfFile:imagePath];

    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [[self client] URLProtocol:self didLoadData:data];
    [[self client] URLProtocolDidFinishLoading:self];
    [response release];
}

- (void)stopLoading
{
    NSLog(@"request cancelled. stop loading the response, if possible");
}

@end

Nick 的代码的问题在于子类NSURLProtocol不需要存储请求。NSURLProtocol已经有请求,可以使用-[NSURLProtocol request]同名的方法或属性访问。由于request他的原始代码中的 ivar 从未被分配,所以它总是nil被分配(如果它被分配,它应该已经在某个地方发布)。该代码不能也不起作用。

其次,我建议在创建响应之前读取文件数据并[data length]作为预期的内容长度而不是 -1 传递。

最后,-[NSURLProtocol stopLoading]不一定是错误,它只是意味着您应该停止响应工作,如果可能的话。用户可能已取消它。

于 2011-11-27T19:29:08.420 回答
2

我希望我正确理解您的问题:

1)加载远程网页......和

2)用应用程序/构建中的文件替换某些远程资产

正确的?


好吧,我正在做的事情如下(由于 Mobile Safari 上的缓存限制为 5MB,我将它用于视频,但我认为任何其他 DOM 内容都应该同样有效):


• 创建一个带有样式标签的本地(使用Xcode 编译)HTML 页面,用于替换应用内/构建内容,设置为隐藏,例如:

<div style="display: none;">
<div id="video">
    <video width="614" controls webkit-playsinline>
            <source src="myvideo.mp4">
    </video>
</div>
</div> 


• 在同一个文件中提供一个内容 div,例如

<div id="content"></div>


•(在这里使用jQuery)从远程服务器加载实际内容并将您的本地(Xcode 导入资产)附加到您的目标div,例如

<script src="jquery.js"></script>
<script>
    $(document).ready(function(){
        $("#content").load("http://www.yourserver.com/index-test.html", function(){
               $("#video").appendTo($(this).find("#destination"));           
        });

    });
</script>


• 将 www 文件(index.html / jquery.js / etc ...使用根级别进行测试)放入项目并连接到目标


• 远程 HTML 文件(位于 yourserver.com/index-test.html)具有

<base href="http://www.yourserver.com/">


• 以及目标 div,例如

<div id="destination"></div>


• 最后在您的 Xcode 项目中,将本地 HTML 加载到 Web 视图中

self.myWebView = [[UIWebView alloc]init];

NSURL *baseURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
NSString *path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[self.myWebView loadHTMLString:content baseURL:baseURL];

对我来说是一种享受,最好与https://github.com/rnapier/RNCachingURLProtocol结合使用,用于离线缓存。希望这可以帮助。F

于 2014-06-05T08:36:24.813 回答
1

诀窍是为现有 HTML 提供显式的基本 URL。

将 HTML 加载到 NSString 中,使用 UIWebViewloadHTMLString: baseURL:和 URL 作为基础。要将 HTML 加载到字符串中,您可以使用 [NSString stringWithContentsOfURL],但这是一种同步方法,在连接速度较慢时会冻结设备。使用异步请求加载 HTML 也是可能的,但涉及更多。继续阅读NSURLConnection

于 2011-04-06T20:36:06.030 回答
0

NSURLProtocolUIWebView一个不错的选择,但是直到现在WKWebView仍然不支持它。对于WKWebView,我们可以构建一个本地 HTTP 服务器来处理本地文件请求,GCDWebServer很好:

self.webServer = [[GCDWebServer alloc] init];

[self.webServer addDefaultHandlerForMethod:@"GET"
                              requestClass:[GCDWebServerRequest class]
                              processBlock:
 ^GCDWebServerResponse *(GCDWebServerRequest *request)
{
    NSString *fp = request.URL.path;

    if([[NSFileManager defaultManager] fileExistsAtPath:fp]){
        NSData *dt = [NSData dataWithContentsOfFile:fp];

        NSString *ct = nil;
        NSString *ext = request.URL.pathExtension;

        BOOL (^IsExtInSide)(NSArray<NSString *> *) = ^(NSArray<NSString *> *pool){
            NSUInteger index = [pool indexOfObjectWithOptions:NSEnumerationConcurrent
                                                  passingTest:^BOOL(NSString *obj, NSUInteger idx, BOOL *stop) {
                                                      return [ext caseInsensitiveCompare:obj] == NSOrderedSame;
                                                  }];
            BOOL b = (index != NSNotFound);
            return b;
        };

        if(IsExtInSide(@[@"jpg", @"jpeg"])){
            ct = @"image/jpeg";
        }else if(IsExtInSide(@[@"png"])){
            ct = @"image/png";
        }
        //else if(...) // other exts

        return [GCDWebServerDataResponse responseWithData:dt contentType:ct];

    }else{
        return [GCDWebServerResponse responseWithStatusCode:404];
    }

}];

[self.webServer startWithPort:LocalFileServerPort bonjourName:nil];

指定本地文件的文件路径时,添加本地服务器前缀:

NSString *fp = [[NSBundle mainBundle] pathForResource:@"picture" ofType:@"jpg" inDirectory:@"www"];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://127.0.0.1:%d%@", LocalFileServerPort, fp]];
NSString *str = url.absoluteString;
[self.webViewController executeJavascript:[NSString stringWithFormat:@"updateLocalImage('%@')", str]];
于 2016-03-22T04:22:11.790 回答