阿里云首页 阿里云公共DNS 相关技术圈

iOS端WebView场景下接入阿里云公共DNS SDK最佳实践方案

本文主要介绍在WebView场景下接入阿里云公共DNS SDK所遇到的一些问题及解决方案。

概述

在App WebView加载网络请求场景下,iOS系统可基于系统API进行网络请求拦截,并实现自定义逻辑注入。但iOS系统在WebView场景下拦截网络请求后,需要自行接管基于IP的网络请求的发送、数据接收、页面重定向、页面解码、Cookie、缓存等逻辑。综合来看,WebView场景下使用SDK解析的IP直连进行网络请求门槛比较高,移动操作系统针对这个场景的支持粒度很粗,且存在一些缺陷,需要开发者具备较强的网络/OS Framework的代码级掌控能力来规避和优化上述问题。

注意

目前UIWebView已经被苹果弃用,而WKWebView没有成熟的IP直连方案,只能使用私有API进行注册拦截的方法,使用私有API有被苹果拒审的风险,请开发者谨慎使用。

UIWebView使用NSURLProtocol拦截请求

方案概述

  • 基于NSURLProtocol可拦截iOS系统上基于上层网络库NSURLConnection/NSURLSession发出的网络请求,UIWebView发出的请求同样包含在内;

  • 通过以下接口注册自定义NSURLProtocol,用于拦截UIWebView上层网络请求,并创建新的网络请求接管数据发送、接收、重定向等处理逻辑,将结果反馈给原始请求。

    [NSURLProtocol registerClass:[CustomProtocol class]];
  • 自定义NSURLProtocol处理过程概述:

    • canInitWithRequest中拦截需要做域名解析的请求;

    • 请求拦截后,做公共DNS域名解析;

    • 解析完成后,同普通请求一样,替换URL.host字段,替换HTTP Header Host域,并接管该请求的数据发送、接收、重定向等处理;

    • 通过NSURLProtocol的接口,将请求处理结果反馈到UIWebView原始请求。

WKWebView使用私有API进行注册拦截请求

方案概述

WKWebView在独立于app进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在WKWebView上直接使用NSURLProtocol无法拦截请求。

目前在WKWebView场景下,没有成熟的IP直连方案,下面介绍下使用私有API进行注册拦截的方法。

  // 注册自己的 protocol
  [NSURLProtocol registerClass:[CustomProtocol class]];
  // 创建 WKWebview
  WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
  WKWebView * wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0,   [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) configuration:config];
  [wkWebView loadRequest:webViewReq];
  [self.view addSubview:wkWebView];
  //注册 scheme
  Class cls = NSClassFromString(@"WKBrowsingContextController");
  SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
  if ([cls respondsToSelector:sel]) {
      // 通过 http 和 https 的请求,同理可通过其他的 Scheme 但是要满足 ULR Loading System
      [cls performSelector:sel withObject:@"http"];
      [cls performSelector:sel withObject:@"https"];
  }

NSURLProtocol使用参考Apple NSURLProtocol API,苹果官方示例代码参考Apple Sample Code - CustomHTTPProtocol

【注意】避免执行太晚,建议在+load方法中执行 。如果在- (void)viewDidLoad中注册,可能会因为注册太晚,引发问题。

WKWebView中的Cookie处理业务场景“IP直连”方案说明

以下讨论类似这样的问题:

  • WKWebView对于Cookie的管理一直是它的短板,那么iOS 11是否有改进,如果有,如何利用这样的改进 ?

  • 采用IP直连方案后,服务端返回的Cookie里的Domain字段也会使用IP。如果IP是动态的,就有可能导致一些问题:由于许多H5业务都依赖于Cookie作登录态校验 ,而WKWebView上请求不会自动携带Cookie。

    WKWebView使用NSURLProtocol拦截请求无法获取 Cookie 信息

    iOS 11推出了新的APIWKHTTPCookieStore可以用来拦截WKWebView的Cookie信息。

    用法示例如下:

    WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
       // get cookies
        [cookieStroe getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
            NSLog(@"All cookies %@",cookies);
        }];
    
        // set cookie
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        dict[NSHTTPCookieName] = @"userid";
        dict[NSHTTPCookieValue] = @"123";
        dict[NSHTTPCookieDomain] = @"xxxx.com";
        dict[NSHTTPCookiePath] = @"/";
    
        NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict];
        [cookieStroe setCookie:cookie completionHandler:^{
            NSLog(@"set cookie");
        }];
    
        // delete cookie
        [cookieStroe deleteCookie:cookie completionHandler:^{
            NSLog(@"delete cookie");
        }];

    利用iOS 11 API WKHTTPCookieStore解决WKWebView首次请求不携带Cookie的问题

  • 问题说明:由于许多H5业务都依赖于Cookie作登录态校验,而WKWebView上请求不会自动携带Cookie。比如,如果你在Native层面做了登录操作,获取了Cookie信息,也使用NSHTTPCookieStorage存到了本地,但是使用WKWebView打开对应网页时,网页依然处于未登录状态。如果是登录也在 WebView 里做的,就不会有这个问题。

  • iOS11的 API 可以解决该问题,只要是存在 WKHTTPCookieStore 里的 cookie,WKWebView 每次请求都会携带,存在 NSHTTPCookieStorage 的 cookie,并不会每次都携带。于是会发生首次 WKWebView 请求不携带 Cookie 的问题。

  • 解决方法:

    在执行-[WKWebView loadReques:] 前将NSHTTPCookieStorage中的内容复制到WKHTTPCookieStore中,以此来达到 WKWebView Cookie 注入的目的。示例代码如下:

    [self copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:^{
                NSURL *url = [NSURL URLWithString:@"https://www.v2ex.com"];
                NSURLRequest *request = [NSURLRequest requestWithURL:url];
                [_webView loadRequest:request];
    }];
    - (void)copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:(nullable void (^)())theCompletionHandler; {
        NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
        WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
        if (cookies.count == 0) {
            !theCompletionHandler ?: theCompletionHandler();
            return;
        }
        for (NSHTTPCookie *cookie in cookies) {
            [cookieStroe setCookie:cookie completionHandler:^{
                if ([[cookies lastObject] isEqual:cookie]) {
                    !theCompletionHandler ?: theCompletionHandler();
                    return;
                }
            }];
        }
    }

    这个是 iOS 11的API,针对iOS 11之前的系统 ,需要另外处理。

    利用 iOS 11之前的API解决WKWebView首次请求不携带Cookie的问题

    通过让所有WKWebView共享同一个WKProcessPool实例,可以实现多个WKWebView之间共享Cookie(session Cookie and persistent Cookie)数据。不过WKWebView WKProcessPool实例在app杀进程重启后会被重置,导致WKProcessPool中的Cookie、session Cookie数据丢失,目前也无法实现 WKProcessPool实例本地化保存。可以采取Cookie放入Header的方法来做。

     WKWebView * webView = [WKWebView new]; 
     NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com/login"]]; 
     [request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; 
     [webView loadRequest:request];

    其中对于skey=skeyValue这个Cookie值的获取,也可以统一通过domain获取,获取的方法可以参照下面的工具类:

    ALIDNSCookieManager.h
    
    #ifndef ALIDNSCookieManager_h
    #define ALIDNSCookieManager_h
    
    // URL匹配Cookie规则
    typedef BOOL (^ALIDNSCookieFilter)(NSHTTPCookie *, NSURL *);
    
    @interface ALIDNSCookieManager : NSObject
    
    + (instancetype)sharedInstance;
    
    /**
     指定URL匹配Cookie策略
    
     @param filter 匹配器
     */
    - (void)setCookieFilter:(ALIDNSCookieFilter)filter;
    
    /**
     处理HTTP Reponse携带的Cookie并存储
    
     @param headerFields HTTP Header Fields
     @param URL 根据匹配策略获取查找URL关联的Cookie
     @return 返回添加到存储的Cookie
     */
    - (NSArray<NSHTTPCookie *> *)handleHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL;
    
    /**
     匹配本地Cookie存储,获取对应URL的request cookie字符串
    
     @param URL 根据匹配策略指定查找URL关联的Cookie
     @return 返回对应URL的request Cookie字符串
     */
    - (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL;
    
    /**
     删除存储cookie
    
     @param URL 根据匹配策略查找URL关联的cookie
     @return 返回成功删除cookie数
     */
    - (NSInteger)deleteCookieForURL:(NSURL *)URL;
    
    @end
    
    #endif /* ALIDNSCookieManager_h */
    
    ALIDNSCookieManager.m
    #import <Foundation/Foundation.h>
    #import "ALIDNSCookieManager.h"
    
    @implementation ALIDNSCookieManager
    {
        ALIDNSCookieFilter cookieFilter;
    }
    
    - (instancetype)init {
        if (self = [super init]) {
            /**
                此处设置的Cookie和URL匹配策略比较简单,检查URL.host是否包含Cookie的domain字段
                通过调用setCookieFilter接口设定Cookie匹配策略,
                比如可以设定Cookie的domain字段和URL.host的后缀匹配 | URL是否符合Cookie的path设定
                细节匹配规则可参考RFC 2965 3.3节
             */
            cookieFilter = ^BOOL(NSHTTPCookie *cookie, NSURL *URL) {
                if ([URL.host containsString:cookie.domain]) {
                    return YES;
                }
                return NO;
            };
        }
        return self;
    }
    
    + (instancetype)sharedInstance {
        static id singletonInstance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            if (!singletonInstance) {
                singletonInstance = [[super allocWithZone:NULL] init];
            }
        });
        return singletonInstance;
    }
    
    + (id)allocWithZone:(struct _NSZone *)zone {
        return [self sharedInstance];
    }
    
    - (id)copyWithZone:(struct _NSZone *)zone {
        return self;
    }
    
    - (void)setCookieFilter:(ALIDNSCookieFilter)filter {
        if (filter != nil) {
            cookieFilter = filter;
        }
    }
    
    - (NSArray<NSHTTPCookie *> *)handleHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL {
        NSArray *cookieArray = [NSHTTPCookie cookiesWithResponseHeaderFields:headerFields forURL:URL];
        if (cookieArray != nil) {
            NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
            for (NSHTTPCookie *cookie in cookieArray) {
                if (cookieFilter(cookie, URL)) {
                    NSLog(@"Add a cookie: %@", cookie);
                    [cookieStorage setCookie:cookie];
                }
            }
        }
        return cookieArray;
    }
    
    - (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL {
        NSArray *cookieArray = [self searchAppropriateCookies:URL];
        if (cookieArray != nil && cookieArray.count > 0) {
            NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieArray];
            if ([cookieDic objectForKey:@"Cookie"]) {
                return cookieDic[@"Cookie"];
            }
        }
        return nil;
    }
    
    - (NSArray *)searchAppropriateCookies:(NSURL *)URL {
        NSMutableArray *cookieArray = [NSMutableArray array];
        NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
        for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
            if (cookieFilter(cookie, URL)) {
                NSLog(@"Search an appropriate cookie: %@", cookie);
                [cookieArray addObject:cookie];
            }
        }
        return cookieArray;
    }
    
    - (NSInteger)deleteCookieForURL:(NSURL *)URL {
        int delCount = 0;
        NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
        for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
            if (cookieFilter(cookie, URL)) {
                NSLog(@"Delete a cookie: %@", cookie);
                [cookieStorage deleteCookie:cookie];
                delCount++;
            }
        }
        return delCount;
    }
    
    @end

    发送请求使用方法示例:

    WKWebView * webView = [WKWebView new]; 
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com/login"]]; 
    NSString *value = [[ALIDNSCookieManager sharedInstance] getRequestCookieHeaderForURL:url];
    [request setValue:value forHTTPHeaderField:@"Cookie"];
    [webView loadRequest:request];

    接收处理请求:

     NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
            if (!error) {
                NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
                // 解析 HTTP Response Header,存储cookie
                [[ALIDNSCookieManager sharedInstance] handleHeaderFields:[httpResponse allHeaderFields] forURL:url];
            }
        }];
        [task resume];

    通过document.cookie设置Cookie解决后续页面(同域)Ajax、iframe请求的Cookie问题;

    WKUserContentController* userContentController = [WKUserContentController new]; 
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; 
    [userContentController addUserScript:cookieScript];

    Cookie包含动态IP导致登录失效问题

    关于Cookie失效的问题,假如客户端登录session存在Cookie,此时这个域名配置了多个IP,使用域名访问会读对应域名的Cookie,使用IP访问则去读对应IP的Cookie,假如前后两次使用同一个域名配置的不同IP访问,会导致Cookie的登录session失效。

    如果App里面的WebView页面需要用到系统Cookie存的登录session,之前App所有本地网络请求使用域名访问,是可以共用Cookie的登录session的,但现在本地网络请求使用HTTPDNS后改用IP访问,导致还使用域名访问的WebView读不到系统Cookie存的登录session了(系统Cookie对应IP了)。IP直连后,服务端返回Cookie包含动态IP导致登录失效。

    使用IP访问后,服务端返回的Cookie也是IP。导致可能使用对应的域名访问,无法使用本地Cookie,或者使用隶属于同一个域名的不同IP去访问,Cookie也对不上,导致登录失效 。

    我们提供的思路是这样的:

  • 应该得干预Cookie的存储,基于域名。

  • 根源上,API域名返回单IP。

    第二种思路将失去DNS调度特性,故不考虑。第一种思路更为可行。

    基于iOS 11 API WKHTTPCookieStore来解决WKWebView的Cookie管理问题

    当每次服务端返回Cookie后,在存储前都进行下改造,使用域名替换下IP。之后虽然每次网络请求都是使用IP访问,但是host我们都手动改为了域名,这样本地存储的Cookie也就能对得上了。

    在网络请求成功后,或者加载网页成功后,主动将本地的domain字段为IP的Cookie替换IP为host域名地址,代码演示:

    - (void)updateWKHTTPCookieStoreDomainFromIP:(NSString *)IP toHost:(NSString *)host {
        WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
        [cookieStroe getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
            [[cookies copy] enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) {
                if ([cookie.domain isEqualToString:IP]) {
                    NSMutableDictionary<NSHTTPCookiePropertyKey, id> *dict = [NSMutableDictionary dictionaryWithDictionary:cookie.properties];
                    dict[NSHTTPCookieDomain] = host;
                    NSHTTPCookie *newCookie = [NSHTTPCookie cookieWithProperties:[dict copy]];
                    [cookieStroe setCookie:newCookie completionHandler:^{
                        [self logCookies];
                        [cookieStroe deleteCookie:cookie
                                completionHandler:^{
                                    [self logCookies];
                                }];
                    }];
                }
            }];
        }];
    }

    iOS 11中也提供了对应的API供我们来处理替换Cookie的时机,那就是下面的API:

    @protocol WKHTTPCookieStoreObserver <NSObject>
    @optional
    - (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore;
    @end
    //WKHTTPCookieStore
    /*! @abstract Adds a WKHTTPCookieStoreObserver object with the cookie store.
     @param observer The observer object to add.
     @discussion The observer is not retained by the receiver. It is your responsibility
     to unregister the observer before it becomes invalid.
     */
    - (void)addObserver:(id<WKHTTPCookieStoreObserver>)observer;
    
    /*! @abstract Removes a WKHTTPCookieStoreObserver object from the cookie store.
     @param observer The observer to remove.
     */
    - (void)removeObserver:(id<WKHTTPCookieStoreObserver>)observer;

    用法如下:

    @interface WebViewController ()<WKHTTPCookieStoreObserver>
    - (void)viewDidLoad {
        [super viewDidLoad];
        [NSURLProtocol registerClass:[WebViewURLProtocol class]];
        NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
        [cookieStorage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];
        WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
        [cookieStroe addObserver:self];
    
        [self.view addSubview:self.webView];
        //... ...
    }
    
    #pragma mark -
    #pragma mark - WKHTTPCookieStoreObserver Delegate Method
    
    - (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore {
        [self updateWKHTTPCookieStoreDomainFromIP:CYLIP toHost:CYLHOST];
    }

    -updateWKHTTPCookieStoreDomainFromIP方法的实现,在上文已经给出。

    这个方案需要客户端维护一个IP—>HOST的映射关系,需要能从IP反向查找到HOST,这个维护成本还时挺高的。下面介绍下,更通用的方法,也是iOS 11之前的处理方法:

    iOS 11之前的处理方法:NSURLProtocal拦截后,手动管理Cookie的存储:

    步骤:做IP替换时将原始URL保存到Header中

    + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
        NSMutableURLRequest *mutableReq = [request mutableCopy];
        NSString *originalUrl = mutableReq.URL.absoluteString;
        NSURL *url = [NSURL URLWithString:originalUrl];
        // 接口获取IP地址
        NSArray *array = [[DNSResolver share] getIpsByCacheWithDomain:domain andExpiredIPEnabled:YES];
        NSString *ip = array.firstObject;
        if (ip) {
            NSRange hostFirstRange = [originalUrl rangeOfString:url.host];
            if (NSNotFound != hostFirstRange.location) {
                NSString *newUrl = [originalUrl stringByReplacingCharactersInRange:hostFirstRange withString:ip];
                mutableReq.URL = [NSURL URLWithString:newUrl];
                [mutableReq setValue:url.host forHTTPHeaderField:@"host"];
                // 添加originalUrl保存原始URL
                [mutableReq addValue:originalUrl forHTTPHeaderField:@"originalUrl"];
            }
        }
        NSURLRequest *postRequestIncludeBody = [mutableReq cyl_getPostRequestIncludeBody];
        return postRequestIncludeBody;
    }

    然后获取到数据后,手动管理Cookie :

    - (void)handleCookiesFromResponse:(NSURLResponse *)response {
        NSString *originalURLString = [self.request valueForHTTPHeaderField:@"originalUrl"];
        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
            NSDictionary<NSString *, NSString *> *allHeaderFields = httpResponse.allHeaderFields;
            if (originalURLString && originalURLString.length > 0) {
                NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:allHeaderFields forURL: [[NSURL alloc] initWithString:originalURLString]];
                if (cookies && cookies.count > 0) {
                    NSURL *originalURL = [NSURL URLWithString:originalURLString];
                    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies forURL:originalURL mainDocumentURL:nil];
                }
            }
        }
    }
    
    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler {
        NSString *location = response.allHeaderFields[@"Location"];
        NSURL *url = [[NSURL alloc] initWithString:location];
        NSMutableURLRequest *mRequest = [newRequest mutableCopy];
        mRequest.URL = url;
        if (location && location.length > 0) {
            if ([[newRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
                // POST重定向为GET
                mRequest.HTTPMethod = @"GET";
                mRequest.HTTPBody = nil;
            }
            [mRequest setValue:nil forHTTPHeaderField:@"host"];
            // 在这里为 request 添加 cookie 信息。
            [self handleCookiesFromResponse:response];
            [XXXURLProtocol removePropertyForKey:XXXURLProtocolHandledKey inRequest:mRequest];
            completionHandler(mRequest);
        } else{
           completionHandler(mRequest);
        }
    }

    发送请求前,向请求中添加Cookie信息:

    + (void)handleCookieWithRequest:(NSMutableURLRequest *)request {
        NSString* originalURLString = [request valueForHTTPHeaderField:@"originalUrl"];
        if (!originalURLString || originalURLString.length == 0) {
            return;
        }
        NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
        if (cookies && cookies.count >0) {
            NSDictionary *cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
            NSString *cookieString = [cookieHeaders objectForKey:@"Cookie"];
            [request addValue:cookieString forHTTPHeaderField:@"Cookie"];
        }
    }
    
    + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
        NSMutableURLRequest *mutableReq = [request mutableCopy];
    //...
         [self handleCookieWithRequest:mutableReq];
        return [mutableReq copy];
    }

302重定向问题

上面提到的Cookie方案无法解决302请求的Cookie问题,比如,第一个请求是http://www.a.com ,我们通过在request header 里带上Cookie解决该请求的Cookie问题,接着页面302跳转到http://www.b.com ,这个时候 http://www.b.com 这个请求就可能因为没有携带Cookie而无法访问。当然,由于每一次页面跳转前都会调用回调函数:

 - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

可以在该回调函数里拦截302请求,copy request,在request header中带上Cookie并重新loadRequest。不过这种方法依然解决不了页面iframe跨域请求的Cookie问题,毕竟-[WKWebView loadRequest:]只适合加载mainFrame请求。

首页 阿里云公共DNS 常见问题 iOS端WebView场景下接入阿里云公共DNS SDK最佳实践方案