AI-③opencode会话管理

问题背景

使用 opencode 时,退出后会提示一句:

1
2
3
4
5
6
7

█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀

Session New session - 2026-05-08T09:32:17.883Z
Continue opencode -s ses_xxxxxxxxxxxxxxxxxxxx

但如果终端窗口关得太快,或忘记复制,这个会话 ID 就丢了,导致无法恢复之前的会话。

Read More

日志系统

前言:主要事项

1、异常数据的发现及补充、日志分类优化
2、日志数据的文件保存、滚动存储
3、日志文件上传
4、日志回捞

一、日志收集

1、日志分类

1.1、日志目标

序号 目标(Target)
1 app
2 sdk 三方库
3 h5 网页

1.2、日志类型

序号 类型 描述
1 api_app app中的网络请求
2 api_app_cache app中的网络请求的网络缓存请求
3 api_buriedPoint 埋点的网络请求
4 sdk_other sdk的各种事件(初始化等)
5 sdk_api sdk中的网络请求
6 dart 语法
7 widget 视图(布局像素越界等)
8 click_other 点击、
9 click_share 分享
10 native_route 路由/跳转
11 h5_route 与网页跳转有关
12 h5_js js交互
13 monitor_network 监控:网络类型变化
14 monitor_lifecycle 监控:生命周期变化
15 buriedPoint_other 埋点数据生成等
16 im IM
17 heartbeat 心跳
18 other 其他

2、日志等级

序号 目标(Level) 描述
1 Normal 正常信息(目前用于请求开始)
2 Success 成功信息(目前用于请求结束:成功)
3 Warning 警告信息(目前用于请求结束:报错)
4 Error 错误日志(目前用于请求结束:失败)
5 Dangerous 危险(处理白屏等) 一般会进行额外的埋点

3、日志整合归类

序号 列表 标志 包含
1 全部 all 所有
2 警告 warning 所有的警告
3 错误 error 所有的错误
4 接口 api api_app、api_cache(不包括sdk_api、api_buriedPoint)
5 点击 click click_share、click_other、h5_js
6 路由 route navite_route、h5_route
7 网页 H5 h5_route、h5_js
8 sdk sdk sdk_api、sdk_other
9 code code dart、widget
10 埋点 buriedPoint api_buriedPoint、buriedPoint_other
11 监控 monitor monitor_lifecycle、monitor_network
12 其他 other other
13 api结果 api_result type=api_app & level != Normal
14 im im 本地缓存消息、历史消息、收到的消息等
15 心跳 heartbeat
devtool_log_home_page

4、日志的补充收集及其策略

要收集的数据已在上述《1.2、日志类型》中说明。

4.1、页面跳转

详见上述 《3、日志整合归类》中的”路由”

4.2、截断的收集

序号 关键字 场景
1 break for 、 swtich
2 return

4.3、用户关键行为

序号 行为 用途的场景说明
1 用户的资源选择 判断选择的本地资源是否上传到错误的存储桶

二、日志数据的文件保存、滚动存储

1、数据的文件保存

写入xlog文件中。

2、数据的文件滚动存储

2.1、日志的过期清理

1、本地日志文件结构

1
2
3
4
5
6
7
-- Document
-- log
-- 2020-10-01
-- xlog_2020-10-01_1.xlog
-- xlog_2020-10-01_2.xlog
-- 2020-10-18
-- xlog_2020-10-18_1.xlog

2、日志保留清理的配置信息(摘自/同 下文的日志接口中的《日志文件配置信息》)

序号 说明 字段 示例
接口回值1 最多保留几个天的日志目录 maxDayDirCount 7
接口回值2 最多每天保留几个文件 maxDayFileCount 10
接口回值3 每个日志文件超过多少后创建新文件 perFileMaxMB 5

3、清理方案

清理时机:在切换前后台的时候,

  • 日志配置信息的更新
  • 根据最新的日志配置信息,延迟10s后(避免影响启动),进行清理判断及清理
    • 遍历总日志目录 Document/log 下的所有日志文件夹
    • 如果天日志文件夹个数超过保留个数(避免是用保留时间,导致保留时间内只有一个日志,也被删掉),则按顺序删除早期日志
      • 遍历天日志目录 Document/log/yyyy-MM-dd 下的所有日志文件夹
      • 如果天日志文件夹个数超过保留个数(避免是用保留时间,导致保留时间内只有一个日志,也被删掉),则按顺序删除早期日志

2.2、日志文件的滚动写入

要增加的日志记录,如果添加后会超过日志文件的大小,则使用新文件写入。

细节点
1、日志文件的大小判断,不要每次都去读取文件,而是记录住大小变化
2、日志不要有一条写一条,而是使用写入缓冲区+定时器,5s尝试写入一次

3、数据的加密、解密

1、加密:

写入日志字符串时候,对编码后的字节数据,额外增加一个三位随机值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// 使用 GZip 压缩算法将字符串进行压缩,并返回压缩后的字节数据(List<int> 类型)。
List<int> _gzipCompressString(String inputString) {
final codec = GZipCodec();

// 将输入字符串 inputString 编码为 UTF-8 格式的字节数据
final encoded = utf8.encode(inputString);

// 对编码后的字节数据进行压缩,并返回压缩后的字节数据。
final compressedData = codec.encode(encoded);

return compressedData;
}

/// 加密日志字符串
String _encryptLogString(String logString) {
final compressedData = _gzipCompressString(logString); // 压缩字符串,并返回压缩后的字节数据(List<int> 类型)
final randomNumber = Random().nextInt(800) + 100;
final confound = compressedData.toString().substring(0, compressedData.toString().length - 1) + ", $randomNumber]"; // 在压缩后的字符串尾部加上一个随机字符串

return confound;
}

2、解密:

使用python脚本对log文件进行解密

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
import sys
import os
import gzip

# 读取.clog文件并解析数据
def parse_clog_file(input_file):
parsed_data = []

with open(input_file, 'r') as clog_file:
current_data = ""
for line in clog_file:
if current_data:
parsed_data.append(current_data)
binary_data = line[:-6] + ']' # 去除加密时候额外添加的三位随机数
byte_data = bytes(eval(binary_data))
decompressed_data = gzip.decompress(byte_data)
# current_data = byte_data.decode('utf-8')
print(decompressed_data)
current_data = decompressed_data.decode('utf-8')

# 添加最后一个数据条目
if current_data:
parsed_data.append(current_data)

return parsed_data

# 将解析后的数据写入与输入文件同名的.log文件
def write_to_log_file(parsed_data, input_file):
base_name = os.path.splitext(input_file)[0]
output_file = base_name + ".log"

with open(output_file, 'w') as log_file:
for data in parsed_data:
log_file.write(data + "\n")
log_file.write("\n")
log_file.write("=================================================================================================" + "\n")
log_file.write("\n")

if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python script.py <clogFileName>")
sys.exit(1)

input_clog_file = sys.argv[1]

# 解析.clog文件
parsed_data = parse_clog_file(input_clog_file)

# 将解析后的数据写入与输入文件同名的.log文件
write_to_log_file(parsed_data, input_clog_file)

print(f"Log data parsed from {input_clog_file} and written to {input_clog_file}.log.")

三、日志文件上传

1、日志文件命名及目录规范

以用户10000012020-01时候上传声音文件 beautiful_scenery.aac为例,其路径完整示例如下:

1
https://media.xxx.com/app1_test1/audio/1/1000001/2020-01/beautiful_scenery.aac

要上传的存储桶:

序号 说明 参数 约定值示例
1 存储桶region
regionGetFunction
网络环境 ap-shanghai
2 存储桶bucket
bucketGetFunction
网络环境 xxx-pro-image-1302324914
3 上传成功后,桶的映射路径值
cosFileUrlPrefixGetFunction
1、图片桶:https://images.xxx.com/
2、媒体桶:https://media.xxx.com/
3、日志桶:https://static.xxx.com/
4 要上传到桶的哪个相对路径下
cosFileRelativePathGetFunction
上传成功后完整路径=以上桶值+此相对路径
见下文 见下文
eg:app1_test1/audio/1/1000001/2020-01/beautiful_scenery.aac

《要上传到桶的哪个相对路径下 cosFileRelativePathGetFunction 》的文件路径分批说明:

序号 层次说明 层次值算法 层次值描述
1.1 存储桶里相对路径的前缀 app标志_环境标志
eg:app1_pro、app1_test1、app1_dev1
文件区分
1.2 是否在存储桶里进行进一步路径区分(可选) 根据场景决定是否对桶进行进一步分类。
if ( mediaType == xxx ) {
if(mediaScene == yyy) {

}
}

1、都是图片,但自拍图片要求独立出来
2、都是多媒体,但音视频要区分开
2.1 用户1级 int.parse(uid) % 1000; // 取余数 用户名取余,有效减少同层上文件夹个数
2.2 用户2级 uid 用户名
3 年月 DateTime.now().toString().substring(0, 7); 年-月
4 文件名 ${fileOriginName}_
${DateTime.now().microsecondsSinceEpoch}
.$fileExtensionType
文件名

附1:MediaType 媒体类型

序号 UploadMediaType 媒体类型 描述
1 unkonw 未知(默认值)
2 image 图片
3 audio 音频
4 video 视频
5 xlog 日志文件

附2:MediaScene场景 说明

序号 UploadMediaScene场景 描述
1 unkonw 未知(默认值)
2 selfie 自拍(安全等级较多)
3 im 会话聊天
4 live 直播

四、日志接口

1、日志记录接口(区别于回捞接口,需要在回捞前就有日志信息)

1、是否记录日志的开关及其配置信息

序号 说明 字段 示例
网页配置1 是否开启日志记录 isLogOn 1:开启、其他:关
网页配置2 要开启日志的用户(灰度上线)
(当且仅当开启上述日志记录时有效)
📢:未配置但开关为开时为全记录
logUserids [“101”, “102”]
网页配置3 要开启日志的设备(灰度上线)
(当且仅当开启上述日志记录时有效)
📢:未配置但开关为开时为全记录
logDeviceIds [“udid001”, “udid002”]
接口回值1 是否进行日志记录(灰度上线) needLog 1:记录、其他:不记录

2、不需要记录什么日志信息

序号 说明 字段 示例
接口回值1 不需要记录的日志类型 ignoreLogLevel [“Normal”, “Success”]
接口回值2 不需要记录的日志等级 ignoreLogType [“api_buriedPoint”, “buriedPoint_other”]

3、日志文件配置信息

序号 说明 字段 示例
接口回值1 最多保留几个天的日志目录 maxDayDirCount 7
接口回值2 最多每天保留几个文件 maxDayFileCount 10
接口回值3 每个日志文件超过多少后创建新文件 perFileMaxMB 5

4、日志文件问题回滚

序号 说明 字段 示例
网页配置1 强制删除什么时间前的日志 removeLogBeforeDate 2020-12-31
网页配置2 强制删除哪个用户的日志 removeUserids [“101”, “102”]
网页配置2 强制删除哪个设备的日志 logDeviceIds [“udid001”, “udid002”]
接口回值1 强制删除什么时间前的日志(不删除时空) removeLogBeforeDate 2020-12-31 或 空值

接口名定义:

1
function getLoggerConfig(userid, deviceId)

接口结果的json示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"logConfig": {
"needLog": 1,
"ignoreLogLevel": ["Normal", "Success"],
"ignoreLogType": ["api_buriedPoint", "buriedPoint_other"],
"maxDayDirCount": 7,
"maxDayFileCount": 10,
"perFileMaxMB": 5
},
"logRevert": {
"removeLogBeforeDate": "2020-12-31"
}
}

2、日志回捞及问题回滚接口

1、日志回捞

序号 说明 字段 示例
网页配置1 要回捞日志的用户 salvageUserids [“101”, “102”]
网页配置2 要回捞日志的设备 salvageDeviceIds [“udid001”, “udid002”]
网页配置3 日志回捞的开始时间 salvageStartDate 2020-01-01
网页配置4 日志回捞的结束时间 salvageEndDate 2020-01-07
接口回值1 此用户此设备日志回捞的开始时间 salvageStartDate 2020-01-01
接口回值1 此用户此设备日志回捞的结束时间 salvageEndDate 2020-01-07

2、日志文件问题回滚

序号 说明 字段 示例
网页配置1 强制删除什么时间前的日志 removeLogBeforeDate 2020-12-31
网页配置2 强制删除哪个用户的日志 removeUserids [“101”, “102”]
网页配置2 强制删除哪个设备的日志 logDeviceIds [“udid001”, “udid002”]
接口回值1 强制删除什么时间前的日志(不删除时空) removeLogBeforeDate 2020-12-31 或 空值

接口名定义:

1
function getLoggerSalvageAndRevert(salvageUserids, salvageDeviceIds)

接口结果的json示例如下:

1
2
3
4
5
6
{
"salvageId": "1001",
"salvageStartDate": "2020-01-01",
"salvageEndDate": "2020-01-07",
"removeLogBeforeDate": "2020-12-31"
}

3、日志文件上传到cos后同步给后台

用途:日志文件上传到cos后同步给后台,后台数据库记录每个用户,每个设备都捞到了什么数据。

接口名定义:

1
function addLoggerFile(userid, deviceId)

接口结果的json示例如下:

1
2
3
{

}

四、日志回捞

1、后台通过接口返回要回捞的信息。

2、前端用户在下次使用,收到后台信息时候,进行本地日志的上传。在腾云存储桶能收到对应日志,即代表成功。

五、方案实施

灰度方案:请参照 灰度系统 。(附:日志记录开关里目前已有灰度策略。)

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

End

框架设计模式-⑦组件化

一、概念了解

1、组件化

就是**”基础库”或者“基础组件”,**意思是把代码重复的部分提炼出一个个组件供给功能使用。

​ 使用:Dialog,各种自定义的UI控件、能在项目或者不同项目重复应用的代码等等。

​ 目的:复用,解耦。

​ 依赖:组件之间低依赖,比较独立。

​ 架构定位:纵向分层(位于架构底层,被其他层所依赖)。

2、模块化

就是**”业务框架”或者“业务模块”**,也可以理解为“框架”,意思是把功能进行划分,将同一类型的代码整合在一起,所以模块的功能相对复杂,但都同属于一个业务。

​ 使用:按照项目功能需求划分成不同类型的业务框架(例如:注册、登录、外卖、直播…..)

​ 目的:隔离/封装 (高内聚)。

​ 依赖:模块之间有依赖的关系,可通过路由器进行模块之间的耦合问题。

​ 架构定位:横向分块(位于架构业务框架层)。

3、总结

​ 其实组件相当于,把一些能在项目里或者不同类型项目中可复用的代码进行工具性的封装。

​ 而模块相应于业务逻辑模块,把同一类型项目里的功能逻辑进行进行需求性的封装。

二、组件化的必要性/产生背景

1、产生背景:

所有模块代码都编写在一个项目中,在项目越来越大后,测试/使用某个模块或功能,需要编译运行整个项目,麻烦

2、组件化思路:

将每个模块作为一个组件,加一个中间层来协调各个模块间的调用,所有的模块间的调用都会经过中间层中转。只让其他模块对中间层产生耦合关系,中间层不对其他模块发生耦合。)

3、组件化好处:

业务划分更佳清晰,新人接手更佳容易,可以按组件分配开发任务。

项目可维护性更强,提高开发效率。

更好排查问题,某个组件出现问题,直接对组件进行处理。

开发测试过程中,可以只编译自己那部分代码,不需要编译整个项目代码。

方便集成,项目需要哪个模块直接通过CocoaPods集成即可。

三、组件化设计思路1:有管理中心

完整的项目:https://github.com/dvlproad/033-Data-Notification-iOS

1、核心思想

启动时候注册关系表,执行时候遍历关系表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 核心思想1:注册,以统一管理,后续调用时候,从注册表中查询并执行对应的操作
void register(url, handle) {
model.url = url;
model.handle = handle;
models.add(model); // 问题1:用什么管理所有数据
}

// 核心思想2:执行,遍历在注册表中的所有操作,找到对应的操作后进行执行
void exec(url) {
for (var model in models) {
if (model.url == url) {
model.handle();
break;
}
}
}

2、实现方案

2.1、方案1:Url+block 路由器

缺点:入参,取参不明显。

2.2、方法2:protocal - class

哈希的本质 哈希冲突的解决:链地址法和开放寻址法。

问:使用多代理模式实现一个类似通知的功能,请问应该用什么来什么结构来存储代理和类的结构?

答:在多代理(Multiple Delegates)模式下,要存储多个代理对象,并且这些代理对象通常是弱引用,以防止循环引用。实现一个类似通知的功能,可以考虑使用 NSPointerArrayNSMapTable 来存储代理对象。如果你只是想管理多个代理对象,NSPointerArray 是更简单的选择。如果你需要将某个对象(如 UIViewController)与多个代理进行关联NSMapTable 更合适,所以我们选择 NSMapTable

将要执行的方法统一放到类中,通过protocal或者类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 根据 protocal 查找 class
// 方法1(可一对多):为 protocal 添加监听者数组属性(通过runtime,形成类似于 protocal.listeners ),
NSHashTable *protocalListeners = objc_getAssociatedObject(protocol, CJ_BROADCAST_PROTOCOL_LISTENER); // 找到之前绑定到这个protocal上的监听者列表。
objc_setAssociatedObject(protocol, CJ_BROADCAST_PROTOCOL_LISTENER, protocalListeners, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

注册的时候 [protocalListeners addObject:listener];

// 方法2(只能一对一):以 protocal 为 key , 通过 protocal 存取 module 模块(模块自身遵循协议)
// 创建一个NSMapTable实例,其中键是强引用,值是弱引用
NSMapTable<NSString *, NSObject *> *weakValueMapTable = [NSMapTable strongToWeakObjectsMapTable];

// 向哈希表中添加一个键值对
NSString *key = @"someKey";
NSObject *value = [[NSObject alloc] init];
[weakRefHashTable setObject:value forKey:key];

模块协议拆分为多个子协议,用户

网络处理协议、数据管理协议(单独)、用户界面更新协议

改进思想

1
2
3
4
5
6
7
8
9
10
// 1、发起请求(LoginViewController)
id<OrderModulePublic> moduler = [[CJMoudleManager shared] modulerForProtocal:@protocal(OrderModulePublic)];
UIViewController *vc = [moduler orderViewControllerWithPageType:xxx];

// 2、实现请求(TSOrderModuleImpl)
[CJProtocolCenter addModule:self forProtocol:@protocol(OrderModulePublic)];
#pragma mark - OrderModulePublic
- (UIViewController *)orderViewControllerWithPageType:(int)pageType

}

上述代码还可以演变成实现通知的一种形式(当注入的类中,有多个类都遵循了(实际上不应该出现)模块协议时候)

1
2
3
4
5
6
7
8
9
10
11
12
// 1、发起请求(LoginViewController)
NSMutableSet<id<CJUserServiceProtocolForModule>> listener = [[CJMoudleManager shared] listenersForProtocal:@protocal(CJUserServiceProtocolForModule)];
for (id<CJUserServiceProtocolForModule> listener in listeners) {
[listener userNameUpadte:userName];
}

// 2、实现请求(OrderHomeViewController)
[CJProtocolCenter addModule:self forProtocol:@protocol(CJUserServiceProtocolForModule)];
#pragma mark - CJUserServiceProtocolForModule
- (void)userNameUpadte:(NSString *)userName

}

下列图片来源于 《架构分层.graffle》 中的【三、模块间设计】

跳转的使用示例:

modular_CJProtocalManager 跳转

通知的实现示例:

modular_CJProtocalManager 通知

跳转 + 数据变化后内部通知的实现示例:

modular_CJProtocalManager 跳转 + 数据变化后内部通知

四、组件化设计思路2:无管理中心

项目示例:CJStandardProject 中 CTMediator+CJDemoModuleMain.h

1、核心思想

核心思想:直接执行,让系统通过反射来找到要响应事件的类和方法

1
2
3
void exec(target, action, params) {

}

2、实现方案

2.1、方案1:CTMediator target+action+param

实现示例:

modular_CTMediator target+action+param

2.2、方案2:XXXService+Implemention

modular_XXXService+Implemention

五、组件化实施过程的思考

1、架构分层

Read More

页面加载体验优化

一、页面初始加载优化

1、数据携带/数据参照

通过前一个页面的已获得数据,对所进入的新页面中的数据进行预填充。

eg1:商品列表 —> 商品详情:使用数据携带

eg2:愿望星count个数:通过count的0与非0,知晓所进入的页面初始状态更有可能是哪种状态

1、如果所进入的页面没有缓存数据,则携带的数据在进入的时候直接使用,后台接口返回实时数据后,再更新

2、如果所进入的页面有缓存数据,则携带的数据只能给缓存数据,而不能是后台接口返回的实时数据

2、管理的数据的缓存

2.1、普通数据的缓存

通过缓存框架,将数据缓存起来(key需携带uid),下次界面展示时候,优先从缓存中获取。

附:缓存框架详见:flutter_cache_kit使用文档

2.2、管理数据的缓存

建立Service层,管理用户所有数据的变动。

好处:与普通数据的缓存相比,能够在将来增加数据变动时候,通过本地通知系统,告知相关页面更新相应数据,而不用等到重新下载后才能显示已知道会更新的数据。

使用要点:

修改的时候,同步数据;下次界面需要数据,优先从用户管理服务中获取初始数据

eg1:用户愿望单收藏、商品收藏、品牌收藏、足迹数据

eg2:用户会员中心数据

3、默认数据

如果没有从前一页携带数据过来,则使用与产品约定的默认数据来加载。

1
2
3
if (携带数据) return 携带的.orderCount; 
if (管理数据) return servier.orderCount;
if (默认数据) return default_orderCount;

4、预览页:美团/饿了么的骨架屏灰底效果

对携带的数据与默认数据中相同的对象,使用携带的数据替换掉,一起整合成初始默认数据(携带的数据优先级高)。

5、添加请求加载圈

6、网络接口的缓存数据

页面处理优化及处理过程中的内容(图片)加载优化如下图所示:

图片来源可查看 《app启动与页面加载.graffle》中的【二、页面加载】版面

page_and_image_load_optimize1

从上图中,可以看出弱网情况下,我们可以针对不同的网络:做不同超时,重试设置,以及取不同质量的图片数据。

二、图片优化

图片优化,请查看我的另一篇文章:图片框架.md

图片高磁盘占用,请查看我的另一篇文章:《高磁盘占用的排查与优化.md

三、网络优化

api数据缓存,请查看我的另一篇文章:《网络框架.md

弱网优化,请查看我的另一篇文章:《弱网优化空间探索.md

Read More

架构模式-①概览

前言:架构模式、框架、设计模式的概念

iOS—架构模式、框架、设计模式的理解

架构模式:架构模式的出现是为了管理复杂的应用程序,这样可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。我们经常说的MVC架构MVVM架构属于此类。

框架:这个最好理解了,通常是代码重用。框架与设计模式的概念容易弄混,两者有相似之处,但却有着根本的不同。设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述规范,它比框架更抽象;框架为已经解决问题的具体实现方法,能直接执行或复用;设计模式是比框架更小的元素,一个框架中往往含有一种或多种设计模式。Xcode自带的FoundationUIKit,以及我们经常使用的AFNetworkingMJExtensionSVProgressHUD就属于这一类。

设计模式:设计模式可以通俗的理解为实现/解决某些问题,而形成的解决方案规范。增加代码的可重用性,让代码能更容易理解和可靠。我们通常说所的代理模式迭代器模式策略模式就属于这一类。对各种设计模式的了解可以帮助我们更快的解决编程过程中遇到的问题。

三者关系:架构(动词)>框架>设计模式。

软件通过架构,可以设计出很多不同的框架。在一个框架中,也可以使用很多的设计模式。设计模式不是哪儿哪儿都可以用的,只有当出现了某一特定的问题时,才利用设计模式去解决。设计模式不是用的越多越好,在维护的时候,过多的设计模式会极大的增添维护成本。

一、架构模式

1、架构模式的演变

1.1、MVC

1、架构的设计其实是为了更好的维护和迭代,而不是只考虑眼前当下的开发。

2、最基本的架构是MVC,试想下如果我一个应用的所有功能都是像”关于“功能的,那我还需要其他架构干嘛?显然MVC就已经很足够,而且还显得不冗余(这里指的是设计或者粒度不冗余)。

3、其他的架构都是从MVC演变而来。再讲其他架构的演变之前,我们先明确最原始的MVC代码是如何的。

①、首先M主要负责数据、V主要负责视图、C主要负责数据的视图显示。而这里的M是原始的瘦Model

所以,我们拿常见的”我的”页面来举例,部分原始的MVC代码一般为如下样子:

1
2
3
4
5
6
7
8
9
UserModel


View


ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = user.sex == 1? "女" :"男";

4、在MVC+瘦Model的演变下,还有MVC+胖Model

胖Model就是瘦Model+部分弱业务逻辑(这些弱业务重复一般都是经常出现,或者说是要求可复用性的,如根据枚举获取性别字符串)。它使得Controller可以从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上.

1
2
3
4
5
6
7
8
9
UserModel
+ (NSString *)sexStringFrom:(int)sex;

View


ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = [UserModel sexStringFrom:user.sex];

1.2、MVP?

5、有时候在MVC+瘦Model的演变下,不是转成MVC+胖Model,而是MVC+瘦Model+Helper

其实这个思路已经很接近MVP了、但是还差提点。MVP还需要为View提供数据

1
2
3
4
5
6
7
8
9
10
UserModel

View

UserHelper
+ (NSString *)sexStringFrom:(int)sex;

ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = [UserHelper sexStringFrom:user.sex];

1.3、MVVM

6、关于MVC+瘦Model+Helper,还可以改为MVC+瘦Model+ViewModel

刚才的胖Model只从Controller移植走了一些简单的弱业务。

而ViewModel则干脆把数据的处理全部从Controller移植了出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
UserModel

View

UserViewModel
name
sexString
- (id)initWithName:(NSString *)name sex:(int)sex;


ViewController
self.userInfoView.name = userViewModel.name;
self.userInfoView.sexString = userViewModel.sexString;

1.4、MVCS -> MVVM+S

拿有数据操作的”购物车“页面来说,

假设其除了有网络数据请求外,还有本地数据库处理。(附:购物车是否需要本地数据库,个人认为是基于服务端的压力来考虑,不然一般是放在服务端合适点。压力设想如下,千万级用户,购物车加减商品频繁,假设放在后台,则操作和计算也就频繁,压力也就大了。如果放在前端,则后台只需要负责是否可加减商品的检验,而不用再建购物车数据表和计算价格等,压力就小了。)

每个页面一般都会有网络请求,这些网络请求最原始的时候,是放在ViewController,也就是MVC的C中。

为了避免MVC中的ViewController后期变得十分臃肿。我们肯定要在MVC的基础上额外增加一层来处理这个。

综合考虑后,我们选用MVCS来处理。

其中S代表着Store,且我们这里的S代表着是整个模块的S,而不是每个页面都有一个S。其下层又包含着Storage和Service,即 数据库DB部分网络Network部分

Store类名,个人习惯是使用Manager来命令。如UserManager。

MVCS1

所以,最终我们选用的是 MVCS+MVVM 或者说 MVVM+S。

S层(service 层)提供一种和外界(比如远程服务 API 或文件系统)交互的独立机制。

MVVM+S 纵向的数据流就不用多说,横向的数据流,我们采用自定义的类系统通知。区别在于它有着系统Notification的一对多方便,又有着delegate的接口对接方便。

MVCS2

抛开数据共享问题(如每个请求都要带userToken),每个页面

有一个iOS的app的购物车功能,添加的商品会放到购物车中,可以在购物车中对该商品进行数量变更,变更后会重新计算价格。购物车可添加多种商品计算,请问这个计算应该是请求接口让后台计算,还是后台提供每个商品的单价(和折扣),然后前端计算?

购物车的价格计算方式选择 前端计算 还是 后端计算,取决于业务需求和系统设计,以下是两种方案的分析:


✅ 方案 1:前端计算(客户端计算总价)

方式:

​ • 后端提供每个商品的单价折扣活动信息等数据。

​ • 客户端在 数量变更 时,直接 本地计算 商品总价(单价 * 数量),无需请求后端。

适用场景:

前端交互体验要求高(如希望用户调整数量后,立即看到价格变更,无需等待网络请求)。

价格计算逻辑简单(单价 * 数量,没有复杂促销规则)。

减少服务器压力(减少频繁的请求)。

缺点:

价格可能不准确,如果 单价变更、促销活动 发生变化,用户的本地价格可能与后端不同。

容易被篡改,黑客可以修改前端逻辑,伪造价格(需要后端校验最终订单金额)。


✅ 方案 2:后端计算(每次变更数量都请求接口)

方式:

​ • 客户端每次数量变更时,向后端发送请求,后端返回最新的价格(包括折扣、活动计算)。

适用场景:

涉及复杂促销计算(如满减、阶梯折扣、优惠券等)。

商品价格变化频繁(避免用户本地计算价格不一致)。

安全要求高(防止价格被篡改,确保支付金额正确)。

缺点:

用户体验下降(每次调整数量都要请求后端,可能会有延迟)。

服务器压力增加(每次改数量都要请求后端)。


🚀 最优解:前端 + 后端结合

建议方案:前端预计算 + 后端校验

​ • 前端 先本地计算实时显示价格,让用户调整数量时立即看到变化(提升体验)。

​ • 提交订单时后端最终计算总价,避免前端被篡改价格,确保安全。

​ • 定期刷新(比如每次进入购物车页面时请求一次后端最新价格),防止前端数据过期。

示例流程:

​ 1. 用户点击 + / - 修改商品数量时,前端本地计算价格,界面立即更新。

​ 2. 后台定期提供 最新单价、促销信息,前端数据保持同步。

​ 3. 下单时,后端再次计算总价,如果价格被修改过(例如后台有最新优惠),前端更新价格,并提醒用户。


🎯 总结

方案 优势 劣势 适用场景
前端计算 快速、减少网络请求 价格可能不准确、安全性较低 简单购物车、价格变动少
后端计算 确保价格准确、安全性高 影响体验、增加服务器压力 复杂促销、多变商品价格
前端 + 后端结合(推荐) 体验流畅,保证安全 需做好数据同步策略 需要兼顾体验和准确性

👉 最佳方案是 “前端计算 + 后端最终校验”,这样既保证了用户体验,又确保了价格的正确性!🚀

二、架构分层(Architectural Layering)

架构分层是一种将系统分解为多个逻辑层次的方法,每一层都有特定的职责和功能。

  • 架构分层有助于实现关注点分离(Separation of Concerns),使得系统的不同部分可以独立开发和维护。
  • 典型的分层包括表示层(或用户界面层)、业务逻辑层、数据访问层等。

    架构分层图片来源于 《架构分层.graffle》中的【一、架构分层】

其他图片见我的项目 CJStandardProject 中的 Screenshots

其他参考文章

iOS:

三、常见的项目目录结构

常见的项目目录结构:

框架①项目目录结构

项目目录结构规范说明 请点击本链接跳转下文附录进行查看

完整的 项目目录结构树 请点击链接,跳转到本问附文中查看。

四、页面与服务类设计

1、核心理念

页面执行自己的动作,调用Service服务类。无需关心数据请求、数据请求结束后是否需要缓存等等。

Service服务中心,内部包含请求和可能的数据存储等处理,不对外暴露。

2、设计举例

页面调用服务类举例

2.1、设计

arc_layer_2

图片来源于 《架构分层.graffle》中的【二、页面与服务类设计】

2.2、服务类中的代码演示

Read More

开发工具

背景

问:我不要频繁安装+卸载来安装我想要的包。我能不能同时装一个测试环境和正式环境?

答:不行。安装两个包,严格上来讲,不用想了,不行。具体原因涉及应用id,及app根据该id配置了各种三方key环境(除非你连三方的也都额外提供一套),太详细的道理我就不讲了。”不行“就对了。但是,我能让你安装一个包,却使用不同环境的功能。或者额外安装一个除与应用id相关的功能外其他皆一样的安装包

一、切换环境常见的场景为:

1、测试时候:换环境不用一直下载;

2、演示时候:某个环境使用不了,不用重新下载;

3、抓包时候:想切换代理,不用重新打包安装;

见下文:app环境与切换

二、额外安装一个除与应用id相关的功能外其他皆一样的安装包的使用场景:

1、app中不能接受切换环境,即严格限制一个包只能有一个环境

不允许切换环境的考虑原因:

测试包不允许切到生产:怕脏数据

生产包不允许切到测试:测试环境不是外界用户真正需要的

见我的另一篇文章:《iOS的重签名.md

一、功能开关

tool_entrance_0

tool_entrance_2log

二、app安装包与切换

1、app安装包信息

devtool_appinfo_0package

1.1、换包:app下载页

devtool_appinfo_0package_downloadurl

1.2、版本记录:历史版本记录页

devtool_appinfo_0package_version_records

2、各文件目录及大小

devtool_appinfo_0package_dirsize

三、app环境与切换

package_env_test package_env_product

流程图:《开发工具(含环境切换等).graffle》中的【一、切换环境】版面

devtool_appenv_home

1、切换环境

2、切换平台

3、添加代理

devtool_appenv_3proxy_1 devtool_appenv_3proxy_2

四、设备信息

devtool_device_0devicetoken

devtool_deviceinfo_0

1、设备网络类型

2、设备屏幕大小

2.1、设备适配验证

devtool_device_uiadapt_check

3、设备ip及代理

4、设备埋点参数

未截取到的其他埋点参数见我的另一篇文章:《埋点规范_结构.md

5、是否模拟器

devtool_device_simulator_check

五、用户相关

devtool_user_info

六、调试页面

支付 支付宝
认证 实名认证、真人认证
选择器 生日选择、活动时间选择、项目多选
分享
关键页面入口 个人主页、发布页、引导页、完善性别/生日
位置相关 地图位置选择

快捷入口

1、检查更新

devtool_debug_0checkversion_1

devtool_debug_0checkversion_2

2、网页测试

devtool_debug_2webview_0home

2.1、网页快捷入口

devtool_debug_2webview_1url_entrance

2.2、白屏测试

devtool_debug_2webview_2blank_test.gif

2.3、JS测试

h5js.md

devtool_debug_2webview_3js

2.4、Route测试

h5_open_app.md

devtool_debug_2webview_4route

七、开发工具安全性处理

见我在”安全”专题里的另一篇文章:《开发工具安全性处理

八、脚本工具

目前的脚本操作如下:

qtool_ui_home

其中自定义的脚本操作有如下:

qtool_ui_custom_home

附1:Charles

文档:《Charles.md

附2:代理的实现(Flutter版)

dio添加代理

1
2
3
4
5
6
7
8
9
10
11
   (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
// config the http client
client.findProxy = (uri) {
CommonUtils.log('fcs-myproxy-begin:${uri is Uri} uri:${uri.toString()}');
return "PROXY 192.168.10.47:8888";//如果设置代理(localhost,127.0.0.1这样的是不行的。必须是电脑的ip)
// return 'DIRECT';// 如果不设置代理
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;//忽略证书
};

End

总目录

Architecture

AI

  • 第一章:ChatGPT
    • [第1节:ChatGPT 注册及基础使用](AI/ChatGPT/ChatGPT 注册及基础使用)
    • [第2节:ChatGPT 串接到 Discord](AI/ChatGPT/ChatGPT 串接到 Discord)
  • 第二章:AIGC
  • 第三章:AI Agent
  • 第四章:AI Tool
    • [第1节:AI Tool-①cc-switch](AI/AI Tool-①cc-switch)
    • [第2节:AI Tool-②cc-connect](AI/AI Tool-②cc-connect)
    • [第3节:AI Tool-③cc-notify](AI/AI Tool-③cc-notify)

安全与破解

管理相关

行业相关

数据结构

算法与数学

自动化

  • 第一章:APP打包自动化
    • 第1节:app打包规范
    • [第2.1节:iOS 安装包0-完整自动化流程](自动化/APP打包自动化/iOS 安装包0-完整自动化流程)
    • [第2.2节:iOS 安装包1-编译&打包](自动化/APP打包自动化/iOS 安装包1-编译&打包)
    • [第2.3节:iOS 安装包2-上传](自动化/APP打包自动化/iOS 安装包2-上传)
    • 第2.4节:iOS进阶_打包
    • [第3节:Flutter iOS 安装包1-编译&打包](自动化/APP打包自动化/Flutter iOS 安装包1-编译&打包)
  • 第二章:APP测试自动化
    • [第1节:iOS 自动化测试](自动化/APP测试自动化/iOS 自动化测试)
    • [第2节:iOS 单元测试](自动化/APP测试自动化/iOS 单元测试)
  • 第三章:通用

混编

iOS部分

Android部分

Flutter部分

React Native 部分

H5-APP

HTML

Weex

上架相关

Script

后台

第一章:服务器相关

阿里云

腾讯云

Database

实用工具部分

开发工具部分

编程工具部分

代码管理

电脑使用

常识类

科学工具

专利

随笔(不入侧边栏)