iOs 는 WKWebView 가 건 너 간 구덩이 로 옮 겼 다.

15423 단어 ioswkwebview구덩이.
머리말
iOS 에 서 는 시스템 의 컨트롤 러 를 제외 하고 웹 페이지 를 불 러 올 수 있 는 두 가지 웹 페이지 보기 가 있 습 니 다.하 나 는 UIWebView 이 고,다른 하 나 는 WKWebView 입 니 다.사실 WKWebView 는 UIWebView 를 대체 하려 는 것 입 니 다.UIWebView 가 메모리 에서 매우 차지 하 는 문제 등 을 알 고 있 기 때 문 입 니 다.그런데 아직도 많은 사람들 이 UIWebView 를 사용 하고 있 습 니 다.왜 일 까요?그리고 공식 적 으로 iOS 12 에서 UIWebView 를 폐기 했다 고 발 표 했 습 니 다.사실은 이런 것들 입 니 다.**페이지 사이즈 문제,JS 상호작용,차단 요청,쿠키 가 가지 고 있 지 않 은 문제 입 니 다.**그래서 이 사 를 하려 면 이런 문제 들 을 해결 해 야 하기 때문에 귀 찮 기 때문에 일일이 해결 합 니 다.
질문
우 리 는 일부 웹 페이지 가 UIWebView 에 잘 표시 되 어 있 는 것 을 알 고 있 습 니 다.WKWebView 를 사용 하면 사이즈 에 문제 가 생 길 수 있다 는 것 을 알 고 있 습 니 다.이 때 는 궁금 합 니 다.안 드 로 이 드 도 할 줄 모 릅 니 다.전단 의 문제 라 고 하지 않 았 습 니까?그러나 사실은 WK WebView 의 홈 페이지 는 적당 한 조합 이 필요 하기 때문에 스스로 JS 를 추가 하 세 요.물론 전단 과 친 하면 추가 하 라 고 할 수 있 습 니 다.다음은 설정 에 있 는 userContentController 를 통 해 JS 를 추가 합 니 다.

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];
configuration.userContentController = wkUController;
JS 상호작용
우 리 는 모두 UIWebView 에서 자신의 JavaScriptCore 를 사용 하여 상호작용 을 하 는 것 이 매우 편리 하 다 는 것 을 안다.자 바스 크 립 트 코어 에 서 는 JScontext(상하 문),JSValue(유형 변환),JSexport(js 조정 OC 모델 방법)가 비교적 많이 사용 된다.
UIWebView 에서 의 편리 한 상호작용 방법

//JSContext           H5   
- (void)webViewDidFinishLoad:(UIWebView *)webView{
 JSContext *jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
 self.jsContext = jsContext;
}

//       js    
[self.jsContext evaluateScript:@"var arr = [3, '3', 'abc'];"];

// ⚠️  JS  ,                 JS  ,                      。
//     
self.jsContext[@"alertMessage"] = ^() {
 NSLog(@"JS   alertMessage        !");
};
//     ,       
 self.jsContext[@"showDict"] = ^(JSValue *value) {
 NSArray *args = [JSContext currentArguments];
 JSValue *dictValue = args[0];
 NSDictionary *dict = dictValue.toDictionary;
 NSLog(@"%@",dict);
 };

//   JS  arr  
JSValue *arrValue = self.jsContext[@"arr"];

//     
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
 weakSelf.jsContext.exception = exception;
 NSLog(@"exception == %@",exception);
};

//  JS        
OMJSObject *omObject = [[OMJSObject alloc] init];
self.jsContext[@"omObject"] = omObject;
NSLog(@"omObject == %d",[omObject getSum:20 num2:40]);

//      object     JSExport   ,js      object    ,            。 JS     getS,OC        getSum    
@protocol OMProtocol <JSExport>
//    -      
JSExportAs(getS, -(int)getSum:(int)num1 num2:(int)num2);
@end
WK WebView 에서 어떻게 할 까요?
위 와 같이 시스템 이 제공 하 는 것 은 다음 과 같은 두 가지 방법 을 통 해 비교적 힘 들 고 전단 에서 message Handler 를 사용 하여 호출 해 야 한다.즉,안 드 로 이 드 와 iOS 를 분리 해서 처리 해 야 한다.

//     js
NSString *jsStr = @"var arr = [3, '3', 'abc']; ";
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
 NSLog(@"%@----%@",result, error);
}];

//         ,js  messageHandlers               

// OC     js   
- (void)viewDidLoad{
 //...
 [wkUController addScriptMessageHandler:self name:@"showtime"];
 configuration.userContentController = wkUController;
}

// JS messageHandlers     OC               OC   
window.webkit.messageHandlers.showtime.postMessage('');

//   ,    
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
 if ([message.name isEqualToString:@"showtime"]) {
  NSLog(@"  !");
 }
 NSLog(@"message == %@ --- %@",message.name,message.body); 
}

//    dealloc    
[self.userContentController removeScriptMessageHandlerForName:@"showtime"];

//                
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"  " message:message preferredStyle:UIAlertControllerStyleAlert];
 [alert addAction:[UIAlertAction actionWithTitle:@"   " style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
  completionHandler();
 }]];
 
 [self presentViewController:alert animated:YES completion:nil];
}
항상 시원 한 대화 로.
우 리 는 위 에 두 가지 상호작용 을 썼 다.비록 사용 할 수 있 지만 매우 간단 하고 가 벼 운 경 지 를 가지 고 있 지 않 기 때문에 하나의 소스 라 이브 러 리 가 있다.WebViewJavaScriptBridge이 소스 라 이브 러 리 는 두 가 지 를 동시에 호 환 할 수 있 고 상호작용 이 매우 간단 하지만,너 는 반드시 전단 을 함께 해 야 한다.그렇지 않 으 면 끊 어 질 것 이다.

//   
self.wjb = [WebViewJavascriptBridge bridgeForWebView:self.webView];
//      VC    UIWebView              (    )
[self.wjb setWebViewDelegate:self];

//   js    
[self.wjb registerHandler:@"jsCallsOC" handler:^(id data, WVJBResponseCallback responseCallback) {
 NSLog(@"currentThread == %@",[NSThread currentThread]);
 NSLog(@"data == %@ -- %@",data,responseCallback);
}];

//   JS
dispatch_async(dispatch_get_global_queue(0, 0), ^{
  [self.wjb callHandler:@"OCCallJSFunction" data:@"OC  JS" responseCallback:^(id responseData) {
   NSLog(@"currentThread == %@",[NSThread currentThread]);
   NSLog(@"   JS    :%@",responseData);
  }];
});
전단 사용 사례 는 다음 과 같 습 니 다.구체 적 인 사용 방법 은 볼 수 있 습 니 다WebViewJavaScriptBridge.

function setupWebViewJavascriptBridge(callback) {
	if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
	if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
	window.WVJBCallbacks = [callback];
	var WVJBIframe = document.createElement('iframe');
	WVJBIframe.style.display = 'none';
	WVJBIframe.src = 'https://__bridge_loaded__';
	document.documentElement.appendChild(WVJBIframe);
	setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

setupWebViewJavascriptBridge(function(bridge) {
	
	/* Initialize your app here */

	bridge.registerHandler('JS Echo', function(data, responseCallback) {
		console.log("JS Echo called with:", data)
		responseCallback(data)
	})
	bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
		console.log("JS received response:", responseData)
	})
})
차단 을 요청 하 다
Google UIWebView 는 초기 에 scheme,host,pathComponents 에 따라 차단 하여 사용자 정의 논리 처 리 를 했 습 니 다.그러나 이런 방법 은 융통성 이 없어 서 NSURLProtocol 을 사용 하여 차단 합 니 다.예 를 들 어 위 챗 이 타 오 바 오 를 차단 하 는 것 처럼 힌트 를 직접 표시 합 니 다.또는 로 컬 인 터 페 이 스 를 호출 하고 카메라,녹음,앨범 등 기능 을 켜 달라 고 요청 합 니 다.또한 기 존의 request 를 직접 차단 한 후 데이터 나 다른 url 로 돌아 가 광 고 를 제거 할 때 사용 할 수 있 습 니 다.
저희 가 사용 할 때 는 NSURLProtocol 의 하위 클래스 를 사용 해 야 합 니 다.사용 하기 전에 사용자 정의 Class 를 등록 해 야 합 니 다.차단 한 후에 표 시 를 해서 순환 이 많이 실행 되 지 않도록 하 세 요.안 타 깝 게 도 WK 웹 뷰 에 서 는 차단 후 처리 하 는 조작 을 할 수 없고 감청 만 할 뿐 바 꿀 수 없다.WKWebView 에서 유래 한 것 은 웹 키 트 로 딩 으로 시스템 의 브 라 우 저 와 같은 메커니즘 을 사용 합 니 다.

//   
@interface OMURLProtocol : NSURLProtocol<NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session;
@end

//   
[NSURLProtocol registerClass:[OMURLProtocol class]];

// 1.            ,  YES              ,NO      
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

// 2.             ,         request,          
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;

// 3.             ,               
- (void)startLoading;

// 4.                 ,                   ,       
//   request              , canInitWithRequest   
+ (nullable id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
//   
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
//     
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
질문
또 주의해 야 할 것 은 차단 처리 가 이 루어 지면 AFN 과 URLSession 을 사용 하여 접근 할 때 차단 하면 데이터 나 요청 헤드 가 차단 처리 후의 데이터 나 요청 이 예상 에 부합 되 지 않 을 수 있 습 니 다.이것 은 우리 가 차단 할 때 A 를 먼저 요청 한 후에 B 를 요 청 했 기 때 문 입 니 다.이것 은 예상 에 부합 되 지 않 기 때 문 입 니 다.URLConnection 은 할 수 없 지만 이미 폐기 되 어 사용 을 권장 할 가치 가 없습니다.저 희 는 차단 할 때 LLDB 를 통 해 session 에 설 정 된 프로 토 콜 을 인쇄 할 때 사용자 정의 프로 토 콜 이 포함 되 어 있 지 않 은 것 을 발 견 했 습 니 다.저 희 는 Runtime 교환 방법 을 통 해 protocolclasses 방법 을 교환 하고 저 희 는 자신의 protocolclasses 방법 을 실현 합 니 다.그러나 시스템 의 원래 속성 을 확보 하기 위해 서 우 리 는 시스템 의 원래 협의 표 에 우리 의 협의 류 를 추가 해 야 한다.현재 우 리 는[NSURLSession shared Session].configuration.protocolclasses 를 통 해시스템 의 기본 프로 토 콜 클래스 를 가 져 옵 니 다.그러나 현재 사용자 정의 클래스 에서 protocolclasses 를 쓰 면 순환 이 됩 니 다.이 속성의 getter 방법 을 교 환 했 기 때 문 입 니 다.저 희 는 클래스 이름 을 저장 하고 NSUserDefaults 에 저장 합 니 다.값 을 가 져 올 때 class 를 복원 합 니 다.

po session.configuration.protocolClasses
<__NSArrayI 0x600001442d00>(
_NSURLHTTPProtocol,
_NSURLDataProtocol,
_NSURLFTPProtocol,
_NSURLFileProtocol,
NSAboutURLProtocol
)

//            
- (NSArray *)protocolClasses {
 NSArray *originalProtocols = [OMURLProtocol readOriginalProtocols];
 NSMutableArray *newProtocols = [NSMutableArray arrayWithArray:originalProtocols];
 [newProtocols addObject:[OMURLProtocol class]];
 return newProtocols;
}

//                        
po session.configuration.protocolClasses
<__NSArrayM 0x60000041a4f0>(
_NSURLHTTPProtocol,
_NSURLDataProtocol,
_NSURLFTPProtocol,
_NSURLFileProtocol,
NSAboutURLProtocol,
OMURLProtocol
)

//           
+ (void)saveOriginalProtocols: (NSArray<Class> *)protocols{
 NSMutableArray *protocolNameArray = [NSMutableArray array];
 for (Class protocol in protocols){
  [protocolNameArray addObject:NSStringFromClass(protocol)];
 }
 NSLog(@"     : %@", protocolNameArray);
 [[NSUserDefaults standardUserDefaults] setObject:protocolNameArray forKey:originalProtocolsKey];
 [[NSUserDefaults standardUserDefaults] synchronize];
}

//           
+ (NSArray<Class> *)readOriginalProtocols{
 NSArray *classNames = [[NSUserDefaults standardUserDefaults] valueForKey:originalProtocolsKey];
 NSMutableArray *origianlProtocols = [NSMutableArray array];
 for (NSString *name in classNames){
  Class class = NSClassFromString(name);
  [origianlProtocols addObject: class];
 }
 return origianlProtocols;
}

+ (void)hookNSURLSessionConfiguration{
 NSArray *originalProtocols = [NSURLSession sharedSession].configuration.protocolClasses;
 [self saveOriginalProtocols:originalProtocols];
 Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
 Method originalMethod = class_getInstanceMethod(cls, @selector(protocolClasses));
 Method stubMethod = class_getInstanceMethod([self class], @selector(protocolClasses));
 if (!originalMethod || !stubMethod) {
  [NSException raise:NSInternalInconsistencyException format:@"           "];
 }
 method_exchangeImplementations(originalMethod, stubMethod);
}
질문
많은 응용 장면 에서 session 을 사용 하여 처리 해 야 합 니 다.UIWebView 에서 이러한 쿠키 를 휴대 하기 쉽 지만 WKWebView 의 메커니즘 이 다 르 기 때문에 도 메 인 을 넘 으 면 쿠키 를 잃 어 버 리 는 상황 이 발생 할 수 있 습 니 다.현재 두 가지 용법 이 있 습 니 다.스 크 립 트 와 수 동 으로 쿠키 를 추가 합 니 다.스 크 립 트 가 믿 을 수 없 으 니 수 동 으로 추가 하 는 것 이 안전 합 니 다.

//        cookie

//    cookie  
- (NSString *)cookieString
{
 NSMutableString *script = [NSMutableString string];
 [script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );
"]; for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) { if ([cookie.value rangeOfString:@"'"].location != NSNotFound) { continue; } [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };
", cookie.name, cookie.kc_formatCookieString]; } return script; } // cookie WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; [[[WKUserContentController alloc] init] addUserScript: cookieScript];

//          cookie     
@interface NSURLRequest (Cookie)

- (NSURLRequest *)fixCookie;

@end

@implementation NSURLRequest (Cookie)

- (NSURLRequest *)fixCookie{
 NSMutableURLRequest *fixedRequest;
 if ([self isKindOfClass:[NSMutableURLRequest class]]) {
  fixedRequest = (NSMutableURLRequest *)self;
 } else {
  fixedRequest = self.mutableCopy;
 }
 //  Cookie  
 NSDictionary *dict = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
 if (dict.count) {
  NSMutableDictionary *mDict = self.allHTTPHeaderFields.mutableCopy;
  [mDict setValuesForKeysWithDictionary:dict];
  fixedRequest.allHTTPHeaderFields = mDict;
 }
 return fixedRequest;
}

@end
 
 
 //     
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
 [navigationAction.request fixCookie];
 decisionHandler(WKNavigationActionPolicyAllow);
}
총결산
이상 은 이 글 의 모든 내용 입 니 다.본 고의 내용 이 여러분 의 학습 이나 업무 에 어느 정도 참고 학습 가 치 를 가지 기 를 바 랍 니 다.여러분 의 저희 에 대한 지지 에 감 사 드 립 니 다.

좋은 웹페이지 즐겨찾기