使用缓存的目的是为了使应用程序能更快速的响应用户输入,是程序高效的运行。有时候我们需要将远程web服务器获取的数据缓存起来,以空间换取时间,减少对同一个url多次请求,减轻服务器的压力,优化客户端网络,让用户体验更良好。

背景:NSURLCache : 在iOS5以前,apple不支持磁盘缓存,在iOS5的时候,允许磁盘缓存,(NSURLCache 是根据NSURLRequest 来实现的)只支持http,在iOS6以后,支持http和https。

缓存的实现说明:由于GET请求一般用来查询数据,POST请求一般是发大量数据给服务器处理(变动性比较大),因此一般只对GET请求进行缓存,而不对POST请求进行缓存。

缓存原理:一个NSURLRequest对应一个NSCachedURLResponse

缓存技术:把缓存的数据都保存到数据库中。

NSURLCache的常见用法:

(1)获得全局缓存对象(没必要手动创建)NSURLCache *cache = [NSURLCache sharedURLCache];

(2)设置内存缓存的最大容量(字节为单位,默认为512KB)- (void)setMemoryCapacity:(NSUInteger)memoryCapacity;

(3)设置硬盘缓存的最大容量(字节为单位,默认为10M)- (void)setDiskCapacity:(NSUInteger)diskCapacity;

(4)硬盘缓存的位置:沙盒/Library/Caches

(5)取得某个请求的缓存- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;

(6)清除某个请求的缓存- (void)removeCachedResponseForRequest:(NSURLRequest *)request;

(7)清除所有的缓存- (void)removeAllCachedResponses;

缓存GET请求:

  要想对某个GET请求进行数据缓存,非常简单

  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

  // 设置缓存策略

  request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;

  只要设置了缓存策略,系统会自动利用NSURLCache进行数据缓存

iOS对NSURLRequest提供了7种缓存策略:(实际上能用的只有4种)

NSURLRequestUseProtocolCachePolicy // 默认的缓存策略(取决于协议)

NSURLRequestReloadIgnoringLocalCacheData // 忽略缓存,重新请求

NSURLRequestReloadIgnoringLocalAndRemoteCacheData // 未实现

NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData // 忽略缓存,重新请求

NSURLRequestReturnCacheDataElseLoad// 有缓存就用缓存,没有缓存就重新请求

NSURLRequestReturnCacheDataDontLoad// 有缓存就用缓存,没有缓存就不发请求,当做请求出错处理(用于离线模式)

NSURLRequestReloadRevalidatingCacheData // 未实现

缓存的注意事项:

缓存的设置需要根据具体的情况考虑,如果请求某个URL的返回数据:

  (1)经常更新:不能用缓存!比如股票、彩票数据

  (2)一成不变:果断用缓存

  (3)偶尔更新:可以定期更改缓存策略 或者 清除缓存

提示:如果大量使用缓存,会越积越大,建议定期清除缓存

NSURLCache的属性介绍:

//获取当前应用的缓存管理对象
+ (NSURLCache *)sharedURLCache;
//设置自定义的NSURLCache作为应用缓存管理对象
+ (void)setSharedURLCache:(NSURLCache *)cache;
//初始化一个应用缓存对象
/*
memoryCapacity 设置内存缓存容量
diskCapacity 设置磁盘缓存容量
path 磁盘缓存路径
内容缓存会在应用程序退出后 清空 磁盘缓存不会
*/
- (instancetype)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(nullable NSString *)path;
//获取某一请求的缓存
- (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
//给请求设置指定的缓存
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
//移除某个请求的缓存
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
//移除所有缓存数据
- (void)removeAllCachedResponses;
//移除某个时间起的缓存设置
- (void)removeCachedResponsesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);
//内存缓存容量大小
@property NSUInteger memoryCapacity;
//磁盘缓存容量大小
@property NSUInteger diskCapacity;
//当前已用内存容量
@property (readonly) NSUInteger currentMemoryUsage;
//当前已用磁盘容量
@property (readonly) NSUInteger currentDiskUsage; 与HTTP服务器进行交互的简单说明:

  Cache-Control头

在第一次请求到服务器资源的时候,服务器需要使用Cache-Control这个响应头来指定缓存策略,它的格式如下:Cache-Control:max-age=xxxx,这个头指指明缓    存过期的时间

Cache-Control头具有如下选项:

  • public: 指示可被任何区缓存

  • private

  • no-cache: 指定该响应消息不能被缓存

  • no-store: 指定不应该缓存

  • max-age: 指定过期时间

  • min-fresh:

  • max-stable:

Last-Modified/If-Modified-Since

Last-Modified 是由服务器返回响应头,标识资源的最后修改时间.

If-Modified-Since 则由客户端发送,标识客户端所记录的,资源的最后修改时间。服务器接收到带有该请求头的请求时,会使用该时间与资源的最后修改时间进行对比,如果发现资源未被修改过,则直接返回HTTP 304而不返回包体,告诉客户端直接使用本地的缓存。否则响应完整的消息内容。

Etag/If-None-Match

Etag 由服务器发送,告之当资源在服务器上的一个唯一标识符。

客户端请求时,如果发现资源过期(使用Cache-Control的max-age),发现资源具有Etag声明,这时请求服务器时则带上If-None-Match头,服务器收到后则与资源的标识进行对比,决定返回200或者304。

文件缓存:借助ETag或Last-Modified判断文件缓存是否有效

Last-Modified

服务器的文件存贮,大多采用资源变动后就重新生成一个链接的做法。而且如果你的文件存储采用的是第三方的服务,比如七牛、青云等服务,则一定是如此。

这种做法虽然是推荐做法,但同时也不排除不同文件使用同一个链接。那么如果服务端的file更改了,本地已经有了缓存。如何更新缓存?

这种情况下需要借助 ETag 或 Last-Modified 判断图片缓存是否有效。

Last-Modified 顾名思义,是资源最后修改的时间戳,往往与缓存时间进行对比来判断缓存是否过期。

在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式类似这样:

        Last-Modified: Fri, 12 May 2006 18:53:33 GMT

客户端第二次请求此URL时,根据 HTTP 协议的规定,浏览器会向服务器传送 If-Modified-Since 报头,询问该时间之后文件是否有被修改过:

        If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT

总结下来它的结构如下:

请求 HeaderValue 响应 HeaderValue
Last-Modified If-Modified-Since

如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。

判断方法用伪代码表示:

if ETagFromServer != ETagOnClient || LastModifiedFromServer != LastModifiedOnClient
GetFromServer
else
GetFromCache

之所以使用

LastModifiedFromServer != LastModifiedOnClient

而非使用:

LastModifiedFromServer > LastModifiedOnClient

原因是考虑到可能出现类似下面的情况:服务端可能对资源文件,废除其新版,回滚启用旧版本,此时的情况是:

LastModifiedFromServer <= LastModifiedOnClient

但我们依然要更新本地缓存。

实例:

/*!
@brief 如果本地缓存资源为最新,则使用使用本地缓存。如果服务器已经更新或本地无缓存则从服务器请求资源。 @details 步骤:
1. 请求是可变的,缓存策略要每次都从服务器加载
2. 每次得到响应后,需要记录住 LastModified
3. 下次发送请求的同时,将LastModified一起发送给服务器(由服务器比较内容是否发生变化) @return 图片资源
*/
- (void)getData:(GetDataCompletion)completion {
NSURL *url = [NSURL URLWithString:kLastModifiedImageURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // // 发送 etag
// if (self.etag.length > 0) {
// [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
// }
// 发送 LastModified
if (self.localLastModified.length > 0) {
[request setValue:self.localLastModified forHTTPHeaderField:@"If-Modified-Since"];
} [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // NSLog(@"%@ %tu", response, data.length);
// 类型转换(如果将父类设置给子类,需要强制转换)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判断响应的状态码是否是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips)
if (httpResponse.statusCode == 304) {
NSLog(@"加载本地缓存图片");
// 如果是,使用本地缓存
// 根据请求获取到`被缓存的响应`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到缓存的数据
data = cacheResponse.data;
} // 获取并且纪录 etag,区分大小写
// self.etag = httpResponse.allHeaderFields[@"Etag"];
// 获取并且纪录 LastModified
self.localLastModified = httpResponse.allHeaderFields[@"Last-Modified"];
// NSLog(@"%@", self.etag);
NSLog(@"%@", self.localLastModified);
dispatch_async(dispatch_get_main_queue(), ^{
!completion ?: completion(data);
});
}] resume];
}

ETag

ETag 是什么?

HTTP 协议规格说明定义ETag为“被请求变量的实体值” (参见 —— 章节 14.19)。 另一种说法是,ETag是一个可以与Web资源关联的记号(token)。它是一个 hash 值,用作 Request 缓存请求头,每一个资源文件都对应一个唯一的 ETag 值,
服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端,以下是服务器端返回的格式:

    ETag: "50b1c1d4f775c61:df3"

    客户端的查询更新格式是这样的:

    If-None-Match: W/"50b1c1d4f775c61:df3"

其中:

  • If-None-Match - 与响应头的 Etag 相对应,可以判断本地缓存数据是否发生变化
    如果ETag没改变,则返回状态304然后不返回,这也和Last-Modified一样。

总结下来它的结构如下:

请求 HeaderValue 响应 HeaderValue
ETag If-None-Match

ETag 是的功能与 Last-Modified 类似:服务端不会每次都会返回文件资源。客户端每次向服务端发送上次服务器返回的 ETag 值,服务器会根据客户端与服务端的 ETag 值是否相等,来决定是否返回 data,同时总是返回对应的 HTTP 状态码。客户端通过 HTTP 状态码来决定是否使用缓存。比如:服务端与客户端的 ETag 值相等,则 HTTP 状态码为 304,不返回 data。服务端文件一旦修改,服务端与客户端的 ETag 值不等,并且状态值会变为200,同时返回 data。

因为修改资源文件后该值会立即变更。这也决定了 ETag 在断点下载时非常有用。
比如 AFNetworking 在进行断点下载时,就是借助它来检验数据的。详见在  AFHTTPRequestOperation 类中的用法:

   //下载暂停时提供断点续传功能,修改请求的HTTP头,记录当前下载的文件位置,下次可以从这个位置开始下载。
- (void)pause {
unsigned long long offset = 0;
if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) {
offset = [[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue];
} else {
offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length];
} NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy];
if ([self.response respondsToSelector:@selector(allHeaderFields)] && [[self.response allHeaderFields] valueForKey:@"ETag"]) {
//若请求返回的头部有ETag,则续传时要带上这个ETag,
//ETag用于放置文件的唯一标识,比如文件MD5值
//续传时带上ETag服务端可以校验相对上次请求,文件有没有变化,
//若有变化则返回200,回应新文件的全数据,若无变化则返回206续传。
[mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"];
}
//给当前request加Range头部,下次请求带上头部,可以从offset位置继续下载
[mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", offset] forHTTPHeaderField:@"Range"];
self.request = mutableURLRequest; [super pause];
}

七牛等第三方文件存储商现在都已经支持ETag,Demo8和9 中给出的演示图片就是使用的七牛的服务,见:

static NSString *const kETagImageURL = @"http://ac-g3rossf7.clouddn.com/xc8hxXBbXexA8LpZEHbPQVB.jpg";

下面使用一个 Demo 来进行演示用法,

以 NSURLConnection 搭配 ETag 为例,步骤如下:

  • 请求的缓存策略使用 NSURLRequestReloadIgnoringCacheData,忽略本地缓存
  • 服务器响应结束后,要记录 Etag,服务器内容和本地缓存对比是否变化的重要依据
  • 在发送请求时,设置 If-None-Match,并且传入 Etag
  • 连接结束后,要判断响应头的状态码,如果是 304,说明本地缓存内容没有发生变化

以下代码详见 Demo08 :

/*!
@brief 如果本地缓存资源为最新,则使用使用本地缓存。如果服务器已经更新或本地无缓存则从服务器请求资源。 @details 步骤:
1. 请求是可变的,缓存策略要每次都从服务器加载
2. 每次得到响应后,需要记录住 etag
3. 下次发送请求的同时,将etag一起发送给服务器(由服务器比较内容是否发生变化) @return 图片资源
*/
- (void)getData:(GetDataCompletion)completion {
NSURL *url = [NSURL URLWithString:kETagImageURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // 发送 etag
if (self.etag.length > 0) {
[request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
} [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { // NSLog(@"%@ %tu", response, data.length);dd
// 类型转换(如果将父类设置给子类,需要强制转换)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判断响应的状态码是否是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips)
if (httpResponse.statusCode == 304) {
NSLog(@"加载本地缓存图片");
// 如果是,使用本地缓存
// 根据请求获取到`被缓存的响应`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到缓存的数据
data = cacheResponse.data;
} // 获取并且纪录 etag,区分大小写
self.etag = httpResponse.allHeaderFields[@"Etag"]; NSLog(@"etag值%@", self.etag);
!completion ?: completion(data);
}];
}

相应的 NSURLSession 搭配 ETag 的版本见 Demo09:

/*!
@brief 如果本地缓存资源为最新,则使用使用本地缓存。如果服务器已经更新或本地无缓存则从服务器请求资源。 @details 步骤:
1. 请求是可变的,缓存策略要每次都从服务器加载
2. 每次得到响应后,需要记录住 etag
3. 下次发送请求的同时,将etag一起发送给服务器(由服务器比较内容是否发生变化) @return 图片资源
*/
- (void)getData:(GetDataCompletion)completion {
NSURL *url = [NSURL URLWithString:kETagImageURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // 发送 etag
if (self.etag.length > 0) {
[request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
} [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // NSLog(@"%@ %tu", response, data.length);
// 类型转换(如果将父类设置给子类,需要强制转换)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判断响应的状态码是否是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips)
if (httpResponse.statusCode == 304) {
NSLog(@"加载本地缓存图片");
// 如果是,使用本地缓存
// 根据请求获取到`被缓存的响应`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到缓存的数据
data = cacheResponse.data;
} // 获取并且纪录 etag,区分大小写
self.etag = httpResponse.allHeaderFields[@"Etag"]; NSLog(@"%@", self.etag);
dispatch_async(dispatch_get_main_queue(), ^{
!completion ?: completion(data);
});
}] resume];
}

一般数据类型借助 Last-Modified 与  ETag 进行缓存

以上的讨论是基于文件资源,那么对一般的网络请求是否也能应用?

控制缓存过期时间,无非两种:设置一个过期时间;校验缓存与服务端一致性,只在不一致时才更新。

一般情况下是不会对 api 层面做这种校验,只在有业务需求时才会考虑做,比如:

  1. 数据更新频率较低,“万不得已不会更新”---只在服务器有更新时才更新,以此来保证2G 等恶略网络环境下,有较好的体验。比如网易新闻栏目,但相反微博列表、新闻列表就不适合。
  2. 业务数据一致性要求高,数据更新后需要服务端立刻展示给用户。客户端显示的数据必须是服务端最新的数据
  3. 有离线展示需求,必须实现缓存策略,保证弱网情况下的数据展示的速度。但不考虑使用缓存过期时间来控制缓存的有效性。
  4. 尽量减少数据传输,节省用户流量

一些建议:

    1. 如果是 file 文件类型,用 Last-Modified 就够了。即使 ETag 是首选,但此时两者效果一致。九成以上的需求,效果都一致。
    2. 如果是一般的数据类型--基于查询的 get 请求,比如返回值是 data 或 string 类型的 json 返回值。那么 Last-Modified 服务端支持起来就会困难一点。因为比如
      你做了一个博客浏览 app ,查询最近的10条博客, 基于此时的业务考虑 Last-Modified 指的是10条中任意一个博客的更改。那么服务端需要在你发出请求后,遍历下10条数据,得到“10条中是否至少一个被修改了”。而且要保证每一条博客表数据都有一个类似于记录 Last-Modified 的字段,这显然不太现实。

      如果更新频率较高,比如最近微博列表、最近新闻列表,这些请求就不适合,更多的处理方式是添加一个接口,客户端将本地缓存的最后一条数据的的时间戳或 id 传给服务端,然后服务端会将新增的数据条数返回,没有新增则返回 nil 或 304。

NSURLCache详解和使用的更多相关文章

  1. AFNetworking 3.0 使用详解 和 源码解析实现原理

    AFN原理&& AFN如何使用RunLoop来实现的: 让你介绍一下AFN源码的理解,首先要说说封装里面主要做了那些重要的事情,有那些重要的类(XY题) 一.AFN的实现步骤: NSS ...

  2. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  3. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  4. EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解

    前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...

  5. Java 字符串格式化详解

    Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...

  6. Android Notification 详解(一)——基本操作

    Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...

  7. Android Notification 详解——基本操作

    Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...

  8. Git初探--笔记整理和Git命令详解

    几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...

  9. Drawable实战解析:Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)

    Android XML shape 标签使用详解   一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...

随机推荐

  1. oracle 创建用户

    /*分为四步 *//*第1步:创建临时表空间  */create temporary tablespace ycjy tempfile 'D:\oracledata\ycjy.dbf' size 50 ...

  2. 开源免费的天气预报接口API以及全国所有地区代码(国家气象局提供)

    天气预报一直是各大网站的一个基本功能,最近小编也想在网站上弄一个,得瑟一下,在网络搜索了很久,终于找到了开源免费的天气预报接口API以及全国所有地区代码(国家气象局提供),具体如下: 国家气象局提供的 ...

  3. LeetCode 168. Excel Sheet Column Title

    Given a positive integer, return its corresponding column title as appear in an Excel sheet. -> A ...

  4. 对想进入Unity开发新人的一些建议

    提前声明:本文只是写给那些非职业游戏开发人士,只面向那些在校本科生,或已就业但无unity背景的同学们,当然是面对程序员方向的.本人刚工作也没多久,资历尚浅,之前在网上有一位同学让我谈谈一些想法,所以 ...

  5. pip install tushare

    1.sudo apt-get install libxml2-dev libxslt1-dev python-dev apt-get install libevent-dev pip install ...

  6. 安装了多个Oracle11g的客户端,哪个客户端的tnsnames.ora会起作用?

    如果我们由于需要安装了多个Oracle的client,哪个客户端的tnsnames.ora会起作用呢? 答案是: 在安装好clinent端后,安装程序会把client的bin目录放到path里面,pa ...

  7. addEventListener循环绑定出现的问题

    今天 碰到这样一个问题 代码如下 var someth = document.getElementsByTagName("a"); for (var i = 0; i < 1 ...

  8. linux故障判断

    系统问题: 带宽 netstat cpu io 磁盘 内存     free ------------------------------------------------------------- ...

  9. click 事件 arguments.callee 每次点击自动* 2

    今天在测试JQUERY(版本3.0,向下兼容3.0)时发现一个很特别的现象,代码如下: $($('button').get(4)).click(function(){ alert($(this).ht ...

  10. MS - 1 - 把二元查找树转变成排序的双向链表

    ## 1. 把二元查找树转变成排序的双向链表 ## ### 题目: 输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表. ### 要求不能创建任何新的结点,只调整指针的指向. 10       ...