iOS之网络请求NSURLSession剖析
2013
年的WWDC
大会上,苹果推出了NSURLSession
,对Foundation URL
加载系统进行了彻底的重构,提供了更丰富的API
来处理网络请求,如:支持http2.0
协议、直接把数据下载到磁盘、同一session
发送多个请求、下载是多线程异步处理和提供全局的session
并可以统一配置等等,提高了NSURLSession
的易用性、灵活性,更加地适合移动开发的需求。
NSURLSession的介绍
1. session类型
Default session
+defaultSessionConfiguration
返回一个标准的 configuration
,这个配置实际上与 NSURLConnection
的网络堆栈(networking stack
)是一样的,具有相同的共享 NSHTTPCookieStorage
,共享 NSURLCache
和共享 NSURLCredentialStorage
。
Ephemeral session
+ephemeralSessionConfiguration
返回一个预设配置,这个配置中不会对缓存Cookie
和证书进行持久性的存储,这对于实现像秘密浏览这种功能来说是很理想的。
Background session
+backgroundSessionConfiguration:(NSString *)identifier
的独特之处在于,它会创建一个后台 session
。后台 session
不同于常规的,普通的 session
,它甚至可以在应用程序挂起,退出或者崩溃的情况下进行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程。
2. 配置属性
基本配置
HTTPAdditionalHeaders
指定了一组默认的可以设置请求(outbound request
)的数据头。这对于跨 session
共享信息,如内容类型、语言、用户代理和身份认证,是很有用的。
// 设置请求的header
NSString *userPasswordString = [NSString stringWithFormat:@"%@:%@", user, password];
NSData * userPasswordData = [userPasswordString dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:@"Basic %@", base64EncodedCredential];
NSString *userAgentString = @"AppName/com.example.app (iPhone 5s; iOS 7.0.2; Scale/2.0)";
configuration.HTTPAdditionalHeaders = @{@"Accept": @"application/json",
@"Accept-Language": @"en",
@"Authorization": authString,
@"User-Agent": userAgentString};
networkServiceType
对标准的网络流量、网络电话、语音、视频,以及由一个后台进程使用的流量进行了区分。大多数应用程序都不需要设置这个。allowsCellularAccess
和discretionary
被用于节省通过蜂窝网络连接的带宽。对于后台传输的情况,推荐大家使用discretionary
这个属性,而不是allowsCellularAccess
,因为前者会把WiFi
和电源的可用性考虑在内。timeoutIntervalForRequest
和timeoutIntervalForResource
分别指定了对于请求和资源的超时间隔。许多开发人员试图使用timeoutInterval
去限制发送请求的总时间,但其实它真正的含义是:分组(packet
)之间的时间。实际上我们应该使用timeoutIntervalForResource
来规定整体超时的总时间,但应该只将其用于后台传输,而不是用户实际上可能想要去等待的任何东西。HTTPMaximumConnectionsPerHost
是Foundation
框架中URL
加载系统的一个新的配置选项。它曾经被NSURLConnection
用于管理私有的连接池。现在有了NSURLSession
,开发者可以在需要时限制连接到特定主机的数量。HTTPShouldUsePipelining
这个属性在NSMutableURLRequest
下也有,它可以被用于开启HTTP
管线化(HTTP pipelining
),这可以显着降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的。sessionSendsLaunchEvents
是另一个新的属性,该属性指定该session
是否应该从后台启动。connectionProxyDictionary
指定了session
连接中的代理服务器。同样地,大多数面向消费者的应用程序都不需要代理,所以基本上不需要配置这个属性。
Cookie 策略
HTTPCookieStorage
存储了session
所使用的cookie
。默认情况下会使用NSHTTPCookieShorage
的+sharedHTTPCookieStorage
这个单例对象,这与NSURLConnection
是相同的。HTTPCookieAcceptPolicy
决定了什么情况下session
应该接受从服务器发出的cookie
。HTTPShouldSetCookies
指定了请求是否应该使用session
存储的cookie
,即HTTPCookieSorage
属性的值。
安全策略
URLCredentialStorage
存储了session
所使用的证书。默认情况下会使用NSURLCredentialStorage
的+sharedCredentialStorage
这个单例对象,这与NSURLConnection
是相同的。TLSMaximumSupportedProtocol
和TLSMinimumSupportedProtocol
确定 `session 是否支持 SSL 协议。
缓存策略
URLCache
是session
使用的缓存。默认情况下会使用NSURLCache
的+sharedURLCache
这个单例对象,这与NSURLConnection
是相同的。requestCachePolicy
指定了一个请求的缓存响应应该在什么时候返回。这相当于NSURLRequest
的-cachePolicy
方法。
自定义协议
protocolClasses
用来配置特定某个 session
所使用的自定义协议(该协议是 NSURLProtocol
的子类)的数组。
3. NSURLSessionTask
NSURLsessionTask
是一个抽象类,其下有 3
个实体子类可以直接使用:NSURLSessionDataTask
、NSURLSessionUploadTask
、NSURLSessionDownloadTask
。这 3
个子类封装了现代程序三个最基本的网络任务:获取数据,比如 JSON
或者 XML
,上传文件和下载文件。
不同于直接使用 alloc-init
初始化方法,task
是由一个 NSURLSession
创建的。每个 task
的构造方法都对应有或者没有 completionHandler
这个 block
的两个版本。
4. 代理
针对NSURLsessionTask
的代理,根代理为NSURLSessionDelegate
,其它的代理直接或者间接继承自改代理,如:NSURLSessionTaskDelegate
、NSURLSessionDataDelegate
、NSURLSessionDownloadDelegate
。其中根代理NSURLSessionDelegate
主要处理鉴权、后台下载任务完成通知等等,NSURLSessionTaskDelegate
主要处理收到鉴权响应、任务结束(无论是正常还是异常),NSURLSessionDataDelegate
处理数据的接收、dataTask
转downloadTask
、缓存等,NSURLSessionDownloadDelegate
主要处理数据下载、数据进度通知等。
NSURLSession应用
1. NSURLSessionDataTask 发送 GET 请求
//确定请求路径
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520&pwd=520&type=JSON"];
//创建 NSURLSession 对象
NSURLSession *session = [NSURLSession sharedSession];
/**
根据对象创建 Task 请求,默认在子线程中解析数据
url 方法内部会自动将 URL 包装成一个请求对象(默认是 GET 请求)
completionHandler 完成之后的回调(成功或失败)
param data 返回的数据(响应体)
param response 响应头
param error 错误信息
*/
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:
^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//解析服务器返回的数据
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
//发送请求(执行Task)
[dataTask resume];
2. NSURLSessionDataTask 发送 POST 请求
//确定请求路径
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
//创建可变请求对象
NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
//修改请求方法
requestM.HTTPMethod = @"POST";
//设置请求体
requestM.HTTPBody = [@"username=520&pwd=520&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
//创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
//创建请求 Task
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM completionHandler:
^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//解析返回的数据
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
//发送请求
[dataTask resume];
3. NSURLSessionDataTask 设置代理发送请求
//确定请求路径
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
//创建可变请求对象
NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
//设置请求方法
requestM.HTTPMethod = @"POST";
//设置请求体
requestM.HTTPBody = [@"username=520&pwd=520&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
//创建会话对象,设置代理
/**
第一个参数:配置信息
第二个参数:设置代理
第三个参数:队列,如果该参数传递nil 那么默认在子线程中执行
*/
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self delegateQueue:nil];
//创建请求 Task
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM];
//发送请求
[dataTask resume];
代理方法:
-(void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask *)dataTask
didReceiveResponse:(nonnull NSURLResponse *)response
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
//子线程中执行
NSLog(@"接收到服务器响应的时候调用 -- %@", [NSThread currentThread]);
self.dataM = [NSMutableData data];
//默认情况下不接收数据
//必须告诉系统是否接收服务器返回的数据
completionHandler(NSURLSessionResponseAllow);
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSLog(@"接受到服务器返回数据的时候调用,可能被调用多次");
//拼接服务器返回的数据
[self.dataM appendData:data];
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"请求完成或者是失败的时候调用");
//解析服务器返回数据
NSLog(@"%@", [[NSString alloc] initWithData:self.dataM encoding:NSUTF8StringEncoding]);
}
设置代理之后的强引用问题
NSURLSession
对象在使用的时候,如果设置了代理,那么session
会对代理对象保持一个强引用,在合适的时候应该主动进行释放- 可以在控制器调用
viewDidDisappear
方法的时候来进行处理,通过调用invalidateAndCancel
方法或者是finishTasksAndInvalidate
方法来释放对代理对象的强引用。
其中,invalidateAndCancel
是直接取消请求然后释放代理对象,而finishTasksAndInvalidate
是等请求完成之后释放代理对象。
4. NSURLSessionDownloadTask 简单下载
//确定请求路径
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/images/minion_02.png"];
//创建请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
//创建会话请求
//优点:该方法内部已经完成了边接收数据边写沙盒的操作,解决了内存飙升的问题
NSURLSessionDownloadTask *downTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//默认存储到临时文件夹 tmp 中,需要剪切文件到 cache
NSLog(@"%@", location);//目标位置
NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]
stringByAppendingPathComponent:response.suggestedFilename];
/**
fileURLWithPath:有协议头
URLWithString:无协议头
*/
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
}];
//发送请求
[downTask resume];
以上方法无法监听下载进度,如要获取下载进度,可以使用代理的方式进行下载。
5. NSURLSessionDownloadTask 代理方式
NSURL * url = [NSURL URLWithString:@"http://e.hiphotos.baidu.com/image/pic/item/63d0f703918fa0ec14b94082249759ee3c6ddbc6.jpg"];
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate:self delegateQueue: [NSOperationQueue mainQueue]];
NSURLSessionDownloadTask * downloadTask =[ defaultSession downloadTaskWithURL:url];
[downloadTask resume];
代理方法:
// 接收数据,可能多次被调用
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
float progress = totalBytesWritten * 1.0/totalBytesExpectedToWrite;
// 主线程更新UI
dispatch_async(dispatch_get_main_queue(),^ {
[self.process setProgress:progress animated:YES];
});
}
// 3.下载完成之后调用该方法
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSString *catchDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [catchDir stringByAppendingPathComponent:@"app.dmg"];
NSError *fileError = nil;
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
[[NSFileManager defaultManager] moveItemAtURL:location toURL:fileURL error:&fileError];
if (fileError) {
NSLog(@"保存下载文件出错:%@", fileError);
} else {
NSLog(@"保存成功:%@", filePath);
}
}
暂停和恢复下载:
方式一:
// 暂停下载
- (IBAction)suspendDownload {
if (self.session) {
__weak typeof(self) weakSelf = self;
[self.task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
weakSelf.receivedData = resumeData;
}];
}
}
// 恢复下载
- (IBAction)resumeDownload {
if (self.session) {
self.task = [self.session downloadTaskWithResumeData:self.receivedData];
}
[self.task resume];
}
方式二:
//暂停
[self.downloadTask suspend];
//恢复
[self.downloadTask resume];
6. NSURLSessionDownloadTask 后台下载
// 后台session
- (NSURLSession* ) backgroundURLSession {
static NSURLSession * session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString * identifier = @"com.yourcompany.appId.BackgroundSession";
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
// 创建并启动任务
- (void)beginDownloadWithUrl:(NSString *)downloadURLString {
NSURL *downloadURL = [NSURL URLWithString:downloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
NSURLSession *session = [self backgroundURLSession];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
}
在appDelegate
中实现application:handleEventsForBackgroundURLSession:completionHandler:
方法,在后台所有的任务完成后会调用给方法,但是我一直没有调用成功,原因未知,高手可以告知一下
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
NSURLSession *backgroundSession = [self backgroundURLSession];
NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);
// 保存 completion handler 以在处理 session 事件后更新 UI
[self addCompletionHandler:completionHandler forSession:identifier];
}
handleEventsForBackgroundURLSession
方法是在后台下载的所有任务完成后才会调用。如果后台任务完成且应用被杀掉,启动应用程序后,该方法会在 application:didFinishLaunchingWithOptions:
方法被调用之后被调用。
//NSURLSessionDelegate委托方法,会在NSURLSessionDownloadDelegate委托方法后执行
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(@"Background URL session %@ finished events.\n", session);
}
之后会调用接收完成的方法:
/*
* 在该方法结束前,需要处理location指向的文件,因为方法结束后,临时文件会被销毁
* 如果用模拟器保存,会出错,因为模拟器上app退出后再启动是,路径会不一样,导致找不到后台下载的文件;而用真机调试则无此问题 !!!
*/
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{}
/*
* 该方法下载成功和失败都会回调,只是失败的是error是有值的,
* 在下载失败时,error的userinfo属性可以通过NSURLSessionDownloadTaskResumeData
* 这个key来取到resumeData(和上面的resumeData是一样的),再通过resumeData恢复下载
*/
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{}
7. NSURLSessionUploadTask上传任务
NSURL*URL = [NSURLURLWithString:@"http://example.com/upload"];
NSURLRequest*request = [NSURLRequestrequestWithURL:URL];
NSData*data = ...;
NSURLSession*session = [NSURLSessionsharedSession];
NSURLSessionUploadTask*uploadTask = [session uploadTaskWithRequest:request fromData:datacompletionHandler:^(NSData*data, NSURLResponse *response,NSError*error) {
// ...
}];
[uploadTask resume];
注意事项
1. 后台下载的配置和限制
作为一个必须实现的委托,您不能对NSURLSession
使用简单的基于 block
的回调方法。后台启动应用程序,是相对耗费较多资源的,所以总是采用HTTP
重定向。后台传输服务只支持HTTP
和HTTPS
,你不能使用自定义的协议。系统会根据可用的资源进行优化,在任何时候你都不能强制传输任务在后台进行。
另外,要注意的是在后台会话中,NSURLSessionDataTasks
是完全不支持的,你应该只出于短期的、小请求等使用这些任务,而不是用来下载或上传。
2. 后台启动新的下载
苹果会对后台的下载任务进行限制,大致流程如下:
- 苹果的
NSURLSession
这个类会维护一个Delay
值(即延时执行时间),用于后台启动任务延时执行时使用; - 当在后台启动一个新任务时,苹果会对这个任务进行延时执行,延时时间苹果那边是有一个默认的延时时间,当后台启动的任务数越多,这个值就会成
2
的N-1
幂倍增长; - 比如:假设苹果设定的延时时间为
Delay
。当在后台启动了第一个任务时,这个任务的延时时间为Delay
,这个任务会在Delay
时间后开始执行;当启动在后台启动第二个任务时,这个任务的延时时间为:2 * Delay
,当启动第三个任务是,该任务的延时执行时间即为:2 * 2 * Delay
;以此类推,在后台启动第N个任务是,该任务的延时执行时间为:2^(N-1)次方 * Delay
; - 但是在应用从后台切到前台或者重新启动时,这个延时时间会重置。
参考示例:
https://github.com/BirdandLion/NSURLSessionDemo
参考资料:
Life Cycle of a URL Session
http://www.jianshu.com/p/63e2ad28459f
http://www.jianshu.com/p/b0ddadd34037
http://www.jianshu.com/p/1211cf99dfc3
http://www.jianshu.com/p/02a5a896c9ed
iOS之网络请求NSURLSession剖析的更多相关文章
- ios htttp网络请求cookie的读取与写入(NSHTTPCookieStorage)
当你访问一个网站时,NSURLRequest都会帮你主动记录下来你访问的站点设置的Cookie,如果 Cookie 存在的话,会把这些信息放在 NSHTTPCookieStorage 容器中共享,当你 ...
- iOS 网络请求NSURLSession
iOS 7 和 Mac OS X 10.9 Mavericks 中一个显著的变化就是对 Foundation URL 加载系统的彻底重构. 现在已经有人在深入苹果的网络层基础架构的地方做研究了,所以我 ...
- iOS开发网络请求——大文件的多线程断点下载
iOS开发中网络请求技术已经是移动app必备技术,而网络中文件传输就是其中重点了.网络文件传输对移动客户端而言主要分为文件的上传和下载.作为开发者从技术角度会将文件分为小文件和大文件.小文件因为文件大 ...
- iOS - Alamofire 网络请求
前言 Alamofire 是 Swift 语言的 HTTP 网络开发工具包,相当于 Swift 实现 AFNetworking 版本.当然,AFNetworking 非常稳定,在 Mac OSX 与 ...
- iOS - AFNetworking 网络请求
前言 在 iOS 开发中,一般情况下,简单的向某个 Web 站点简单的页面提交请求并获取服务器的响应,用 Xcode 自带的 NSURLConnection 是能胜任的.但是,在绝大部分下我们所需要访 ...
- iOS - ASIHTTPRequest 网络请求
前言 使用 iOS SDK 中的 HTTP 网络请求 API,相当的复杂,调用很繁琐,ASIHTTPRequest 就是一个对 CFNetwork API 进行了封装,并且使用起来非常简单的一套 AP ...
- iOS - NSURLConnection 网络请求
前言 @interface NSURLConnection : NSObject class NSURLConnection : NSObject DEPRECATED: The NSURLConne ...
- Cocoa Touch(五):网络请求 NSURLSession/AFNetworking, GCD, NSURLResquest
NSURLRequest 网络请求的关键的就是NSURLRequest类,它的实例表示了请求报文实体以及请求的缓存策略等等,各种网络框架的最终目标都是把这个对象编译成为请求报文发送出去.下面用一个实例 ...
- iOS 多网络请求同步并发
iOS中经常会用到多线程,在多线程中有一个线程组的概念(group),创建多个线程组任务,多组任务都完成之后,就会进入dispatch_group_notify队列中. 同时多线程中还有一个信号量的概 ...
随机推荐
- mysql性能监控工具
参考文档: http://www.linuxidc.com/Linux/2012-09/70459.htm 1.记录慢查询SQL #配置开启 (linux)修改my.cnf: log-slow-que ...
- win10下安装python
1. 在官网下载python:https://www.python.org/ftp/python/3.5.2/python-3.5.2-amd64.exe 这里下载的是3.5.2版. 2. 双击exe ...
- JavaWeb 后端 <十二> 之 过滤器 filter 乱码、不缓存、脏话、标记、自动登录、全站压缩过滤器
一.过滤器是什么?有什么? 1.过滤器属于Servlet规范,从2.3版本就开始有了. 2.过滤器就是对访问的内容进行筛选(拦截).利用过滤器对请求和响应进行过滤
- 网络请求工具类WebServiceUtils
如果对WebService一无所知的话,建议先看看这两篇博客,对你WebService很有帮助. http://blog.csdn.NET/eyu8874521/article/details/912 ...
- JavaScript一个函数式编程-------求标准差
利用JavaScript中的map函数和reduce函数实现函数式编程. 注意: 输出都在浏览器的控制台中. 代码如下: <script type="text/javascript&q ...
- 面向对象15.3String类-常见功能-获取-1
API使用: 查API文档的时候,有很多方法,首先先看返回的类型 下面的方法函数有的是有覆写Object类的如1.1图,如果没有复写的话是写在1.2图片那里的,如果找到了相对于的方法,可以点击进去可以 ...
- MYSQL的日志与备份还原
一.错误日志 当数据库出现任何故障导致无法使用时,第一时间先去查看该日志 1.服务器启动关闭过程中的信息 2.服务器运行过程中的错误信息 日志存放路径,可以通过命令查看: 日志文件命名格式:host_ ...
- Android - AIDL 使用
AIDL(Android Interface Definition Language) 程序员可以利用AIDL自定义编程接口,在客户端和服务端之间实现进程间通信(IPC).在Android平台上,一个 ...
- Android5.1 - 通讯录建立群组
[问题] 在没有账户的时候,不应该有添加联系人群组的选项. 我们要把这个选项干掉. [相关log]06-23 17:25:00.804: E/GroupEditorFragment(6030): No ...
- nyoj_7:街区最短路径问题
做这题时,先假设目标点在某个位置,然后对其稍微移动dx,dy,分析对ans的影响.最终得,选点时,使一半的横坐标比目标点横坐标小,一半的纵坐标比目标点小,这样得到的ans最小. 题目链接: http: ...