网络框架

[toc]

思维脑图:请点击查看 网络框架.xmind

二、网络重试

三、网络缓存优化

四、网络耗时

五、网络使用过程中的优化

一、网络权限与初始化

系统网络权限弹窗机制:有代码执行到网络相关事项时候才会系统自动弹出。常见的场景:1监听网络状态变化;2进行任一网络请求。

验证代码:详见 CJNetworkDemo

network_system_window_permission1

图片来源于 《app启动与页面加载.graffle》 中的 【app启动(含网络与用户协议)(详细)】

问:如何确保网络初始化过程的安卓安全合规问题?

答:核心是将除网络库外的三方库统一放到同意用户协议后,避免其他三方库也可能调用获取敏感信息。至于网络库是否也放到同意协议后可用有两种方式。

处理方式一:同意协议前就可以初始化网络库,但是使用中涉及的敏感信息在未同意前使用自定义的特殊值(其他三方库先不初始化),且同意协议前仅网络库可初始化,其他三方库统一还是放到同意协议后,避免其他三方库也可能调用获取敏感信息。

优点:可以避免监听蜂窝的方式没弹出网络授权弹窗,导致网络请求始终调不通。(因为网络授权弹窗只有要么发起网络请求、要么监听网络请求的时候才可能弹出。但触发请求的次数明显比监听次数多)

缺点:代码里容易遗忘敏感信息的获取如安卓deviceId要先判断是否同意了用户协议,这是经常安卓审核合规不通过的常见原因。

使用注意:使用中一旦涉及敏感信息,一定要先判断是不是同意了用户协议,如果没同意则不要去获取。如安卓deviceId未同意用户协议前使用deviceId_not_agree_user_privacy

处理方式二:同意协议后才初始化三方库(含网络库)

优点:可以避免一些如deviceId的提前获取导致不符合安卓合规。

以下功能一般会牵涉到附中提到的 网络拦截器

二、网络重试

network_1retry_flow1

图片来源于 《网络框架的一生.graffle》 中的 【网络重试】

三、网络接口缓存流程

API缓存过程涉及附文中提到的 promise的resolve与next的流程

network_cache_flow1

图片来源于 《网络框架的一生.graffle》 中的 【网络缓存优化】

1、API缓存+可考虑加:Last-Modified/ETag

其他参考文档: ETag 、 Last-Modified 的介绍

使用 Last-Modified、ETag 其实就是在原本(见《001-UIKit-CQDemo-Flutter》 中的 【flutter_network_kit 库里判断responseModel.isSameToBefore部分】)缓存的基础上省去了这个判断。如果返回304,那就是代表没有新数据,使用beforeResponseModel。

1
2
3
4
5
if (beforeResponseModel != null) {
if (responseModel.isEqualToResponse(beforeResponseModel)) {
responseModel.isSameToBefore = true;
}
}

request.cachePolicy = NSURLRequestUseProtocolCachePolicy; 这行代码设置了请求的缓存策略,告诉NSURLSession根据HTTP协议的缓存相关头部(如Cache-ControlETagLast-Modified等)来决定是否使用缓存。

“If-Modified-Since”:服务器返回的 “Last-Modified”

“If-None-Match”:服务器返回的 “ETag”

1.1、在iOS上使用Last-Modified和ETag实现缓存

后端返回:

1
2
3
4
5
6
7
app.get('/your-endpoint', function(req, res) {
const lastModified = xxx;
const etag = 'unique-etag-value';
res.set('Last-Modified', lastModified.toUTCString());
res.set('ETag', etag);
res.send('Your resource content');
});

前端使用并回传:

1.1.1、自定义缓存逻辑 NSCache,写出使用Last-Modified和ETag来实现缓存的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
NSURL *url = [NSURL URLWithString:@"http://example.com/resource"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60];

// 检查并设置If-Modified-Since和If-None-Match头部
NSString *cachedLastModified = // 从本地获取Last-Modified值
NSString *cachedETag = // 从本地获取ETag值
if (cachedLastModified) {
[request setValue:cachedLastModified forHTTPHeaderField:@"If-Modified-Since"];
}
if (cachedETag) {
[request setValue:cachedETag forHTTPHeaderField:@"If-None-Match"];
}

NSString *storageKey = "LastModified_url和已去除timestamp等无关参数的参数 组成的key"
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSString *newLastModified = httpResponse.allHeaderFields[@"Last-Modified"];
NSString *newETag = httpResponse.allHeaderFields[@"ETag"];
if (httpResponse.statusCode == 200) {
// 资源有更新,保存新数据和新的Last-Modified、ETag
[self saveData:data LastModified:newLastModified ETag:newETag forKey:storageKey]
callback(data, isCacheData:false);
} else if (httpResponse.statusCode == 304) {
// 资源未更新,使用本地缓存数据(从本地再次加载缓存数据)
cacheData = [self loadCachedDataForKey:storageKey];
callback(data, isCacheData:true);
}

// 处理数据...
}];

[dataTask resume];
1.1.2、在不自定义缓存逻辑,且完全依赖 NSURLCache 来管理缓存的情况,写出使用Last-Modified和ETag来实现缓存的代码。

当使用 NSURLCache 并依赖系统的缓存机制时,通常不需要手动设置 Last-ModifiedETag 请求头字段。NSURLCache 会自动处理这些缓存相关的 HTTP 头字段,包括:

  • 在条件 GET 请求中自动添加 If-Modified-SinceIf-None-Match 请求头,使用之前响应中收到的 Last-ModifiedETag 值。
  • 当接收到来自服务器的响应时,根据响应中的 Last-ModifiedETag 头字段更新缓存条目。

如果你完全依赖 NSURLCache 来处理缓存逻辑,你的代码中不需要显式设置这些字段。NSURLCache 会在内部管理这些值,并根据 HTTP 协议标准执行适当的缓存行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 1. 为 NSURLSessionConfiguration 配置一个 NSURLCache 实例(设置合适的内存和磁盘缓存容量)。
NSUInteger memoryCapacity = 4 * 1024 * 1024; // 内存缓存大小(如4MB)
NSUInteger diskCapacity = 20 * 1024 * 1024; // 磁盘缓存大小(如20MB)
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:memoryCapacity diskCapacity:diskCapacity diskPath:nil];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.URLCache = urlCache;

// 2. 使用配置好的 NSURLSessionConfiguration 创建 NSURLSession。
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig];

// 3. 创建并发送请求。如果之前有缓存,并且缓存包含了 Last-Modified 或 ETag,则 NSURLCache 会根据这些信息自动处理条件请求。
NSURL *url = [NSURL URLWithString:@"http://example.com/resource"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error != nil) {
......
return;
}

NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
switch (httpResponse.statusCode) {
case 200:
// 资源有更新,NSURLCache 会自动更新缓存
callback(data, isCacheData:false);
break;
case 304:
// 资源未修改,从 NSURLCache 获取缓存的数据
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; if (cachedResponse) {
NSData *cachedData = cachedResponse.data;
callback(cachedData, isCacheData:true); // 由于是缓存的数据,通常不需要更新UI,除非有特殊需求
}
break;
default:
// 处理其他HTTP状态码
break;
}
}];

[dataTask resume];

四、网络耗时及其优化

网络耗时及其优化.md

五、网络安全

请在安全篇章中查看

文档:《网络加密.md

流程图: 《网络加密.graffle

六、文件分片上传与分片下载

文件分片上传与分片下载.md

七、网络模拟

网络模拟文档请查看:网络ApiMock

Next:第2节:网络耗时及其优化

##

附1:网络拦截器

类型 时机 顺序:
请求拦截器(Request Interceptor) 请求开始前拦截 按照它们被添加的顺序,依次执行,修改即将发出的请求。
响应拦截器(Response Interceptor) 请求成功后拦截
错误拦截器(Error Interceptor) 请求失败后拦截

拦截器的执行顺序是根据它们各自被添加到请求实例的顺序决定的。(顺序只发生在同种类型间,不同类型间不影响。)

问1:如果第一个请求拦截器把参数修改了,第二个请求拦截器会怎么样?

答:如果第一个请求拦截器修改了请求参数,第二个请求拦截器接收到的将是修改后的请求参数。每个请求拦截器都会接收到前一个请求拦截器返回的RequestOptions对象,并且可以对其进行进一步的修改。

问2:如果是先添加响应拦截器,再添加请求拦截器会怎么样?

答:即使响应拦截器是先添加的,当实际发送请求时,请求拦截器会先执行,响应拦截器会在请求完成后执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Dio dio = Dio();

// 第一个请求拦截器
dio.interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options) async {
// 假设我们修改了请求的queryParameters
options.queryParameters['firstInterceptor'] = 'modified';
print("第一个请求拦截器修改后的参数: ${options.queryParameters}");
return options;
},
));

// 第二个请求拦截器
dio.interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options) async {
// 接收到第一个拦截器修改后的参数
// 我们可以再次修改它或者添加新的参数
options.queryParameters['secondInterceptor'] = 'added';
print("第二个请求拦截器接收到的参数: ${options.queryParameters}");
return options;
},
));

// 发送请求
dio.get('https://example.com')
.then((response) {
print("请求发送成功,并接收到响应: ${response.data}");
}).catchError((error) {
print("请求发送失败,并接收到错误: $error");
});

// 添加响应拦截器
dio.interceptors.add(InterceptorsWrapper(
onResponse: (Response response) {
// 响应拦截器逻辑
print("响应拦截器");
return response;
},
));

// 添加错误拦截器
dio.interceptors.add(InterceptorsWrapper(
onError: (DioError err, ErrorInterceptorHandler handler) {
// 错误拦截器逻辑
print("错误拦截器");
return handler.next(err); // 根据错误处理逻辑可能重新抛出错误或返回自定义响应
},
));

例如,Dio提供了三种类型的拦截器:请求拦截器(Request Interceptor)、响应拦截器(Response Interceptor)和错误拦截器(Error Interceptor)。每种类型的拦截器内部的执行顺序是固定的,但是不同类型拦截器之间的执行顺序则取决于它们被添加到Dio实例的顺序。

附2、promise的resolve与next的流程

使用 resolve 方法可以中断当前执行中的请求拦截器链,至于是否执行后续的响应拦截器取决于resolve 方法的第二个参数 callFollowingResponseInterceptor ,并直接返回一个 Response 对象作为请求的结果。

在 Dio 的 InterceptorsWrapper 中,使用 handler.resolve不一定会直接将响应返回给最初的请求调用者,而是将控制权交回给 Dio实例,由 Dio 来决定接下来的流程。resolve 方法实际上并不会退出拦截器链,而是根据 callFollowingResponseInterceptor 参数的值来决定是否执行后续的响应拦截器。但无论 callFollowingResponseInterceptor 设置为 true 还是 false调用者最终都能得到 Response 对象。这是因为响应拦截器的目的是处理和可能修改响应,但最终仍然需要将响应传递回调用者。不同之处在于,设置为 true 时会先执行响应拦截器,而设置为 false 时会直接返回响应给调用者,跳过响应拦截器。

network_promise_resolve

图片来源于 《网络框架的一生.graffle》 中的 【网络缓存优化】版面里的【promise的resolve与next】图层

End

其他参考文档: