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

[toc]

页面加载网络时长-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
{
"page_duration": 8000,
"api": {
"total_duration": {
"start": 1000,
"end": 5000,
"duration": 4000,
},
"apis": [
{
"url": "api1",
"start": 1000,
"end": 5000,
"duration": 4000,
},
{
"url": "api2",
"start": 1000,
"end": 5000,
"duration": 4000,
}
]
}
}

步骤:

步骤 问题
1 一个页面加载过程必须进行的所有网络请求有哪些 8 梳理页面接口,建立并管理【页面(含页面类型)–到–页面加载过程必须进行的所有网络请求】的关联表
请求与页面加载的绑定:请求是哪个页面哪次进入发起的 基于梳理的页面接口,在请求时候
请求接口的开始和结束的记录及网络库对该数据的对外提供处理
请求接口的开始和结束的日志记录及展示处理
页面请求管理器(UnfinishPageApiManager):计算一个页面加载过程执行完所有必须的网络请求的耗时 4
页面绘制所需请求耗时信息的管理(AllPageTimeManager)
页面绘制各耗时信息总管理器(事件间等待,总完成的上报时机优化) 8

1、梳理页面接口,建立【页面(含页面类型)–到–页面加载过程必须进行的所有网络请求】的关联表管理器(静态、变更时需自己更新维护)

①梳理优先处理的核心页面

1
2
3
4
5
6
7
8
9
10
// allneedPage_ApiManager.dart
class AllNeedPageApiManager {
static Map<String, dynamic> allNeedPageApiMaps =
{
"ContentHomePage": [api1, api2],
"ContentDetailPage": [api1, api2],
"MallHomePage": [api1, api2],
"MallDetailPage": [api1, api2],
};
}

增加Type,区分一个页面不同业务使用。如订单列表中,我送出的和我收到的,同属于一个页面。

却通过type

2、网络库接口中,

  • 请求是哪个页面哪次进入发起的:

    为请求创建页面key:每次进入页面,创建页面唯一key,避免等下网络请求时候,不晓得是哪个页面哪个进入发起的。

  • 记录请求进行表

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
102
103
104
105
106
// unfinishPage_ApiManager.dart
class UnfinishPageApiManager {
// 所有页面的所有请求
static Map<String, dynamic> unfinishPageApiMaps = {};
// 示例
static Map<String, dynamic> unfinishPageApiMaps =
{
"ContentHomePage": [
{
apiUrl: api1,
apiStart: apiStart,
apiEnd: apiEnd,
},
{
apiUrl: api2,
apiStart: apiStart,
apiEnd: apiEnd,
}
],
"ContentDetailPage": [
{
apiUrl: api1,
apiStart: apiStart,
apiEnd: apiEnd,
},
{
apiUrl: api2,
apiStart: apiStart,
apiEnd: apiEnd,
}
],
};

// 页面请求开始
static void startApi({required String api, required String pageKey}) {
// 加锁🔐
// 获取指定页面已有的请求列表
List<Map<String, dynamic>> pageRequestingApiMaps = unfinishPageApiMaps[pageKey] ?? [];
// 添加新请求
Map<String, dynamic> newApiInfoMap = {
apiUrl: api,
apiStart: apiStart,
};
pageRequestingApiMaps.add(newApiInfoMap);
// 解锁🔐
}

// 页面请求结束
static void finishApi({required String api, required String pageKey}) {
// 加锁🔐
// 获取指定页面已有的请求列表,及其个数
List<Map<String, dynamic>> pageRequestingApiMaps = unfinishPageApiMaps[pageKey] ?? [];
// 找到此请求的开始信息,并补充上此请求的结束信息
for (Map<String, dynamic> item in pageRequestingApiMaps) {
item.addAll({"apiEnd": apiEnd});
}
int hasFinishCount = pageRequestingApiMaps.length;

// 获取指定页面原本应该有几个请求
List<Map<String, dynamic>> pageNeedApiMaps = PageApiManager.allNeedPageApiMaps[pageKey] ?? [];
int needCount = pageNeedApiMaps.length;

// 判断是否是最后一个请求,是的话上报本次页面的所有所需结束
if (hasFinishCount >= needCount) {
_notifyToPage(pageRequestingApiMaps);
}
// 解锁🔐
}

// 通知页面请求完成
static _notifyToPage(List<Map<String, dynamic>> pageRequestingApiMaps) {
int minStart;
int maxEnd;
for (Map<String, dynamic> item in pageRequestingApiMaps) {
int itemStart = item["apiStart"];
int itemEnd = item["apiEnd"];
if (minStart == null) {
minStart = itemStart;
} else {
minStart = min(minStart, itemStart);
}
if (maxEnd == null) {
maxEnd = itemEnd;
} else {
maxEnd = max(maxEnd, itemEnd);
}
}
if (minStart == null || maxEnd == null) {
debugPrint("发生错误了");
return;
}
int pageApiTotalDuration = maxEnd - minStart;
Map<String, dynamic> pageApiMap = {
"page_duration": pageApiTotalDuration,
"api": {
"total_duration": {
"start": 1000,
"end": 5000,
"duration": 4000,
},
"apis": pageRequestingApiMaps
}
}
eventBus.fire(PageFinishLoadRequest(pageApiMap: pageApiMap));
}
}

页面接收

1
2
3
4
5
6
7
8
9
10
class AllPageTimeManager {
pageRequestEvent = eventBus.on<PageFinishLoadRequest>().listen((event) {
Map<String, dynamic> pageApiMap = event.pageApiMap;
});


_uploadToSevice({required Map<String, dynamic> uploadPageInfo}) {

}
}

End

页面加载-1整体时长-2数据分析

[toc]

页面加载-1整体时长-2数据分析

五、数据分析

1、需求一期

1、原始数据整理

按版本整理数据,整理频率为常态下的下个版本提审时,统计上个版本。页面各时长数据:sheet_原始数据

page_load_data_ori1

2、原始数据分析1–异常数据

1、制定时长异常的参考标准。页面各时长数据:sheet_ref

image-20231102162146321

2、单独看每条记录,按异常程度,列出”超标”数据,并分析异常原因。

示例:

image-20231102161544919

3、合并看前后版本记录,比较版本差异数据

image-20231102162759893

2、需求二期

需求:页面时长变化趋势,不区分版本。绘制excel变化曲线图。

五、数据分析

前端APP/游戏,核心页面、核心接口的(首页4个tab,内容详情,商品详情,商城,订单,我的;主态农场,客态农场、下一家,排行榜,消息,收豆,抢羊,福利榜)。 TTI时间,游戏的加载时长,按照TP90/95统计数据,按天出数据。如何从大数据日志里抓数据,做成grafana监控大盘

核心页面数据分析,按周

核心页面控制在1s内,按90分位整理

End

高磁盘占用的排查与优化

[toc]

高磁盘占用的排查与优化

图片缓存区太大的案例

1、案例描述

在我们的Flutter打包的app上,出现测试人员对首页的无限列表疯狂频繁刷,冷启动刷的时候还好,越刷越后面越卡。

2、案例分析过程

经测试验证在其他机器上无法重现。后通过观察应用的磁盘占比,发现出现崩溃的该设备该应用的磁盘占比巨大,达好几G(只要占用有网络点播视频缓存、打赏动画视频、图片)。进一步观察发现在图片不大的情况下,其中图片的占比竟然也能接近上G,说明图片数量巨大。由此猜测是图片数量太多导致。随后通过高磁盘占用环境模拟,确实也出现了同样的情况。由此我们开始设想磁盘对应用性能的影响 的几种原因,并进行对应修复。经过代码排查图片库内部的同步、阻塞问题,发现了一个潜在的同步危险。

1
if (cacheFlie.existsSync()) {  /// existsSync是一个同步方法,用于检查具有该路径的文件系统实体是否存在。

3、为什么原生iOS上没发现类似问题/微信为什么没出现类似问题?

可能原因:①之前原图相同的图片显示在不同地方,也只是原图和缩略图两套而已,而不像图片万象这样,可能会存在好多套。②原生的SDWebImage在监测到内存不足的时候会自动进行清除。

图片缓存(无、太小、太大)的问题

小结:

图片缓冲区默认太小时(一定有问题) 图片缓冲区设大后(可能有问题)
出现的平台 Flutter上默认的缓存上限1000个图片或者100MB内。
原生iOS的SDWebImage一般不设大小,只根据maxCacheAge
Flutter上extended_image判断图片是否已缓存时候有性能问题,而导致卡顿。
原因滑动列表会进行很多图片的获取,从而由于任务过多,对性能产生影响,例如增加CPU使用率,导致电池消耗加快,进而可能引发崩溃。
后果 加载过的图片很容易再次出现。 可能:存的图片数量太大。影响图片存取,而崩溃。
出现的案例 ①列表划过之后再回来;
②列表页面离开后再回来。
测试人员频繁刷,导致刷满图片缓冲区后,图片数量太大,影响图片存取,从而崩溃。
解决方案 图片存储进行分区。对 url 进行 md5 取值,对 md5 取前两个字母为新文件夹。

1、图片缓冲区默认太小时

在Flutter中加载图片(一般是网络图片),我们常常会遇到下面几个问题:Flutter 图片缓存问题分析

  1. 同页面内,加载过的图片,再次出现的时候,会重新加载,特别是列表的图片;

    根源:Flutter 内置的缓存机制 PaintingBinding.instance.imageCache 的 maximumSizemaximumSizeBytes 属性默认的缓存上限1000个图片或者100MB内。

    1
    2
    3
    > const int _kDefaultSize = 1000;
    > const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
    >

    即图片没有加载到100MB,加载到1000个图片,也会开始根据LRU的规则清理释放缓存。在某些情况下,比如电商,一整个页面80%的元素都用图片占满,小到图标,大到广告banner,个数很容易就到1000了,但是经过压缩剪裁,图片普遍都控制在几十kb甚至10kb以下,即使1000张也远远达不到内存的上限,并且100MB的上限对于某些机型来讲也相对较小了。

    解决:设置大一点

    1
    2
    3
    > PaintingBinding.instance.imageCache.maximumSize = 10000;
    > PaintingBinding.instance.imageCache.maximumSizeBytes = 800 << 20; // 800 MiB
    >

    附:除了 Flutter 内置的缓存机制外,还有第三方库如 cached_network_image 提供了更丰富的图片缓存功能,包括硬盘缓存等 。如果需要更复杂的图片缓存策略,可以考虑使用这类第三方库来扩展 Flutter 的图片加载和缓存能力。

    附2:extended_image 提供了一套完整的图片加载和缓存解决方案,其缓存的控制也还是通过修改 ImageCache 的配置来控制内存缓存的大小和数量。

    附2:SDWebImage 默认情况下会缓存图片一周(maxCacheAge 的默认值),并且没有对缓存空间大小设置限制(maxCacheSize 默认值未设定),这意味着理论上应用中的图片缓存可以占满整个设备存储空间。

    1
    2
    > [SDImageCache sharedImageCache].maxCacheSize = 50 * 1024 * 1024; *// 50MB*
    >
  2. 列表快速滑动时,加载完成再往回滑动,之前的图片还是需要重新加载;

    根源:等同于按需加载。

    解释:加载图片时,为了避免过快滑动,使得同时加载的图片过多导致卡顿甚至崩溃。所以若是内存中已有缓存,则直接返回缓存,若是没有则判断是否在快速滑动,若是正在快速滑动,则下一帧再加入队列处理,若是图片已经被移出屏幕(即没有在tree上),可能会被跳过。

    解决:快速滑动如此,可不处理。

  3. 有时返回上一页面时,上一页面已经加载完成的图片,会重新加载,假如没有占位图会特别明显的闪动。

    同现象1一样。

2、图片缓冲区设大后

图片分区:

SDWebImage 所有的图片都放在 _diskCachePath 目录下,_diskCachePath是只有一层吗?还是里面还会再分文件目录?

SDWebImage 的 _diskCachePath 目录是 SDWebImage 用来存储磁盘缓存的根目录。在这个根目录下,SDWebImage 会进一步组织文件结构,以避免所有图片都放在单一目录下造成文件系统性能问题。具体来说,图片文件会根据图片 URL 的 MD5 值进行散列,然后根据这个散列值来创建子目录和文件名,从而在磁盘上形成一个树状的目录结构

这种目录结构的设计有助于分散文件系统操作,减少单个目录下的文件数量,从而提高文件检索和写入的性能。此外,SDWebImage 还提供了设置选项,允许开发者自定义图片的存储路径,以及通过 addReadOnlyCachePath: 方法添加额外的只读缓存路径 。

总的来说,_diskCachePath 不是单一层级,而是一个包含多个子目录的复杂结构,每个子目录下存储着根据 MD5 散列值组织好的图片文件,以此来优化磁盘缓存的性能和效率 。

一、磁盘对应用性能的影响

1、原因猜测

可能原因:应用使用过程时候,存在对磁盘中的某个文件的操作,从而导致应用性能问题。操作的可能性如下:

序号 阶段 如果 后果
1 访问 文件量太大 访问效率问题
2 读写 文件大小太大 读写问题

2、排查方案

高磁盘占用的排查与优化

图片来源于:高磁盘占用的排查与优化.graffle

二、高磁盘占用环境模拟

步骤 阶段 操作 目的
1 资源准备1 提供图片的万象接口,以通过万象能获取到各种图片地址 避免得去找非常多的图片数据源
2 资源准备2 提供下载万象图片的接口(指定宽范围),以通过万象图片能下载到很多图片 避免得去找非常多的图片数据源
3 资源下载 下载指定宽范围的万象图片,得到各种图片
每次只添加 100 张,避免模拟功能在同步状态下执行出错;
通过下载占据磁盘空间
4 数据验证1 打印磁盘(沙盒)目录,验证下载
iOS-查看沙盒文件(真机+模拟器)
验证确实磁盘空间被增加
5 数据验证2 计算磁盘大小 验证确实磁盘空间被增加

相应的代码实现,见 附1:高磁盘占用环境模拟的相关代码

三、高磁盘占用环境优化

序号 解决 方案
1 避免单文件夹太大 存储分区处理
2 单文件夹已经太大 存储空间清理

1、避免单文件夹太大—-存储分区处理

1、背景:图片文件下文件数量太多,导致搜索效率下降

1
2
3
4
5
6
7
8
9
10
final Directory _cacheImagesDirectory = Directory(
join((await getTemporaryDirectory()).path, cacheImageFolderName));

// exist, try to find cache image file
if (_cacheImagesDirectory.existsSync()) { /// existsSync是一个同步方法,用于检查具有该路径的文件系统实体是否存在。
final File cacheFlie = File(join(_cacheImagesDirectory.path, md5Key));
if (cacheFlie.existsSync()) { /// existsSync是一个同步方法,用于检查具有该路径的文件系统实体是否存在。

}
}

2、优化方案:对图片存储,进行分区存储。获取方式同理。

3、优化步骤如下

对 url 进行 md5 取值

对 md5 取首字母为新文件夹

将该url所对应的图片存储在图片存储目录下的子目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// get md5 from key
static String keyToMd5(String key) => md5.convert(utf8.encode(key)).toString();

/// 获取网络图片最后的本地存储文件夹路径
static Future<String> getLastDirPath(String imageUrl, {bool? useSubDir}) async {
Directory cacheDir = await getTemporaryDirectory();
String imageCacheHomeDirPath = join(cacheDir.path, cacheImageFolderName);
if (useSubDir != true) { // 不使用子目录,直接一个文件夹存放
return imageCacheHomeDirPath;
}

String subDirHome = keyToMd5(imageUrl);
String imageCacheSubDirName = subDirHome.substring(0, 1);
String lastDirPath = "$imageCacheHomeDirPath/$imageCacheSubDirName";
return lastDirPath;
}

4、优化验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// 计算图片文件的查找耗时(查找失败返回-1)
static Future<int> calculateFindDuration(String imageUrl, {bool? useSubDir}) async {
String md5Key = keyToMd5(imageUrl);
final String lastDirPath = await getLastDirPath(imageUrl, useSubDir: useSubDir);
final Directory cacheImagesDirectory = Directory(lastDirPath);

int foundStartTime = DateTime.now().millisecondsSinceEpoch;
// exist, try to find cache image file
if (cacheImagesDirectory.existsSync()) { /// existsSync同步方法,检查该路径的文件夹是否存在
final File cacheFlie = File(join(cacheImagesDirectory.path, md5Key));
if (cacheFlie.existsSync()) { /// existsSync同步方法,检查该路径的文件是否存在
int foundEndTime = DateTime.now().millisecondsSinceEpoch;
int foundDuration = foundEndTime - foundStartTime;
return foundDuration;
}
}

return -1;
}

2、单文件夹已经太大—-存储空间清理

文件的属性有创建时间、修改时间

原理:变化过期时间,遍历文件属性,删除修改时间大于该过期时间的文件

1、设置过期时间的初始值,遍历文件属性,删除修改时间大于该过期时间的文件;

2、检查清理后的大小,如果大小还不在规定大小内,则继续清理;

3、继续清理:设置每次清理的过期时间增加值,对过期时间的初始值增加,并重复以上步骤;

四、优化方案的实施

目的:避免功能异常,出现集体性问题。

灰度方案:请参照 灰度系统

五、延伸

问题:一旦修改分区规则(如原本使用url进行md5后的首位值,后面改成md5后的两位值),旧文件就得重新下载。

解决:使用一致性哈希算法。16 张图解带你掌握一致性哈希算法

1、对存储节点进行hash,创建节点和hash值的哈希映射,如 {100001: “a”, 300001: “a”, 400001: “a”}

2、对数据url进行hash寻址

  • 首先,对 key 进行哈希计算,确定此 key 在环上的位置;
  • 然后,从这个位置沿着顺时针方向走,遇到的第一节点就是存储 key 的节点。(对「数据」进行哈希映射得到一个结果要怎么找到存储该数据的节点呢?)

结果:在一致哈希算法中,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响

假设节点数量从 3 减少到了 2,比如将节点 A 移除:你可以看到,key-02 和 key-03 不会受到影响,只有 key-01 需要被迁移节点 B。

image-20231008173625108

隐患:一致性哈希算法虽然减少了数据迁移量,但是存在节点分布不均匀的问题。如下:

image-20231008173700010

说明:一致哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而一致哈希算法是对 2^32 进行取模运算,是一个固定的值

附:hash算法

使用一致性哈希(Consistent Hashing)可以帮助您将大批图片文件分散存储到不同的文件中,其中每个文件对应一个字母或数字。一致性哈希算法能够提供高效的数据分布和负载均衡,同时允许动态添加或删除文件节点。

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

class ConsistentHashing {
List<String> nodes;
Map<int, String> ring;

// 构造hash环
ConsistentHashing(List<String> nodes) {
this.nodes = nodes;
this.ring = {};

for (String node in nodes) {
addNode(node);
}
}

// ring[_hash(node + n.toString())] = node;
void addNode(String node) { // a、b...
for (int n = 0; n < 3; n++) { // 每个节点有3个虚拟节点(a0、a1、a2、b0、b1、b2...)
String virtualNode = node + n.toString();
int hashValue = _hash(virtualNode);
ring[hashValue] = node; // {hash_a0: "a", hash_a1: "a", hash_a2: "a"}
}
}

// ring.remove(_hash(node + n.toString()));
void removeNode(String node) { // a、b...
for (int n = 0; n < 3; n++) { // 每个节点有3个虚拟节点(a0、a1、a2、b0、b1、b2...)
String virtualNode = node + n.toString();
int hashValue = _hash(virtualNode);
ring.remove(hashValue);
}
}

// 从小到大排序所有虚拟节点的hash值,并进行遍历,找到最接近(第一个>=hashValue的节点)的虚拟节点。判断值应该存储的节点
String getNode(String key) {
if (ring.isEmpty) {
return null;
}

int hashValue = _hash(key);
List<int> sortedKeys = ring.keys.toList()..sort(); // [hash_a0, hash_b0, hash_a1, hash_b1, ...]
for (int ringKey in sortedKeys) {
if (hashValue <= ringKey) { // hash_xxx <= hash_a1 遍历找到第一个大于等于 hash_xxx 的节点
return ring[ringKey]; // ring[hash_a1] = "a" => 返回 "a"
}
}

return ring[sortedKeys[0]];
}

int _hash(String key) {
Digest hash = md5.convert(utf8.encode(key));
int hashValue = int.parse(hash.toString(), radix: 16);
return hashValue;
}
}

void main() {
// 创建36个文件节点,每个节点对应一个字母或数字
List<String> nodes = List.generate(26, (index) => String.fromCharCode(97 + index))
..addAll(List.generate(10, (index) => index.toString()));

// 初始化一致性哈希环
ConsistentHashing ring = ConsistentHashing(nodes);

// 假设有一批图片文件需要存储
List<String> imageFiles = ['image1.jpg', 'image2.jpg', 'image3.jpg'];

for (String imageFile in imageFiles) {
// 根据文件名获取对应的节点
String node = ring.getNode(imageFile);

// 将文件存储到对应的节点中
// 这里可以根据需要自行实现文件存储逻辑
print('Storing $imageFile to node $node');
}
}

附1:高磁盘占用环境模拟的相关代码

1、ts_toomany_image_data_vientiane.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// [图片处理方式:快速缩略模板](https://cloud.tencent.com/document/product/460/6929)
/// [图片处理方式:数据万象-缩放thumbnail](https://cloud.tencent.com/document/product/460/36540)
/// [图片处理方式:数据万象-旋转rotate](https://cloud.tencent.com/document/product/460/36542)
class TSTooManyImageDataVientiane {
static String newImageUrl(String imageUrl, {required int width}) {
String thumbnail = '?imageMogr2/thumbnail/';
thumbnail += '${width}x/';
thumbnail += 'format/webp/auto-orient/quality/100';

String newImageUrl = "$imageUrl$thumbnail";
// debugPrint('newImageUrl = $newImageUrl');
return newImageUrl;
}
}

2、ts_toomany_image_download_util.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:io';

import 'package:extended_image/extended_image.dart';
import 'ts_toomany_image_data_vientiane.dart';

class TSTooManyImageDownloadUtil {
static String vientianeImageUrl = "https://images.xihuanwu.com/mcms/uploads/1647604960983901.jpg";

static void simulate_download_too_many(int imageWidthStart, int imageWidthEnd) {
int imageCount = imageWidthEnd - imageWidthStart;
for (var i = 0; i < imageCount; i++) {
int iImageWidth = imageWidthStart + i;
String iImageUrl = TSTooManyImageDataVientiane.newImageUrl(vientianeImageUrl, width: iImageWidth);
getNetworkImageData(iImageUrl);
sleep(const Duration(milliseconds: 10)); // 延迟10ms,避免下载太多计算问题
}
}
}

3、ts_toomany_image_optimize_check_util.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:extended_image_library/extended_image_library.dart';

import 'ts_toomany_image_data_vientiane.dart';
import 'ts_toomany_image_download_util.dart';

class TSTooManyImageOptimizeCheckUtil {
/// 计算图片的查找时长
static checkImageFindDuration() async {
String foundImageUrl = TSTooManyImageDataVientiane.newImageUrl(TSTooManyImageDownloadUtil.vientianeImageUrl,
width: 6600);
getNetworkImageData(foundImageUrl);
int foundDuration = await TSTooManyImageOptimizeCheckUtil.calculateFindDuration(foundImageUrl);
debugPrint("$foundImageUrl 的查找时间为:$foundDuration");
}
}

End

终端常用命令

一、brew 安装

Mac系统是使用brew命令install,而brew的安装需要通过ruby安装

1
2
# 安装ruby
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

其他:

brew update -》更新homebrew到最新版本

brew doctor -》检查homebrew状态

二、brew 使用

建议安装前,先执行遍brew update

1、使用 brew 安装/卸载 node、npm

1
2
brew install node
brew uninstall node

使用 brew install node 安装即可。(如果已存在,则使用brew upgrade node)

安装完成npm就ok啦~

可以使用 npm -v 看下效果

2、使用brew安装nvm

brew update
brew install nvm

npm install npm@latest -g

常用命令

1、版本相关命令

命令描述 命令举例 备注
查看node版本 node –v
查看版本详情 npm version
查看npm最新版本号 npm view npm version
查看npm所有的版本号 npm view npm versions

2、升级相关命令

命令描述 命令举例
安装最新稳定版 nvm install stable
升级到最新版 npm install npm@latest -g
升级到指定版本 npm install npm@xx -g

image-20230420193528874

第1节:Jenkins的安装与启动

[toc]

第1节:Jenkins的安装与启动

自己的文章:

①、Jenkins(一)初步认识

一、Jenkins的安装

①、直接下载Jenkins安装。

1
2
3
4
brew install jenkins-lts

# Stable:稳定版本。
# LTS(Long Term Support):长期演进版

②、安装成功后,其会自动打开http://localhost:8080

此时如果我们发现打开错误。原因是我们缺少Jenkins的运行环境,所以我们需要安装java的jdk。如果要验证是否是这个原因的话,我们可以通过命令行查看当前的java版本
Java
果然发现没有安装,则我们通过下载jdk来进行安装。
java的jdk下载地址
jenkins_jdk1
jenkins_jdk2
安装完后,我们再运行一下验证命令,发现可以运行了,即我们安装好了。
Java

③、java环境安装成功之后,接下来我们就可以正常访问Jenkins了。Jenkins的访问地址为http://localhost:8080。打开的页面显示如下:
jenkins_3

④、提示我们需要输入其指定文件夹下的密码来访问。则我们访问其指定文件,发现没法访问该文件内容
jenkins_4
jenkins_noPermission1

所以这里我们通过修改权限,让读取该文件内容,如下:
jenkins_noPermission2
⑤、将读取后的密码输入到登录网页上,即可登录,登录后的结果为:
jenkins_start1

⑥、因为此时为首次登陆,所以其会提示我们安装Jenkins建议的插件。则我们按照步骤进行Jenkins默认插件的安装。安装过程如下所示:
jenkins_start2
jenkins_start3

⑦、一路安装过来,输入用户名,密码。
之后继续登录http://localhost:8080/可以看到最终的显示结果为:
jenkins_use1
此时则我们的Jenkins的初始安装完毕(之后我们可能还需要安装一些自己需要的插件)。

二、Jenkins的启动

没有权限问题的Jenkins正确的启动方式如下(按下面方式打开,才不会出现权限问题):

1
2
$ sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist
$ java -jar /Applications/Jenkins/jenkins.war --httpPort=8080

当你执行完这两行命令的时候,你可以在浏览器上输入http://localhost:8080来访问Jenkins了。(如果你只执行了第一行,没执行第二行,会出现无法访问)

三、Jenkins的升级

1、如何查看jenkins 的版本号

jenkins页面的右下角就是jenkins版本号信息

image-20200927143448781

image-20200927143111985

image-20200927233412864

image-20200927234036120

要执行下载好的版本

image-20200928001116531

游戏随笔

有这么一个技能,当怪物血量高于总血量的70%时,你的伤害增加50%。

以血量1000下的不同攻击力分别计算带该技能后,相当于整个攻击过程增加了多少的攻击力?
情况1,攻击力10:未带技能时攻击次数是100次,有带技能时加成与未加成的攻击次数分别是20和70(有带技能且加成时攻击力是15,打20次后降到70%以下的血,剩700要用攻击力10打),相当于攻击力增加10%。附(20+70)/100=0.90
情况2,攻击力100:未带技能时攻击次数是10次,有带技能时加成与未加成的攻击次数分别是2和7(有带技能且加成时攻击力是150,打2次后降到70%以下的血,剩700要用攻击力100打),相当于攻击力增加10%。附(2+7)/10=0.90
情况3,攻击力200:未带技能时攻击次数是5.0次,有带技能时加成与未加成的攻击次数分别是1和3.5(有带技能且加成时攻击力是300,打1次后降到70%以下的血,剩700要用攻击力200打),相当于攻击力增加10%,即(1+3.5)/5.0=0.90
情况4,攻击力300:未带技能时攻击次数是3.3次,有带技能时加成与未加成攻击次数分别是1和1.83(有带技能且加成时攻击力是450,打1次后降到70%以下的血,剩550要用攻击力300打),相当于攻击力增加15%,即(1+1.83)/3.3≈0.85
情况5,攻击力400:未带技能时攻击次数是2.5次,有带技能时加成与未加成的攻击次数分别是1和1.0(有带技能且加成时攻击力是600,打1次后降到70%以下的血,剩400要用攻击力400打),相当于攻击力增加20%,即(1+1.0)/2.5=0.80
情况6,攻击力500:未带技能时攻击次数是2.0次,有带技能时加成与未加成的攻击次数分别是1和0.5(有带技能且加成时攻击力是750,打1次后降到70%以下的血,剩250要用攻击力500打),相当于攻击力增加25%,即(1+0.5)/2.0=0.75

攻击力 未带技能时
攻击次数
有带技能且生效时
攻击力、可打次数和输出伤害
有带技能但失效时
剩余血量和还需攻击次数
攻击次数比 增长
1 10 1000/10 = 100次 15功 * 20次 = 300血 700血 / 10功 = 70次 (20+70)/100 = 0.90 10%
2 100 10次 150功 * 2次 = 300血 700血 / 100功 = 7.0次 (2+7.0)/10 = 0.90 10%
3 200 5.0次 300功 * 1次 = 300血(剩<700) 700血 / 200功 = 3.5次 (1+3.5)/5.0 = 0.90 10%
4 300 3.3次 450功 * 1次 = 450血 550血 / 300功 ≈ 1.83次 (1+1.83)/3.3≈0.85 15%
5 400 2.5次 600功 * 1次 = 600血 400血 / 400功 = 1.0次 (1+1.0)/2.5 = 0.80 20%
6 500 2.0次 750功 * 1次 = 750血 250血 / 500功 = 0.5次 (1+0.5)/2.0 = 0.75 25%
7 600 1000/600≈1.66次 900功 * 1次 = 900血 100血 / 600功 ≈ 0.16次 (1+0.16)/1.66≈0.70 30%
8 666 1000/666.6=1.5次 1000功 * 1次 = 1000血 0血 / 666.66功 = 0.0次 (1+0.0)/1.5 ≈ 0.66 33%
9 700 1000/700 ≈1.42次 1050功 * 1次 = 1050血 0血 / 700功 = 0.0次 (1+0.0)/1.42 ≈ 0.70 30%
10 800 1000/800 =1.25次 1200功 * 1次 = 1200血 0血 / 800功 = 0.0次 (1+0.0)/1.25 = 0.80 20%
11 900 1000/900 ≈1.11次 1350功 * 1次 = 1350血 0血 / 900功 = 0.0次 (1+0.0)/1.11 ≈ 0.90 10%
12 1000 1000/1000 =1.0次 1500功 * 1次 = 1500血 0血 / 1000功 = 0.0次 (1+0.0)/1.0 = 1.0 0%

最大增长率是33% 。发生在带技能后技能生效一枪爆头,把技能的增效才充分利用了。即最大增长率发生在 (1+50%)*1枪=处决即未加攻时1.5枪,加功时1枪。(1+0.0)/1.5 ≈ 0.66,增长了33%,差不多略小于 50%*70% = 35%

如果技能是当怪物血量高于总血量的70%时,伤害可以增加60%(而不是50%)呢?

攻击力 未带技能时
攻击次数
有带技能且生效时
攻击力、可打次数和输出伤害
有带技能但失效时
剩余血量和还需攻击次数
攻击次数比 增长
1 10 1000/10 = 100次 16功 * 19次 = 304血 696血 / 10功 = 70次 (19+70)/100 = 0.89 11%
2 100 10次 160功 * 2次 = 320血 680血 / 100功 = 6.8次 (2+6.8)/10 = 0.88 12%
3 200 5.0次 320功 * 1次 = 320血(剩<700) 680血 / 200功 = 3.4次 (1+3.4)/5.0 = 0.88 12%
4 300 3.3次 480功 * 1次 = 480血 520血 / 300功 ≈ 1.73次 (1+1.73)/3.3≈0.82 18%
5 400 2.5次 640功 * 1次 = 640血 360血 / 400功 = 0.9次 (1+0.9)/2.5 = 0.76 24%
6 500 2.0次 800功 * 1次 = 800血 200血 / 500功 = 0.4次 (1+0.4)/2.0 = 0.70 30%
7 600 1000/600≈1.66次 960功 * 1次 = 960血 40血 / 600功 ≈ 0.06次 (1+0.06)/1.66≈0.64 36%
8 625 1000/625 =1.6次 1000功 * 1次 = 1000血 0血 / 625功 = 0.0次 (1+0.0)/1.6 = 0.625 37.5%
9 700 1000/700 ≈1.42次 1120功 * 1次 = 1120血 0血 / 700功 = 0.0次 (1+0.0)/1.42 ≈ 0.70 30%
10 800 1000/800 =1.25次 1280功 * 1次 = 1280血 0血 / 800功 = 0.0次 (1+0.0)/1.25 = 0.80 20%
11 900 1000/900 ≈1.11次 1440功 * 1次 = 1440血 0血 / 900功 = 0.0次 (1+0.0)/1.11 ≈ 0.90 10%
12 1000 1000/1000 =1.0次 1600功 * 1次 = 1600血 0血 / 1000功 = 0.0次 (1+0.0)/1.0 = 1.0 0%

最大增长率是37.5% 。发生在带技能后技能生效一枪爆头,把技能的增效才充分利用了。即最大增长率发生在 (1+60%)*1枪=处决即未加攻时1.6枪,加功时1枪。(1+0.0)/1.6 = 0.625,增长了37.5%,差不多略小于 60%*70% = 42%

如果技能是当怪物血量高于总血量的50%(不用到70%)就可以生效伤害增加50%的效果呢?

攻击力 未带技能时
攻击次数
有带技能且生效时
攻击力、可打次数和输出伤害
有带技能但失效时
剩余血量和还需攻击次数
攻击次数比 增长
1 10 1000/10 = 100次 15功 * 34次 = 510血 490血 / 10功 = 49次 (34+49)/100 = 0.73 27%
2 100 10次 150功 * 4次 = 600血 400血 / 100功 = 4.0次 (4+4.0)/10 = 0.80 20%
3 200 5.0次 300功 * 2次 = 600血 400血 / 200功 = 2.0次 (2+2.0)/5.0 = 0.80 10%
4 300 3.3次 450功 * 2次 = 900血 100血 / 300功 ≈ 0.33次 (2+0.33)/3.3≈0.70 30%
333.33 1000/333.33=3.0次 500功 * 1次 = 500血(剩<500) 500血 / 333.33功=1.5次 (1+1.5)/3.0≈0.83 17%
5 400 2.5次 600功 * 1次 = 600血(剩<500) 400血 / 400功 = 1.0次 (1+1.0)/2.5 = 0.80 20%
6 500 2.0次 750功 * 1次 = 750血 250血 / 500功 = 0.5次 (1+0.5)/2.0 = 0.75 25%
7 600 1000/600≈1.66次 900功 * 1次 = 900血 100血 / 600功 ≈ 0.16次 (1+0.16)/1.66≈0.70 30%
8 666 1000/666.6=1.5次 1000功 * 1次 = 1000血 0血 / 666.66功 = 0.0次 (1+0.0)/1.5 ≈ 0.66 33%
9 700 1000/700 ≈1.42次 1050功 * 1次 = 1050血 0血 / 700功 = 0.0次 (1+0.0)/1.42 ≈ 0.70 30%
10 800 1000/800 =1.25次 1200功 * 1次 = 1200血 0血 / 800功 = 0.0次 (1+0.0)/1.25 = 0.80 20%
11 900 1000/900 ≈1.11次 1350功 * 1次 = 1350血 0血 / 900功 = 0.0次 (1+0.0)/1.11 ≈ 0.90 10%
12 1000 1000/1000 =1.0次 1500功 * 1次 = 1500血 0血 / 1000功 = 0.0次 (1+0.0)/1.0 = 1.0 0%

CocoaPods库部署

[toc]

如果你在终端执行pod search xxx,未得到上面的结果,是因为cocoaPods的本地search_index.json文件有问题,
方法①:请前往~/Library/Caches/CocoaPods/search_index.json将该文件删除,
方法②:或者直接在终端输入:rm ~/Library/Caches/CocoaPods/search_index.json
删除后,再重新执行命令即可。

一、远程库

1、添加库的版本

1.1、添加公有库的版本

1.2、添加私有库的版本

1
2
3
4
5
6
 #验证方法1:
pod lib lint CQBaseUIKit.podspec --sources='https://github.com/CocoaPods/Specs.git,https://gitee.com/dvlproad/dvlproadSpecs' --allow-warnings --use-libraries --verbose
#验证方法2:
pod lib lint CQBaseUIKit.podspec --sources=master,dvlproad --allow-warnings --use-libraries --verbose
#提交方法:
pod repo push dvlproad CQBaseUIKit.podspec --sources=master,dvlproad --allow-warnings --use-libraries --verbose

不需要过多验证的话,简洁版:

1
pod repo push dvlproad CQThemeUIKit.podspec --allow-warnings

2、删除库的某个版本

2.1、删除公有库的某个版本

2.2、删除私有库的某个版本

CocoaPods创建公有Pod库后删除特定版本来纠正意外推送

可以删除一个POD的特定版本来纠正意外推送。

1
pod trunk delete PODNAME VERSION

3、放弃整个库

你也可以放弃整个POD和所有版本。

1
pod trunk deprecate PODNAME

确认时,请回复一个”y”(小写字母 y)

二、本地库

1、库的本地位置

1
2
# 库目录
~/.cocoapods/repos/dvlproad

podspec写法

Podspec编辑方法:
首先确认编辑环境,podspec的引号”是否正确

如果Podspec有依赖的系统库,则添加方法如下:

1
s.libraries = "sqlite3"

如果Podspec有依赖的第三方库,则添加方法如下:

1
s.dependency 'FMDB', '~> 2.5'

如果Podspec有依赖的资源,则添加方法如下:

1
s.resources = "CJSliderViewController/**/*.png"

podspec常见错误

SVN配置代码库1

解答:参考Cocoapod compilation fails when loading .xib file
即错误原因为:把非.h.m的文件都放到resouces中,而不要放到soucres中

Git

Git feature

使用Git Flow工作流:

image-20220301145230642

image-20220301145504810

输入对应的生产环境分支名和开发分支名,如果输入的分支不存在,则会弹出如下提示:

image-20220301152403882

git 项目默认自动创建 master 主分支;
点击仓库–> git flow 或 hg fow –>初始化仓库,初始化仓库并创建develop开发分支;

在这里插入图片描述

创建 feature 功能分支:

双击 Sourcetree 左侧的 develop 开发分支,切换到 develop 开发分支;
点击仓库–> git flow 或 hg fow –>建立新的功能,输入功能名称,点击确定即可;

  1. 在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

完成 feature 功能分支:

当 feature 功能分支开发完成后,即可将 feature 中的代码合并到 develop 分支中。

提交 feature 功能分支的代码到远程;
在 feature 分支上,点击仓库–> git flow 或 hg fow –>完成功能;

在这里插入图片描述

feature 功能分支中的代码合并到了develop 开发分支,feature 分支被删除;

创建 release 发布分支:

双击 Sourcetree 左侧的 develop 开发分支,切换到 develop 开发分支;
点击仓库–> git flow 或 hg fow –>建立新的发布版本,输入发布版本号,点击确定即可;
完成 release 发布分支:

在 release 分支上,点击仓库–> git flow 或 hg fow –>完成发布版本,发布完成后 release 分支的代码同时合并到了 master 分支和 develop 分支。

创建 hotfix 补丁分支:

在 master 分支上,点击仓库–> git flow 或 hg fow –>建立新的修复补丁。

完成 hotfix 补丁分支:

在 hotfix 分支上,点击仓库–> git flow 或 hg fow –>完成修复补丁,合并完成后 hotfix 的代码同时合并到了 master 分支和 develop 分支。