采用[NSURLProtocol registerClass:[HybridNSURLProtocol class]];注册协议,自定义scheme,用canInitWithRequest函数拦截指定图片资源下载地址,建立PHAsset,通过requestImageDataForAsset获取本地图片对象,新建NSURLResponse并返回图片数据流给js(在代理函数startLoading中实现)。
需求场景:js通过设置函数标签,加载客户端本地图片资源,达到图片预览的目的。
例如:我们的图片分两步上传,第二步就用到该应用场景达到上传时的图片预览功能。我们应用的图片上传是分两步实现了。第一步:是服务器的js通过客户端注册函数下发图片选择请求,用户选择图片后通过回调机制发送选在的本地图片地址拼装消息数组数据给服务器,注意这一步只是向服务器发送加工过的图片本地地址到服务器,并没有上传图片二进制流。
第二步:1.JS页面通过客户端注册函数下发单个加工过的本地图片地址及上传消息类型,客户端根据这个自定义URL schemes的本地图片地址获取本地图片数据并上传数据到阿里云,上传成功js继续下发相同的消息,客户端按照这个逻辑继续上传图片到服务器。
2.JS页面下上传图片消息的同时通过标签函数调用下载图片数据,客户端通过注册自定义URL schemes拦截该自定义的URL schemes图片下载请求并且返回图片数据,达到在图片上传图片过程中就在js页面显示图片的目的。注意:图片的URL schemes不能使用系统自带的https,不然会出现,post请求body参数被清空,js页面无法通过post请求传递参数的问题。具体参照:《【腾讯Bugly干货分享】WKWebView 那些坑》。
由于我是做app开发的,无法在服务器上放置代码让你测试,所以可以采用HybridNSURLProtocol协议拦截图片等资源可以去github上这个地址下载这个demo。不过这个demo有一个严重问题,由于它采用是 [NSURLProtocol wk_registerScheme:@“http”];
[NSURLProtocol wk_registerScheme:@“https”];拦截,导致post请求body参数被清空,js页面无法通过post请求传递参数的问题,上面有介绍,我们在开发过程中也遇到过。所以你的页面需要通过post的方式上传参数时,一定不能按照demo那么用,要自定义URL schemes。
下面来些干货:
第一步:下载哪个demo。
第二步:其中HybridNSURLProtocol.h(.m)和NSURLProtocol+WKWebVIew.h(.m)文件加入你的工程中。
第三步:在AppDelegate.m文件导入头文件#import “HybridNSURLProtocol.h”,加入下面的代码:
-
(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[HybridNSURLProtocol class]];return YES;
}
第四步:在你需要拦截js标签函数WKWebView页面导入头文件#import “NSURLProtocol+WKWebVIew.h”,加入如下代码: -
(void)viewDidLoad {
[super viewDidLoad];
[self registerNSURLProtocolScheme];
} -
(void)registerNSURLProtocolScheme
{
[NSURLProtocol wk_registerScheme:@“yxLocalFile”];
[NSURLProtocol wk_registerScheme:@“yxlocalfile”];
// [NSURLProtocol wk_registerScheme:@“https”];
}
第五步:HybridNSURLProtocol.m加入图片等资源拦截与取本地图片资源组装响应消息给js。我们的应用只对图片地址进行拦截并返回本地数据给js。cs和js等资源拦截可以参照这个写。代码如下:
#import “HybridNSURLProtocol.h”
#import “TZImagePickerController.h”
#import <UIKit/UIKit.h>
static NSStringconst sourUrl = @“https://m.baidu.com/static/index/plus/plus_logo.png”;
static NSStringconst sourIconUrl = @“http://m.baidu.com/static/search/baiduapp_icon.png”;
static NSString*const localUrl = @“http://mecrm.qa.medlinker.net/public/image?id=57026794&certType=workCertPicUrl&time=1484625241”;
static NSString* const KHybridNSURLProtocolHKey = @“KHybridNSURLProtocol”;
@interface HybridNSURLProtocol ()
@property (nonnull,strong) NSURLSessionDataTask *task;
@end
@implementation HybridNSURLProtocol
-
(BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *scheme = [[request URL] scheme];
NSString *urlStr = request.URL.absoluteString;NSLog(@“canInitWithRequest request.URL.absoluteString = %@,请求方式 == %@,scheme:%@,request.allHTTPHeaderFields:%@”,urlStr,request.HTTPMethod,scheme, request.allHTTPHeaderFields);
if ( ([scheme caseInsensitiveCompare:@“yxLocalFile”] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@“yxlocalfile”] == NSOrderedSame ))
{
//看看是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:KHybridNSURLProtocolHKey inRequest:request])
return NO;
if ([urlStr containsString:@“yxLocalFile://”] || [urlStr containsString:@“yxlocalfile://”]) {
return YES;
}
return NO;
}
return NO;
} -
(NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
} -
(BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
-
(void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//给我们处理过的请求设置一个标识符, 防止无限循环,
[NSURLProtocol setProperty:@YES forKey:KHybridNSURLProtocolHKey inRequest:mutableReqeust];NSString *urlStr = mutableReqeust.URL.absoluteString;
NSLog(@“mutableReqeust.URL.absoluteString = %@”,urlStr); NSLog(@“请求方式 == %@”,mutableReqeust.HTTPMethod);
NSString *fileName = [self getYxLocalFile:urlStr];
if(!isEmptyString(fileName))
{
NSString *MIMETypeStr = @"";
NSString *fileExtend = [self getFileExtend:urlStr];
if(fileExtend && ([fileExtend compare:@“png” options:NSCaseInsensitiveSearch |NSNumericSearch] ==NSOrderedSame))
{
MIMETypeStr =@“image/png”;
}
else if(fileExtend && (([fileExtend compare:@“bmp” options:NSCaseInsensitiveSearch |NSNumericSearch] ==NSOrderedSame)
|| ([fileExtend compare:@“gif” options:NSCaseInsensitiveSearch |NSNumericSearch] ==NSOrderedSame)))
{
//bmp\dib
MIMETypeStr =@“image/bmp”;
}
else if(fileExtend && ([fileExtend compare:@“gif” options:NSCaseInsensitiveSearch |NSNumericSearch] ==NSOrderedSame))
{
MIMETypeStr =@“image/gif”;
}
else
{
//苹果拍照的图片都是JPG格式的图片,jpe\jpeg\jpg
MIMETypeStr =@“image/jpeg”;
}
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[mutableReqeust URL]
MIMEType:MIMETypeStr
expectedContentLength:-1
textEncodingName:nil];NSMutableArray *tempArr = [NSMutableArray array]; [tempArr addSafeObject:fileName]; PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:tempArr options:nil]; @weakify(self); [self fetchImageWithAsset:fetchResult.firstObject imageBlock:^(NSData *imageData) { @strongify(self); NSLog(@"imageData:%@", imageData); [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; [[self client] URLProtocol:self didLoadData:imageData]; [[self client] URLProtocolDidFinishLoading:self]; }]; return;
}
}
-(NSString *)getYxLocalFile:(NSString )localId
{
NSString urlStr = localId;
NSLog(@“localId = %@”,localId);
if(!([urlStr containsString:@“yxLocalFile://”] || [urlStr containsString:@“yxlocalfile://”])) {
return nil;
}
NSArray arr = [urlStr componentsSeparatedByString:@“yxLocalFile://”];
NSArray arr2 = [urlStr componentsSeparatedByString:@“yxlocalfile://”];
if((arr.count <= 1) && (arr2.count <= 1))
{
return nil;
}
else
{
if(arr.count == 1)
{
arr = arr2;
}
NSString yxLocalFile = arr[arr.count - 1];
NSArray jpgArr = [yxLocalFile componentsSeparatedByString:@"."];
if(jpgArr.count == 0)
{
return nil;
}
else
{
NSString *fileName = jpgArr[0];
return fileName;
}
}
}
-(NSString *)getFileExtend:(NSString )localId
{
NSString urlStr = localId;
NSLog(@“localId = %@”,localId);
if(!([urlStr containsString:@“yxLocalFile://”] || [urlStr containsString:@“yxlocalfile://”])) {
return nil;
}
NSArray arr = [urlStr componentsSeparatedByString:@“yxLocalFile://”];
NSArray arr2 = [urlStr componentsSeparatedByString:@“yxlocalfile://”];
if((arr.count <= 1) && (arr2.count <= 1))
{
return nil;
}
else
{
if(arr.count == 1)
{
arr = arr2;
}
NSString yxLocalFile = arr[arr.count - 1];
NSArray jpgArr = [yxLocalFile componentsSeparatedByString:@"."];
if(jpgArr.count <= 1)
{
return nil;
}
else
{
NSString *fileExtend = jpgArr[jpgArr.count-1];
return fileExtend;
}
}
}
/**
通过资源获取图片的数据
@param mAsset 资源文件
@param imageBlock 图片数据回传
*/
-
(void)fetchImageWithAsset:(PHAsset*)mAsset imageBlock:(void(^)(NSData* imageData))imageBlock {
@weakify(self);
NSLog(@“mAsset:%@, mAsset.className:%@”, mAsset, mAsset.className);
[[PHImageManager defaultManager] requestImageDataForAsset:mAsset options:nil resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
@strongify(self);
if (orientation != UIImageOrientationUp) {
UIImage* image = [UIImage imageWithData:imageData];
// 尽然弯了,那就板正一下
image = [self fixOrientation:image];
// 新的 数据信息 (不准确的)
imageData = UIImageJPEGRepresentation(image, 1.0);
// NSString *filePath = [self getChooseImageDirWithFileName:@“test”];
// [self storageImageWithFilePath:filePath image:image];
}// 直接得到最终的 NSData 数据 if (imageBlock) { imageBlock(imageData);
// UIImage* image = [UIImage imageWithData:imageData];
// NSString *filePath = [self getChooseImageDirWithFileName:@“test”];
// [self storageImageWithFilePath:filePath image:image];
}
}];
}
-
(UIImage *)fixOrientation:(UIImage *)aImage {
// No-op if the orientation is already correct
if (aImage.imageOrientation == UIImageOrientationUp)
return aImage;// We need to calculate the proper transformation to make the image upright.
// We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
CGAffineTransform transform = CGAffineTransformIdentity;switch (aImage.imageOrientation) {
case UIImageOrientationDown:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.width, aImage.size.height);
transform = CGAffineTransformRotate(transform, M_PI);
break;case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: transform = CGAffineTransformTranslate(transform, aImage.size.width, 0); transform = CGAffineTransformRotate(transform, M_PI_2); break; case UIImageOrientationRight: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, 0, aImage.size.height); transform = CGAffineTransformRotate(transform, -M_PI_2); break; default: break;
}
switch (aImage.imageOrientation) {
case UIImageOrientationUpMirrored:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;case UIImageOrientationLeftMirrored: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, aImage.size.height, 0); transform = CGAffineTransformScale(transform, -1, 1); break; default: break;
}
// Now we draw the underlying CGImage into a new context, applying the transform
// calculated above.
CGContextRef ctx = CGBitmapContextCreate(NULL, aImage.size.width, aImage.size.height,
CGImageGetBitsPerComponent(aImage.CGImage), 0,
CGImageGetColorSpace(aImage.CGImage),
CGImageGetBitmapInfo(aImage.CGImage));
CGContextConcatCTM(ctx, transform);
switch (aImage.imageOrientation) {
case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
// Grr…
CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.height,aImage.size.width), aImage.CGImage);
break;default: CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.width,aImage.size.height), aImage.CGImage); break;
}
// And now we just create a new UIImage from the drawing context
CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
UIImage *img = [UIImage imageWithCGImage:cgimg];
CGContextRelease(ctx);
CGImageRelease(cgimg);
return img;
} -
(void)stopLoading
{
if (self.task != nil)
{
[self.task cancel];
}
} -
(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];completionHandler(NSURLSessionResponseAllow);
} -
(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[[self client] URLProtocol:self didLoadData:data];
} -
(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error {
[self.client URLProtocolDidFinishLoading:self];
}
@end
注意:不能在canInitWithRequest函数打印html这个源代码日志(NSLog(@“canInitWithRequest request.URL.absoluteString html = %@”,[NSString stringWithContentsOfURL:request.URL encoding: NSUTF8StringEncoding error:nil]);),不然应用在拦截https协议测试时发现整个应用卡死。
- (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *scheme = [[request URL] scheme];
NSString *urlStr = request.URL.absoluteString;
// NSLog(@“canInitWithRequest request.URL.absoluteString = %@,请求方式 == %@,scheme:%@,request.allHTTPHeaderFields:%@”,urlStr,request.HTTPMethod,scheme, request.allHTTPHeaderFields);
NSLog(@“canInitWithRequest request.URL.absoluteString html = %@”,[NSString stringWithContentsOfURL:request.URL encoding: NSUTF8StringEncoding error:nil]);
if ( ([scheme caseInsensitiveCompare:@“yxLocalFile”] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@“yxlocalfile”] == NSOrderedSame ))
{
//看看是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:KHybridNSURLProtocolHKey inRequest:request])
return NO;
if ([urlStr containsString:@“yxLocalFile://”] || [urlStr containsString:@“yxlocalfile://”]) {
return YES;
}
return NO;
}
return NO;
}
这个是拦截的图片资源地址:mutableReqeust.URL.absoluteString = yxlocalfile://9B445E4B-6559-4352-9186-69A82A3E5332/L0/001.PNG和mutableReqeust.URL.absoluteString = yxlocalfile://3F0D46B1-A473-4020-9D3A-D55751D44163/L0/001.GIF。
其中yxlocalfile://是自定义URL schemes,9B445E4B-6559-4352-9186-69A82A3E5332/L0/001和3F0D46B1-A473-4020-9D3A-D55751D44163/L0/001是应用本地图片唯一标识localIdentifier,后缀PNG和GIF是获取的本地图片格式,要解析出来根据他们设置相应的MIMEType字段。
至于这个字符串如何获取呢,请看下文:
TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] initWithMaxImagesCount:self.homeParamsEntity.chooseImageEntity.count delegate:self];
// You can get the photos by block, the same as by delegate.
// 你可以通过block或者代理,来得到用户选择的照片.
[imagePickerVc setDidFinishPickingPhotosHandle:^(NSArray<UIImage *> *photos, NSArray *assets, BOOL isSelectOriginalPhoto) {
@strongify(self);
[self.wkWebWeipaiJSBridgeModel chooseImageResponseWithPhotos:photos assets:assets];
}];
[self presentViewController:imagePickerVc animated:YES completion:nil];
通过TZImagePickerController组件获取到选择图片的PHAsset对象数组。下面是如何通过PHAsset对象数组拼装消息给js。
-(void)chooseImageResponseWithPhotos:(NSArray<UIImage *> *)photos
assets:(NSArray *)assets
{
if((photos.count == 0) || (assets.count == 0))
{
return;
}
NSMutableArray *localIds = [NSMutableArray array];
for(NSUInteger i = 0; (i < assets.count) && (i < photos.count); i++)
{
PHAsset *phAsset = assets[i];
NSString *localIdentifier = phAsset.localIdentifier;
NSString*fileName=[phAsset valueForKey:@"filename"];
NSLog(@"File name %@",fileName);
NSArray* arr = [fileName componentsSeparatedByString:@"."];
NSString *fileExtend = @"";
if(arr.count != 0)
{
fileExtend = [NSString stringWithFormat:@".%@", arr[arr.count - 1]];
}
NSString *yxLocalFile = [NSString stringWithFormat:@"yxLocalFile://%@%@", localIdentifier,fileExtend];
[localIds addSafeObject:yxLocalFile];
}
NSLog(@"localIds :%@", localIds);
NSString *localIdsString = [localIds getJsonString];
NSLog(@"localIdsString :%@", localIdsString);
NSString *callBackStr = @"";
if(isEmptyString(localIdsString))
{
callBackStr = @"wxCallBack.chooseImage.fail();";
}
else
{
callBackStr = [NSString stringWithFormat:@"wxCallBack.chooseImage.success({\"localIds\":%@});", localIdsString];
}
NSLog(@"callBackStr :%@", callBackStr);
[self callBackWithCallBackStr:callBackStr];
}
-(void)callBackWithCallBackStr:(NSString *)callBackStr
{
FLDDLogVerbose(@"callBackStr :%@", callBackStr);
if(isEmptyString(callBackStr))
{
return;
}
yx_dispatch_main_async_safe(^{
//oc调用js的shareResult方法并且传递参数
[self.homeParamsEntity.wkWebView evaluateJavaScript:callBackStr completionHandler:^(id _Nullable data, NSError * _Nullable error) {
}];
});
}
下面是NSArray扩展类的函数:
-(NSString *)getJsonString
{
NSError *error;
NSData *infoData = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&error];
NSString *infoString = @"";
if (infoData) {
infoString = [[NSString alloc] initWithData:infoData encoding:NSUTF8StringEncoding];
infoString = [infoString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
infoString = [infoString stringByReplacingOccurrencesOfString:@"\n" withString:@""];
infoString = [infoString stringByReplacingOccurrencesOfString:@" " withString:@""];
}
return infoString;
}