Telegram

第一步:创建一个 Telegram 机器人

1.1 打开 BotFather

在 Telegram 中搜索@BotFather(官方机器人管理器),然后开始聊天。

⚠️ Make sure it’s the verified official BotFather — don’t use third-party imitations.
⚠️ 请确保使用的是经过验证的官方 BotFather 账号——切勿使用第三方仿冒账号。

1.2 创建新机器人

发送命令 /newbot 。BotFather 会要求你输入名称和用户名。

1.3 Set the Bot Name 1.3 设置机器人名称

请输入你的机器人的显示名称(例如: cc-connect )。

1.4 Set the Bot Username 1.4 设置机器人的用户名

请输入用户名(用户名必须以 bot 结尾,例如 cc_connect_bot )。

💡 Naming rules: 💡 命名规则:

  • Must end with bot (case-insensitive)
    必须以 bot 结尾(不区分大小写)
  • Only letters, numbers, and underscores
    只能包含字母、数字和下划线。
  • Must be globally unique
    必须具有全球唯一性

1.5 Get the Bot Token 1.5 获取机器人令牌

After creation, BotFather will reply with something like:
创建完成后,BotFather 会回复类似这样的内容:

1
2
3
4
5
Done! Congratulations on your new bot...
Use this token to access the HTTP API:
1234567890:ABCdefGHIjklMNOpqrsTUVwxyz-123456

Keep your token secure...

⚠️ Save this token immediately — it’s only shown once! If lost, use /mybots → select bot → API TokenRevoke current token to regenerate.
⚠️ 请立即保存此令牌——它只会显示一次而已!如果丢失了,请使用 /mybots → 选择机器人 → API TokenRevoke current token 来重新生成。


Telegram_1newbot

得到 HTTP API

二、获取 个人聊天 ID 或者 群聊 ID

1、借助三方机器人 @get_id_bot

搜索 @get_id_bot 机器人,然后也有两种使用方式

方式一:进入与其私聊界面,选择标记1,弹出如下指示面板(注意不是发消息,而是选择面板里的选项)。

选择 My ID 即为个人聊天 ID;

选择 My Group ,则在分享后获得群聊 ID。

Telegram_2id_1

方式二:通过 ”添加到群组或者频道“ ,将其添加到你的群组里

Telegram_2id_2_1 Telegram_2id_2_2

2、不借助三方机器人,纯靠自己的机器人 token

2.1 获取您的个人聊天 ID

  1. 将以下地址的 {{TOKEN}} 替换为您的机器人的Token,换完后访问以下网址:

    1
    https://api.telegram.org/bot{{TOKEN}}/getUpdates
  2. 如果复制粘贴到浏览器地址栏,没发消息就回车请求。不出意外你会得到如下 response

    1
    2
    3
    4
    {
    "ok": true,
    "result": []
    }
  3. 所以我们要向你的机器人发送任何消息。然后刷新网址,得到类似如下的结果

  4. 在返回的 JSON 数据中找到 chat.id 字段,即为你的用户id。

2.2 获取群聊 ID

  1. 将该机器人添加到某个群组中。
  2. 在群组中发送消息时,请务必提及@your_bot。
  3. 请查看 getUpdates 网址——群组聊天 ID 通常为负数。

三、设置机器人指令(可选)

1. Set Command Menu 设置命令菜单

在 BotFather 中发送:

1
/setcommands

请选择您的机器人,然后输入命令列表:

1
2
3
help - Show available commands
new - Start a new session
list - List sessions

2. Set Bot Description 设置机器人描述

1
/setdescription

请输入描述内容——用户首次使用该机器人时就会看到这些文字。

四、重置Token

方式1:输入命令(如图中①②)

  1. 进入 BotFather 聊天界面,输入/revoke
  2. 请选择你的机器人。

方式2:选择小程序(如图中③)

Telegram_5revoke

服务器相关

前言:阿里云

阿里云相关文档

阿里云:云服务器ECS文档

阿里云:云数据库RDS文档

阿里云:对象存储OSS文档

image-20260325175009911

一、服务器资源 和 配套云资源

服务类型 通俗理解 本质 是否在云服务器“里面”
云服务器 (ECS) 一台虚拟电脑,给你CPU、内存、系统盘。你可以自己安装各种软件 。 提供基础计算资源的基础设施 -
云数据库 (RDS) 一个开箱即用的数据库。你不用操心安装、备份、高可用,它本身就是一套专业的数据库系统 。 提供专业数据库服务的平台服务 不是。 它是独立的,通过内网IP地址与你的云服务器连接 。
云存储 (OSS/S3) 一个海量的、无限容量的网盘。专门存文件、图片、视频,不用担心硬盘空间不够 。 提供海量数据存储的基础服务 不是。 它也是独立的,通过API或SDK供云服务器调用 。
负载均衡 (SLB/ELB) 一个智能流量分发器。它站在你的多台云服务器前面,把用户请求均匀地分给它们 。 提供流量分发和高可用的网络服务 不是。 它是独立的入口,配置好后,自动将流量分发到后端的多台云服务器上 。

1、它们之间是什么关系?

它们是 “协作”关系,不是“包含”关系。在部署涂鸦私有云时,你的架构会是这样的:

  1. **云服务器 (ECS)**:涂鸦的核心软件就运行在这里,负责处理业务逻辑。
  2. 云数据库 (RDS):涂鸦软件在配置时,会指定数据库的内网地址。所有设备信息、用户数据都会存储在这里,而不是存在云服务器的本地硬盘上。这样更安全,且支持自动备份和一键扩容 。
  3. **云存储 (OSS/S3)**:设备上报的图片、固件升级包等文件,会被涂鸦软件通过API存入云存储,获得一个访问链接 。
  4. **负载均衡 (SLB/ELB)**:如果你为了高可用部署了多台云服务器,就需要配置一个负载均衡器。用户的App请求会先打到负载均衡器,再由它分发给后端的某一台云服务器处理,即使其中一台服务器出故障,业务也不会中断 。

2、为什么不能都装在云服务器里?

虽然你也可以在云服务器里自己安装MySQL、自己搭建文件存储、自己配置Nginx来做负载均衡,但使用独立的云服务有巨大的优势:

  • 高可用与容灾:云数据库RDS默认就是主备架构,数据实时同步,一台物理机宕机,系统自动切换,业务无感知 。而自己搭建的数据库,一旦服务器出问题,数据就可能丢失或服务中断。
  • 弹性伸缩:业务量上涨时,云数据库RDS可以一键扩容规格,云存储OSS则近乎无限容量,完全不用操心 。
  • 免维护:你不需要为数据库打补丁、不需要为存储做磁盘RAID,这些都由云厂商负责 。

所以,涂鸦私有云的部署方案要求使用这些独立的云服务,正是为了确保你最终搭建的平台是稳定、可靠、可扩展的,而不是一个把所有鸡蛋放在一个篮子里的脆弱系统。

End

哈希表

二、Hash

1、背景

为了提高在一堆数据中查找指定数据的速度,我们引入hash。

问:为什么需要hash?

答:为了提高查找的速度。(附:主要是用于在Hash Table查询成员用的)

问:和数组相比,基于hash值索引的Hash Table【查找某个成员的过程】就是

Step 1: 通过hash值直接找到查找目标的位置;
Step 2: 如果目标位置上有多个相同hash值得成员, 此时再按照数组方式进行查找。

问:基于hash值索引的Hash Table在【查找某个成员的过程】如何确定找的对象是我们想要的?或hash方法与isEqual的关系?(优化判等)

答:我们以基于hash的NSSet和NSDictionary举例,其在判断成员是否相等时, 为了优化判等的效率,会这样做
Step 1: 集成成员的hash值是否和目标hash值相等, 如果相同进入Step 2, 如果不等, 直接判断不相等;
Step 2: hash值相同(即Step 1)的情况下, 再进行对象判等, 作为判等的结果。

2、Hash 表的实际应用

  • 应用1:找出两文件找出重复的元素

假设有两个文件,文件中均包含一些短字符串,字符串个数分别为n。它们是有重复的字符串,现在需要找出所有重复的字符串。
最笨的解决办法可能是:遍历文件 1 中的每个元素,取出每一个元素分别去文件 2 中进行查找,这样的时间复杂度为O(n^2)。
但是借助 Hash 表可以有一种相对巧妙的方法,我们知道相同元素的 Hash 值相同。所以我们分别遍历文件 1 中的元素和文件 2 中的元素,然后放入 Hash Table 中,对于遍历的每一个元素我们只要简单的做一下计数处理即可。最后遍历整个 Hash 列表,找出所有个数大于 1 的元素即为重复的元素。

  • 应用2:找出两文件找出出现次数最多的元素

找出两文件找出重复的元素这样的问题解决方案类似,只是在最后遍历的时找计数最大的元素,即为出现次数最多的元素。

3、hash什么时候调用

hash方法只在对象被添加至NSSet和设置为NSDictionary的key时会调用。

HashTable是一种基本数据结构,NSSet和NSDictionary都是使用HashTable存储数据的,因此可以可以确保他们查询成员的速度为O(1)。而NSArray使用了顺序表存储数据,查询数据的时间复杂度为O(n)。

三、哈希(Hash)表/散列表

参考文章:

哈希(Hash)的本质是一种算法,它能够将任意长度的输入(通常是字符串)通过一系列复杂的计算转换成固定长度的输出,这个输出被称为哈希值或哈希码。

NSDictionary 通常是基于哈希表的数据结构。NSDictionary 中,键(key)通过哈希函数转换成一个哈希值,然后使用这个哈希值来找到键值对在内存中的存储位置。

哈希表提供了快速的查找、插入和删除操作,这些操作的平均时间复杂度是 O(1)。

1、哈希函数 & Hash地址

要想知道什么是哈希表,那得先了解哈希函数

hash函数就是根据key计算出应该存储地址的位置,而哈希表是基于哈希函数建立的一种查找表

地址index=H(key)

通过Hash函数和关键字计算出来的存储位置(注意这里的存储位置只是表中的存储位置,并不是实际的物理地址)称作为Hash地址。

重要:相同元素的 Hash 值相同。附:两个相等的实例,他们的hash值一定相等。但是hash值相等的两个实例,不一定相等。

2、哈希函数的构造方法

除留余数法用的较多,H(key)=key MOD p (p<=m,m为表长,p为质数时候可以减少地址冲突)

比如key = 7,39,18,24,33,21时取表长m为9,p为7 那么存储如下

index 0 1 2 3 4 5 6 7 8
key 7 21(冲突后移) 24 39 18(冲突后移) 33(冲突后移)

哈希冲突的解决

哈希冲突即不同key值产生相同的地址,H(key1)=H(key2)。

比如我们上面说的存储7和21,p取7时 7 MOD 7 == 21MOD 7。此时7和21都发生了hash冲突。上面的哈希冲突解决方案我们使用的是开放寻址法(Open Addressing):当发生冲突时,会寻找表中的下一个空闲位置来存储元素。这通常通过某种探测序列(如线性探测、二次探测或双重散列)来实现。开放寻址法处理冲突的基本原则就是出现冲突后按照一定算法查找一个空位置存放。

其他哈希冲突解决方案:

链地址法(Chaining):每个哈希表槽位(或称为“桶”)都关联一个链表,所有映射到该索引的元素都存储在这个链表中。这种方法在处理冲突时比较简单,且无堆积现象,非同义词决不会发生冲突,因此平均查找长度较短。链地址法适用于经常进行插入和删除的情况,且在用链地址法构造的散列表中,删除结点的操作易于实现。

哈希表的扩容:

哈希表的大小,即桶(buckets)的数量,是根据存储在其中的对象数量动态变化的。当字典中的元素数量增加到某个阈值时,哈希表会进行扩容,通常是通过增加桶的数量来实现的,以保持操作的效率。

当哈希表进行扩容时,表中的桶(buckets)数量会增加,这意味着原有的哈希值可能不再适用于新的桶数组。因此,当扩容发生时,存储在哈希表中的所有键值对的哈希值通常会重新计算,以便将它们分配到新的桶位置。

在处理哈希冲突时,可能使用开放定址法来解决冲突,这种方法会在哈希表中寻找下一个空闲的桶来存储新的键值对。当哈希表的负载因子(即已使用的桶与总桶数量的比率)达到一定阈值时,哈希表会进行扩容,通常是通过增加桶的数量来实现的。这样可以保持哈希表的操作效率,使得查找、插入和删除操作的平均时间复杂度接近 O(1)。附:数组查找元素需要 O(n) 的时间复杂度,因为它可能需要遍历整个数组。所以哈希表相比数组,查找效率更高。除此之外,哈希表可以在插入时快速检查重复,这对于确保集合中元素的唯一性非常有用。

4、hash表的查找

查找过程和造表过程一致,假设采用开放定址法处理冲突,则查找过程为:
对于给定的key,计算hash地址index = H(key)
如果数组arr【index】的值为空 则查找不成功
如果数组arr【index】== key 则查找成功
否则 使用冲突解决方法求下一个地址,直到arr【index】== key或者 arr【index】==null

哈希表(hash table,也叫散列表),是根据键(key)直接访问访问在内存储存位置的数据结构。

哈希表本质是一个数组,数组中的每一个元素成为一个箱子,箱子中存放的是键值对。根据下标index从数组中取value。关键是如何获取index,这就需要一个固定的函数(哈希函数),将key转换成index。

哈希表—什么是哈希表 有助于理解哈希表

高质量的项目

一、管理

1、规范代码管理

2、搭建项目框架,制定代码规范

3、制订接口文档规范

4、统一封装满足多样化网络请求的网络库基础接口

5、组件的解耦封装

6、在当前工程中导入另一个工程文件

可扩展性:构建可扩展的iOS应用架构

稳定性治理:监控和分析崩溃日志来识别和修复潜在的问题。内存泄露。

iOS应用的稳定性可以通过多种方式来提升。首先,通过监控和分析崩溃日志来识别和修复潜在的问题。其次,通过优化代码和内存管理来减少卡死和崩溃的发生。例如,避免死锁、线程饥饿和内存泄漏等问题。此外,使用性能监控工具来检测应用的性能瓶颈,如卡顿和延迟,也是提高稳定性的重要手段。

灵活性:面向协议编程

二、风险预测

1、风险可能

1、不要过分相信服务器返回的数据会永远的正确。

2、在对数据处理上,要进行容错处理,进行相应判断之后再处理数据,这是一个良好的编程习惯。

2、思考:如何防止存在潜在崩溃方法的崩溃

  • 众所周知,Foundation框架里有非常多常用的方法有导致崩溃的潜在危险。对于一个已经将近竣工的项目,若起初没做容错处理又该怎么办?你总不会一行行代码去排查有没有做容错处理吧!——– 别逗逼了,老板催你明天就要上线了!
  • 那有没有一种一劳永逸的方法?无需动原本的代码就可以解决潜在崩溃的问题呢?

3、解决方案

解决方案:拦截存在潜在崩溃危险的方法,在拦截的方法里进行相应的处理,就可以防止方法的崩溃

步骤:

1
2
3
1、通过category给类添加方法用来替换掉原本存在潜在崩溃的方法。
2、利用runtime方法交换技术,将系统方法替换成我们给类添加的新方法。
3、利用异常的捕获来防止程序的崩溃,并且进行相应的处理。

4、消息转发机制

iOS的消息转发机制详解

5、消息转发机制的运用

1、简单问题引导

用Runtime解决服务器返回NSNull问题

思路:重写NSNull的消息转发方法, 让他能处理这些异常的方法.

使用 Method Swizzling 交换 objectForKey: 和 objectAtIndex: 是 方法交换(Method Swizzling),并不是 消息转发机制(Message Forwarding)

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
✅ 使用 Runtime 交换方法,拦截 NSNull,避免手动检查
✅ 提升代码健壮性,防止 NSNull 访问崩溃
✅ 适用于 NSDictionaryNSArray,避免手动 if ([obj isKindOfClass:[NSNull class]])
#import <objc/runtime.h>

@implementation NSDictionary (Safe)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(objectForKey:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(safe_objectForKey:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

- (id)safe_objectForKey:(id)key {
id value = [self safe_objectForKey:key]; // 调用原方法(已被交换)
if ([value isKindOfClass:[NSNull class]]) {
return nil; // 拦截 NSNull,返回 nil
}
return value;
}

@end



@implementation NSArray (Safe)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(objectAtIndex:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(safe_objectAtIndex:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

- (id)safe_objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
return nil; // 避免数组越界
}
id value = [self safe_objectAtIndex:index]; // 调用原方法
if ([value isKindOfClass:[NSNull class]]) {
return nil; // 拦截 NSNull
}
return value;
}

@end

🔥 使用消息转发机制(Message Forwarding)解决 NSNull 问题

🔹 原理

​ 1. 当访问 NSNull 的方法时,系统先在 NSNull 查找对应的方法

​ 2. 如果 NSNull 没有该方法,会触发消息转发流程

​ 3. 重写 forwardingTargetForSelector: 或 methodSignatureForSelector: + forwardInvocation: 来拦截并提供安全返回值

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
NSNull *nullValue = [NSNull null];
// ✅ 伪装成 NSString
NSLog(@"Length: %lu", (unsigned long)[nullValue length]); // 输出 0
// ✅ 伪装成 NSNumber
NSLog(@"Integer: %ld", (long)[nullValue integerValue]); // 输出 0



✅ 方案 1:使用 forwardingTargetForSelector:(快速消息转发),返回要让哪个tagert去执行aSelector方法。
#import <Foundation/Foundation.h>

@interface NSNull (SafeForwarding)
@end

@implementation NSNull (SafeForwarding)

- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"⚠️ NSNull 被调用方法: %@", NSStringFromSelector(aSelector));

static NSString *emptyString = @"";
static NSNumber *zeroNumber = @0;

// 常见的返回值处理
if ([emptyString respondsToSelector:aSelector]) {
return emptyString; // 例如 `length` 方法返回空字符串的长度
} else if ([zeroNumber respondsToSelector:aSelector]) {
return zeroNumber; // 例如 `integerValue` 返回 0
}

return nil; // 其他情况返回 nil,避免崩溃
}

@end

✅ 方案 2:完全转发,略
// 方法签名的理解:方法签名(NSMethodSignature)是一个 描述方法参数和返回值信息的对象。NSMethodSignature 允许我们查询方法的返回值类型和参数类型。
假设 NSString 有这样一个方法:
- (NSUInteger)length;
它的 方法签名 大致如下:
NSMethodSignature *sig = [NSString instanceMethodSignatureForSelector:@selector(length)];

防止Crash 组件

2、系统性解决:

iOS runtime实用篇 —避免常见崩溃

其中AvoidCrash的代码如下:AvoidCrash源代码

附:NSException

iOS被开发者遗忘在角落的NSException-其实它很强大

利用OC的消息转发机制实现多重代理

三、资源

app瘦身

四、代码

1、单元测试

单元测试分为3种:

1
2
3
逻辑测试:测试逻辑方法
异步测试:测试耗时方法(用来测试包含多线程的方法)
性能测试:测试某一方法运行所消耗的时间

为什么要使用单元测试:

1
2
3
经济上的问题:

假设要开发的是对接获取验证码接口的方法,难道运行一次就真的请求一个短信验证码?短信下发平台可是会¥扣钱¥的。更何况,万一第三方平台没有响应或超时,我们的测试就失败了,这种异步的、不确定的测试,无论从金钱还是时间上衡量,都不够经济,因此很难实现。

什么情况下时序使用单元测试(单元测试使用的注意事项):

1
2
3
4
5
6
1、不是所有的方法都需要测试。
例如:私有方法不需要测试!只有暴露在 .h 中的方法需要测试!面向对象有一个原则:开闭原则!
2、所有跟 UI 有关的都不需要测试,也不好测试。
把 业务逻辑 代码封装出来!变成可以测试的代码,让程序更加健壮!
3、一般而言,代码的覆盖度大概在 50% ~ 70%
从github上得知:YYModel测试覆盖度为83%,AFNetworking测试覆盖度为77%,两者都是比较高的。

2、优化多线程处理,改善多线程嵌套严重,请求耗时的问题

解决:优化多线程处理,改善多线程嵌套严重,请求耗时的问题。

详细:原本项目,采用多线程嵌套的同步方式处理多个线程请求到数据后,再执行最后操作。经优化多线程处理为异步执行时,改善了多线程嵌套严重,请求耗时的问题。

3、定时器使用的优化

4、UITableView等的性能优化

5、优化代码逻辑处理(耗电量、耗流量)

性能优化

ComfyUI入门

一、安装与试运行

1、安装环境

安装教程见官网:https://github.com/Comfy-Org/ComfyUI?tab=readme-ov-file#installing

1.1 Accelerated PyTorch training on Mac

1.1.1 确认为 python3.12 ,需要等下安装 PyTorch 需要3.10之后
1
2
3
4
5
6
# 查看版本
python3 --version
python3.12 --version

# 安装 3.12 版本
brew install python@3.12
1.1.2 安装 pytorch 相关库

按照 https://developer.apple.com/metal/pytorch/ 提示安装 torch 即可。但在这之前为了避免与系统环境冲突,您可以创建一个虚拟环境来安装 Python 包。以下是创建和激活虚拟环境的步骤:

1
2
3
4
5
# 创建虚拟环境
python3.12 -m venv ~/Project/AI/comfy_env

# 激活虚拟环境
source ~/Project/AI/comfy_env/bin/activate

请将 ~/Project/AI/comfy_env 替换为您希望创建虚拟环境的路径。

source 之后,之后的操作即会在虚拟环境中。

附:查看PyTorch版本及其 MPS 支持情况

1
2
3
4
5
6
7
8
9
10
11
uname -m
# 如果输出为 x86_64,说明你的Mac使用的是Intel x86架构。
# 如果输出为 arm64,说明你的Mac使用的是Apple Silicon (ARM架构)

# 输入以下命令:
python -c "import torch; print(f'PyTorch 版本: {torch.__version__}'); print(f'MPS 是否可用: {torch.backends.mps.is_available()}'); print(f'MPS 是否已构建: {torch.backends.mps.is_built()}')"

# 得到以下结果:
PyTorch 版本: 2.12.0
MPS 是否可用: True
MPS 是否已构建: True

1.2 ComfyUI manual installation (Windows, Linux)

1.2.1 Git clone this repo
1
2
cd ~/Project
git clone https://github.com/Comfy-Org/ComfyUI.git
1.2.2 Put your SD checkpoints (the huge ckpt/safetensors files) in: models/checkpoints

1、选择要下载的文生图模型

文生图 Model 描述 下载地址 大小
rpg_v5.safetensors 专精型选手,这是一个专门用于生成 “角色扮演游戏风格”人物肖像 的 Stable Diffusion 模型 https://civitai.com/models/1116?modelVersionId=124626 1.99G 10.6k
Animagine XL 4.0 专注日系动漫的生态之王
基于SDXL微调,社区成熟,拥有大量特殊标签可精细控制,生成结果非常符合日系动漫审美。
https://civitai.com/models/1188071/animagine-xl-40 6.46G 245k
DreamShaper 全能型选手,试图覆盖写实、动漫、奇幻、艺术等多种风格 https://civitai.com/models/4384/dreamshaper?modelVersionId=128713 1.99G 5.4M

2、将下载的 checkpoints 放进 models 中的 checkpoints 文件夹下

1.2.3 Put your VAE in: models/vae

1、选择要下载的vae模型

VAE (Variational Autoencoder,变分自编码器) 可以理解为一个 “色彩与细节修复大师”。在像 SDXL 这样的文生图模型中,它主要负责两个工作:

  1. 解码生成:将模型内部生成的“潜在信息”最终转换成我们看到的 生动、色彩饱满的图像
  2. 修复缺陷:专门用来改善图像中最常见的两个问题:
    • 色彩发灰:让画面色彩更鲜艳、通透。
    • 细节模糊:让边缘更清晰,尤其是在面部、眼睛、纹理等关键细节上。
vae Model 描述 下载地址 大小
SDXL VAE https://civitai.com/models/296576/sdxl-vae 319.14MB

2、将下载的 vae 分别放进 models 中的 vae 文件夹下,如上图。

1.3、ComfyUI dependencies.

1
2
3
cd ~/Project/ComfyUI
# 使用指定版本安装包和运行脚本
pip3.12 install -r requirements.txt

注意:这里的安装都必须要在虚拟环境中执行

2、运行 python3.12 main.py

1
2
# 使用指定版本安装包和运行脚本
python3.12 main.py

二、ComfyUI 入门教程

https://docs.comfy.org/zh/tutorials/video/ltx/ltx-2-3

ComfyUI 入门教程

正向:beautiful scenery nature glass bottle landscape, purple galaxy bottle,

负向:watermark

checkpoint:rpg_v5.safetensors

采样器名称:euler

调度器:normal

好用的工具安装

1、Prompt Assistant / 提示词小助手

当你进行翻译时候可能出现如下错误:

1
Using SOCKS proxy, but the 'socksio' package is not installed. Make sure to install httpx using 'pip install httpx[socks]".

解决方式:在该虚拟环境下运行以下代码安装所需的包。

1
pip3.12 install 'httpx[socks]'

End

设计模式-③观察者模式(Notification&KVO)

设计模式-③观察者模式(Notification&KVO)

精选文章

iOS 语言之KVO

ProtocolCenter

从两张图片可以看到,最大的区别是调度的地方。

虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调的,所以观察者模式的订阅者与发布者之间是存在依赖的,而发布/订阅模式则不会。

观察者模式

UserManager

postUserNameUpdate:xxx

img

发布订阅模式

img

END

设计模式-④适配器、策略、责任链模式(Adapter、Strategy、Chain of Responsibility)

设计模式 核心概念 场景
适配器模式 将一个类的接口变换成所期待的另一种接口 各支付SDK支付服务接口的整合
策略模式 定义共同行为,各策略遵循,以达到可切换策略 支付SDK的选择、计价方案的选择
责任链模式 添加责任,形成责任链,成功则传递以执行下一个责任 请求拦截器的责任链、内容发布(订单创建)

零、组合模式

场景:评论系统的评论和回复、组织架构、文件系统

判断代码是否使用了组合模式,可以检查以下几个关键点:

  1. 对象的树形结构:代码中是否存在对象的树形结构,即对象间存在父子关系,并且可以递归地组织在一起。
  2. 共享接口:是否定义了一个共享接口或抽象类,它被叶子对象和容器对象所实现或继承。
  3. 叶子对象(Leaf):是否存在叶子对象,它们是树形结构中的末端节点,没有子节点。

如果你在代码中看到了这些特征,那么它很可能使用了组合模式。组合模式的目的是将对象组合成树形结构,并使得客户端可以统一地对待叶子对象和容器对象。

一、适配器模式(Adapter)

适配器模式(Adapter Pattern):将一个类的接口变换成所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

说人话:这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

案例1:各支付SDK

1.1、背景:假设各支付SDK对外提供的支付接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 微信支付
class WeChatPay: NSObject {
// completion
func payWithMoney(amount: Double, completion: @escaping (Bool, Error?) -> Void) {
completion(true, nil) // 假设支付成功
}
}
// 支付宝支付
class AlipayPayment: NSObject {
// success + failure
func payWithAmount(amount: Double, success: (Bool) -> Void, failure: (Error) -> Void) {
success(true, nil) // 假设支付成功
}
}

适配器模式,将支付宝支付由 success + failure 改成 completion 方式,使得不兼容的接口转换为可兼容的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
class AlipayPaymentAdapter: NSObject {
private let alipayPayment = AlipayPayment()

func payWithAmount(amount: Double, completion: @escaping (Bool, Error?) -> Void) {
alipayPayment.payWithAmount(amount: amount) { success in
if success {
completion(true, nil)
} else {
completion(false, NSError(domain: "PaymentError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Payment failed"]))
}
}
}
}

1.2、适配器模式优化

1.2.1、适配器模式+面向接口编程

为了更好的使用适配器模式,我们可以通过面向接口编程,提前定义适配器接口(其实这个公共接口也可作为策略接口)。

面向协议编程(POP)和面向接口编程(IOP)都是设计原则,它们都强调了依赖抽象而不是具体实现。在某些方面,面向协议可以看作是面向接口的一个扩展或特定语言环境下的实现。

面向接口编程(IOP)

面向接口编程是一种设计原则,它强调通过接口来定义对象的行为,而不是依赖于具体的类。接口定义了一组方法签名,但不提供实现。类通过实现这些接口来声明它们具有特定的行为。

面向协议编程(POP)

面向协议编程是面向接口编程的一个特定实现,特别是在支持协议的语言(如Swift)中。协议定义了一组方法、属性或其他要求,类、结构体或其他类型的实例需要遵循这些要求。协议可以包含默认实现,这使得它们更加灵活。

包含关系

从概念上讲,面向协议编程可以看作是面向接口编程的一个扩展。在面向协议编程中,你定义协议(类似于接口),但协议提供了更多的功能,如:

  1. 默认实现:协议可以提供方法的默认实现,这在接口中通常不可行。
  2. 扩展性:协议可以被扩展,添加更多的方法或属性。
  3. 多重继承:协议支持多重继承,即一个类可以同时遵循多个协议。

在iOS开发中,实现支付系统集成时,可以使用适配器模式(将不兼容的接口转换为可兼容的接口)来创建一个统一的支付接口,然后为每种支付方式(如支付宝、微信支付)提供一个具体的适配器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protocol PaymentProtocol {
func pay(amount: Double, completion: @escaping (Bool, Error?) -> Void)
}

class WeChatPayAdapter: PaymentProtocol {
private let weChatPay = WeChatPay()
func pay(amount: Double, completion: @escaping (Bool, Error?) -> Void) {
weChatPay.payWithMoney(amount: amount, completion: completion)
}
}
class AlipayAdapter: PaymentProtocol {
private let alipayPayment = AlipayPayment()
func pay(amount: Double, completion: @escaping (Bool, Error?) -> Void) {
alipayPayment.payWithAmount(amount: amount) { success in
if success {
completion(true, nil)
} else {
completion(false, NSError(domain: "PaymentError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Payment failed"]))
}
}
}
}

案例2:UITableView优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation SectionAdapterViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 处理UI和数据的步骤省略....
GameCellAdapter *cellAdapter = [[GameCellAdapter alloc] initWithTableView:self.tableView datas:datas];
GameSectionAdapter *fpsSectionAdapter = [[GameSectionAdapter alloc] initWithCellAdapter:cellAdapter sectionTitle:@"FPS Games" sectionHeight:60];
GameSectionAdapter *roleSectionAdapter = [[GameSectionAdapter alloc] initWithCellAdapter:cellAdapter sectionTitle:@"Role Play Games" sectionHeight:60];

self.adapter = [[GameDetailListAdapter alloc] init];
self.adapter.sections = [@[fpsSectionAdapter, roleSectionAdapter] mutableCopy];

self.tableView.dataSource = self.adapter;
self.tableView.delegate = self.adapter;
}

以上UITableView使用适配器模式优化的代码摘自:iOS模式分析 使用适配器模式重构TableView

二、策略模式(Strategy)

app正常业务请求、埋点请求、数美请求、同盾请求。

为了更好的使用策略模式,经常会配合适配器模式,以使得接口统一。

策略模式(Strategy Pattern)是一种行为设计模式,它定义了一系列的算法,并将每一个算法封装起来,使它们可以互换使用。这种模式让算法的变化独立于使用算法的客户。策略模式属于对象行为型模式,它能够让你根据不同的情况选择不同的算法或行为。

策略模式的主要组成

  1. 策略接口(Strategy Interface):定义了一个公共的接口,各种算法以不同的方式实现这个接口。
  2. 具体策略类(Concrete Strategy):实现策略接口的具体算法。
  3. 上下文(Context):使用策略接口,维护一个对策略对象的引用,可以设置和切换不同的策略。

策略模式的优点

  1. 算法的多样性:可以在运行时选择不同的算法,增加新的算法而不需要修改原有代码。
  2. 避免使用多重条件判断:策略模式可以将算法族封装在不同的策略类中,避免在条件判断中使用大量的if-elseswitch-case语句。
  3. 扩展性:增加新的策略时,不需要修改原有代码,符合开闭原则。

策略模式的使用场景

  • 当需要在运行时选择算法或行为时。
  • 当需要避免使用多重条件判断时。

案例1:支付SDK选择

1、不使用策略模式时候,支付接口使用的常见写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PayUtil: NSObject {
// PayUtil 统一管理支付逻辑:解决支付接口分散问题
static func pay(amount: Double, payType: PaymentMethod, completion: @escaping (Bool, Error?) -> Void) {
if payType == .wechat {
WeChatPay().payWithMoney(amount: amount, completion: completion)
} else {
AlipayPaymentAdapter().payWithAmount(amount: amount, completion: completion)
}
}
}

// 使用示例:
PayUtil.pay(amount: 100, payType:.alipay) { success, error in
if success {
print("支付成功")
} else {
print("支付失败: \(error?.localizedDescription ?? "未知错误")")
}
}

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
enum PaymentMethod {
case alipay
case wechat
}
class PaymentService {
private var paymentAdapter: PaymentProtocol?

// 如果你希望进一步实现依赖注入,可以将 paymentAdapter 作为依赖注入到 PaymentService 中,而不是在 PaymentService 内部创建。这样可以**更好地解耦和提高可测试性**。
// func init(adapter: PaymentProtocol) { // 构造器注入
// func setPaymentAdapter(adapter: PaymentProtocol) { // 属性注入
// paymentAdapter = adapter
// }
func setPaymentMethod(method: PaymentMethod) {
switch method {
case .alipay:
paymentAdapter = AlipayAdapter()
case .wechat:
paymentAdapter = WeChatPayAdapter()
}
}

func payment(amount: Double, completion: @escaping (Bool, Error?) -> Void) {
paymentAdapter?.pay(amount: amount, completion: completion)
}
}

// 使用示例:
let paymentService = PaymentService()
paymentService.setPaymentMethod(method: .alipay)
paymentService.payment(amount: 100) { success, error in
if success {
print("支付成功")
} else {
print("支付失败: \(error?.localizedDescription ?? "未知错误")")
}
}

3、问:不使用PayUtil,而是选择使用策略模式的原因/使用策略模式的优点?

3.1、更符合开闭原则

当需要添加银联支付时候,即使是使用策略模式,也需要对PaymentService进行修改。即在 PaymentService 中添加一个新的 case 来处理新的支付方式,这样,当需要支付时,可以根据用户选择的支付方式动态地切换支付策略。该操作确实和PayUtil一样涉及到对现有代码的修改,这在一定程度上违反了开闭原则的理想状态,即不应该通过修改现有代码来扩展系统。然而,在实际的软件开发中,完全避免修改任何现有代码是非常困难的,特别是在设计初期可能未能预见到所有未来需求的情况下。开闭原则更多地是一种设计哲学,指导我们尽可能地设计出可扩展的系统,而不是绝对的规则。但是相比之下使用策略模式对中间类PaymentService改动还是更小的。

3.2、策略接口多的时候相比Util其就更符合开闭原则了

此时策略模式的优势可能还没发挥出来,但当我们的各平台钱包SDK,如果除pay操作外,还有余额查询,查询交易记录等接口时候,策略模式的优势就显示出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PaymentService {
private var paymentAdapter: PaymentProtocol?

func setPaymentMethod(method: PaymentMethod) {
switch method {
case .alipay:
paymentAdapter = AlipayAdapter()
case .wechat:
paymentAdapter = WeChatPayAdapter()
}
}
// 支付
func payment(amount: Double, completion: @escaping (Bool, Error?) -> Void) {
paymentAdapter?.pay(amount: amount, completion: completion)
}
// 查询余额
func checkBalance(completion: @escaping (Bool, Error?) -> Void) {
paymentAdapter?.pay(completion: completion)
}
// 查询交易记录
func checkOrders(completion: @escaping (Bool, Error?) -> Void) {
paymentAdapter?.pay(completion: completion)
}
}

案例2:计价

假设咖啡添加不同成分时,计价不同,常见的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 咖啡基类
abstract class BaseCoffee {
int get price;
}

class Coffee extends BaseCoffee {
Sugar? sugar;
Milk? milk;

int get price {
if (sugar != nil) {
if (milk != nil) {
return 15;
} else {
return 6;
}
} else {
return 10;
}
}
}

而使用策略模式时候,会将不同成分的咖啡分成不同策略(策略中有计价方式):

好处:每个策略看起来很简洁,而不是堆在一块。

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
// 定义一个定价策略协议
protocol PricingStrategy {
func calculatePrice() -> Int
}

// 定义各种定价策略:没有额外成分的、添加糖的、添加牛奶的、同时添加糖和牛奶的
struct BasePricingStrategy: PricingStrategy {
func calculatePrice() -> Int {
return 10
}
}
struct SugarPricingStrategy: PricingStrategy {
func calculatePrice() -> Int {
return 6
}
}
struct MilkPricingStrategy: PricingStrategy {
func calculatePrice() -> Int {
return 10
}
}
struct SugarAndMilkPricingStrategy: PricingStrategy {
func calculatePrice() -> Int {
return 15
}
}

调用

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
// 定义 Coffee 类
class Coffee {
var pricingStrategy: PricingStrategy
private var _sugar = false
private var _milk = false

init() {
self.pricingStrategy = BasePricingStrategy()
}

var sugar: Bool {
get {
return _sugar
}
set {
_sugar = newValue
updateStrategy()
}
}

var milk: Bool {
get {
return _milk
}
set {
_milk = newValue
updateStrategy()
}
}

private func updateStrategy() {
if _sugar && _milk {
self.pricingStrategy = SugarAndMilkPricingStrategy()
} else if _sugar {
self.pricingStrategy = SugarPricingStrategy()
} else if _milk {
self.pricingStrategy = MilkPricingStrategy()
} else {
self.pricingStrategy = BasePricingStrategy()
}
}

func getPrice() -> Int {
return pricingStrategy.calculatePrice()
}
}

// 使用示例
let coffee = Coffee()
print("Basic Coffee: \(coffee.getPrice())") // 输出 10

coffee.sugar = true
print("Coffee with Sugar: \(coffee.getPrice())") // 输出 6

coffee.milk = true
print("Coffee with Sugar and Milk: \(coffee.getPrice())") // 输出 15

let coffee2 = Coffee()
coffee2.milk = true
print("Coffee with Milk: \(coffee2.getPrice())") // 输出 10

三、责任链模式

常见场景:请求拦截器、内容发布、订单创建(创建前有存不同定金的可获取不同优惠券)

浅谈对责任链模式的理解?应用场景?

场景1:订单创建(创建前有存不同定金的可获取不同优惠券)

场景3:请求拦截器的责任链

场景2:内容发布的责任链

  1. 内容验证/检查:在内容发布之前,可以有一个验证器来检查内容是否符合特定的规则,比如检查是否有敏感词汇、是否符合格式要求等。
  2. 权限检查:在内容发布之前,可以检查用户是否有发布内容的权限。
  3. 内容格式化:内容可能需要经过格式化处理,比如自动添加引用、格式化代码块等。
  4. 发布
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
// 责任链遵循的协议
abstract class ContentHandler {
bool handleContent(String content);
}

// 创建一个链来管理这些处理器,拦截器管理类
class ContentChain {
List<ContentHandler> _handlers = [];
void addHandler(ContentHandler handler) {
_handlers.add(handler);
}

void handle(String content) {
for(ContentHandler iHandler in _handlers) {
bool success = iHandler.handleContent(content);
if (!success) {
return;
}
}
}
}

// 责任链调用方法
void main() {
var chain = ContentChain();
chain.addHandler(LogContentHandler());
chain.addHandler(ValidateContentHandler());
chain.addHandler(FormatContentHandler());
chain.addHandler(PublishContentHandler());

chain.handle("This is a sample content with badword.");
}

// 各责任链定义与实现
abstract class ContentHandler {
void handleContent(String content, Function next);
}

class LogContentHandler extends ContentHandler {
@override
bool handleContent(String content) {
print('Logging content: $content');
return true;
}
}

class ValidateContentHandler extends ContentHandler {
@override
bool handleContent(String content) {
if (content.contains('badword')) {
print('Content validation failed.');
return false;
}
print('Content is valid.');
return true;
}
}

class FormatContentHandler extends ContentHandler {
@override
bool handleContent(String content) {
// 假设这里对内容进行了格式化
String formattedContent = content.replaceAll('\n', '<br/>');
print('Formatted content: $formattedContent');
return true;
}
}

class PublishContentHandler extends ContentHandler {
@override
bool handleContent(String content) {
print('Publishing content: $content');
return true;
}
}

代码示例中,ContentHandler 抽象类定义了一个 handleContent 方法,该方法返回一个布尔值来指示处理是否成功。ContentChain 类管理处理器链,并在 handle 方法中遍历链中的每个处理器,调用 handleContent 方法,并根据返回值决定是否继续执行。

这种方法确实可以工作,并且它简化了责任链的实现。然而,它有几个潜在的问题:

  1. 内容修改:如果处理器需要修改内容并希望这些修改对后续处理器可见,这种方法就无法满足需求。在当前的设计中,内容的修改不会传递给链中的下一个处理器。
  2. 错误处理:如果某个处理器失败,整个链将停止执行。但是,没有提供一种机制来通知调用者哪个处理器失败了,或者为什么失败。
  3. 异步处理(最常见):如果处理器需要执行异步操作(例如网络请求),当前的设计不支持异步处理。

为了解决这些问题,我们可以对责任链模式进行一些改进。以下是一个改进后的示例:

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
abstract class ContentHandler {
void handleContent(String content, {required void Function(String) success, required void Function() failure});
}

class LogContentHandler extends ContentHandler {
@override
void handleContent(String content, {required void Function(String) success, required void Function() failure}) {
print('Logging content: $content');
success(content);
}
}

class ValidateContentHandler extends ContentHandler {
@override
void handleContent(String content, {required void Function(String) success, required void Function() failure}) {
if (content.contains('badword')) {
print('Content validation failed.');
failure();
return;
}
print('Content is valid.');
success(content);
}
}

class FormatContentHandler extends ContentHandler {
@override
void handleContent(String content, {required void Function(String) success, required void Function() failure}) {
try {
String formattedContent = content.replaceAll('\n', '<br/>');
print('Formatted content: $formattedContent');
success(formattedContent);
} catch (e) {
failure();
}
}
}

class PublishContentHandler extends ContentHandler {
@override
void handleContent(String content, {required void Function(String) success, required void Function() failure}) {
try {
print('Publishing content: $content');
success(content);
} catch (e) {
failure();
}
}
}

class ContentChain {
List<ContentHandler> _handlers = [];

void addHandler(ContentHandler handler) {
_handlers.add(handler);
}

void handle(String content) {
_functionChain(0, content);
}

void _functionChain(int index, String content) {
if (index >= _handlers.length) return;

_handlers[index].handleContent(content,
success: (String newContent) {
_functionChain(index + 1, newContent);
},
failure: () {
print("Error handling content. Stopping the chain.");
}
);
}
}

void main() {
var chain = ContentChain();
chain.addHandler(LogContentHandler());
chain.addHandler(ValidateContentHandler());
chain.addHandler(FormatContentHandler());
chain.addHandler(PublishContentHandler());

chain.handle("This is a sample content with badword.");
}

四、装饰器模式

1、继承的缺点

使用继承的缺点包括:

  1. 类爆炸:随着新特性的增加,你需要创建越来越多的子类来组合这些特性,这会导致类的数目急剧增加。
  2. 耦合性:子类与父类高度耦合,父类的任何改变都可能影响到子类。
  3. 违反开闭原则:继承结构通常违反开闭原则,因为添加新功能需要修改现有类。

装饰器模式**通过组合(这里说的不是组合模式)**而不是继承来解决这些问题,提供了更大的灵活性和可扩展性。

装饰模式(Decorator),动态地为一个对象添加额外的职责,是继承的替代方案,属于结构型模式。通过装饰模式扩展对象的功能比继承子类方式更灵活,使用继承子类的方式,是在编译时静态决定的,即编译时绑定,而且所有的子类都会继承相同的行为。然而,如果使用组合的方式扩展对象的行为,就可以在运行时动态地进行扩展,将来如果需要也可以动态的撤销,而不会影响原类的行为。

实现装饰器模式的关键在于创建一个包装类,该类持有对被装饰对象的引用,并包含被装饰对象的行为。包装类应该能够扩展被装饰对象的行为,并在需要时将其附加到被装饰对象的逻辑中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Student{
var name: String
init(name: String) {
self.name = name
}
}

// 角色二: 具体的装饰者 服饰
class StudentClothes {
var student : Student
var clothes: String?
init(student: Student) {
self.student = student
self.clothes = "校服"
}
....
}

// 实际更常用:直接将 clothes 设为 Student 的属性。因为正常情况下不用为了避免继承出的子类臃肿,而使用装饰器模式
class DetailStudent extends Student {
var clothes: String?
}

上述代码参见:ios-装饰器模式

举个例子,如果你有一个Coffee类。但如果你想要为咖啡添加糖或奶,使用继承就不太合适,因为可能会创建出大量的子类(如MilkSugarCoffeeSugarEspresso等),这会导致类的爆炸。这时,装饰器模式就显得更为合适,你可以创建一个CoffeeDecorator类,它包裹一个Coffee对象,并在其中添加添加糖或奶的方法。

使用装饰器模式来为Coffee类添加额外的组件(比如牛奶或糖)时,代码可以按照下面的结构来实现:

附1:策略模式在Flutter中的使用代码示例

常见场景:支付(微信支付、支付宝支付、银联支付、信用卡支付)

缓存?磁盘缓存、内存缓存?

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
// 1、定义策略接口:
// 里氏替换原则:应用程序中任何父类对象出现的地方,我们都可以用其子类的对象来替换,并且可以保证原有程序的逻辑行为和正确性。因为这里父类是抽象类,所以肯定遵守里氏替换原则。
abstract class PaymentStrategy {
void pay(double amount);
}

// 2、定义具体策略:
class CreditCardPayment implements PaymentStrategy {
@override
void pay(double amount) {
print('Paying $amount using Credit Card');
}
}

class AlipayPayment implements PaymentStrategy {
@override
void pay(double amount) {
print('Paying $amount using Alipay');
}
}

class WeChatPayment implements PaymentStrategy {
@override
void pay(double amount) {
print('Paying $amount using WeChat Pay');
}
}


// 3、依赖注入
class ShoppingCart {
PaymentStrategy? _paymentStrategy; // 依赖倒置原则:ShoppingCart高层模块依赖于PaymentStrategy抽象(接口或抽象类),而不是具体实现

void setPaymentStrategy(PaymentStrategy? strategy) {
_paymentStrategy = strategy;
}

void checkout(double amount) {
_paymentStrategy?.pay(amount);
}
}

使用

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
void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Strategy Pattern Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Strategy Pattern Demo'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
ShoppingCart _cart = ShoppingCart();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
child: Text('Pay with Credit Card'),
onPressed: () {
_cart.setPaymentStrategy(CreditCardPayment());
_cart.checkout(100.0);
},
),
ElevatedButton(
child: Text('Pay with Alipay'),
onPressed: () {
_cart.setPaymentStrategy(AlipayPayment());
_cart.checkout(100.0);
},
),
ElevatedButton(
child: Text('Pay with WeChat Pay'),
onPressed: () {
_cart.setPaymentStrategy(WeChatPayment());
_cart.checkout(100.0);
},
),
],
),
),
);
}
}

END

通讯-②跨端间的通讯

Flutter 与 Android iOS 原生的通信有以下三种方式
使用场景
BasicMessageChannel 调用原生端的相机功能来拍照,并且获取拍摄的照片数据 支持双向通信,即Flutter可以向原生发送消息,并且原生可以回复这些消息。
MethodChannel 向原生发送一次性的命令或请求,并等待响应
EventChannel 适合用于数据流的通信,如监听传感器数据、网络变化等事件。 单向通信,只能由原生平台向Flutter发送事件流

参考文档:

Flutter通过BasicMessageChannel与Android iOS 的双向通信

纹理Texture

纹理Texture可以理解为GPU内代表图像数据的一个对象

Flutter展示Native端大数据:外接纹理

背景

问:在Flutter与原生的混合开发中,资源在Native和Flutter重复加载,导致内存占了双份的性能问题怎么解决?

答:用Texture外接纹理的方式缓解内存压力。

Flutter 多引擎渲染,外接纹理实践

二、外接纹理

三、共享纹理

ShareGroup是OpenGL ES中的一个概念,用于在不同的EAGLContext之间共享纹理和其他资源。即在Flutter和iOS之间实现纹理共享,可以通过使用ShareGroup来实现。(在iOS开发中,ShareGroup对应于EAGLSharegroup,而在Flutter中,可以通过Texture控件来使用共享的纹理 。)

使用ShareGroup实现Flutter和iOS原生纹理共享的基本步骤:

1
2
3
4
5
6
7
8
9
10
11
12
// 1、创建一个 EAGLContext 并设置其 sharegroup,这个 sharegroup 将被用于后续创建共享纹理。
EAGLSharegroup *sharegroup = [[EAGLSharegroup alloc] init];
EAGLContext *sharedContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3 sharegroup:sharegroup];

// 2、创建纹理并设置为共享:使用上述创建的 EAGLContext 来生成纹理,并确保这些纹理可以被共享。
// 创建纹理,配置纹理参数(省略具体参数设置)
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
.......
// 确保纹理可以被共享
[sharedContext presentRenderbuffer:GL_RENDERBUFFER];
1
2
3
4
5
6
7
8
9
10
11
12
// 在 Flutter 中使用
import 'package:flutter/widgets.dart';

class SharedTextureWidget extends StatelessWidget {
SharedTextureWidget({required this.textureId});

@override
Widget build(BuildContext context) {
int textureId = getNativeTextureId(); // 调用原生方法获取纹理ID
return Texture(textureId: textureId);
}
}
  1. 配置Flutter的Texture控件:在Flutter代码中,创建一个Texture控件,并通过PlatformView注册一个视图工厂,该工厂将返回一个实现了FlutterTexture协议的对象。这个对象负责管理原生纹理,并实现copyPixelBuffer方法来提供纹理数据给

以下内容摘自:谈一谈Flutter外接纹理

1 背景知识

当我们用flutter做实时视频渲染时,往往是要对视频或者相机画面做滤镜处理的,如图:

img

如果我们要用flutter定义的消息通道机制来实现这个功能,就需要将摄像头采集的每一帧图片都要从原生传递到flutter中,这样做代价将会非常大,因为将图像或视频数据通过消息通道实时传输必然会引起内存和CPU的巨大消耗。为此,flutter提供了两种机制实现这一功能:

  • PlatformView
  • Texture Widget

PlatformView实质上是将原有的NativeView嵌入到Flutter中显示,虽然使用和移植很简单,但并不是性能最优的做法。

Texture Widget是flutter提供的另一种机制,可以将native纹理共享给flutter进行渲染。但由于native纹理与flutter是两个OpenGL Context,如果直接使用的话,需要经过GPU -> CPU -> GPU的转换开销,这对于实时视频渲染是很难令人接受的。

所以解决方案就是共享纹理

一个方案提升Flutter内存利用率(干货)