AFNetworking到底长啥样(下)

AFNetworking到底长啥样(上)中简单介绍了AFN涉及的主要类及其结构,接下来以一个简单的POST请求探寻其内部是如何实现的。

一、环境搭建

  1. 服务器配置

    本例中直接使用iMac自带的Apache,并为其开启PHP支持。在服务器目录下编写index.php文件如下:

    <?php
    echo @"This is Layne‘s Response";
    ?>
  2. 编写测试App

    创建一个测试App,在主界面上增加一个按钮,在按钮的点击函数中发起网络请求,如下:

    - (AFHTTPSessionManager *)manager{//lazy
       if(!_manager){
           _manager = [AFHTTPSessionManager manager];
       }
       return _manager;
    }
    
    - (void)click{
       [self.manager POST:@"http://www.layne.com"
               parameters:@{@"name":@"layne",@"age":@30}
                  headers:@{@"TestName":@"myTest"}
                 progress:nil
            success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                    NSLog(@"success:%@",responseObject);
            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                    NSLog(@"fail:%@",error);
       }];
    }

二、一个POST请求的前世今生

启动测试App,点击按钮。接下里让我们看看AFN是如何优雅的管理网络请求的。

1.初始化

  • AFHTTPSessionManager的初始化

    self.managerAFHTTPSessionManager的实例,它使用类方法+ manager初始化。这个类方法最终调用如下方法:

    - (instancetype)initWithBaseURL:(NSURL *)url
             sessionConfiguration:(NSURLSessionConfiguration *)configuration
    {
      self = [super initWithSessionConfiguration:configuration];//①configuration=nil
      if (!self) {
          return nil;
      }
    
      // Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected
      if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
          url = [url URLByAppendingPathComponent:@""];
      }
    
      self.baseURL = url;
    
      self.requestSerializer = [AFHTTPRequestSerializer serializer];//②
      self.responseSerializer = [AFJSONResponseSerializer serializer];//③
    
      return self;
    }

    ①中调用父类的方法初始化父类相关属性,这包括:

    operationQueue
    responseSerializer(为AFJSONResponseSerializer实例)
    securityPolicy(为默认,即不进行SSL验证)
    reachabilityManager
    mutableTaskDelegatesKeyedByTaskIdentifier(管理{taskID:delegate})
    lock(用于操作mutableTaskDelegatesKeyedByTaskIdentifier时保证线程安全)
    session(NSURLSession实例,设置其代理为self,queue为operationQueue)

    ②中设置其requestSerializer为AFHTTPRequestSerializer实例,③重设其responseSerializer为AFJSONResponseSerializer实例。

  • AFHTTPRequestSerializer的初始化

    requestSerializer被设置成了AFHTTPRequestSerializer的实例,初始化是通过类方法+serializer实现的。内容包括:

    stringEncoding(为NSUTF8StringEncoding)
    mutableHTTPRequestHeaders(保存用户设置的header,默认会添加"Accept-Language"和"User-Agent",其中"User-Agent"还进行了ICU转换以保证必须为ASCII字符)
    HTTPMethodsEncodingParametersInURI(为GET、HEAD、DELETE)
    mutableObservedChangedKeyPaths (监听"allowsCellularAccess"、"cachePolicy"、"HTTPShouldHandleCookies"、"HTTPShouldUsePipelining"、"networkServiceType"、"timeoutInterval",一旦用户设置了这6个属性,则对应属性的名会被存入,最后其值会加到request中去,本质是增量添加:“谁改变添加谁”)
  • AFJSONResponseSerializer的初始化

    responseSerializer被设置成为了AFJSONResponseSerializer的实例,初始化是通过类方法+serializer实现的。内容包括:

    readingOptions(为0)
    acceptableStatusCodes(可接受的code,初始化为[200~199])
    acceptableContentTypes(可接受的contenttype,设置为application/json、text/json、text/javascript)
    注:其父类AFHTTPResponseSerializer的acceptableContentTypes=nil,即接受任何Content。

至此必要的初始化已完成。

2.发起请求

(1) POST

进入到POST函数中:

- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                             parameters:(nullable id)parameters
                                headers:(nullable NSDictionary <NSString *, NSString *> *)headers
                               progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
                                success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure
{
    NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST" URLString:URLString parameters:parameters headers:headers uploadProgress:uploadProgress downloadProgress:nil success:success failure:failure];

    [dataTask resume];

    return dataTask;
}

即最终生成一个dataTask,然后resume启动,并将dataTask返回。

(2) 生成dataTask

生成的dataTask是通过如下函数实现的,本例中传入的参数值也已标明:

-  dataTaskWithHTTPMethod:  //@"POST"
                URLString:  //@"http://www.layne.com"
               parameters:  //@{@"name":@"layne",@"age":@30}
                  headers:  //@{@"TestName":@"myTest"}
           uploadProgress:  //nil
         downloadProgress:  //nil
                  success:  //successBlock{NSLog(@"success:%@",responseObject);}
                  failure:  //failureBlock{NSLog(@"fail:%@",error);}

其内部执行的步骤如下:

Step1:调用requestSerializer的如下方法创建mutableRequest:

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method   //@"POST"
                                 URLString:(NSString *)URLString//@"http://www.layne.com"
                                parameters:(id)parameters//@{@"name":@"layne",@"age":@30}
                                     error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(URLString);

    NSURL *url = [NSURL URLWithString:URLString];

    NSParameterAssert(url);

    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    mutableRequest.HTTPMethod = method;//设置为POST

    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }//根据用户是否设置了allowsCellularAccess、cachePolicy、HTTPShouldHandleCookies、HTTPShouldUsePipelining、networkServiceType、timeoutInterval来设置mutableRequest对应字段值。本例中由于未进行任何设置,因此上述逻辑不会有效果。

    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];//①

    return mutableRequest;
}

①是调用AFURLRequestSerialization协议方法对mutableRequest进行进一步处理,包括:

  • 设置默认header,如在requestSerializer初始化时设置默认header:User-AgentAccept-Language

    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
          if (![request valueForHTTPHeaderField:field]) {
              [mutableRequest setValue:value forHTTPHeaderField:field];
          }
    }];
  • 处理参数

    若用户自定义了处理参数的block(self.queryStringSerialization),则使用该block;否则使用默认的处理方式,会被最终处理成如下格式:name=layne&age=30

  • 根据HTTPMethod决定参数串放到哪里:GET/HEAD/DELETE直接拼接到url;其他(如POST)方法放到HTTPBody中(使用stringEncoding(NSUTF8StringEncoding)编码),并设置Content-Typeapplication/x-www-form-urlencoded

至此,生成的mutableRequest结构如下:

mutableRequest{
URL:          "http://www.layne.com"
Method:       "POST"
Header:       "Accept-Language" = "en;q=1";
              "User-Agent" = "TestApp/1.0 (iPhone; iOS 13.1; Scale/3.00)";
              "Content-Type" = "application/x-www-form-urlencoded";
HttpBody      <6167653d 3330266e 616d653d 6c61796e 65>  //name=layne&age=30(utf-8)
}

Step2:合并传入的header

for (NSString *headerField in headers.keyEnumerator) {
        [request addValue:headers[headerField] forHTTPHeaderField:headerField];
}

执行之后mutableRequest结构如下:

mutableRequest{
URL:          "http://www.layne.com"
Method:       "POST"
Header:       "Accept-Language" = "en;q=1";
              "User-Agent" = "TestApp/1.0 (iPhone; iOS 13.1; Scale/3.00)";
              "Content-Type" = "application/x-www-form-urlencoded";
              "TestName":"myTest";
HttpBody      <6167653d 3330266e 616d653d 6c61796e 65>  //name=layne&age=30(utf-8)
}

Step3:判断Step1中生成mutableRequest的过程是否出错,若出错,则调用failureBlock并返回。

if (serializationError) {
        if (failure) {
            dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
                failure(nil, serializationError);
            });
        }
        return nil;
 }

Step4:根据mutableRequest生成dataTask。

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request //request
                               uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock//nil
                             downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock//nil
                            completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject,  NSError * _Nullable error))completionHandler /*block{
                                  if (error) {
                                      if (failure) {
                                          failure(dataTask, error);
                                      }
                                  } else {
                                      if (success) {
                                          success(dataTask, responseObject);
                                      }
                                  }
                                             }*/
{

    __block NSURLSessionDataTask *dataTask = nil;
    url_session_manager_create_task_safely(^{ //①
        dataTask = [self.session dataTaskWithRequest:request];
    });

    [self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler];//②

    return dataTask;//③
}

说明:

①中不直接生成dataTask,是因为在iOS8以下的系统上,若是在并行队列上创建dataTask会导致completionHandler调用出错。因此为了解决这个问题,针对iOS8以下系统,AFN使用自己维护的一个串行队列来创建dataTask。具体问题描述如下:

Due to this bug: http://openradar.appspot.com/radar?id=5871104061079552 in NSURLSessionTask, creating tasks on a concurrent queue can cause incorrect completionHandlers to get called.

When a duplicate taskIdentifier is returned by the task, the previous completionHandler gets cleared out and replaced with the new one. If the data for the first request comes back before the second request‘s data, the first response is then called against the second completionHandler.

I‘m not sure what AFNetworking should do here — it could enforce creating tasks on a serial queue or it could just advise people to do so. We could also add a test to assert that the taskIdentifier is not a duplicate?

②的作用是为dataTask生成对应的delegate,以调用回调方法。

- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask //dataTask
                uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock//nil
              downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock//nil
             completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler/*block{
                                  if (error) {
                                      if (failure) {
                                          failure(dataTask, error);
                                      }
                                  } else {
                                      if (success) {
                                          success(dataTask, responseObject);
                                      }
                                  }
                                             }*/
{
    AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] initWithTask:dataTask];//a
    delegate.manager = self;//b
    delegate.completionHandler = completionHandler;//c

    dataTask.taskDescription = self.taskDescriptionForSessionTasks;//d
    [self setDelegate:delegate forTask:dataTask];//e

    delegate.uploadProgressBlock = uploadProgressBlock;//nil
    delegate.downloadProgressBlock = downloadProgressBlock;//nil
}

a. 为dataTask生成AFURLSessionManagerTaskDelegate实例。delegate内部维护着:

一个mutableData用来保存收到的response data。

一个uploadProgress和downloadProgress用来标明上传/下载进度。它们的cancel、suspend和resume操作与dataTask进行了绑定,即通过对它们进行cancel、suspend和resume操作就可以操作dataTask的cancel、suspend和resume。此外,还采用KVO监听uploadProgress和downloadProgress的进度变化,从而调用用户自定义的block:uploadProgressBlockdownloadProgressBlock

b. delegate弱引用当前的manager。

c.delegate保存完成回调。

③返回最终的dataTask,并启动。

(3) 处理Response

AFN是基于NSURLSession的,涉及以下几个协议:

NSURLSessionDelegate

 - URLSession:didBecomeInvalidWithError:
 - URLSession:didReceiveChallenge:completionHandler:
 - URLSessionDidFinishEventsForBackgroundURLSession:

NSURLSessionTaskDelegate

 - URLSession:willPerformHTTPRedirection:newRequest:completionHandler:
 - URLSession:task:didReceiveChallenge:completionHandler:
 - URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
 - URLSession:task:needNewBodyStream:
 - URLSession:task:didCompleteWithError:
 - URLSession:task:didFinishCollectingMetrics:

NSURLSessionDataDelegate

 - URLSession:dataTask:didReceiveResponse:completionHandler:
 - URLSession:dataTask:didBecomeDownloadTask:
 - URLSession:dataTask:didReceiveData:
 - URLSession:dataTask:willCacheResponse:completionHandler:

NSURLSessionDownloadDelegate

 - URLSession:downloadTask:didFinishDownloadingToURL:
 - URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:
 - URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:

回调调用顺序:

① URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:

由于使用的是POST方式,因此该回调是首先调用的。逻辑:找到task对应的delegate=>调用delegate的URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:来更新delegate维护的uploadProgress。若用户自定义了taskDidSendBodyData block,则调用。

② URLSession:task:didFinishCollectingMetrics:

该方法调用时机不定,用来收集整个请求的信息。逻辑:找到task对应的delegate=>调用delegate的URLSession:task:didFinishCollectingMetrics:来为delegate.sessionTaskMetrics赋值。若用户自定义了taskDidFinishCollectingMetrics block,则调用。

③ URLSession:dataTask:didReceiveData:
收到response data时执行。逻辑:找到task对应的delegate=>调用delegate的URLSession:task:didCompleteWithError:来更新delegate内部维护的downloadProgress并使用mutableData保存response data。若用户自定义了dataTaskDidReceiveData block,则调用。

④ URLSession:task:didCompleteWithError:

这是最后执行的回调。逻辑:找到task对应的delegate=>调用delegate的URLSession:task:didCompleteWithError:,并删除delegate和task的对应关系。若用户自定义了taskDidComplete block,则调用。

数据的处理主要在delegate的URLSession:task:didCompleteWithError:中,主要做了以下几件工作:

  • 构造userInfo字典:

    userInfo{
    AFNetworkingTaskDidCompleteResponseSerializerKey:<AFJSONResponseSerializer: 0x600003db6820>,
    AFNetworkingTaskDidCompleteSessionTaskMetrics:"...",
    AFNetworkingTaskDidCompleteResponseDataKey:"<length=24,bytes=0x567283d>"//response data
    /*若出错则会包含以下key*/
    AFNetworkingTaskDidCompleteErrorKey:"error" //error数据
    }
  • 若出错,则直接调用completeHandler回调,之后将上面的userInfo以通知AFNetworkingTaskDidCompleteNotification的形式广播出去。
  • 若未出错,则使用responseSerializer(AFJSONResponseSerializer实例)将data转换为NSDictionary。若转换过程未出错,则在userInfo中增加{AFNetworkingTaskDidCompleteSerializedResponseKey:responseObject};若出错,则增加{AFNetworkingTaskDidCompleteErrorKey:error}。之后调用completeHandler回调,并将userInfo以通知AFNetworkingTaskDidCompleteNotification的形式广播出去。

经过上面的处理,最终要么error要么responseObject会返回到POST的回调中去。整个网络请求就完成了。

三、AFNetworking中的干货

上面我们跟着一个简单的POST请求了解了AFN的整个工作流程,但还有一些细节我觉得还是值得我们学习的。

  1. 如果response不是标准格式的JSON数据或者我们需要对原始data进行加密解密操作该如何做?

    答:将responseSerializer设置为AFHTTPResponseSerializer的实例,这样response data 会以原始data的形式返回给上层。AFHTTPResponseSerializer仅针对code和contenttype进行校验,而默认的AFJSONResponseSerializer除了校验code和contenttype之外还对JSON格式进行校验,并直接解析成NSDictionary。当然,如果想对response data想要做最全面的自定义处理,最直接的方式当然是自定义AFHTTPResponseSerializer的子类,并重写协议方法

    - (id)responseObjectForResponse:(NSHTTPURLResponse *)response
                              data:(NSData *)data
                             error:(NSError *__autoreleasing *)error
  2. 如果返回的JSON数据中包含NSNull数据该如何处理?

    答:将responseSerializer(AFJSONResponseSerializer实例)的removesKeysWithNullValues属性设置为YES。这样一来在JSON转换为NSDictionary之后会将NSDictionary中的NSNull值去除。

  3. 如果我想自己更改请求参数的格式(即不用默认的name=layne&age=30这种)该如何设置?

    答:调用requestSerializer的-setQueryStringSerializationWithBlock:设置自定义的格式。在requestSerializer处理参数的时候会先去判断queryStringSerialization block是否为空,若不为空,则使用该block处理参数。若为空,则使用默认的格式(如name=layne&age=30)生成参数串。

  4. 除了使用GET/POST方法的回调,还可以通过什么方式获得网络请求结果?

    答:还可以使用notification。AFN中有一个名为AFNetworkingTaskDidCompleteNotification的通知,可以通过监听该通知获取网络请求结果。AFURLSessionManagerTaskDelegate的URLSession:task:didCompleteWithError:中在调用completeHandler之后会发送通知:

    dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
                   if (self.completionHandler) {
                       self.completionHandler(task.response, responseObject, serializationError);//回调
                   }
    
                   dispatch_async(dispatch_get_main_queue(), ^{
                       [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];//将task和userInfo一起广播出去
                   });
               });
  5. 如何更改回调所在的线程?

    答:指定manager的completeQueue。这样回调就会在其他线程中执行。

    _manager.completionQueue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
  6. AFURLSessionManager如何获取NSURLSession维护的task?

    答:使用getTasksWithCompletionHandler:API并使用信号量保证线程安全。

    - (NSArray *)tasksForKeyPath:(NSString *)keyPath { //tasks、uploadTask、downloadTasks
       __block NSArray *tasks = nil;
       dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
       [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
           if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
               tasks = dataTasks;
           } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
               tasks = uploadTasks;
           } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
               tasks = downloadTasks;
           } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
               tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
           }
    
           dispatch_semaphore_signal(semaphore);
       }];
    
       dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
       return tasks;
    }
  7. 若我想取消一个刚发起的网络请求该如何做?

    答:AFHTTPSessionManager的POST/GET方法返回的是task对象,直接[task cancel]即可。如:

    NSURLSessionDataTask *task = [self.manager POST:@"http://www.layne.com"
               parameters:@{@"name":@"layne",@"age":@30}
                  headers:@{@"TestName":@"myTest"}
                 progress:nil
            success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                    NSLog(@"success:%@",responseObject);
            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                    NSLog(@"fail:%@",error);
       }];
    [task cancel];//直接cancel
  8. 若我想取消所有task该如何做?

    答:使用如下方法。建议resetSession传入YES将session重置,否则下次网络请求会crash,并提示:“Attempted to create a task in a session that has been invalidated

    - (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks resetSession:(BOOL)resetSession {
       if (cancelPendingTasks) {
           [self.session invalidateAndCancel];
       } else {
           [self.session finishTasksAndInvalidate];
       }
       if (resetSession) {
           self.session = nil;
       }
    }
    例如:
    [self.manager invalidateSessionCancelingTasks:YES resetSession:YES];

至此AFNetworking是如何工作的我们就知道了。看了源码之后不得不感叹作者真是神人,不仅优雅的给出了NSURLSession的使用范例,而且还包含了业务层面的巧妙设计。嗯,阅读源码使我快乐^_^。

原文地址:https://blog.51cto.com/laynestone/2455938

时间: 2024-10-08 20:28:35

AFNetworking到底长啥样(下)的相关文章

Linux 内核到底长啥样

今天,我来为大家解读一幅来自 TurnOff.us 的漫画 “InSide The Linux Kernel” . TurnOff.us 是一个极客漫画网站,作者Daniel Stori 画了一些非常有趣的关于编程语言.Web.云计算.Linux 相关的漫画.今天解读的便是其中的一篇. 在开始,我们先来看看这幅漫画的全貌! 这幅漫画是以一个房子的侧方刨面图来绘画的.使用这样的一个房子来代表 Linux 内核. 地基 作为一个房子,最重要的莫过于其地基,在这个图片里,我们也从最下面的地基开始看起:

从2018数博会看华为云智能“黑土地”长啥样?

(上图为华为云BU总裁郑叶来) "未来二三十年,人类将进入智能社会."2018年3月,任正非在华为产品与解决方案.2012实验室管理团队座谈会上讲话,强调:"(华为)要成为智能社会的使能者和推动者,这将是一个持久的.充满挑战的历史过程."其中,华为的研发"要坚持客户需求和技术创新双轮驱动,打造强大的'基础平台',这个基础平台就像东北的黑土地." "黑土地"一说,最早源自任正非在2017年底的公司内部邮件,重点提到华为未来的使命

听你声音6秒,AI就能推断你长啥样了!| 技术前沿洞察

大家好,本周的技术前沿又跟大家见面了!就在大家震惊加水就能跑的汽车时候,本周北美高校乃至全球高校都有一些让小探震惊不已的新研究成果...... 比如,AI 能够通过你的声音,推测出你长啥样!(当然,现在好像亚洲人还无法做到......); 只需要一滴唾液或者汗液,就能诊断你的压力有多大...(老板,请你一定要看到这条,说明我请假不是假的--) 到底是怎样发生的?硅谷洞察赶紧带你来看! MIT:听音辨别人的长相 近日,麻省理工学院人工智能实验室(MIT CSAIL)发布了一项令人惊讶的研究--只需

未来智能互联汽车长啥样

新霸哥发现,现实生活中我们常用的手机和生活中使用的汽车好像没有任何的交集硬件,但是在未来的移动智能互联的大潮中就会有充分的融合的效果,可是未来智能互联汽车长啥样,下面新霸哥将详细的为你介绍未来智能互联汽车技术,以及消费等等. 未来汽车的新技术 不久的将来车可能成为我们生活中最重要的一部分,车将变成和我们住房一样不可缺少的必需品了.全自动,无人驾驶是不是很先进,同时能够自动的学习,修复,配置,集成,还有一项重要的工具就是可以自主社交. 车与车联网的其他职能设备自动的完成信息集成,能够将信息自动配置

价值1400美元的CEH(道德黑客)认证培训课程长啥样?(2)实验室

价值1400美元的CEH(道德黑客)认证培训课程长啥样?(2)实验室 这是我收到的CEH官方发来的邮件,参加CEH认证培训原价为1424.25刀,可以给我便宜到1282刀.只有一个感觉,心在流血.站在这价值1200刀的血泊中,给大家介绍下CEH的课程体系,感谢我的无私吧. 上一节价值1400美元的CEH(道德黑客)认证培训课程长啥样?(1)课程体系中,和大家过了一下CEH的课程内容,本节主要探讨一下它的实验环境 2.1 基础环境 CEH课程实验环境的载体系统为Windows,其他系统如Kali

UFO长啥样?--Python数据分析来告诉你

前言 真心讲,长这么大,还没有见过UFO长啥样,偶然看到美国UFO报告中心有关于UFO时间记录的详细信息,突然想分析下这些记录里都包含了那些有趣的信息,于是有了这次的分析过程. 当然,原始数据包含的记录信息比较多,我只是进了了比较简单的分析,有兴趣的童鞋可以一起来分析,别忘了也给大家分享下您的分析情况哦. 本次分析的主要内容涉及以下几个方面: UFO长啥样? UFO在哪些地方出现的次数较多? UFO在哪些年份出现的次数较多? 热力图同时显示哪些州和哪些年UFO出现次数最多 import pand

优质“工作流”长啥样?

工作流真正的价值是帮助企业梳理流程,提高执行力,通过记录事实.监督执行.跟踪处理过程. 分析流程瓶颈.持续改善而达到一个真正的执行能力,可是一些特权思想下产品的需求却从另一个角度上是阻碍了流程的优化和执行力的提升. 事实上中国处于发展中国家,很多企业,特别是国企.政府单位对流程的价值认识上处于一个成长的空间,对于开发人员来说特权思想下的需求也经常是不得不提供 的,E8工作流从实践中总结了非常多的经验,它的策略是特殊的需求作为特殊权限来实现,缺省不提供,也不建议这样的配置,但可以先让用户看到部分的

比特币到底长什么模样?

"区块链"."比特币"这两个概念在2017年非常热.但是,2018年这个概念可能会更热. 各大互联网公司相继发力区块链:百度推出了基于区块链的莱茨狗游戏,网易推出了基于区块链的星球,阿里巴巴在区块链的专利申请已经是国内企业的第一位. 区块链发源于比特币,那么比特币到底长什么样呢?下面,我们就一起来看看比特币的真容! 上面的图片当然不是真的比特币.比特币只是一串数字而已. 01 创世区块 2007年,中本聪提出: "我相信存在一种不依赖信用的货币,我无法阻止

AFNetworking到底做了什么?

前言 作为一个iOS开发,也许你不知道NSUrlRequest.不知道NSUrlConnection.也不知道NSURLSession...(说不下去了...怎么会什么都不知道...)但是你一定知道AFNetworking. 大多数人习惯了只要是请求网络都用AF,但是你真的知道AF做了什么吗?为什么我们不用原生的NSURLSession而选择AFNetworking? 本文将从源码的角度去分析AF的实际作用. 先从最新的AF3.x讲起吧: 首先,我们就一起分析一下该框架的组成. 将AF下载导入工