基础数据基类--用户

[toc]

基础数据基类–用户

一、用户种类

描述 类名
用户基类 包含必须属性 BaseUserModel

一、用户

1
2
3
4
5
6
7
8
9
10
11
12
13
class UserBaseBean {
final String userId;
final String? avatar;
final String? nickname;
final int? consumerLevel; // 用户等级

UserBaseBean({
required this.userId,
this.avatar,
this.nickname,
this.consumerLevel,
});
}

End

基础数据基类--商品

[toc]

基础数据基类–商品

一、商品种类

描述 类名
商品基类 包含必须属性 BaseGoodsBean
商城里展示的商品 有多sku SpuGoodsBean
用户想要购买的商品 SkuGoodsBean

枚举的使用

在Flutter中

packages/app_models/lib/goods/base_goods_enum.dart

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
import 'package:flutter_data_helper/flutter_data_helper.dart';

// -------------------------- 商品售卖状态 --------------------------
// 商品售卖状态(上架:up; 下架:down)
enum GoodsSaleState {
up, // 上架中
down, // 已下架
}

///string转枚举类型
GoodsSaleState goodsSaleStateFromString(String value) {
Iterable<GoodsSaleState> values = [
GoodsSaleState.up, // 上架中
GoodsSaleState.down, // 已下架
];
return EnumStringUtil.enumFromString(values, value);
}

///枚举类型转string
String goodsSaleStateStringFromEnum(o) {
return EnumStringUtil.enumToString(o);
}

// -------------------------- 商品物品类型 --------------------------
// 商品物品类型(实体商品:physical; 虚拟物品:virtual)
enum GoodsPhysicalType {
physical, // 实体商品
virtual, // 虚拟物品
}

在iOS中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 协议定义
protocol GoodsPriceProtocol {
var marketPrice: Int? { get set }
var salePrice: Int? { get set }
}

protocol GoodsBrandProtocol {
var brandName: String? { get set }
var brandId: String? { get set }
}

// 类实现协议
class BaseGoodsBean: GoodsPriceProtocol, GoodsBrandProtocol {
var marketPrice: Int?
var salePrice: Int?
var brandName: String?
var brandId: String?

// 类的其他属性和方法
}

在Flutter中

packages/app_models/lib/goods/base_goods_bean.dart

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
49
50
51
52
53
54
55
56
57
58
59
60
61
// 商品价格
import 'base_goods_enum.dart';

abstract class GoodsPriceModel {
int? marketPrice; // 市场价格
int? salePrice; // 销售价格
}

// 商品品牌
abstract class GoodsBrandModel {
String? brandId; // 品牌id
String? brandName; // 品牌名称
}

// 商品门店
abstract class GoodsShopModel {
String? shopId; // 门店id
String? shopName; // 门店名称
}

// 商品基类
class BaseGoodsBean implements GoodsPriceModel, GoodsBrandModel, GoodsShopModel {
// 商品必备属性
final String id; // 商品id
final String name; // 商品名称
final String imageOrVideoUrl; // 商品主图图片/视频
final GoodsSaleState goodsSaleState; // 商品售卖状态(上架up; 下架down)
final GoodsPhysicalType physicalType; // 商品物品类型(实体商品:physical; 虚拟物品:virtual)

// 商品价格
@override
int? marketPrice;
@override
int? salePrice;

// 商品品牌
@override
String? brandId;
@override
String? brandName;

// 商品门店
@override
String? shopId;
@override
String? shopName;

BaseGoodsBean({
required this.id,
required this.name,
required this.imageOrVideoUrl,
required this.goodsSaleState,
required this.physicalType,
this.marketPrice,
this.salePrice,
this.brandId,
this.brandName,
this.shopId,
this.shopName,
});
}

展示的商品

选中的商品

packages/app_models/lib/goods/selected_goods_bean.dart


End

基础数据基类

[toc]

基础数据基类

原则

1、属性

序号 方案 使用平台 推荐星数 其他
1 继承 iOS、Android、Flutter
2 组合 iOS、Android、Flutter
3 协议 iOS
4 抽象类 Flutter

1.1、继承

1.2、组合

1.3、协议(iOS)

在iOS开发中,除了使用Swift语言创建基类和组合属性的方式之外,还可以考虑使用协议(Protocol)来实现类似的效果。协议可以定义一组属性和方法的要求,然后其他类可以遵循该协议并提供相应的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 商品价格
protocol GoodsPriceProtocol {
var marketPrice: Int? { get set } // 市场价格
var salePrice: Int? { get set } // 销售价格
}

// 商品品牌
protocol GoodsBrandProtocol {
var brandName: String? { get set } // 品牌名
var brandId: String? { get set } // 品牌id
}

// 商品基类
class BaseGoodsBean: GoodsPriceProtocol, GoodsBrandProtocol {
var marketPrice: Int?
var salePrice: Int?
var brandName: String?
var brandId: String?
}

1.4、抽象类

在Flutter中,没有与iOS中的协议(Protocol)直接对应的语言特性。然而,您可以使用抽象类(Abstract Class)来实现类似的效果。抽象类可以定义一组抽象方法和属性,然后其他类可以继承该抽象类并提供相应的实现。

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
// 商品价格
abstract class GoodsPriceModel {
int? marketPrice; // 市场价格
int? salePrice; // 销售价格
}

// 商品品牌
abstract class GoodsBrandModel {
String? brandName; // 品牌名称
String? brandId; // 品牌id
}

// 商品基类
class BaseGoodsBean implements GoodsPriceModel, GoodsBrandModel {
// 商品价格
@override
int? marketPrice;
@override
int? salePrice;

// 商品品牌
@override
String? brandName;
@override
String? brandId;
}

End

图片框架

[toc]

图片框架

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

流程图:请点击查看图片框架.graffle

一、图片优化

1、图片占位图、异常图

2、图片加载动画

可适当在图片视图本身加入loading动画

3、图片缓存

3.1、直接图片视图加载

3.2、先加载图片数据,再赋值到图片视图上

3.3、去除多余的图片切换动画

4、降低图片质量(缩略图/数据万象)

4.1、使用缩略图

让后台返回缩略图地址,而不是大图地址

4.2、图片数据万象及其优化\根据视图大小,获取保持比例的合适目标图片(完整图)

后台不是给缩略图地址的情况下,自己通过图片地址,配置对应的参数,来展示。

图片数据万象的优化,请继续查看下文 数据万象/根据视图大小,获取保持比例的合适目标图片(完整图))

二、数据万象/根据视图大小,获取保持比例的合适目标图片(完整图)

1、背景

在服务端,我们保存的是图片的原图。但实际在app使用过程中,我们常常只需要该图的缩略图。那么如何及合理的得到我们想要的缩略图呢?

2、改进前

2.1、方法

缩略图的获取,最基本的方法是使用数据万象提供的接口,通过为其添加尺寸参数,即可得到缩略图片。

添加后的缩略图地址形如:https://www.demo.com/1.jpg?width=44&height=44

2.2、存在的问题

使用上述方法,我们虽然得到了自适应每个image图片视图自身大小的缩略图。

但同时也引入了另一个问题:就是如果某张图需要出现在不同大小的image视图中,则我们会下载及缓存多份数据。这无形中增加了①流量的消耗、②手机存储空间的消耗;③同时也因为认为要下载新缩略图,而导致无法使用就近的缩略图。所以我们需要改进。

示例:

1
2
3
4
5
6
优化前:在 44*44 和 50*50 大小的视图区域上的图片地址分别如下:
https://www.demo.com/1.jpg?width=44&height=44
https://www.demo.com/1.jpg?width=50&height=50

优化后:两个区域统一为使用 64 * 64 大小的视图区域上的图片地址,从而如果避免获取到太多份不同大小的同图?
https://www.demo.com/1.jpg?width=64&height=64

3、改进后

3.1、改进方法

改进方式,在进行万象拼接前,我们通过提前新增缩略图梯度,将相近大小的缩略图归为使用同一张。

梯度的层数,可根据自己实际项目图片规范设计。

举例:以宽为 375pt 的手机屏幕为例。假设你图片规范是 [ 64pt 、188pt、375pt ]。

则当你的视图是 100pt 和 120pt 的视图都使用 188pt 的图片。

3.2、改进的好处

通过上述方法,我们能够达到①减少流量的消耗、②减少手机存储空间的消耗;③同时使得对于同一图片在相近大小的image图片视图的地方,由于计算出的缩略图处在同一梯度,而可以不用重新下载等待,而是直接使用缓存渲染,从而大大提升了用户体验。

4、流程

4.1、主要流程

获取梯度范围 -> 完成万象拼接

4.2、主要流程图

以下为”获取梯度范围”的流程

image-20230531212509071

图片的更多完整流程图,请查看 图片框架.graffle

三、图片高磁盘占用的排查与优化

文档:《高磁盘占用的排查与优化.md

四、图片展示

1、竖直滚动列表上的图片展示

2、图片大图浏览

image-20240725230554593

图片来源:图片框架.xmind

附1:图片优化的相关代码

1、图片占位图、异常图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
return CachedNetworkImage(
width: width,
height: height,
fit: fit,
imageUrl: imageUrl,
placeholder: placeholder,
errorWidget: (context, url, error) {
if (this.errorWidget != null) {
return this.errorWidget(context, url, error);
} else {
return Container();
}
},
);

2、图片加载动画

可适当在图片视图本身加入loading动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
return CachedNetworkImage(
width: width,
height: height,
fit: fit,
imageUrl: imageUrl,
placeholder: placeholder,
errorWidget: (context, url, error) {
if (this.errorWidget != null) {
return this.errorWidget(context, url, error);
} else {
return Container();
}
},
placeholderFadeInDuration: placeholderFadeInDuration,
fadeOutDuration: fadeOutDuration,
fadeInDuration: fadeInDuration,
progressIndicatorBuilder: (context, url, progress) {
if (this.progressIndicatorBuilder != null) {
return this.progressIndicatorBuilder(context, url, progress);
} else {
return Container(color: Color(0xFFF0F0F0));
}
},
);

3、图片缓存

3.1、直接图片视图加载

1
2
3
4
5
TolerantNetworkImage(
imageUrl: networkUrl,
width: 100,
height: 300,
)

3.2、先加载图片数据,再赋值到图片视图上

eg:首页频道图片切换

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义
ImageProvider imageProvider_network;

// 生成数据
imageProvider_network = CachedNetworkImageProvider(networkUrl);

// 使用数据
return Image(
image: imageProvider_network,
width: 100,
height: 300,
fit: BoxFit.cover,
);

3.3、去除多余的图片切换动画

eg:许个愿图片切换

1
2
3
4
5
6
7
8
9
10
11
12
return CachedNetworkImage(
width: width,
height: height,
fit: fit,
imageUrl: imageUrl,
placeholderFadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
fadeInDuration: Duration.zero,
progressIndicatorBuilder: (context, url, progress) {
return Container(color: Color(0xFFF0F0F0));
},
);

End

文件分片上传与分片下载

[toc]

前言

iOS m3u8本地缓存播放(控制下载并发、暂停恢复)

传统上传 VS 分片上传

传统上传方法的问题 分片上传的优点
大文件上传耗时长,容易导致超时。 将大文件拆分成较小的分片,更快更可靠地上传。
占用服务器和网络带宽资源,可能影响其他用户的访问速度。 监控并显示上传进度,提高用户体验。
如果上传中断,需要重新上传整个文件,效率低下。 充分利用浏览器的并发上传能力,减轻服务器负载。
难以显示和控制上传进度。 实现断点续传功能,避免重新上传已上传的分片。

传统文件下载 VS 文件分片下载

文件分片下载是一种通过将大文件拆分成较小的片段(分片)并同时下载它们来提高文件下载效率的技术。

问题/技术 传统文件下载 文件分片下载
长时间等待 用户可能需要等待很长时间才能开始使用大文件 只需下载第一个分片,客户端就可以开始使用文件
网络拥堵 如果网络带宽被大文件下载占用,其他用户可能会遇到下载速度慢的问题 可以使用多个并行请求来下载分片,充分利用带宽并提高整体下载速度
难以恢复下载 如果网络故障或用户中断,整个文件必须重新下载 如果下载被中断,只需重新下载未完成的分片,而不是整个文件
下载效率 下载速度较慢,特别是在网络不稳定或速度较慢的情况下 通过将大文件拆分成较小的片段并同时下载,提高文件下载效率
并行下载 不支持 支持,可以使用多个并行请求来下载分片
下载管理 整个文件作为一个整体进行下载 每个分片可以单独管理和下载,提供更好的灵活性

一、分片上传的实现方法

1、获取文件所有切片/分片:按指定的每个切片大小切(中断后的重新上传时,要过滤掉已上传完成的切片)。切片方法 [fileHandle readDataOfLength:chunkSize];

2、上传分片:提交时除了文件data,同时传入:总片数、当前是第几片。

3、上传所有分片后,有时候这里会额外调用服务器API告诉后台已完成全部上传。实际上后台自己也可以根据上传个数是否等于总数判断。服务端得到所有的数据片后合并数据。

其他注意点:

如果要上传的文件是录制的视频或者从系统相册中选择的视频,则该视频文件的路径必须是app的相对路径。缺点视频可被从相册中删除。

1. 定义分片上传的辅助类

首先,定义一个辅助类来管理分片上传的状态和数据。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// FileChunk.m 文件切片的数据模型
@interface FileChunk : NSObject
@property (nonatomic, strong) ChunkFileModel *fileModel; // 切片来源于哪个文件(包含文件路径、文件大小,每个分片大小,分片总数)
@property (nonatomic, assign) NSUInteger index; // 数据块的位置(为了中断后的重新上传时候,过滤掉已上传完成的切片)
@property (nonatomic, assign) NSUInteger offset; // 数据块的起始位置(可能不同大小的文件,切片大小不一样,data的offset就不一样)
@property (nonatomic, strong) NSData *data; // 数据块内容
@property (nonatomic, strong) NSURLResponse *response; // 上传响应(会包含每片的上传状态)

// 获取文件单个切片:按指定的每个切片大小切
- (FileChunk *)initWithFileHandle(NSFileHandle *)fileHandle chunkSize:(NSUInteger)chunkSize chunkIndex:(NSUInteger)chunkIndex {
self = [super init];
if (self) {
chunk.data = [fileHandle readDataOfLength:chunkSize];// 执行后文件句柄的内部偏移量会根据读取的数据长度自动向前移动
chunk.offset = [fileHandle offsetInFile]; // 返回当前文件的偏移量,即文件句柄的当前位置
chunk.index = chunkIndex;
}
return self;
}

// 要上传的切片的要设置给 HTTPHeaderField:@"Content-Range"
- (NSString *)uploadContentRangeString {
return [NSString stringWithFormat:@"bytes %@-%@/total-size", @(chunk.offset), @(chunk.offset + chunk.data.length)];
}
@end



// FileChunkHelper.m
+ (void)addFinishSaveChunk:(FileChunk *)chunk forFilePath:(NSString *)filePath {
// 以key: chunk.belongfileMD5 保存已上传完成的切片索引 [0, 1, 2, chunk.index, ...]
}
+ (void)getFinishSaveChunkForFilePath:(NSString *)filePath {
// 以key: chunk.belongfileMD5 获取已上传完成的切片索引 [0, 1, 2, chunk.index, ...]
}

// 获取文件所有切片:按指定的每个切片大小切(中断后的重新上传时,要过滤掉已上传完成的切片)
+ (NSArray *)chunksFromFilePath:(NSString *)filePath
chunkSize:(NSUInteger)chunkSize
withoutChunkIndexs:(NSArray<NSUInteger>)chunkIndexs
{
NSMutableArray<FileChunk *> *chunks = [NSMutableArray array];

NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
[fileHandle seekFileOffset:0]; // 先定位到文件头部
int chunkIndex = 0;
FileChunk *chunk = [[FileChunk alloc] initWithFileHandle:fileHandle chunkSize:chunkSize chunkIndex:chunkIndex];
while (chunk.data) {
chunkIndex++;
if ([chunkIndexs contains chunk.index]) { // 为了中断后的重新上传时候,过滤掉已上传完成的切片
continue;
}
[chunks addObject:chunk];
}
[fileHandle closeFile];

return chunks;
}

// 单个分片的上传方法
+ (void)uploadChunk:(FileChunk *)chunk
uploadId:(NSString *)uploadId
completionHandler:(void (^)(BOOL success, NSError *error))completionHandler
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"your-upload-chunk-api-endpoint"]];
request.HTTPMethod = @"PUT";
request.HTTPBody = chunk.data;
[request setValue:chunk.uploadContentRangeString forHTTPHeaderField:@"Content-Range"];

NSURLResponse *response = nil;
NSError *error = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if (error) {
completionHandler(NO, error);
return;
}

chunk.response = response; // 如果上传成功,更新状态
[self addFinishSaveChunk:chunk forFilePath:chunk.filePath];
completionHandler(YES, nil);
}

+ (void)uploadChunks:(NSArray<FileChunk *> *)chunks
withUploadId:(NSString *)uploadId
completionHandler:(void (^)())completionHandler
{
dispatch_group_t group = dispatch_group_create();
NSMutableArray *uploadTasks = [NSMutableArray array];
for (FileChunk *chunk in chunks) {
dispatch_group_enter(group);
[FileChunkHelper uploadChunk:chunk uploadId:uploadId completionHandler:^(BOOL success,NSError *error) {
completionHandler(success, error);
dispatch_group_leave(group);
}];

[uploadTasks addObject:@{@"task": @"uploadChunk", @"chunk": chunk}];
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
completionHandler();
});
}

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
// 请求文件是否上传过。(未上传过:返回用于上传的upload;正在上传中:返回upload;已上传过:返回url)
- (void)requestUploadInfoForFilePath:(NSString *)filePath
{
NSDictionary *parameters = @{
@"file_name": @"filename.txt", // 替换为实际文件名
@"file_size": @([FileChunkHelper getFileSize:filePath]), // 调用方法获取文件大小
@"part_size": @(partSize)
};
if (resultModel.uploadFinish) {
NSString *url = resultModel.url;
return;
}

NSString *uploadId = resultModel.uploadId;
//NSArray<NSInteger> finishChunkIndexs = resultModel.finishChunkIndexs; // 个人感觉不需要后台返回未完成的
// 1、获取文件所有切片:按指定的每个切片大小切(中断后的重新上传时,要过滤掉已上传完成的切片)
NSUInteger chunkSize = 1024 * 1024; // 1M
NSArray<NSUInteger> chunkIndexs = [FileChunkHelper getFinishSaveChunkForFilePath:filePath];
NSArray<FileChunk *> *chunks = [FileChunkHelper chunksFromFilePath:filePath chunkSize:chunkSize withoutChunkIndexs:chunkIndexs];
// 2、上传分片:提交时除了文件data,同时传入、总片数、当前是第几片
[FileChunksUpload uploadChunks:chunks withUploadId:uploadId completionHandler:^(void){
// 3、上传所有分片后,有时候这里会额外调用服务器API告诉后台已完成全部上传。实际上后台自己也可以根据上传个数是否等于总数判断
}];
}

二、文件分段下载

iOS 文件分段下载

iOS文件下载,断点续传,后台下载.

NSURLSessionDataTask 方式断点续传之下载

原理:
每次向服务器下载数据的时候,告诉服务器从整个文件数据流的某个还未下载的位置开始下载,然后服务器就返回从那个位置开始的数据流.通过设置请求头Range可以指定每次从服务器下载数据包的大小.

Range示例
bytes = 0-499 从0到499的头500个字节
bytes = 500-999 从500到999第二个500个字节
bytes = 1000- 从1000字节以后所有的字节
bytes = -500 最后500个字节
bytes = 0-499,500-999 同时指定几个范围

1
2
3
4
>  /// 下次请求的时候,设置请求头,每次从本地取数据把保存好的下载长度放进请求头Range中
> NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength];
> [request setValue:range forHTTPHeaderField:@"Range"];
>

数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>  /// 初始化:通过NSOutputStream流向文件写出数据 ,append是否向尾部追加下载的数据
> self.stream = [[NSOutputStream alloc] initToFileAtPath:filePath append:YES];
>
> // 获取到服务器信息后,打开流,block回调允许接收下载数据
> [self.stream open];
> completionHandler(NSURLSessionResponseAllow);
>
> // 下载数据的回调:写入数据
> [self.stream write:[data bytes] maxLength:data.length];
>
> // 下载成功和失败都会来
> [self.stream close];
> self.stream = nil;
>

大文件视频上传:断点续传

End

网络框架

[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

其他参考文档:

网络耗时及其优化

[toc]

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

一、网络耗时流程及优化方案结论

发起请求 -> 域名DNS解析 -> TCP三次握手 ( -> TLS握手 -> ) -> request -> response -> json解析 -> 业务

1、过程中的优化方案

序号 阶段 耗时根源 优化方案
1 发起请求 每个接口都要建立连接 前端配置想要的数据,后台通过一个接口返回
如商品详情页,请求[Base,Detai,Images]:商品基础信息+详情+图片
其他减少请求参数的body数据。
2 域名DNS解析 DNS解析过程不受控制,⽆法保证解析到最快的IP 方案1:IP直连 点击链接可跳转到下文介绍
HTTP(HTTPS)请求,底层基于TCP连接,需要有IP和端口信息。
⾃⼰做域名解析⼯作,通过HTTP请求后台去拿到域名对应的IP地址。
方案2:HTTPDNS
阿里云的DNS SDK:AlicloudPDNSAlicloudHTTPDNS
附:iOS14原生加密DNS方案
3 TCP三次握手 TCP:面向连接的协议
UDP:无连接协议
TCP 比 UDP 要慢
方案1:用HTTP实现长连接 点击链接可跳转到下文介绍
方案2:使用HTTP/3.0 采用了基于 UDP 的 QUIC 协议来替代传统的 TCP 协议
HTTP协议各版本使用情况
4 TLS握手 —— ——
5 request 服务端排查是否有耗时逻辑
6 response 数据体积大 删减冗余数据,开启gzip,数据分页、Protobuf、WebP等方式进行优化
Protobuf:见我的另一篇文章 protobuf的安装.md
分页:见我的另一篇文章 分页规范.md
7 json解析 解析耗时 采用解析效率更高的库,如 YYModel
8 业务 —— ——

上述优化方案的参考文档来源于:

2、其他优化方案

2.1、弱网优化:针对不同的网络,做不同超时,重试设置,以及取不同质量的图片数据。

2.2、缓存(必备:API缓存+可考虑加:Last-Modified/ETag)

见我的另一篇文章:

发布的内容,优先写入数据库,视频切片使用另一个请求异步获取。

二、网络耗时分布统计

1
2
3
4
5
6
7
8
9
class ResponseModel {
int statusCode;
String? message;
dynamic result;
bool? isCache;
bool? isSameToBefore; // 网络新数据是否和之前数据一样(重试/缓存)
final ResponseDateModel dateModel; // 这次结果的时间。
List<ResponseDateModel>? cacheDateModels; // 请求缓存接口过程的所有时间段(缓存数据获取时间、缓存数据执行时间、网络数据获取时间)。因为有时候得到一个结果是不仅经过一次,而是多次请求才得到的(如缓存),所以一个请求有多条时间线的时候。
}

1、ResponseDateModel dateModel; // 这次结果的时间

2、List? cacheDateModels; // 请求缓存接口过程的所有时间段(缓存数据获取时间、缓存数据执行时间、网络数据获取时间)。因为有时候得到一个结果是不仅经过一次,而是多次请求才得到的(如缓存),所以一个请求有多条时间线的时候。

1、网络请求各阶段的耗时(iOS)

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
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) 
{
// 首先检查是否是iOS 10.0或更高版本,如果不是,则退出方法
if (!@available(iOS 10.0, *)) {
return; // 退出方法
}

// 由于使用了@available注解,下面的代码默认只在iOS 10.0或更高版本上执行
for (NSURLSessionTaskTransactionMetrics *sMetric in metrics.transactionMetrics) {
NSInteger dom = timeDiff(sMetric.domainLookupStartDate, sMetric.domainLookupEndDate); // 域名解析
NSInteger sec = timeDiff(sMetric.secureConnectionStartDate, sMetric.secureConnectionEndDate);// TLS
NSInteger con = timeDiff(sMetric.connectStartDate, sMetric.connectEndDate); // 连接耗时(包括TLS)
NSInteger req = timeDiff(sMetric.requestStartDate, sMetric.requestEndDate); // 请求
NSInteger res = timeDiff(sMetric.responseStartDate, sMetric.responseEndDate); // 回调
NSInteger tot = timeDiff(sMetric.fetchStartDate, sMetric.responseEndDate); // 总耗时

NSString *locip = @"";
NSString *remip = @"";
if (@available(iOS 13.0, *)) {
locip = [NSString stringWithFormat:@"%@", sMetric.localAddress];
remip = [NSString stringWithFormat:@"%@", sMetric.remoteAddress];
}

NSLog(@"metric path:%@ 总耗时:%ldms, 域名解析:%ldms, 连接耗时:%ldms(包括TLS:%ldms), 请求:%ldms, 回调:%ldms l:%@ r:%@", sessionMetric.request.URL.lastPathComponent, (long)tot, (long)dom, (long)con, (long)sec, (long)req, (long)res, locip, remip);
}
}

// 辅助方法,计算两个NSDate之间的时间差,单位为毫秒
- (NSInteger)timeDifferenceInMilliseconds:(NSDate *)start to:(NSDate *)end {
return (NSInteger)((end.timeIntervalSince1970 - start.timeIntervalSince1970) * 1000);
}

Next:第3节:弱网优化空间探索

附1:HTTP协议各版本使用情况

类型 发布时间与现状
HTTP/1.1 在1997年被标准化,是目前互联网上广泛使用的HTTP协议版本
HTTP/2.0 于2015年发布,也是目前互联网上广泛使用的HTTP协议版本 不是对HTTP/1.1的重写,而是在性能方面的改进
HTTP/3.0 目前尚未成为主流,还在逐步推广和应用中 是基于Google的QUIC协议开发

附2:DNS

1、DNS介绍

1.1、DNS劫持/DNS污染前后

未劫持 VS 劫持

dns flow

1.2、DNS劫持模拟

1.3、如何检测 DNS 劫持

  • iOS 用户推荐 iNetTools
  • Android 用户推荐 LanDroid
  • 如何检测 DNS 劫持

    ping 一个不存在的 IP 地址却仍获得解析,则 DNS 很可能已被黑客入侵。

2、DNS的优化

方案1:IP直连

方案2:HTTPDNS

2.1、IP直连

iOS IP 直连原理剖析

为什么大家会选择直接使用 IP 来进行连接呢?它具有多方面的优势:

  • 防劫持,可以绕过运营商 LocalDNS 解析过程,避免域名劫持,提高网络访问成功率
  • 降低延迟,DNS 解析是一个相对耗时的工作,跳过这个过程可以降低一定的延迟
  • 精准调度,运营商解析返回的节点不一定是最优的,自己获取 IP 可以基于自己的策略来获取最精准的、最优的节点

对于获取 IP,我了解到的是两种方案,

一种是直接接入腾讯或者阿里的 HTTPDNS 服务,在发起请求的时候是通过 HTTPDNS 获取 IP,然后直接使用 IP 来进行业务访问;一种是内置 Server IP,可以在启动等阶段由服务端下发域名和 IP 的对应列表,客户端来进行缓存,发起网络请求的时候直接根据缓存 IP 来进行业务访问。

附3:用HTTP实现长连接

长连接:长连接的特点是一旦通过三次握手建立链接之后,该条链路就一直存在,而且该链路是一种双向的通行机制,适合于频繁的网络请求,避免 Http 每一次请求都会建立链接和关闭链接的操作,减少浪费,提高效率。

在首部字段中设置Connection:keep-alive 和Keep-Alive: timeout=60,表明连接建立之后,空闲时间超过60秒之后,就会失效。如果在空闲第58秒时,再次使用此连接,则连接仍然有效,使用完之后,重新计数,空闲60秒之后过期。

设置HTTP长连接,无过期时间:
在首部字段中只设置Connection:keep-alive,表明连接永久有效。

其他文章:tcp长短连接、http长短连接、心跳包、tcp KeepAlive保活机制

  1. TCP 是 HTTP 的默认传输层协议:HTTP 通常运行在 TCP 之上,因为 TCP 提供了可靠的、面向连接的服务,确保数据按顺序、完整无误地从源传送到目的地。这对于 Web 浏览器和服务器之间的通信非常重要,因为它们需要确保请求和响应的完整性。

End

其他参考文档:

项目 :iOS-Monitor-Platform 中的网络部分 https://github.com/aozhimin/iOS-Monitor-Platform?tab=readme-ov-file#network

异常与崩溃

[toc]

异常与崩溃

BUGLY崩溃问题汇总.md

一、异常捕获

通过三方捕获的方式,可以选择 Buly 友盟 等。详细的不展开,下面我们只讲底层自己进行异常捕获。

1、iOS异常捕获

iOS异常捕获

开发iOS应用,解决Crash问题始终是一个难题。Crash分为两种,

一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本进程的内存地址,有可能是访问已被释放的内存;

另一种是未被捕获的Objective-C异常(NSException),导致程序向自身发送了SIGABRT信号而崩溃。其实对于未捕获的Objective-C异常,我们是有办法将它记录下来的,如果日志记录得当,能够解决绝大部分崩溃的问题。

UncaughtExceptionHandler

2、Flutter异常捕获

runZoned 中 handleUncaughtError 拦截未处理的异步错误

文档:《Flutter实战第二版:Flutter异常捕获:最终的错误上报代码

flutter_error_catch

点击查看源码

如果我想在应用程序退出时执行一些清理工作,我应该在 RunLoop 的哪个阶段进行操作?

1
2
3
4
// 程序被手动杀死。即将终止,可以在这里执行一些必要的清理工作
- (void)applicationWillTerminate:(UIApplication *)application {

}

applicationWillTerminate:这个方法并不总是可靠的,因为它的调用取决于多种因素,包括用户如何退出应用(例如,通过按 Home 键或通过系统设置)和系统状态。相比之下,RunLoop Observer 提供了一种更为可靠的方法来监听应用生命周期的特定阶段。通过在 RunLoop 的退出阶段添加一个 Observer,你可以确保在 RunLoop 即将退出时执行一些操作,这通常意味着应用即将进入后台或者终止。(还未亲自验证!!!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在 AppDelegate.m 文件中
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 创建 RunLoop Observer,并将 Observer 添加到主线程的 RunLoop
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopExit, TRUE, 0,&exitRunLoopCallback);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 由于这是一个全局的 Observer,我们不需要在这里释放它。它会在应用退出时自动释放。

// 其他初始化代码...
return YES;
}

// Observer 的回调函数
void exitRunLoopCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (activity == kCFRunLoopExit) {
// 执行清理工作
NSLog(@"Performing cleanup work before the application exits.");

// 这里添加你的清理代码
}
}

二、如何追踪app

iOS 如何追踪app

三、起死回生/回光返照

1、iOS 使用Runloop实现起死回生/回光返照

核心:创建一个Runloop,将主线程的所有Runmode都拿过来跑,作为应用程序主Runloop的替代。

但是:这样固然可以实现我们想要做的事情,但是会带来一个问题:因为我们为了继续执行程序而没有将控制权返回给导致崩溃的调用函数,并且我们启动了自己的Runloop,所以永远不会返回到原始的Runloop中去了,这将意味着导致异常的线程使用的堆栈内存将永久泄漏。因此这种类型的方法应被视为调试工具或最后手段,所以,不要在Debug以外的环境使用它。本段摘自:线程保活提醒

视频:“Runloop起死回生/回光返照” 见 00:57:55–01:03:00 的视频 2021-08-21_Crash分析.wmv

文档:RunLoop总结:RunLoop的应用场景(五)阻止App崩溃一次

示例代码:https://github.com/Haley-Wong/RunLoopDemos

页面加载时长-1需求规划

[toc]

页面加载时长-1需求规划

其他:

一、需求一期

1、新增原生页面的 异常Type、TTI

1、改造火山添加FP、FCP、FMP主要的时长数据。

①???页面时长支持从 跳转init 开始;

②新增 FMP_TYPE 类型,当类型异常时候,不进行时长上报,避免弱网等异常环境下对主数据的平均统计的干扰。

Type类型的说明如下:

序号 标识 描述 场景
1 Success 成功 用户进来放着等待到完成加载完,数据量少,但是是准确的时长
2 PointDown 用户点击 用户进来可能马上滑动或跳转到下一页,时间快
3 TimeOut 超时 弱网超时等,时间长

处理后,提供的数据表示例如下:

序号 页面描述 FP FCP FMP FMP_TYPE
1 作品首页 🆕 🆕 🆕 🆕
2 商城首页 🆕 🆕 🆕 🆕
3 发布页面 🆕 🆕 🆕 🆕
4 消息首页 🆕 🆕 🆕 🆕
5 我的首页 🆕 🆕 🆕 🆕

2、设计并新增TTI功能,及其数据类型。

序号 页面描述 FP FCP FMP TTI FMP_TYPE TTI_TYPE
1 作品首页 —- —- —- 🆕 —- 🆕
2 商城首页 —- —- —- 🆕 —- 🆕
3 发布页面 —- —- —- 🆕 —- 🆕
4 消息首页 —- —- —- 🆕 —- 🆕
5 我的首页 —- —- —- 🆕 —- 🆕

二、需求二期

1、原生视图–路由页内的标签页

路由页内的标签页,需要集成基类,统一在基类的init中开启记录,dispose的时候结束记录。

序号 页面描述 FP FCP FMP TTI FMP_TYPE TTI_TYPE
1 🆕作品–推荐 —- —- —- —- —- —-
2 🆕作品–同城 —- —- —- —- —- —-
3 🆕作品–热单 —- —- —- —- —- —-
4 🆕作品–关注 —- —- —- —- —- —-

2、非原生页面–H5

新增游戏等H5(非原生页面)的时长数据

描述 FP FCP FMP TTI FMP_TYPE TTI_TYPE
🆕H5-游戏 —- —- —- —- —- —-
🆕H5-用户协议 —- —- —- —- —- —-
🆕H5-隐私政策 —- —- —- —- —- —-
🆕H5-证件信息 —- —- —- —- —- —-
🆕H5-活动页 —- —- —- —- —- —-

三、需求三期

1、新增网络加载时长数据。

作用:当网络加载时长异常时,查看整个页面完整显示前的整条网络链路的耗时。

附:因为必须每个页面改动到业务逻辑代码,所以对改动顺序做以下优先级排序。

①优先以非主要页面切入,当系统稳定再进行后续核心页面的处理;

②在网络耗时功能测试稳定后,优先覆盖一级和二级的主要页面,继而再处理其他页面;

③处理其他页面;

描述 FP FCP FMP 网络加载时长
一级页面 —- —- —- 🆕
二级页面 —- —- —- 🆕

四、需求四期(单条记录表)

1、新增异常的的资源链接收集

新增异常的的资源链接收集,并上报到我们自己的后台。

作用:当网络加载时长异常时,通过上报的后台数据,查看并分析异常原因,

以《商品详情的FMP时长》异常为例:

1、筛选后台数据库,查看

①page == 商品详情页

②type != success

得到 商品详情的FMP时长 异常的所有数据记录

描述 FP FCP FMP 网络加载时长 失败的图片/视频链接 超时的图片/视频链接
H5-游戏 —- —- —- —- 🆕 🆕
H5-用户协议 —- —- —- —- 🆕 🆕
H5-其他 —- —- —- —- 🆕 🆕

五、需求五期(网络耗时)

本需求已独立为《页面加载-2网络时长-1需求规划》

End