当前位置:首页 > 通信资讯 > 正文

wkwebview使用(wkwebview加载完成)

wkwebview使用(wkwebview加载完成)

优化背景

  • 众所周知,H5 的部分优势(开发快,迭代快,热更新)是很明显的,公司客户端的部分业务都是由 H5 来实现的,网络好的情况下体验也是很不错的
  • 但是其实 H5 的体验是比原生差的,这就需要想办法如何提高 H5 加载速度,优化体验,首屏的加载速度还是很影响体验的

加载速度

关于加载速度慢有很多文章都已经详细解释了,h5在加载工作中做了很多事

初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片

wkwebview使用(wkwebview加载完成)

一般页面在 dom 渲染后才能展示,可以发现,H5 首屏渲染白屏问题的原因关键在于,如何优化减少从请求下载页面到渲染之间这段时间的耗时。

前后端优化

这其中可做的优化特别多,前后端能够做的是:

  • 降低请求量:减少 HTTP 请求数, 合并资源,minify / gzip 压缩,webP,lazyLoad。
  • 因为手机浏览器同时响应请求是 4 个,4 个的请求数也许不是特别靠谱,没有查到出处,但是肯定是越少越好。
  • HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage。
  • 加快请求速度:预解析 DNS,减少域名数,并行加载,CDN 分发。
  • 渲染:JS/CSS 优化,加载顺序,服务端渲染模板直出。

一般情况下,只要对照这个列表,对比差异就基本能搞定绝大部分前端性能问题了。不过我们在里面仔细再分析下,对首屏启动速度影响最大的就是网络请求,包括请求 HTML、css、image 等静态资源和展示数据的请求。所以客户端内,优化最关键的其实就是如何缓存这些网络资源,也就是离线包缓存方案。

离线包方案的实践

方案选型是两种

  • 基于 LocalWebServer 实现 WKWebView 离线资源加载
  • 使用 WKURLSchemeHandler 实现 WKWebView 离线资源加载

LocalWebServer

基于 iOS 的 local web server,目前大致有以下几种较为完善的框架:

  • CocoaHttpServer (支持 iOS、macOS 及多种网络场景)
  • GCDWebServer (基于 iOS,不支持 https 及 webSocket)
  • Telegraph (Swift 实现,功能较上面两类更完善)

当时采用的是 GCDWebServer,在打开 APP 后直接启动Webserver,H5 的链接直接替换成本地 localhost + 端口号链接的地址。

本来的方案是本地服务器和远端h5服务器同步下载资源,下载后客户端请求本地服务器的路径,如未找到相应的资源再请求远端服务器的资源文件。

测试过程中碰到很多奇怪的问题(暂不一一举例),也有提到以下问题并且时间紧急所以并未做进一步的深入:

  • 资源访问权限安全问题
  • APP 前后台切换时,服务重启性能耗时问题
  • 服务运行时,电量及 CPU 占有率问题
  • 多线程及磁盘 IO 问题

WKURLSchemeHandler

关于离线包

前端项目的静态资源直接打包成 zip 包,APP 在启动时开始下载该包并解压到本地。WKWebview 通过 WKURLSchemeHandler 拦截并加载本地资源文件。关于离线包的分发,就是普通的 zip 离线包和一个版本控制的 json 文件,每次打离线包会修改 json 文件里的版本号,并附有离线包下载地址。此处可以优化的更好,但暂时并不需要太复杂。

离线包的下载和解压

只是简单的下载并解压到本地资源路径,关于版本比对的代码这里没有展示出来,自行注意,避免每次都全量更新。

  1. /*创建网络下载对象*/
  2. AFURLSessionManager*manager=[[AFURLSessionManageralloc]initWithSessionConfiguration:[NSURLSessionConfigurationdefaultSessionConfiguration]];
  3. /*下载地址*/
  4. NSURL*url=[NSURLURLWithString:request.urlParameters.path];
  5. NSURLRequest*request=[NSURLRequestrequestWithURL:url];
  6. /*下载路径*/
  7. //获取Document文件
  8. NSString*docsdir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)objectAtIndex:0];
  9. NSString*zipFilePath=[docsdirstringByAppendingPathComponent:@"zip"];//将需要创建的串拼接到后面
  10. NSString*H5FilePath=[docsdirstringByAppendingPathComponent:@"H5"];
  11. NSFileManager*fileManager=[NSFileManagerdefaultManager];
  12. BOOLzipIsDir=NO;
  13. BOOLH5IsDir=NO;
  14. //fileExistsAtPath判断一个文件或目录是否有效,isDirectory判断是否一个目录
  15. BOOLzipexisted=[fileManagerfileExistsAtPath:zipFilePathisDirectory:&zipIsDir];
  16. BOOLH5Existed=[fileManagerfileExistsAtPath:H5FilePathisDirectory:&H5IsDir];
  17. if(!(zipIsDir==YES&&zipexisted==YES)){//如果文件夹不存在
  18. [fileManagercreateDirectoryAtPath:zipFilePathwithIntermediateDirectories:YESattributes:nilerror:nil];
  19. }
  20. if(!(H5IsDir==YES&&H5Existed==YES)){
  21. [fileManagercreateDirectoryAtPath:H5FilePathwithIntermediateDirectories:YESattributes:nilerror:nil];
  22. }
  23. //删除
  24. //[[NSFileManagerdefaultManager]removeItemAtPath:zipFilePatherror:nil];
  25. NSString*filePath=[zipFilePathstringByAppendingPathComponent:url.lastPathComponent];
  26. /*开始请求下载*/
  27. NSURLSessionDownloadTask*downloadTask=[managerdownloadTaskWithRequest:requestprogress:^(NSProgress*_NonnulldownloadProgress){
  28. NSLog(@"下载进度:%.0f%",downloadProgress.fractionCompleted*100);
  29. }destination:^NSURL*_Nonnull(NSURL*_NonnulltargetPath,NSURLResponse*_Nonnullresponse){
  30. /*设定下载到的位置*/
  31. return[NSURLfileURLWithPath:filePath];
  32. }completionHandler:^(NSURLResponse*_Nonnullresponse,NSURL*_NullablefilePath,NSError*_Nullableerror){
  33. NSTimeIntervaldelta=CACurrentMediaTime()-self->start;
  34. NSLog(@"下载完成,耗时:%f",delta);
  35. //filePath就是你下载文件的位置,你可以解压,也可以直接拿来使用
  36. NSString*imgFilePath=[filePathpath];//将NSURL转成NSString
  37. NSString*zipPath=imgFilePath;
  38. //删除
  39. //[[NSFileManagerdefaultManager]removeItemAtPath:H5FilePatherror:nil];
  40. [fileManagercreateDirectoryAtPath:H5FilePathwithIntermediateDirectories:YESattributes:nilerror:nil];
  41. //解压
  42. [SSZipArchiveunzipFileAtPath:zipPathtoDestination:H5FilePath];
  43. //清理缓存
  44. [DLCommenHelperclearWebCache];
  45. }];
  46. [downloadTaskresume];

WKWebview 缓存池

美团有篇文章提到,在使用 iOS 10 的模拟器测试 WKWebView 的加载速度,首次初始化的时间耗时有 700 多毫秒。其实本人用 iOS 13 的真机,发现初始化的时间约在 200 毫秒左右甚至更短。虽然只占整个加载时间的特别小的一部分,但是本着能优则优的原则还是做了处理,也就是预加载 Webview。

  • 新建了一个单例类 SDIWKWebViewPool,默认缓存池里的数量是 10 个
  1. +(instancetype)sharedInstance{
  2. staticdispatch_once_tonceToken;
  3. staticSDIWKWebViewPool*instance=nil;
  4. dispatch_once(&onceToken,^{
  5. instance=[[superallocWithZone:NULL]init];
  6. });
  7. returninstance;
  8. }
  9. +(id)allocWithZone:(struct_NSZone*)zone{
  10. return[selfsharedInstance];
  11. }
  12. -(instancetype)init
  13. {
  14. self=[superinit];
  15. if(self){
  16. self.initialViewsMaxCount=10;
  17. self.preloadedViews=[NSMutableArrayarrayWithCapacity:self.initialViewsMaxCount];
  18. }
  19. returnself;
  20. }
  • 在合适的地方提前调用 //预加载wkwebview [[SDIWKWebViewPool sharedInstance] prepareWithCount:10];,自行选择在 delegate 或主页面初始化的时候调用。
  1. /**
  2. 预初始化若干WKWebView
  3. @paramcount个数
  4. */
  5. -(void)prepareWithCount:(NSUInteger)count{
  6. NSTimeIntervalstart=CACurrentMediaTime();
  7. //Actuallydoesnothing,onlyinitializationmustbecalled.
  8. while(self.preloadedViews.count<MIN(count,self.initialViewsMaxCount)){
  9. idpreloadedView=[selfcreatePreloadedView];
  10. if(preloadedView){
  11. [self.preloadedViewsaddObject:preloadedView];
  12. }else{
  13. break;
  14. }
  15. }
  16. NSTimeIntervaldelta=CACurrentMediaTime()-start;
  17. NSLog(@"=======初始化耗时:%f",delta);
  18. }
  19. /**
  20. 从池中获取一个WKWebView
  21. @returnWKWebView
  22. */
  23. -(WKWebView*)getWKWebViewFromPool{
  24. if(!self.preloadedViews.count){
  25. NSLog(@"不够啦!");
  26. return[selfcreatePreloadedView];
  27. }else{
  28. idpreloadedView=self.preloadedViews.firstObject;
  29. [self.preloadedViewsremoveObject:preloadedView];
  30. returnpreloadedView;
  31. }
  32. }
  • 创建 webview 的方法如下,需要注意的是 kWKWebViewReuseScheme,WKWebView 需要注册这个 scheme 才能实现拦截,这个是 WKWebview 拦截需要的准备工作。
  • SDICustomURLSchemeHandler 是我的自定义拦截类

关于这里的版本为什么设置成 iOS 12 以上,WKURLSchemeHandler 是苹果 iOS 11 就已推出,但是有发现某款机型在 iOS 11.2 上拦截失效,导致产生 Webview 白屏。所以这里一刀切,直接 12 以上才处理。其实 iOS 12 一下的用户量特别少,所以不需要太担心。

  1. //scheme定义
  2. #definekWKWebViewReuseScheme@"kwebview"
  3. /**
  4. 创建一个WKWebView
  5. @returnWKWebView
  6. */
  7. -(WKWebView*)createPreloadedView{
  8. WKUserContentController*userContentController=WKUserContentController.new;
  9. WKWebViewConfiguration*configuration=[[WKWebViewConfigurationalloc]init];
  10. NSString*cookieSource=[NSStringstringWithFormat:@"document.cookie='API_SESSION=%@';",[SAMKeychainusertoken]];
  11. WKUserScript*cookieScript=[[WKUserScriptalloc]initWithSource:cookieSourceinjectionTime:WKUserScriptInjectionTimeAtDocumentStartforMainFrameOnly:NO];
  12. [userContentControlleraddUserScript:cookieScript];
  13. //赋值userContentController
  14. configuration.userContentController=userContentController;
  15. configuration.preferences.javaScriptEnabled=YES;
  16. configuration.suppressesIncrementalRendering=YES;//是否支持记忆读取
  17. [configuration.preferencessetValue:@YESforKey:@"allowFileAccessFromFileURLs"];//支持跨域
  18. //WKWebViewConfiguration*wkWebConfig=[[WKWebViewConfigurationalloc]init];
  19. //WKUserContentController*wkUController=[[WKUserContentControlleralloc]init];
  20. //wkWebConfig.userContentController=wkUController;
  21. if(@available(iOS12.0,*)){
  22. [configurationsetURLSchemeHandler:[[SDICustomURLSchemeHandleralloc]init]forURLScheme:kWKWebViewReuseScheme];
  23. }else{
  24. //Fallbackonearlierversions
  25. }
  26. WKWebView*wkWebView=[[WKWebViewalloc]initWithFrame:CGRectZeroconfiguration:configuration];
  27. //根据自己的业务
  28. wkWebView.allowsBackForwardNavigationGestures=YES;
  29. returnwkWebView;
  30. }

替换 url scheme

  1. if(@available(iOS12.0,*)){
  2. if([urlStringhasPrefix:@"http"]&&[urlStringcontainsString:@"ui-h5"]){
  3. urlString=[urlStringstringByReplacingOccurrencesOfString:@"https"withString:kWKWebViewReuseScheme];
  4. }
  5. }

这里是通过规则直接把 https 替换为 kWKWebViewReuseScheme,也就是替换 url scheme http(s) 为自定义协议,完成这一步后,拦截生效。

需要注意的有两点:

  • 前端这边加载 js 等资源都是用相对路径,前端的 ajax 请求,像 post 请求,scheme 使用 http(s) 不使用自定义协议,这样native 不会拦截,完全交给 H5 与服务器交互,就不会发生发送 post 请求,body 丢失的情况。
  • 在我的项目里,H5 对服务器的请求都是通过 native 端来转发的,所以也不存在拦截 post 请求,body 丢失的情况。所以上面这样的改动对 H5 端是无侵入式的,不需要修改业务代码。

最最重要的自定义 SDICustomURLSchemeHandler 类

  1. -(void)webView:(WKWebView*)webViewstartURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask
  2. API_AVAILABLE(ios(12.0)){
  3. dispatch_sync(self.serialQueue,^{
  4. [_taskVaildDicsetValue:@(YES)forKey:urlSchemeTask.description];
  5. });
  6. NSDictionary*headers=urlSchemeTask.request.allHTTPHeaderFields;
  7. NSString*accept=headers[@"Accept"];
  8. //当前的requestUrl的scheme都是customScheme
  9. NSString*requestUrl=urlSchemeTask.request.URL.absoluteString;
  10. NSString*fileName=[[requestUrlcomponentsSeparatedByString:@"?"].firstObjectcomponentsSeparatedByString:@"ui-h5/"].lastObject;
  11. NSString*replacedStr=[requestUrlstringByReplacingOccurrencesOfString:kWKWebViewReuseSchemewithString:@"https"];
  12. self.replacedStr=replacedStr;
  13. //Interceptandloadlocalresources.
  14. if((accept.length>=@"text".length&&[acceptrangeOfString:@"text/html"].location!=NSNotFound)){//html拦截
  15. [selfloadLocalFile:fileNameurlSchemeTask:urlSchemeTask];
  16. }elseif([selfisMatchingRegularExpressionPattern:@"\\.(js|css)"text:requestUrl]){//js、css
  17. [selfloadLocalFile:fileNameurlSchemeTask:urlSchemeTask];
  18. }elseif(accept.length>=@"image".length&&[acceptrangeOfString:@"image"].location!=NSNotFound){//image
  19. NSString*key=[[SDWebImageManagersharedManager]cacheKeyForURL:[NSURLURLWithString:replacedStr]];
  20. [[SDWebImageManagersharedManager].imageCachequeryImageForKey:keyoptions:SDWebImageRetryFailedcontext:nilcompletion:^(UIImage*_Nullableimage,NSData*_Nullabledata,SDImageCacheTypecacheType){
  21. if(image){
  22. NSData*imgData=UIImageJPEGRepresentation(image,1);
  23. NSString*mimeType=[selfgetMIMETypeWithCAPIAtFilePath:fileName]?:@"image/jpeg";
  24. [selfresendRequestWithUrlSchemeTask:urlSchemeTaskmimeType:mimeTyperequestData:imgData];
  25. }else{
  26. [selfloadLocalFile:fileNameurlSchemeTask:urlSchemeTask];
  27. }
  28. }];
  29. }else{
  30. //returnanemptyjson.
  31. NSData*data=[NSJSONSerializationdataWithJSONObject:@{}options:NSJSONWritingPrettyPrintederror:nil];
  32. [selfresendRequestWithUrlSchemeTask:urlSchemeTaskmimeType:@"text/html"requestData:data];
  33. }
  34. }
  35. -(BOOL)isMatchingRegularExpressionPattern:(NSString*)patterntext:(NSString*)text{
  36. NSError*error=NULL;
  37. NSRegularExpression*regex=[NSRegularExpressionregularExpressionWithPattern:patternoptions:NSRegularExpressionCaseInsensitiveerror:&error];
  38. NSTextCheckingResult*result=[regexfirstMatchInString:textoptions:0range:NSMakeRange(0,[textlength])];
  39. returnMHObjectIsNil(result)?NO:YES;
  40. }
  • 上面的代码是拦截资源请求后的处理代码。收到拦截请求后,先获取本地资源包对应的资源,转换成 data 回传给 webView 进行渲染处理;若本地没有,则 customScheme 替换成 https 的 url 重发请求通知 webview,这就是基本流程。
  • 以下就是加载本地资源和重发请求的代码
  1. //Loadlocalresources,eg:html、js、css...
  2. -(void)loadLocalFile:(NSString*)fileNameurlSchemeTask:(id<WKURLSchemeTask>)urlSchemeTaskAPI_AVAILABLE(ios(11.0)){
  3. if(![self->_taskVaildDicboolValueForKey:urlSchemeTask.descriptiondefault:NO]||!urlSchemeTask||fileName.length==0){
  4. return;
  5. }
  6. NSString*docsdir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)objectAtIndex:0];
  7. NSString*H5FilePath=[[docsdirstringByAppendingPathComponent:@"H5"]stringByAppendingPathComponent:@"h5"];
  8. //Iftheresourcedonotexist,re-sendrequestbyreplacingtohttp(s).
  9. NSString*filePath=[H5FilePathstringByAppendingPathComponent:fileName];
  10. if(![[NSFileManagerdefaultManager]fileExistsAtPath:filePath]){
  11. NSLog(@"开始重新发送网络请求");
  12. if([self.replacedStrhasPrefix:kWKWebViewReuseScheme]){
  13. self.replacedStr=[self.replacedStrstringByReplacingOccurrencesOfString:kWKWebViewReuseSchemewithString:@"https"];
  14. NSLog(@"请求地址:%@",self.replacedStr);
  15. }
  16. self.replacedStr=[NSStringstringWithFormat:@"%@?%@",self.replacedStr,[SAMKeychainh5Version]?:@""];
  17. start=CACurrentMediaTime();//开始加载时间
  18. NSLog(@"web请求开始地址:%@",self.replacedStr);
  19. @weakify(self)
  20. NSMutableURLRequest*request=[NSMutableURLRequestrequestWithURL:[NSURLURLWithString:self.replacedStr]];
  21. NSURLSession*session=[NSURLSessionsessionWithConfiguration:[NSURLSessionConfigurationdefaultSessionConfiguration]];
  22. NSURLSessionDataTask*dataTask=[sessiondataTaskWithRequest:requestcompletionHandler:^(NSData*_Nullabledata,NSURLResponse*_Nullableresponse,NSError*_Nullableerror){
  23. @strongify(self)
  24. if([self->_taskVaildDicboolValueForKey:urlSchemeTask.descriptiondefault:NO]==NO||!urlSchemeTask){
  25. return;
  26. }
  27. [urlSchemeTaskdidReceiveResponse:response];
  28. [urlSchemeTaskdidReceiveData:data];
  29. if(error){
  30. [urlSchemeTaskdidFailWithError:error];
  31. }else{
  32. NSTimeIntervaldelta=CACurrentMediaTime()-self->start;
  33. NSLog(@"=======web请求结束地址%@:::%f",self.replacedStr,delta);
  34. [urlSchemeTaskdidFinish];
  35. }
  36. }];
  37. [dataTaskresume];
  38. [sessionfinishTasksAndInvalidate];
  39. }else{
  40. NSLog(@"filePath:%@",filePath);
  41. if(![self->_taskVaildDicboolValueForKey:urlSchemeTask.descriptiondefault:NO]||!urlSchemeTask||fileName.length==0){
  42. NSLog(@"return");
  43. return;
  44. }
  45. NSData*data=[NSDatadataWithContentsOfFile:filePathoptions:NSDataReadingMappedIfSafeerror:nil];
  46. [selfresendRequestWithUrlSchemeTask:urlSchemeTaskmimeType:[selfgetMIMETypeWithCAPIAtFilePath:filePath]requestData:data];
  47. }
  48. }
  49. -(void)resendRequestWithUrlSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask
  50. mimeType:(NSString*)mimeType
  51. requestData:(NSData*)requestDataAPI_AVAILABLE(ios(11.0)){
  52. if(![self->_taskVaildDicboolValueForKey:urlSchemeTask.descriptiondefault:NO]||!urlSchemeTask||!urlSchemeTask.request||!urlSchemeTask.request.URL){
  53. return;
  54. }
  55. NSString*mimeType_local=mimeType?mimeType:@"text/html";
  56. NSData*data=requestData?requestData:[NSDatadata];
  57. NSURLResponse*response=[[NSURLResponsealloc]initWithURL:urlSchemeTask.request.URL
  58. MIMEType:mimeType_local
  59. expectedContentLength:data.length
  60. textEncodingName:nil];
  61. [urlSchemeTaskdidReceiveResponse:response];
  62. [urlSchemeTaskdidReceiveData:data];
  63. [urlSchemeTaskdidFinish];
  64. }

整个过程中遇到的一些踩坑点

1. 'The task has already been stopped'崩溃问题

  • _taskVaildDic 是一个 NSMutableDictionary,它里面存的是以当前的 urlSchemeTask做 key,拦截开始时设置 YES,收到停止通知时设置 NO。这是由于在快速切换 webview 时,之前的 urlSchemeTask 已经停止但是后面再次调用了它的方法就会产生该崩溃。
  • 在实际使用过程中,用 bugly 监控到还是会有该崩溃发生,只不过次数特别少,一天约四五条左右。还在寻找问题的原因中。
  1. -(void)webView:(WKWebView*)webViewstartURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask
  2. API_AVAILABLE(ios(12.0)){
  3. dispatch_sync(self.serialQueue,^{
  4. [_taskVaildDicsetValue:@(YES)forKey:urlSchemeTask.description];
  5. });
  6. }
  7. -(void)webView:(nonnullWKWebView*)webViewstopURLSchemeTask:(nonnullid<WKURLSchemeTask>)urlSchemeTaskAPI_AVAILABLE(ios(12.0)){
  8. NSError*error=[NSErrorerrorWithDomain:urlSchemeTask.request.URL.absoluteStringcode:0userInfo:NULL];
  9. NSLog(@"weberror:%@",error);
  10. dispatch_sync(self.serialQueue,^{
  11. [self->_taskVaildDicsetValue:@(NO)forKey:urlSchemeTask.description];
  12. });
  13. }

2. WKWebview 的默认缓存策略问题

之前未考虑到 WKWebview 的默认缓存策略(WKWebView 默认缓存策略完全遵循 HTTP 缓存协议)。

在 h5 打包上线并更新离线包后,H5 的资源文件修改是变更 md5 文件名的。由于缓存策略默认时间是一个小时,会导致缓存的 url 加载不到修改后的 js,css 等文件(无论是本地离线包和远端服务器都已经没有这个 md5 文件)。

简单的解决方案是通过资源链接加版本号后缀,每次更新资源的时候变更版本号,在上面的代码中有做这部分处理。既保证了实时的更新,又保证了加载速度。

3. uni-app 图片 CDN 问题

做完上述的离线包优化后,发现新下载 APP 的情况,会偶发加载很慢问题。iOS 出现,但是 android 并未出现。

H5 部分是用 uni-app 开发的,所以发现这个问题后由前端同事修复后恢复正常。

4. chunk-vendors.js 文件过大

这个问题也是抓包发现的,在未打开离线包缓存开关时,发现h5的加载速度过慢,发现加载的 chunk-vendors.js 文件过大约 1.7M。 stopURLSchemeTask 方法里会报 error 错误信息 Error Domain= 的错误信息。也由前端同事处理了这个问题。

最终效果

统计了 APP 在不开离线包方案时,webview 平均加载时长在 1.5-2 秒的范围内(这里是计算的 webview开始加载到导航完成的时间),在上述优化完成后,打开的时长在 0.25-0.3 秒之间。

所以效果还是很显著的,用户的直观感受就是接近于秒开的体验。

总结

上面的优化过程中踩了很多坑,但是也重新梳理了 Webview 的加载过程,默认缓存策略机制等内容。上面的方案肯定不是最优的,只是一个快速达到 WKWebview 接近秒开效果的一个方案。

有什么更好的解决方案或者上述文中有不对的地方,希望大家指出,欢迎共同讨论~

原文链接:https://mp.weixin.qq.com/s/NHlwR5zPqO8aEpeRmKJ_Xg

如果您对该产品感兴趣,请填写办理(客服微信:xiaoxiongyidong)

为您推荐:

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。