iOS App秒開H5實(shí)戰(zhàn)總結(jié)

iOS app秒開H5實(shí)戰(zhàn)總結(jié)

《iOS app秒開H5優(yōu)化探索》一文中簡單介紹了優(yōu)化的方案以及一些知識(shí)點(diǎn),本文繼續(xù)介紹使用WKURLSchemeHandler攔截加載離線包優(yōu)化打開速度的一些細(xì)節(jié)以及注意事項(xiàng),閱讀本文前請(qǐng)先大概了解一下上篇文章的內(nèi)容以及WKURLSchemeHandler的基本用法。

離線包下載優(yōu)化

在上一篇《iOS app秒開H5優(yōu)化探索》中,離線包下載處理有很多不合理的地方,如資源分散下載,不僅增加后續(xù)更新邏輯的復(fù)雜度,而且會(huì)造成系統(tǒng)資源浪費(fèi)。為此可以把所有資源文件(js/css/html等)整合成zip包,一次性下載至本地,使用SSZipArchive解壓到指定位置,更新version即可。 此外,下載時(shí)機(jī)在app啟動(dòng)和前后臺(tái)切換都做一次檢查更新,效果更好。

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *downLoadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
  if (!location) {
      return ;
  }

  //下載成功,移除舊資源
  [fileManager removeFileAtPath:dirPath fileExtesion:nil];

  //腳本臨時(shí)存放路徑
  NSString *downloadTmpPath = [NSString stringWithFormat:@"%@pkgfile_%@.zip", NSTemporaryDirectory(), version];
  // 文件移動(dòng)到指定目錄中
  NSError *saveError;
  [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:downloadTmpPath] error:&saveError];
  //解壓zip
  BOOL success = [SSZipArchive unzipFileAtPath:downloadTmpPath toDestination:dirPath];
  if (!success) {
      LogError(@"pkgfile: unzip file error");
      [fileManager removeItemAtPath:downloadTmpPath error:nil];
      [fileManager removeFileAtPath:dirPath fileExtesion:nil];
      return;
  }
  //更新版本號(hào)
  [[NSUserDefaults standardUserDefaults] setValue:version forKey:pkgfileVisionKey];
  [[NSUserDefaults standardUserDefaults] synchronize];
  //清除臨時(shí)文件和目錄
  [fileManager removeItemAtPath:downloadTmpPath error:nil];
}];
[downLoadTask resume];
[session finishTasksAndInvalidate];
復(fù)制代碼

WKWebView復(fù)用池

在調(diào)試過程中,發(fā)現(xiàn)首次加載頁面時(shí)間比后續(xù)打開時(shí)間都慢很多,原因預(yù)計(jì)是 webView 首次初始化時(shí)候需要啟動(dòng)資源和服務(wù)較多,于是嘗試預(yù)先初始化 webView 復(fù)用方案,速度會(huì)快很多。

WKWebView復(fù)用池原理:預(yù)選準(zhǔn)備兩個(gè)NSMutableSet<WKWebView *>,一個(gè)正被使用visiableWebViewSet、一個(gè)空閑待用reusableWebViewSet,在+ (void)load初始化一個(gè)WKWebView,并加入reusableWebViewSet中,當(dāng)H5頁面需要使用時(shí),從reusableWebViewSet中取出并放入visiableWebViewSet中,使用完(dealloc)放回reusableWebViewSet中。若該WKWebView異常則拋棄重新創(chuàng)建WKWebView,以免發(fā)生一些莫名其妙的問題。

1、初始化

+ (void)load {
    __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
        dispatch_async(dispatch_get_main_queue(), ^{
            WKWebView *webview = [[WKWebView alloc] init];
            [self->_reusableWebViewSet addObject:webview];
        });

        [[NSNotificationCenter defaultCenter] removeObserver:observer];
    }];
}
復(fù)制代碼

2、獲取復(fù)用池中的webview

- (WKWebView *)getReusedWebViewForHolder:(id)holder {
    if (!holder) {
#if DEBUG
        NSLog(@"WKWebViewPool must have a holder");
#endif
        return nil;
    }

    WKWebView *webView;

    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);

    if (_reusableWebViewSet.count > 0) {
        webView = [_reusableWebViewSet anyObject];
        [_reusableWebViewSet removeObject:webView];
        [_visiableWebViewSet addObject:webView];

    } else {
        [_visiableWebViewSet removeAllObjects];
        webView = [[WKWebView alloc] init];
        [_visiableWebViewSet addObject:webView];
    }
    webView.holderObject = holder;

    dispatch_semaphore_signal(_lock);

    return webView;
}
復(fù)制代碼

其中holder使用runtime為WKWebView添加的屬性,傳入使用復(fù)用池的當(dāng)前VC即可,以供后續(xù)回收判斷復(fù)用池是否正在使用。

3、用完回收

- (void)recycleReusedWebView:(WKWebView *)webView {
    if (!webView) {
        return;
    }

    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);

    if ([_visiableWebViewSet containsObject:webView]) {
        //將webView重置為初始狀態(tài)
        [webView webViewEndReuse];

        [_visiableWebViewSet removeObject:webView];
        [_reusableWebViewSet addObject:webView];

    } else {
        if (![_reusableWebViewSet containsObject:webView]) {
#if DEBUG
            NSLog(@"Don't use the webView");
#endif
        }
    }
    dispatch_semaphore_signal(_lock);
}

其中webViewEndReuse為WKWebView的擴(kuò)展方法:
- (void)webViewEndReuse {
    self.holderObject = nil;

    if ([self isKindOfClass:[WKWebView class]]) {
        WKWebView *webView = (WKWebView *)self.webView;
        webView.delegate = nil;
        webView.scrollView.delegate = nil;
        [webView stopLoading];
        [webView setUIDelegate:nil];
        [webView loadHTMLString:@"" baseURL:nil];
    }
}
復(fù)制代碼

復(fù)用池原理很簡單,此外對(duì)收到內(nèi)存警告后清除web緩存等回收處理等等,此處不再贅述。

WebViewController改造

通常項(xiàng)目中處理H5頁面都會(huì)放在統(tǒng)一的WebViewController中,所以要結(jié)合開關(guān)、要優(yōu)化的業(yè)務(wù)來分開復(fù)用池和普通webView的使用,以免出問題。

1、替換url scheme

  NSString *urlString = @"https://www.test.com/abc?id=123456";
  if ([YH_Global sharedInstance].isGrassLocalOpen && SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0")) {             
    urlString = [urlString stringByReplacingOccurrencesOfString:@"https" withString:@"customScheme"];
  }
  WebViewController *vc = [[WebViewController alloc] initWithUrl:urlString];
  [self.navigationController pushViewController:vc animated:YES];
復(fù)制代碼

此處替換url scheme http(s)為自定義協(xié)議,使用攔截生效。 此處需要特別說明的是,前端H5請(qǐng)求的js、css等資源使用自適應(yīng)的協(xié)議,如:src='//www.test.com/abc.js',這樣native端使用不同scheme請(qǐng)求,H5就會(huì)使用對(duì)應(yīng)的scheme進(jìn)行請(qǐng)求加載。另外一個(gè)重要的點(diǎn)是,前端的ajax請(qǐng)求,像post請(qǐng)求,scheme使用http(s)不使用自定義協(xié)議,這樣native不會(huì)攔截,完全交給H5與服務(wù)器交互,就不會(huì)發(fā)生發(fā)送post請(qǐng)求,body丟失的情況。

2、初始化webView

- (instancetype)initWithUrl:(NSString *)url {
    if (self = [super init]) {
        if ([self checkMatchingWithUrl:url]) {//符合條件,使用復(fù)用池
            self.webView = [[WKWebViewPool sharedInstance] getReusedWebViewForHolder:self];
        }
        self.url = url;
    }
    return self;
}
復(fù)制代碼

此處在initWithUrl中,而不在viewDidLoad中獲取webView,是因?yàn)樵趇nit中,頁面打開速度會(huì)快很多。

3、預(yù)先添加數(shù)據(jù)腳本,提升體驗(yàn)

這一步根據(jù)筆者公司的app的業(yè)務(wù)特性所有:用戶社區(qū)帖子列表(native) => 帖子詳情(H5實(shí)現(xiàn))=> 個(gè)人中心等(H5)。從列表點(diǎn)擊進(jìn)入H5詳情時(shí),預(yù)先將帖子的部分?jǐn)?shù)據(jù),如頭像、首圖縮略圖、內(nèi)容等傳給前端(,前端拿到數(shù)據(jù),預(yù)先加載這部分?jǐn)?shù)據(jù),同時(shí)對(duì)首圖縮略圖增加漸變出現(xiàn)的效果,這時(shí)打開H5,頁面從模糊的縮略圖漸變至高清大圖,以達(dá)到原生打開頁面的體驗(yàn)(文末的最終效果圖)。注意,這里圖片傳給前端的是url,并不是圖片數(shù)據(jù),下文會(huì)繼續(xù)說明如何使用圖片數(shù)據(jù)。

native與H5交互的部分代碼:

Model *modelMake = model;//列表點(diǎn)擊的item數(shù)據(jù)
NSString *key = [NSString stringWithFormat:@"native_list_%@", modelMake.articleId];
NSData *data = [NSJSONSerialization dataWithJSONObject:[modelMake dictionaryValue] options:NSJSONWritingPrettyPrinted error:nil];
NSString *value = [[NSString alloc] initWithData:data?data:[NSData data] encoding:NSUTF8StringEncoding];
NSString *javaScript = [NSString stringWithFormat:@"!window.predatas && (window.predatas = []);predatas.push({key: \"%@\", value: %@ })", key, value];

WKUserContentController *userContentController = wkWebView.configuration.userContentController;
WKUserScript *userScript = [[WKUserScript alloc] initWithSource:javaScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:userScript];

復(fù)制代碼

4、側(cè)滑返回

由于業(yè)務(wù)使用H5開發(fā),從列表到詳情再到個(gè)人中心,這時(shí)側(cè)滑會(huì)直接回到列表頁,并不像原生導(dǎo)航那樣一層層返回。解決這個(gè)問題,首先想到使用WKWebView的allowsBackForwardNavigationGestures屬性,結(jié)合webView的goBack方法,的確可以層層側(cè)滑返回,但是最后出現(xiàn)會(huì)先回到第一次打開的詳情頁面,然后才會(huì)回到列表的情況以及一些其他異常問題。嘗試了一些方案后,最終采用自己添加手勢(shì)實(shí)現(xiàn)側(cè)滑返回功能。

手勢(shì)創(chuàng)建:

self.leftSwipGes = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(leftSwipGesAction:)];
self.leftSwipGes.edges = UIRectEdgeLeft;
self.leftSwipGes.delegate = self;
[self.webView addGestureRecognizer:self.leftSwipGes];

復(fù)制代碼

實(shí)現(xiàn):

- (void)leftSwipGesAction:(UIScreenEdgePanGestureRecognizer *)ges {
    if (UIGestureRecognizerStateEnded == ges.state) {
        if (self.webView.backForwardList.backList.count > 0) {
            WKBackForwardListItem *item = webView.backForwardList.backList.lastObject;
            if (![self.webView.URL.absoluteString isEqualToString:self.url]) {
                [webView goToBackForwardListItem:item];
            } else {
                [self nativeBack:nil];
                [webView goToBackForwardListItem:item];
            }
        } else {
            [self nativeBack:nil];
        }
    }
}
復(fù)制代碼

其中,nativeBack()為native的返回方法。原理:側(cè)滑時(shí),當(dāng)前webview的url不是初始H5頁面的url時(shí),webView的backForwardList退后一級(jí),當(dāng)退到初始頁面時(shí),直接返回列表。此外,注意處理自定義手勢(shì)跟其他手勢(shì)沖突的問題;同時(shí)還要禁用系統(tǒng)的側(cè)滑返回,以及禁用FDFullscreenPopGesture等第三方庫的側(cè)滑返回。

攔截加載離線包

前提創(chuàng)建WKWebview時(shí)注冊(cè)好自定義協(xié)議,具體結(jié)合自己項(xiàng)目實(shí)現(xiàn),只要保證創(chuàng)建WKWebView時(shí)注冊(cè)即可:

WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];  
[configuration setURLSchemeHandler:[CustomURLSchemeHandler new] forURLScheme: @"customScheme"];    
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
復(fù)制代碼

攔截

上文也分析了,打開一個(gè)H5頁面會(huì)有一段時(shí)間白屏,是因?yàn)樗隽撕芏嗍虑椋?/p>

初始化 webview -> 請(qǐng)求頁面 -> 下載數(shù)據(jù) -> 解析HTML -> 請(qǐng)求 js/css 資源 -> dom 渲染 -> 解析 JS 執(zhí)行 -> JS 請(qǐng)求數(shù)據(jù) -> 解析渲染 -> 下載渲染圖片

所以當(dāng)打開以自定義協(xié)議customScheme為scheme的H5頁面時(shí),webview請(qǐng)求頁面,native會(huì)依次收到html、js、css、圖片類型的攔截響應(yīng):

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
API_AVAILABLE(ios(11.0)){

    NSDictionary *headers = urlSchemeTask.request.allHTTPHeaderFields;
    NSString *accept = headers[@"Accept"];

    //當(dāng)前的requestUrl的scheme都是customScheme
    NSString *requestUrl = urlSchemeTask.request.URL.absoluteString;
    NSString *fileName = [[requestUrl componentsSeparatedByString:@"?"].firstObject componentsSeparatedByString:@"/"].lastObject;

    //Intercept and load local resources.
    if ((accept.length >= @"text".length && [accept rangeOfString:@"text/html"].location != NSNotFound)) {//html 攔截
      [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
    } else if ([self isMatchingRegularExpressionPattern:@"\\.(js|css)" text:requestUrl]) {//js、css
        [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
    } else if (accept.length >= @"image".length && [accept rangeOfString:@"image"].location != NSNotFound) {//image
      NSString *replacedStr = [requestUrl stringByReplacingOccurrencesOfString:kUrlScheme withString:@"https"];
      NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:[NSURL URLWithString:replacedStr]];
      [[SDWebImageManager sharedManager].imageCache queryCacheOperationForKey:key done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
          if (image) {
              NSData *imgData = UIImageJPEGRepresentation(image, 1);
              NSString *mimeType = [self getMimeTypeWithFilePath:fileName] ?: @"image/jpeg";
              [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:mimeType requestData:imgData];
          } else {
              [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
          }
      }];
    } else {//return an empty json.
        NSData *data = [NSJSONSerialization dataWithJSONObject:@{ } options:NSJSONWritingPrettyPrinted error:nil];
        [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:@"text/html" requestData:data];
    }
}

    //Load local resources, eg: html、js、css...
- (void)loadLocalFile:(NSString *)fileName urlSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask API_AVAILABLE(ios(11.0)){
    if (fileName.length == 0 || !urlSchemeTask) {
        return;
    }

    //If the resource do not exist, re-send request by replacing to http(s).
    NSString *filePath = [kGrassH5ResourcesFiles stringByAppendingPathComponent:fileName];
    if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        if ([replacedStr hasPrefix:kUrlScheme]) {
            replacedStr = [replacedStr stringByReplacingOccurrencesOfString:kUrlScheme withString:@"https"];
        }

        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:replacedStr]];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

            [urlSchemeTask didReceiveResponse:response];
            [urlSchemeTask didReceiveData:data];
            if (error) {
                [urlSchemeTask didFailWithError:error];
            } else {
                [urlSchemeTask didFinish];

                NSString *accept = urlSchemeTask.request.allHTTPHeaderFields[@"Accept"];
                if (!(accept.length >= @"image".length && [accept rangeOfString:@"image"].location != NSNotFound)) { //圖片不下載
                    [data writeToFile:filePath atomically:YES];
                }
            }
        }];
        [dataTask resume];
        [session finishTasksAndInvalidate];
    } else {
        NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:nil];
        [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:[self getMimeTypeWithFilePath:filePath] requestData:data];
    }
}

- (void)resendRequestWithUrlSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
                              mimeType:(NSString *)mimeType
                           requestData:(NSData *)requestData  API_AVAILABLE(ios(11.0)) {
    if (!urlSchemeTask || !urlSchemeTask.request || !urlSchemeTask.request.URL) {
            return;
        }

        NSString *mimeType_local = mimeType ? mimeType : @"text/html";
        NSData *data = requestData ? requestData : [NSData data];
        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:urlSchemeTask.request.URL
                                                            MIMEType:mimeType_local
                                               expectedContentLength:data.length
                                                    textEncodingName:nil];
        [urlSchemeTask didReceiveResponse:response];
        [urlSchemeTask didReceiveData:data];
        [urlSchemeTask didFinish];
    }
}

復(fù)制代碼

我這里只簡單貼了部分?jǐn)r截資源請(qǐng)求后的處理代碼:收到攔截請(qǐng)求后,先獲取本地資源包對(duì)應(yīng)的資源,轉(zhuǎn)換成data回傳給webView進(jìn)行渲染處理;若本地沒有,則customScheme替換成https的url重發(fā)請(qǐng)求通知webview,這就是基本流程。實(shí)際開發(fā)調(diào)試過程中還有很多細(xì)節(jié)需要處理,如本地資源沒有時(shí),根據(jù)服務(wù)器預(yù)先下發(fā)的匹配規(guī)則重發(fā)請(qǐng)求;又如加載替換使用不同的html,又如打開頁面一直白屏等等問題,這里就不列出了。

但還要特別說明兩點(diǎn):

1、代碼中替換圖片的邏輯,先查找本地圖片的目的是為了實(shí)現(xiàn)上文所說的WebViewController改造第三條:預(yù)先添加數(shù)據(jù)腳本,提升體驗(yàn),獲取列表中已展示縮略圖的SDWebImage緩存?zhèn)鹘owebView進(jìn)行預(yù)加載,以實(shí)現(xiàn)漸變出現(xiàn)的效果。然后本地就重發(fā)請(qǐng)求通知webview。 到這里,你應(yīng)該明白,優(yōu)化實(shí)現(xiàn)秒開,中心思想就是要減少資源的網(wǎng)絡(luò)請(qǐng)求,把第一頁要展示的原素盡量預(yù)先加載。

2、在測試過程中,在一些機(jī)型較差的機(jī)器上,頻繁快速的打開H5頁面,會(huì)出現(xiàn)崩潰。查閱WKURLSchemeTask的官方解釋:

An exception will be thrown if you try to send a new response object after the task has already been completed.
An exception will be thrown if your app has been told to stop loading this task via the registered WKURLSchemeHandler object.

經(jīng)分析,發(fā)現(xiàn)在處理本地不存在的圖片時(shí),先判斷本地是否存在而后又發(fā)起請(qǐng)求,時(shí)間跨度比較長,當(dāng)前urlSchemeTask由于某些原因提前結(jié)束了(會(huì)收到stopURLSchemeTask回調(diào)),這時(shí)重發(fā)的請(qǐng)求又訪問了WKURLSchemeTask的實(shí)例方法(didReceiveResponse等)就導(dǎo)致了崩潰。
解決辦法:新增NSMutableDictionary成員變量,以當(dāng)前的urlSchemeTask做key,攔截開始時(shí)設(shè)置YES,收到停止通知時(shí)設(shè)置NO,每次通知webview前判斷當(dāng)前的urlSchemeTask是否結(jié)束,提前結(jié)束了就不做處理。 這么做,帶來的影響就是當(dāng)前的圖片會(huì)不顯示,退出再次進(jìn)來還是會(huì)出現(xiàn)的,結(jié)合出現(xiàn)的異常場景以及發(fā)生崩潰,這點(diǎn)影響還是可以接受的。

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
API_AVAILABLE(ios(11.0)){

    dispatch_sync(self.serialQueue, ^{
        [self.holderDicM setObject:@(YES) forKey:urlSchemeTask.description];
    });
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
API_AVAILABLE(ios(11.0)){
    dispatch_sync(self.serialQueue, ^{
        [self.holderDicM setObject:@(NO) forKey:urlSchemeTask.description];
    });
}
復(fù)制代碼

注意要添加串行隊(duì)列對(duì)數(shù)據(jù)進(jìn)行保護(hù),防止多線程同時(shí)訪問修改數(shù)據(jù),造成數(shù)據(jù)異常。

總結(jié)

到這里,優(yōu)化基本上完成,打開H5頁面確實(shí)快了很多。我們的方案大致就是這樣,這個(gè)肯定不是最優(yōu)的方案,多多少少會(huì)有些問題,我相信讀者會(huì)有更好的優(yōu)化方案,或者遇到上述出現(xiàn)的問題有更合理的解決方法,歡迎大家一起討論。

最后展示一下,我們優(yōu)化后的打開H5頁面的效果(iPhone 7):

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 注:本篇研究重點(diǎn)不在于某個(gè)離線方案的具體使用,而在于對(duì)方案的優(yōu)缺點(diǎn)分析、探究和選型,以及一些我個(gè)人的看法。 前言 ...
    LotLewis閱讀 10,557評(píng)論 7 16
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,644評(píng)論 1 32
  • 一、Native開發(fā)中為什么需要H5容器 Native開發(fā)原生應(yīng)用是手機(jī)操作系統(tǒng)廠商(目前主要是蘋果的iOS和go...
    攻城獅GG閱讀 762評(píng)論 0 1
  • WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView 組件,用以替代 UIKit 中笨...
    Aiana閱讀 4,803評(píng)論 1 8
  • 轉(zhuǎn)載:http://www.cnblogs.com/NSong/p/6489802.html 導(dǎo)語 WKWebVi...
    李小威閱讀 4,976評(píng)論 8 9

友情鏈接更多精彩內(nèi)容