开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Appstore过审应用的玄学:幸存者偏差

发表于 2024-04-22

前言

什么是幸存者偏差?

幸存者偏差指的是当取得资讯的渠道,仅来自于幸存者时,此资讯可能会与实际情况存在偏差。

幸存者偏差,是由优胜劣汰之后自然选择出的一个道理:未幸存者已无法发声。

人们只看到经过某种筛选而产生的结果,而没有意识到筛选的过程,因此忽略了被筛选掉的关键信息。

产生的背景

幸存者偏差最早来源于第二次世界大战期间,美国哥伦比亚大学统计学亚伯拉罕·沃德教授接受美国海军要求,运用他在统计方面的专业知识给出关于“飞机应该如何加强防护,才能降低被炮火击落的几率”的建议。

沃德教授针对联军的轰炸机遭受攻击后返回营地的轰炸机数据,进行研究后发现:机翼是最容易被击中的位置,机尾则是最少被击中的位置。沃德教授的结论是“我们应该强化机尾的防护”,而军方指挥官认为“应该加强机翼的防护,因为这是最容易被击中的位置”。

举个简单的例子

1
2
3
4
5
6
css复制代码张三在Appstore检索竞品APP-A,
随机感慨道:这款APP违规支付了,怎么也过审了?
过了一会儿,在Appstore检索竞品APP-B,
又感慨道:啊?这款APP的关键词里面,竟然包含了xxx关键词?这也太神奇了吧?!
随后,张三开始写调研报告,结论为:
这类产品非常容易过审,即使包含xxx功能与xxx敏感类型的关键词依旧可以顺利通过审核!

幸存者偏差,就是忽略了筛选条件,把片面的筛选条件结果判定为一个有效的结论。

如何提高新老产品的过审几率?

  • 确保自身产品的独特性。何为独特性?不要参与在红海的类目,这里具体指:社交、算命、记账、日记。这类产品饱和度极高,且顾客转换成本本身也极高。除非有更加吸引人的设计功能,更有创意的想法,或者远超同行的用户体验。不然从竞品中转移用户是很具挑战性的事情。

注解:顾客转换成本是当消费者从一个产品或服务的供应商转向另一个供应商时所产生的一次性成本。

  • 认真阅读苹果开发者指南,真正的做一个合规化的产品,不要心存侥幸。该内购的内购,不要对审核团队的隐藏、欺骗。同时,保证自身产品在Appstore的风评。这里是指用户自然评价,而非机刷!机刷风险极高!
  • 不在元数据中名称、简介中,堆砌关键词。比如:竞品名称。这种行为会极高的触发苹果封号的可能性。侵犯第三方利益,混淆用户视听。
  • 提前准备好对应的资质内容,主动提交过审率远高于苹果审核团队索要。毕竟审核团队的耐心是有限的,而且会增加审核时长。提审之后的博弈时间越久,风险越高。 和苹果持久性的扯皮中,开发者一直都是弱势的群体,而且会更加被动。
  • 产品定位要精准,内容不要大杂烩。更不要把其他子类产品中好的功能,进行二合一。

最后,祝各位开发。大吉大利,今晚过审!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

iOS - Runloop在实际开发中的应用 iOS - R

发表于 2024-04-22

iOS - Runloop在实际开发中的应用

  1. 控制线程生命周期(线程保活)

如果需要经常在子程序执行任务,可能希望一个线程可以重复使用,避免每次都要创建、销毁带来不必要的开销

ZSXPermenantThread.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
objectivec复制代码typedef void (^ZSXPermenantThreadTask)(void);

@interface ZSXPermenantThread : NSObject

/**
开启线程
*/
//- (void)run;

/**
在当前子线程执行一个任务
*/
- (void)executeTask:(ZSXPermenantThreadTask)task;

/**
结束线程
*/
- (void)stop;

@end

ZSXPermenantThread.m

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
objectivec复制代码#import "ZSXPermenantThread.h"

/** ZSXThread **/
@interface ZSXThread : NSThread
@end
@implementation ZSXThread
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end

/** ZSXPermenantThread **/
@interface ZSXPermenantThread()

@property (strong, nonatomic) ZSXThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;

@end

@implementation ZSXPermenantThread

#pragma mark - public methods
- (instancetype)init {
if (self = [super init]) {
self.stopped = NO;

__weak typeof(self) weakSelf = self;

self.innerThread = [[ZSXThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];

[self.innerThread start];
}
return self;
}

- (void)executeTask:(ZSXPermenantThreadTask)task {
if (!self.innerThread || !task) return;

[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop {
if (!self.innerThread) return;

[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc {
NSLog(@"%s", __func__);

[self stop];
}

#pragma mark - private methods
- (void)__stop {
self.stopped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}

- (void)__executeTask:(ZSXPermenantThreadTask)task {
task();
}

@end

在 ViewController中使用

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
less复制代码@interface ViewController ()

@property (strong, nonatomic) ZSXPermenantThread *thread;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.thread = [[ZSXPermenantThread alloc] init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.thread executeTask:^{ NSLog(@"执行任务 - %@", [NSThread currentThread]);
}];
}


- (IBAction)stop:(UIButton *)sender {
[self.thread stop];
}


- (void)dealloc
{
NSLog(@"%s", __func__);
}

@end

运行结果:

  1. 解决NSTimer在滑动时停止工作的问题

2.1. 案例

使用NSTimer创建一个定时器,循环打印

1
2
3
objectivec复制代码[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];

当scrollView 滚动时,定时器就停止了

2.2 解决

1
2
3
4
5
6
7
8
9
10
objectivec复制代码NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// NSDefaultRunLoopMode、UITrackingRunLoopMode 才是真正存在的模式
// NSRunLoopCommonModes 并不是一个真的模式,它只是一个标记而已
// time 能在_commonModes数组中存放的的模式下工作。也就是包含NSDefaultRunLoopMode、UITrackingRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  1. 监控应用卡顿

  1. 性能优化

@oubijiexi

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python开源工具库使用之图片去水印IOPaint 前言

发表于 2024-04-22

前言

最近发现一款去除图片水印的开源软件 IOPaint,感觉很不错,分享一下。它是一款基于 SOTA AI 模型的软件,可以实现从图片中擦除任何不需要的物体、缺陷、人,还可以替换掉(通过stable diffusion),并且还能够支持 CPU 运行(当然替换功能 stable diffusion 使用 CPU 太慢了,不建议使用),以往的深度学习软件只能使用 GPU,这款工具还可以支持 CPU 就很满足需求。并且这款工具还通过插件支持其它功能,如脸部修复、图片超分辨率、移除背景等

  • github地址:github.com/Sanster/IOP…

一、去除水印

1.1 安装部署

通过pip安装

1
2
pip install torch==2.1.2 torchvision==0.16.2
pip install iopaint

在安装完成后,通过如下命令启动,可以指定 model 路径

1
iopaint start --model=lama --model-dir=models --device=cpu --port=8080

执行命令后,会从 github上下载模型文件,如果下载太慢,可手动下载,并放在models\torch\hub\checkpoints下面

github模型文件:github.com/Sanster/mod…

1.2 实战

当部署完成后,可通过localhost:8080来访问web界面

动画.gif
二、面部修复
======

2.1 安装运行

按照下面命令安装运行

1
2
3
4
pip install facexlib
pip install tb-nightly -i https://mirrors.aliyun.com/pypi/simple
pip install gfpgan
iopaint start --model=lama --model-dir=models --device=cpu --port=8080 --enable-gfpgan --gfpgan-device cpu

在运行时,会从 github 下载模型文件,同样可以手动下载放在和上面模型同样位置

github 模型文件地址:

  • github.com/TencentARC/…
  • github.com/xinntao/fac…
  • github.com/xinntao/fac…

2.2 使用

动画2.gif
三、图像提升分辨率
=========

3.1 安装

1
2
3
pip install realesrgan

iopaint start --model=lama --model-dir=models --device=cpu --port=8080 --enable-realesrgan --realesrgan-model RealESRGAN_x4plus --realesrgan-device cpu

github 模型文件地址:github.com/xinntao/Rea…

3.2 使用

动画3.gif
四、移除背景
======

4.1 安装

1
2
3
4
pip install rembg
pip install huggingface
set HF_ENDPOINT=https://hf-mirror.com
iopaint start --model=lama --model-dir=models --device=cpu --port=8080 --enable-remove-bg

4.2 使用

动画4.gif
五、报错及解决方法
=========

5.1 ERROR: Cannot install gfpgan

1
2
3
4
 from facexlib.version import __version__
ModuleNotFoundError: No module named 'facexlib'
During handling of the above exception, another exception occurred:
ERROR: Cannot install gfpgan==0.2.3, gfpgan==0.2.4, gfpgan==1.3.0, gfpgan==1.3.1, gfpgan==1.3.2, gfpgan==1.3.4, gfpgan==1.3.5, gfpgan==1.3.6, gfpgan==1.3.7 and gfpgan==1.3.8 because these package versions have conflicting dependencies.

原因:没有安装facexlib

解决方法:

1
pip install facexlib

5.2 The conflict is caused by: gfpgan 1.3.8 depends on tb-nightly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ERROR: Cannot install gfpgan==0.2.1, gfpgan==0.2.3, gfpgan==0.2.4, gfpgan==1.3.0, gfpgan==1.3.1, gfpgan==1.3.2, gfpgan==1.3.4, gfpgan==1.3.5, gfpgan==1.3.6, gfpgan==1.3.7 and gfpgan==1.3.8 because these package versions have conflicting dependencies.

The conflict is caused by:
gfpgan 1.3.8 depends on tb-nightly
gfpgan 1.3.7 depends on tb-nightly
gfpgan 1.3.6 depends on tb-nightly
gfpgan 1.3.5 depends on tb-nightly
gfpgan 1.3.4 depends on tb-nightly
gfpgan 1.3.2 depends on tb-nightly
gfpgan 1.3.1 depends on tb-nightly
gfpgan 1.3.0 depends on tb-nightly
gfpgan 0.2.4 depends on tb-nightly
gfpgan 0.2.3 depends on tb-nightly
gfpgan 0.2.1 depends on tb-nightly
ERROR: ResolutionImpossible: for help

原因:依赖问题,当前源为清华源

解决方法:换源安装 tb-nightly

1
pip install tb-nightly -i https://mirrors.aliyun.com/pypi/simple

5.3 Initialize RemoveBG plugin Traceback

1
2
3
4
ConnectTimeoutError: (<urllib3.connection.HTTPSConnection object at 0x000000004842BA60>, 'Connection to huggingface.co
timed out. (connect timeout=10)')
LocalEntryNotFoundError: An error happened while trying to locate the file on the Hub and we cannot find the requested
files in the local cache. Please check your connection and try again or make sure your Internet connection is on

原因:需要从 huggingface.co 下载模型,直连超时,换个镜像

解决方法:

1
2
pip install huggingface
set HF_ENDPOINT=https://hf-mirror.com

参考

  1. www.iopaint.com/

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

安卓高版本HTTPS抓包:终极解决方案

发表于 2024-04-22

虽然市面上有好多抓包工具,但是 Android 高版本都需要安装抓包工具的证书到系统目录,才能抓 https 协议的包。本文就以 Charles这个抓包工具来介绍,如何安装证书到 Android 的系统目录,实现 https 抓包。

修改证书名称

启动 Charles,通过菜单栏中的 Help → SSL Proxying → Save Charles Root Certificate… 将 Charles 的证书导出。
使用 OpenSSL 查看证书在 Android 系统中对应的文件名,并重命名证书文件

1
2
shell复制代码openssl x509 -subject_hash_old -in charles-ssl-proxying-certificate.pem | head -n 1  #cdfb61bc
mv charles-ssl-proxying-certificate.pem cdfb61bc.0

将证书安装到系统证书目录下

使用 adb push 命令将我们的证书文件放到 SD 卡中

1
shell复制代码adb push cdfb61bc.0 /sdcard/Download

使用 adb 连接手机并切换到 root 用户

1
2
shell复制代码adb shell
su

将证书文件移动到 /system/etc/security/cacerts 目录下,由于 /system 默认是只读的,所以要先重新挂载为其添加写入权限

1
2
3
4
5
shell复制代码cat /proc/mounts  #查看挂载信息,这里我的 /system 是直接挂载到 / 的

mount -o rw,remount /
mv /sdcard/Download/cdfb61bc.0 /system/etc/security/cacerts
chmod 644 /system/etc/security/cacerts/cdfb61bc.0 #设置文件权限

如果👆的步骤你都能成功,就不用继续往下看了。

终极解决方案

我用我手上的手机都试了一下,用上面的方式安装正式,发现不能成功,一直提示 Read-only file system,但是HttpToolkit这个软件确可以通过 Android Device Via ADB来抓 https 的包。
它是怎么实现的呢?
这下又开始了漫长的谷歌之旅,最后在他们官网找到一篇文章,详细讲述了 通过有root权限的adb 来写入系统证书的神奇方案。

  1. 通过 ADB 将 HTTP Toolkit CA 证书推送到设备上。
  2. 从 /system/etc/security/cacerts/ 中复制所有系统证书到临时目录。
  3. 在 /system/etc/security/cacerts/ 上面挂载一个 tmpfs 内存文件系统。这实际上将一个可写的全新空文件系统放在了 /system 的一小部分上面。 将复制的系统证书移回到该挂载点。
  4. 将 HTTP Toolkit CA 证书也移动到该挂载点。
  5. 更新临时挂载点中所有文件的权限为 644,并将系统文件的 SELinux 标签设置为 system_file,以使其看起来像是合法的 Android 系统文件。

关键点就是挂载一个 内存文件系统, 太有才了。
具体命令如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码# 创建一个独立的临时目录,用于存储当前的证书
# 如果不这样做,在我们添加挂载后将无法再读取到当前的证书。
mkdir -m 700 /data/local/tmp/htk-ca-copy
# 复制现有的证书到临时目录
cp /system/etc/security/cacerts/* /data/local/tmp/htk-ca-copy/
# 在系统证书文件夹之上创建内存挂载点
mount -t tmpfs tmpfs /system/etc/security/cacerts
# 将之前复制的证书移回内存挂载点中,确保继续信任这些证书
mv /data/local/tmp/htk-ca-copy/* /system/etc/security/cacerts/
# 将新的证书复制进去,以便我们也信任该证书
cp /data/local/tmp/c88f7ed0.0 /system/etc/security/cacerts/
# 更新权限和SELinux上下文标签,确保一切都和之前一样可读
chown root:root /system/etc/security/cacerts/*
chmod 644 /system/etc/security/cacerts/*
chcon u:object_r:system_file:s0 /system/etc/security/cacerts/*
# 删除临时证书目录
rm -r /data/local/tmp/htk-ca-copy

注意:由于是内存文件系统,所以重启手机后就失效了。可以将以上命令写成 shell 脚本,需要抓包的时候执行下就可以了。

本文的目的只有一个就是学习更多的逆向技巧和思路,如果有人利用本文技术去进行非法商业获取利益带来的法律责任都是操作者自己承担,和本文以及作者没关系,本文涉及到的代码项目可以去 爱码者说 知识星球自取,欢迎加入知识星球一起学习探讨技术。关注公众号 爱码者说 及时获取最新推送文章。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字节跳动云原生防护体系实践 背景 防护加固 应用案例 后续规

发表于 2024-04-22

背景

随着 Kubernetes 在企业中大规模使用和落地,逐渐形成了 “业务 - 中台 - 基础设施” 的分层技术体系;这种分层能够屏蔽平台和基础设施层的复杂概念,让应用专注于业务层的研发,但同时也会导致上层应用的稳定性强依赖底层基础设施的支持,从而对基础设施在大规模集群下的稳定性提出极大的挑战:

  • 由于集群规模庞大,任何单一不起眼的小问题都可能被无限放大,带来系统性风险;
  • 场景的复杂性和多样性,也使得运维操作出现不符合预期的行为难以彻底避免。

这就要求我们对于 Kubernetes 所管理的资源和对象进行更有效的极端风险防护,尽可能缓解由于误操作、组件版本与配置的错误、或者管控代码 bug 对业务造成不可挽回的影响。

尽管 Kubernetes 原生提供了一系列的防护机制,例如严格的 RBAC 校验机制、使用 PodDisruptionBudget(PDB)对 Eviction API 执行校验、较为丰富的 Admission Plugins 等,但是在实际生产实践中,我们仍然发现有很多无法覆盖的场景。

在此背景下,字节跳动内部对 Kubernetes 系统进行了扩展与改造,增加了一系列的防御性校验措施与操作约束,为运行在 Kubernetes 上的业务提供更强有力的稳定性支撑,降低极端风险。

防护加固

Kubernetes 是个相当复杂的分布式系统,但其架构设计上的核心思想还是非常简单的。Kubernetes 通过 APIServer 提供统一的 API 接口实现对集群状态的访问与修改能力;各种自动化组件能以标准化的方式与集群通信持续获取数据,并通过本地计算当前集群状态与预期集群状态之间的区别,派生出一系列的变更操作;最终通过 kubelet 在每个节点上执行这些状态变更,将集群朝着预期的状态推进。

由此可见,Kubernetes 组件间的交互和运行状态可以大致分成以下三层

  • KV 存储系统(如 etcd、Kine、Kubebrain)与 apiserver 间的交互,提供 key-value 级别的读写操作与事件监听;
  • apiserver 与各种内建或附加 controller/operator 间(以及 apiserver 与用户间)通过 API 请求交互;
  • apiserver 与单机节点组件间的交互。

image.png

img_v3_02a2_09e3108d-4ae2-4625-aeea-c867bbe15d0g.jpg

根据上述分层,我们可以针对性梳理出一系列常见的系统性风险,并分别采取对应的措施进行加固以降低极端风险。

数据防护

存储与 apiserver 之间的交互风险主要集中数据异常方面,例如数据的损坏与丢失等;存储系统是 Kubernetes 的核心,是整个基于事件驱动的分布式系统的基石,一旦出现数据异常可能直接或间接地派生出一系列的故障。具体来说可能包括但不限于以下的常见极端风险问题:

  • 存储集群运维操作失误导致存储下线,导致整个 Kubernetes 集群不可用;
  • 管理员直接删除 etcd 中的数据,未经过 apiserver 做校验,可能导致一些非预期关键对象如 namespace、deployment、pod 等被直接删除,并触发对象的级联删除,导致业务大面积受损;
  • 管理员因误操作直接修改 etcd 中的数据,损坏了数据格式导致 apiserver 无法 decode 数据。

针对这些问题,我们在生产环境中采取了一系列措施——首先,尽可能标准化地约束对存储集群的运维和数据操作,在存储系统侧开启 TLS 双向认证,尽量避免除了 Kubernetes 以外的用户直接访问存储,降低数据损坏或丢失的风险;其次,对存储进行定时的备份,在极端情况下,当发生不可逆的数据损失时,基于备份能快速恢复数据,降低损失的影响;此外,通过对其他组件进行加固,尽可能降低数据异常派生的非预期事件对于业务的直接冲击。

控制面防护

自动化组件与 apiserver 之间的交互风险,主要集中在非预期操作方面。正常情况下,用户或平台将预期的状态提交到 apiserver,而其他内部组件将立即根据当前状态和预期状态的区别派生出一系列的动作,从而使集群产生变更;而一旦错误的预期状态被提交,集群将快速并且难以逆转地朝着目标状态进行变更。

针对这一类问题的主要防护思路,就是对关键对象的操作进行一些额外的限制,例如要求在操作时额外添加一些冗余操作,形成 double check 机制,降低由于误操作或者管控代码 bug 引发风险的概率;具体来说,操作防护通过 Kubernetes 原生提供的扩展机制 ValidatingAdmissionWebhook 来实现。我们通过 label 和 annotation 来标记需要进行操作防护的关键对象,并通过 selector 配置对这些关键对象以及对应的操作进行筛选,在 Webhook 中实现一系列的约束以达到防护的目的,其中包括但不限于以下这些策略:

  • 防止级联删除针对 Namespace、CRD 等根对象,一旦被删除会导致级联地触发派生出的其他对象的删除操作。因此我们在 Webhook 中对这些类型的关键对象的删除进行拦截,避免误操作引发级联删除操作引发灾难性后果。
  • 显式副本修改当需要调整关键 workload 资源副本数量时,为了避免意外地将副本数量缩减至 0,我们要求在通过 UPDATE 或者 PATCH 请求调整副本数的同时,还需要显式地给对象添加特定 annotation 写入预期调整的数值作为 double check;在 Webhook 中校验关键 workload 对象进行变更时 .spec.replicas 字段中的值是否与 annotation 中提供的值保持一致,确保任何对于关键 workload 副本数的修改都是有意且明确的。
  • 显式资源删除当需要删除关键 workload 对象时,要求在删除对象之前先通过修改操作将 workload 的副本数降至 0;通过这种约束,我们得以避免一些误操作,例如某些关键的 workload 对象未经确认直接,可能会触发更多级联的删除操作,导致业务受损。
  • 操作程序约束对于一些特定的业务,对于业务规格的变更有着严格的变更事件窗口限制,例如业务只接受在非繁忙时段对镜像、环境变量等配置进行变更,这样可以降低因为规格更改引起的潜在问题,以及相应的业务中断风险。我们通过 CRD 定义了可变更窗口、可变更字段等约束并暴露给用户,在 Webhook 中根据用户配置进行相应的校验,这样可以确保在出现故障时,影响尽量少的终端用户,确保有相对充分的故障处理时间,最大程度的减少潜在损失,降低系统风险。

此外,线上生产环境中经常会遇到一些客户端的异常,例如 OOM、大量缓存穿透等问题,这些异常往往会引发大量的开销极大的读请求,引发控制面异常甚至雪崩。针对线上异常流量的防护问题,我们对用户行为进行了一定限制,禁止了一些开销极大的读穿透行为。其次,我们在控制面前置了针对 kube-apiserver 流量特征专门定制的七层网关 KubeGateway,它解决了 kube-apiserver 负载不均衡的问题,同时实现了对 kube-apiserver 请求的完整治理,包括请求路由、分流、限流、降级等,显著提高了 Kubernetes 集群的可用性。另外,我们对 Kubernetes 的审计日志进行扩展,将一些流量相关的信息附加到审计日志上,在此基础上进行分析得到用户画像。在异常的场景下,将用户画像、流量监控指标与控制面前置的七层网关 KubeGateway 的限流能力相结合,对给控制面提供巨大压力的 Client 进行流量控制,尽可能降低雪崩风险。

节点防护

在大多数场景下,pod 的删除应该分成两个阶段执行:首先由中心化的 Controller 或者用户通过发起 Delete 请求将 pod 标记为删除状态(即添加 DeletionTimestamp),然后应该由 kubelet 负责对业务发起优雅退出,等待业务终止且资源释放之后,由 kubelet 来通过 APIServer 提供的接口将 pod 彻底移除。但在生产实践中,我们遇到诸多了问题,可能导致 kubelet 因为异常而非预期地终止业务 pod,例如:

  • 由于配置错误或者代码 bug,导致 kubelet 重启后 reject 正在运行的业务 pod,导致业务受损;
  • 由于控制面存储出现的数据损坏或其他异常,导致 kubelet 发现本地实际运行的 pod 与控制面提供的本地应该运行的 pod 不一致,进而引起非预期的业务退出。

针对这类问题,我们对 kubelet 进行了一系列的改造,涵盖 admit、housekeeping 等环节。通过改造给 kubelet 删除 pod 的操作加入前置约束:在尝试删除关键 pod 时,首先检查 pod 是否被显式地进行标记删除,如果 pod 未被标记删除,则不允许 kubelet 触发 pod 的删除操作。基于这种显式删除的约束,我们得以大幅度降低因为各种 Kubernetes 组件异常而引发的节点层面的业务运行风险。

小结

在生产环境中,我们主要根据 Kubernetes 组件之间的交互过程识别和梳理出关键风险,通过特定的 label 与 annotation 对关键的对象进行标记,分别采取措施进行一定的加固:

  • 数据防护主要是约束运维操作,收敛数据访问入口,标准化存储操作的各种行为以减小风险;
  • 控制面防护主要是通过定制 ValidatingAdmissionWebhook 进行扩展,在对于一些关键对象的变更过程中,要求主动引入一些冗余的操作与校验,降低误操作风险;
  • 节点防护主要是通过对 kubelet 的进行改造,严格约束关键 pod 必须显式删除,降低极端情况下的系统性风险。

应用案例

字节基于原生 Kubernetes 生态定制了较多的功能以支持个性化的场景,整体的研发、迭代和交付的效率都非常高,对集群稳定性造成更大的挑战,即使在交付流程规范上严格把控,也不能完全杜绝异常情况下的极端异常风险;结合实践过程出现过的故障案例和场景诉求,字节云原生团队从元集群、控制面、数据面、业务定制等多个角度,构建了较为全面的防御体系,有效避免线上大规模事故的发生。

数据防护:元集群级联删除

字节内部的集群数量众多,为实现自动化运维和集群管理,需要构建元集群描述业务集群的状态;在这种情况下,元集群自身的异常可能会触发更大范围的故障。在字节早期,集群缺乏防护能力,SRE 在运维过程中使用过高权限,误删除了某个 region 元集群中用于描述 Node 状态的 CRD,因为没有防御系统拦截,CRD 被删除后会引发全量 CR 的级联删除,导致元集群控制器认为几乎所有的节点都需要下线,引发全量 pod 物理停服。该次故障最终引发单 region 生产集群在 30 分钟内持续标记删 3W+ 节点,实际删除 9K 节点后及时止损,影响面巨大且手动止损窗口很短。 在该案例中,接入防御体系能够实现在多个点位实现防御能力

  • 前置拦截:通过标记 CRD 为 critial 避免全量误删除引发级联问题;
  • 集群下线限流:集群大范围下线通常并不是常见的运维操作,控制节点下线的频率和安全水位,保证即使出现异常的级联删除行为,也能够尽量控制故障域;
  • 数据备份和恢复:当发生物理对象删除行为后,能够通过备份数据实现快速的恢复。

控制面防护:异常流量识别与限流

控制面异常通常源自于不合理的客户端行为和不够准确的服务端资源预估,由于场景过于复杂,在缺乏精细治理的情况下,最终因各种原因导致服务端过载;通常从现象上,会伴随着客户端大量的 List 请求和 APIServer OOM,进一步引发全量客户端 Relist,恶性循环直至集群出现雪崩。对于控制面的极端异常,字节内部通过接入 7 层的 gateway ,配合全链路的自动化流量 tracing,实现灵活智能的 API 请求防护

  • 常态限流:针对客户端和资源对象的组合和常态流量分析,定制限流规则,避免瞬时大量请求对服务端的压力
  • 容灾场景熔断:当集群出现明显异常或者雪崩时,通过手动熔断止损,并逐步放开限流以恢复集群正常

节点防护:异常版本升级触发大面积驱逐

相对于控制面,数据面的版本和配置通常更加复杂多样,迭代通常会更加频繁,更容易因为不当的组件运维操作引发不可预期的极端风险。某次 SRE 在升级 Kubelet 版本的过程中,应用了不符合预期的混部资源配置,在 Kubelet 重启后,大量 Running 中的 pod 因为资源归属识别错误,导致 admit 失败而被 delete,同时,原生的 delete API 不过 PDB 拦截,预期会引发大量业务容量的损失;但由于已经上线防护能力,最终没有引发严重的线上问题。在该案例中,接入防御体系能够同时在单机和中心上提供防御能力

  • 单机拦截:对于已经处于 Running 状态的核心服务,默认补充 explict-deletion 标签,确保只有显式地通过 API 标记删除 (设置 deletionTimestamp),能够保证因为数据面异常发版后,不影响业务实例的运行,给人为介入处理提供足够的时间
  • 中心拦截:对于核心服务补充 Delete 与 DeleteCollection 两种 API 进行校验,避免类似非预期的删除 pod 行为对业务造成影响

后续规划

字节防护实践未来会逐渐集成火山引擎 VKE 产品中,为云上的服务提供更加可靠的稳定性保证;除此之外,我们也会持续增强云原生防护的功能特性,收敛并解决更多可能对云上服务造成稳定性风险的场景,包括如下内容

  • 控制面 Delete pod API 防护内建的 PDB 防护机制仅作用于 Evict pod API,校验性能不佳。当存在大量 PDB 对象时,Evict pod API 耗时会大幅度劣化,请求时延远超 Delete pod,因此有很多组件刻意不使用 Evict pod 而直接 Delete pod,例如调度器发起抢占等。由于控制面 Delete pod 的内置校验较少,直接使用该接口容易导致业务 pod 的健康比例低于预期,影响业务正常运行。为避免这类风险,我们一方面需要优化 Evict pod 的性能,另一方面需要通过扩展对 Delete pod 操作进行更严格的校验,以保证业务运行的 pod 健康比例不低于预期。
  • 收敛静态校验策略当前我们在控制面做的防护工作主要依托于对 Validating Admission Webhook 机制,这一方面会 apiserver 在处理请求过程中引入额外的外部过程,提高延迟与出错概率,另一方面也会一定程度提高集群运维的复杂度。在 Kubernetes 1.26 版本中,引入了新的 Admission Plugin,支持使用 CEL (Common Expression Language)对请求进行一些静态校验。后续我们会将控制面防护的一些冗余操作校验迁移到 CEL,对上述问题进行改善。
  • 场景定制防护策略对于 Redis 和分布式训练等带存储状态的业务来说,其编排模型和运维方案上有比较多的定制需求,为此,防御体系需要针对其业务特点 (如存储分片、纵向资源调整、原地重启等),补充完善更多精细化的策略以匹配特有的极端异常风险。

总结

本文主要介绍了字节跳动内部生产环境中 Kubernetes 应用过程中发现的主要系统风险与提出一系列防护措施。具体来说,我们从 Kubernetes 组件的交互过程的角度出发,划分为数据、控制面、节点三个层面,并通过具体示例说明了常见问题,包括误操作和管控组件版本错误等等,并且针对这些常见问题,简单介绍了我们构建的一系列防御性措施,包括但不限于,约束组件访问权限、主动添加冗余操作与相关校验等等。通过这些防御性措施,我们能够降低已知问题给业务带来的风险,为业务提供稳定的基础服务。

除了必要的防御性加固措施,日常维护集群时的标准化变更流程也至关重要。通过控制集群规模并充分进行灰度验证,可以降低故障的影响范围。在生产环境中,只有综合利用系统自我防御性措施和标准化运维等多种手段,才能最大程度地降低风险和故障损失。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kotlin-通过Java反编译撕开Kotlin的高端语法糖

发表于 2024-04-22

前言

书接上回,上回跟大家聊了下kotlin的一些常用的语法和如何通过将kotlin转换为java代码来深入了解koltin语法的本质,今天我们继续了解一下kotlin的其他一些特性。

其实我在写上一篇文章的时候,介绍的一些方法主要是为了跟大家分享一下,如何去理解kotlin语法糖背后的本质,不过后来想想前面的内容又太单一简单了,因此就继续水一篇文章。我们继续了解一下一下kotlin其他的语法糖。

密封类和枚举类

枚举类我们就不详细展开了,相信大家都很了解。它常常用于定义有限的集合。因此常被我们当做单例和多例模式。

1
2
3
4
5
6
7
kotlin复制代码enum class Gender{
MALE,// 定义男性
FEMALE// 定义女性
}

// 使用方式,判断是否是男性
fun isMale(people : Gender) = people == MALE

那么什么是密封类呢?刚接触密封类的时候,我还是比较奇怪的,首先我们来看下密封类的使用方式

1
2
3
4
5
6
7
8
kotlin复制代码// 首先定义一个密封类,人类
sealed class People {
class Male : People() // 定义男性
class Female : People() // 定义女性
}

// 使用方式,判断是否是男性
fun isMale(people : People) = people is Male

在形式上看似比较像,但是这里有一个很大的区别,枚举类里面我们使用的是 == 进行判断,密封类我们是使用is(类似Java的instanceOf),看到这里大家可能已经有一个大致的区分。枚举类定义出来的,已经是类的实例对象,而密封类定义的是类的子类。他们都用于表示限定的类层次结构,但是方式不同。因此,在使用上,我们常常拿枚举当做单例或者多例模式。而密封类常常当做一个被限定子类的集合,但是子类的实例可以有多个,就好比如我们去请求一个接口,返回的结果成功、失败、异常,但是可以有很多次成功、失败、异常。
再回到上面的例子看,枚举里的男性、女性我们通常可以设定为一个属性,而密封类的男性、女性我们设定为一个子类特征集合,他们互不冲突,两者是可以互补的,我们讲上述的例子结合一下。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码// 首先定义一个密封类,
enum class Gender{
MALE,// 定义男性
FEMALE// 定义女性
}

sealed class People(gender : Gender) {
class Male : People(Gender.MALE) // 定义男性
class Female : People(Gender.FEMALE) // 定义女性
}

我们将密封类和枚举类结合,枚举类的Gender是密封类People的一个性别属性。看到这里,大家基本上就可以分清楚枚举类和密封类的区别了。

我们将上述代码转换成Java代码看一下:

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
scala复制代码// Gender.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;
// 定义的枚举类型
public enum Gender {
MALE,
FEMALE;
}
// People.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;

// 定义的密封类,我们发现
// 其实我们定义的密封类在Java当中是一个抽象类
public abstract class People {
private People(Gender gender) {
}

// $FF: synthetic method
public People(Gender gender, DefaultConstructorMarker $constructor_marker) {
this(gender);
}

// 我们定义的Male子类是密封类的静态内部子类
public static final class Male extends People {
public Male() {
super(Gender.MALE, (DefaultConstructorMarker)null);
}
}
// 我们定义的Female子类是密封类的静态内部子类
public static final class Female extends People {
public Female() {
super(Gender.FEMALE, (DefaultConstructorMarker)null);
}
}
}

看到这里,大家应该就能理解枚举类和密封类的本质区别在哪里了。
我们知道,在kotlin中,枚举类和密封类都是支持when(类似Java的switch-case语句)的判定的。我们来看下以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码// 通过枚举类型的判定
fun isMaleByGender(gender: Gender) : Boolean =
when(gender){
Gender.MALE-> true
Gender.FEMALE -> false
}

// 通过密封类的判定
fun isMaleByPeople(people: People) : Boolean=
when(people){
is People.Male -> true
is People.Female -> false
}

枚举类型的判定是直接通过when(param) -> value 的方式判定的,而密封类的是通过when(param) -> is value的方式进行的。

kotlin的运算符重载

kotlin和C++一样,是支持运算符重载的,可是Java是没有运算符重载的。那么为什么Java没有运算符重载呢?因为Java的设计者认为,运算符本质上也是函数的调用。没错,其实kotlin的运算符本质上也是方法调用,只不过kotlin的编译器帮助我们能够使用重载之后的运算符进行开发,提高我们的开发效率。

下面,我们来看一个运算符重载的例子

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码// 一个简单的数据类
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Foo) : Foo = Foo(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
// 使用的时候
val f1 = Foo(10, 20)
val f2 = Foo(30, 40)
// 直接用+运算符代替plus函数,事实上会调用plus函数
println(f1 + f2) // 打印内容为Foo(x=40, y=60)
}

在上述例子当中,我们实现了加法运算符号的重载来计算两个坐标相加的功能。其实例子很简单,我们来简单了解下运算符重载的本质是什么。老规矩,我们看下翻译成Java代码

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
java复制代码// Point.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class Point {
private final int x;
private final int y;

@NotNull
public final Point plus(@NotNull Point other) {
Intrinsics.checkNotNullParameter(other, "other");
return new Point(this.x + other.x, this.y + other.y);
}

public final int getX() {
return this.x;
}

public final int getY() {
return this.y;
}

public Point(int x, int y) {
this.x = x;
this.y = y;
}
...
}
// KtOperatorKt.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

public final class KtOperatorKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
Point f1 = new Point(10, 20);
Point f2 = new Point(30, 40);
Point var3 = f1.plus(f2);
System.out.println(var3);
}
}

我们发现,在Java当中,我们的“+”的操作符,被转换成了plus方法,这是kotlin帮我们做的。所以,本质上,kotlin的运算符重载就是方法调用,其实我们使用的block() 亦是如此,他重载了invoke。更多支持重载的运算符,这里就不一一展开了,可以查看kotlin的中文网的说明。

kotlin的解构

在kotlin当中,我们可以将对象的属性赋值给多个新定义的属性,这种被称为kotlin的解构。我们看下下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码data class Human(
val name : String,
val age : Int,
val gender : Int
)

fun deconstruct(){
val human = Human(
name = "Jack",
age = 18,
gender = 1
)
// 直接定义解构接收的多个参数
// 接收的参数类型需要对齐
val (name, age , gender ) = human
println(
"name:$name,age:$age,gender:$gender"
)
}

这里是不是觉得好理解,其实本质上应该就是参数的赋值吧。那么究竟是不是呢,我们来看一下Java对应的代码

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
kotlin复制代码// Deconstruct.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;

@Metadata(
mv = {1, 8, 0},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0003"},
d2 = {"Lcom/yuanyi/myapplication/kt/Deconstruct;", "", "()V", "app_debug"}
)
public final class Deconstruct {
}
// Human.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Metadata(
mv = {1, 8, 0},
k = 1,
d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\f\n\u0002\u0010\u000b\n\u0002\b\u0004\b\u0086\b\u0018\u00002\u00020\u0001B\u001d\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005\u0012\u0006\u0010\u0006\u001a\u00020\u0005¢\u0006\u0002\u0010\u0007J\t\u0010\r\u001a\u00020\u0003HÆ\u0003J\t\u0010\u000e\u001a\u00020\u0005HÆ\u0003J\t\u0010\u000f\u001a\u00020\u0005HÆ\u0003J'\u0010\u0010\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u00052\b\b\u0002\u0010\u0006\u001a\u00020\u0005HÆ\u0001J\u0013\u0010\u0011\u001a\u00020\u00122\b\u0010\u0013\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0014\u001a\u00020\u0005HÖ\u0001J\t\u0010\u0015\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\b\u0010\tR\u0011\u0010\u0006\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\n\u0010\tR\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u000b\u0010\f¨\u0006\u0016"},
d2 = {"Lcom/yuanyi/myapplication/kt/Human;", "", "name", "", "age", "", "gender", "(Ljava/lang/String;II)V", "getAge", "()I", "getGender", "getName", "()Ljava/lang/String;", "component1", "component2", "component3", "copy", "equals", "", "other", "hashCode", "toString", "app_debug"}
)
public final class Human {
@NotNull
private final String name;
private final int age;
private final int gender;

@NotNull
public final String getName() {
return this.name;
}

public final int getAge() {
return this.age;
}

public final int getGender() {
return this.gender;
}

public Human(@NotNull String name, int age, int gender) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
this.gender = gender;
}

// 我们发现,多了component1~3的方法
@NotNull
public final String component1() {
return this.name;
}

public final int component2() {
return this.age;
}

public final int component3() {
return this.gender;
}
// 这里还多了一个copy的方法
@NotNull
public final Human copy(@NotNull String name, int age, int gender) {
Intrinsics.checkNotNullParameter(name, "name");
return new Human(name, age, gender);
}

// $FF: synthetic method
public static Human copy$default(Human var0, String var1, int var2, int var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
var1 = var0.name;
}

if ((var4 & 2) != 0) {
var2 = var0.age;
}

if ((var4 & 4) != 0) {
var3 = var0.gender;
}

return var0.copy(var1, var2, var3);
}

@NotNull
public String toString() {
return "Human(name=" + this.name + ", age=" + this.age + ", gender=" + this.gender + ")";
}

public int hashCode() {
String var10000 = this.name;
return ((var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age)) * 31 + Integer.hashCode(this.gender);
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Human) {
Human var2 = (Human)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age && this.gender == var2.gender) {
return true;
}
}

return false;
} else {
return true;
}
}
}
// DeconstructKt.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;

@Metadata(
mv = {1, 8, 0},
k = 2,
d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
d2 = {"deconstruct", "", "app_debug"}
)
public final class DeconstructKt {
public static final void deconstruct() {
Human human = new Human("Jack", 18, 1);
// 关键在这里,我们发现,参数的或并不是通过class#getParams()的方式,而是通过class#componentN()的方式赋值的
String name = human.component1();
int age = human.component2();
int gender = human.component3();
String var4 = "name:" + name + ",age:" + age + ",gender:" + gender;
System.out.println(var4);
}
}

那么为什么不是使用getParams的方式,而是要多几个方法呢?这不是多此一举吗?其实不然,不知道大家发现没有,上述的class我使用的是data class,在kotlin当中,data class都会默认生成componentN和copy的方法,原因就是这个,当我们去掉data的,让其变成一个普通的class的时候,我们发现会报如下错误。

image.png

我们按照提示,将componentN的方法加上去如下:

image.png

我们发现,代码顺利通过。这里我们惊奇的发现,componentN前面有个关键字,是operator,没错,解构的本质就是kotlin的操作符重载,之所以把解构放在操作符重载后面讲就是这个原因。

解构的方式其实我们还可以用在lamda表达式当中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码fun deconstruct(){
val human = Human(
name = "Jack",
age = 18,
gender = 1
)

blockDeconstruct(human){
(name,age,gender)-> // 前提是支持解构
run {
println(
"name:$name,age:$age,gender:$gender"
)
}
}
}

fun blockDeconstruct(human: Human,block : (Human) -> Unit) = block(human)

其实kotlin帮我们封装了很多的方法,来提升我们的开发效率,比如Collections当中的filter、map都是依靠支持迭代器(Iterator)的扩展函数实现的,因此我们在学习kotlin的语法糖的时候,要找对方法。不过也不是所有的语法糖都能通过java代码可以看出来的,有些很多也是依赖编译器一起实现的,例如协程的原因。那么本次介绍的语法糖就到这里吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于 swift 中的字符串,你可能还不知道的事情

发表于 2024-04-22

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

字符串是 swift 中的一个非常基础的类型,大家平时使用最多的想必也是这个类型了,但是有一些底层实现和隐藏能力,很多人还不知道,今天就来聊聊这块内容。

长度问题

不知道大家平时开发时有没有注意过,swift 中的 String 和 NSString 是可以用 as 无缝转换的,但是 String 的 count 属性和 NSString 的 length 的值可能不同。

比如:

1
2
3
4
swift复制代码let swiftString = " ‍ ‍ ‍ "
let nsString = swiftString as NSString
print(swiftString.count) // 打印 1
print(nsString.length) // 打印 11

同样是一个 emoji 符号,swift String 的 count 打印 1,但是转成 NSString 使用 length 打印 11。

这是因为,count 属性是测量的屏幕上显示的字符数,这里的 emoji 在屏幕上只显示一个字符,所以值是 1。

而 NSString 是使用 UTF-16 编码的,它的 length 属性实际上是 UTF-16 编码的长度。这在 NSString 的文档上有标注:

swift 的字符串也可以转成 UTF-16 编码,这样,打印的 count 也是 11 了:

1
2
swift复制代码let swiftString = " ‍ ‍ ‍ "
print(swiftString.utf16.count) // 打印 11

关于 Unicode 编码

前面提到了 Unicode 编码,很多程序员都搞不明白,这里顺带提一句,Unicode 是一种计算机文本编码标准,简单来说就是让字符显示在屏幕上的。

在 Unicode 编码出现之前,世界上还有很多编码标准,比如 ASCII、ISO 8859-1 等,编码方式不统一,解码的时候就会出现乱码的情况,后来就设计了 Unicode 编码,大家用同一套标准,这样就不会乱码了。

上边说到的 UTF-16 其实是 Unicode 的一种编码方案,为了在内存使用、兼容性和编码效率之间取得平衡,Unicode 标准定义了 UTF-8、UTF-16、UTF-32 几种不同的编码方案,iOS 内部使用的就是 UTF-16。

其实 swift 提供了几种编码的转换函数,可以自由切换:

1
2
3
4
scss复制代码let swiftString = " ‍ ‍ ‍ "
print(swiftString.utf8.count) // utf8 打印 25
print(swiftString.utf16.count) // utf16 打印 11
print(swiftString.unicodeScalars.count) // utf32 打印 7

检查字符串

之前看到一些开源项目在判断空字符串时使用 string.count == 0,每当看到这样的写法,我都会顺手提个 PR 改成 string.isEmpty。

相比之下,用 string.isEmpty 来判断空字符串的性能更好,因为在底层实现中 count 属性需要遍历字符串的元素。

系统对字符串的性能优化

写入时复制(Copy-on-Write)

我们都知道 Swift 的字符串是值类型的,这意味着每个字符串变量都拥有独立的数据副本。原本每次读写都会产生一份新的副本,但这会产生较高的性能开销。

为了减少性能开销,Swift 引入了一种称为“写入时复制”(Copy-on-Write, COW)的策略来优化这一过程。

具体来说,当你复制一个字符串时,Swift 并不立即复制字符串的数据,而是让新旧字符串实例共享相同的的数据缓冲区。只有当你尝试修改其中一个字符串时,Swift 才会进行实际的数据复制操作。也就是在只有真正需要时才会发生数据的复制,从而减少了不必要的性能开销。

这种机制带来一个后果,如果你有多个字符串实例共享同一个数据缓冲区,那么对其中一个字符串进行修改的操作可能会导致 O(n) 的时间和空间开销,因为需要复制整个数据缓冲区。这里的 n 指的是字符串的长度。

缓冲区的指数增长策略

Swift 的字符串还采用了一种缓冲区的指数增长策略来优化字符串的追加操作。当你向字符串追加内容时,如果当前的数据缓冲区已满,Swift 会分配一个新的更大的缓冲区,并将现有数据复制到这个新缓冲区中。

这个新缓冲区的大小并不是简单地加上新增数据的大小,而是按照一定的比例(通常是翻倍)增加,以便为未来的追加操作留出空间。这种策略的好处是,虽然某次特定的追加操作可能需要重新分配缓冲区并复制数据(这是一次较为昂贵的操作),但随着缓冲区大小的增长,这种情况发生的频率会逐渐减少。在对大量追加操作进行平均时,每次追加操作的平均时间成本会趋近于常数(O(1)),从而提高了追加操作的整体性能。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

LangChain之各类提示模板的使用 Model I/O

发表于 2024-04-22

Model I/O

在LangChain中,Model I/O被称为:模型的输入与输出,其有输入提示(Format)、调用模型(Predict)、输出解析(Parse)等三部分组成。

1
2
3
4
5
makefile复制代码1.提示模板: LangChain的模板允许动态选择输入,根据实际需求调整输入内容,适用于各种特定任务和应用。

2.语言模型: LangChain 提供通用接口调用不同类型的语言模型,提升了灵活性和使用便利性。

3.输出解析: 利用 LangChain 的输出解析功能,精准提取模型输出中所需信息,避免处理冗余数据,同时将非结构化文本转换为可处理的结构化数据,提高信息处理效率。

在这里插入图片描述

提示模板

在LangChain的Model I/O中,提示模板是其组成之一,这里也主要申明记录提示模板(Format)的使用。

概述

语言模型的提示是用户提供的一组指令或输入,用于指导模型的响应,帮助模型理解上下文并生成相关且连贯的基于语言的输出,例如回答问题、完成句子或参与某项活动、对话。

“提示”指的是模型的输入,这个输入很少是硬编码的,而是通常从多个组件构建而成的,恰哈提示模板就是负责构建这个输入的。

LangChain提示模板特点:

1
2
3
4
5
6
7
8
9
makefile复制代码1.清晰易懂的提示: 提高提示文本的可读性,使其更易于理解,尤其是在处理复杂或涉及多个变量的情况下。

2.增强可重用性: 使用模板,可以在多个地方重复使用,简化代码,无需重复构建提示字符串。

3.简化维护: 使用模板后,如果需要更改提示内容,只需修改模板,无需逐个查找所有用到该提示的地方。

4.智能处理变量: 模板可以自动处理变量的插入,无需手动拼接字符串。

5.参数化生成: 模板可以根据不同的参数生成不同的提示,有助于个性化文本生成。

类型

在LangChain中,可以看到以下类型的提示模板:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码1.LLM提示模板 PromptTemplate:常用的String提示模板

2.聊天提示模板 ChatPromptTemplate: 常用的Chat提示模板,用于组合各种角色的消息模板,传入聊天模型。
消息模板包括:ChatMessagePromptTemplate、HumanMessagePromptTemplate、AIlMessagePromptTemplate SystemMessagePromptTemplate等

3.样本提示模板 FewShotPromptTemplate:通过示例来教模型如何回答

4.部分格式化提示模板:提示模板传入所需值的子集,以创建仅期望剩余值子集的新提示模板。

5.管道提示模板 PipelinePrompt: 用于把几个提示组合在一起使用。

6.自定义模板:允许基于其他模板类来定制自己的提示模板。

模板导入方式如下:

1
2
3
4
5
6
7
8
9
10
python复制代码from langchain.prompts.prompt import PromptTemplate
from langchain.prompts import FewShotPromptTemplate
from langchain.prompts.pipeline import PipelinePromptTemplate
from langchain.prompts import ChatPromptTemplate
from langchain.prompts import (
ChatMessagePromptTemplate,
SystemMessagePromptTemplate,
AIMessagePromptTemplate,
HumanMessagePromptTemplate,
)

设置环境变量

1
2
3
python复制代码import os
os.environ["OPENAI_BASE_URL"] = "https://xxx.com/v1"
os.environ["OPENAI_API_KEY"] = "sk-fDqouTlU62yjkBhF46284543Dc8f42438a9529Df74B4Ce65"

PromptTemplate提示模板

创建提示模板

创建一个PromptTemplate提示模板,有2种方式来创建。

1.通过from_template方法从字符串模板中创建提示模板

1
2
3
4
5
6
7
8
9
python复制代码# 导入LangChain中的提示模板
from langchain.prompts import PromptTemplate
# 创建原始模板
template = "您是一位专业的文案写手。\n对于信息 {text} 进行简短描述"
# 根据原始模板创建LangChain提示模板
prompt = PromptTemplate.from_template(template)
# 打印LangChain提示模板的内容
print(prompt)
print(prompt.format(text="猪八戒吃人参果"))

提示模板的具体内容如下:

1
2
3
python复制代码input_variables=['text'] template='您是一位专业的文案写手。\n对于信息 {text} 进行简短描述'
您是一位专业的文案写手。
对于信息 猪八戒吃人参果 进行简短描述

2.直接生成提示模板

通过提示模板类的构造函数,在创建模板时手工指定input_variables

1
2
3
4
5
6
7
python复制代码from langchain.prompts import PromptTemplate

prompt = PromptTemplate(
input_variables=["text"],
template="您是一位专业的文案写手。\n对于信息 {text} 进行简短描述"
)
print(prompt.format(text="猪八戒吃人参果"))

使用提示模板

调用语言模型,让模型帮写文案,并返回文案结果。

将模板实例化,将 {text}替换为 "猪八戒吃人参果",形成具体的提示:“您是一位专业的文案写手。对于信息 猪八戒吃人参果 进行简短描述”

1
2
3
4
5
6
7
8
9
10
python复制代码# 导入LangChain中的OpenAI模型接口
from langchain_openai import OpenAI
# 创建模型实例
model = OpenAI(model_name='gpt-3.5-turbo-instruct')
# 输入提示
input = prompt.format(text="猪八戒吃人参果")
# 得到模型的输出
output = model(input)
# 打印输出内容
print(output)
1
python复制代码猪八戒是一位贪吃的神仙,他最爱的美食就是人参果。每当他闻到人参果的香味,就会忍不住大快朵颐,吃得津津有味。然而,人参果却是一种珍稀的仙果,吃多了会让猪八戒变得更加贪婪和暴躁,甚至会影响他的神仙身份。因此,猪八戒每次都要克制自己的食欲,才能保持神仙的本性。尽管如此,每当有人提起人参果,他仍然会忍不住流口水,渴望再次品尝这种美味的禁果。

复用提示模板

复用提示模板,可以同时生成多个结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码# 导入LangChain中的OpenAI模型接口
from langchain_openai import OpenAI

# 创建模型实例
model = OpenAI(model_name='gpt-3.5-turbo-instruct')
# 多种花的列表
objList = ["猪八戒吃人参果", "孙悟空吃人参果"]

# 生成多种花的文案
for obj in zip(objList):
# 使用提示模板生成输入
input_prompt = prompt.format(text=obj)
# 得到模型的输出
output = model(input_prompt)
# 打印输出内容
print(output)

模型输出如下:

1
2
3
4
python复制代码猪八戒是一位贪吃的妖怪,但他最爱吃的不是普通的食物,而是人参果。这种奇特的水果具有神奇的功效,能够增强生命力,让人变得更加健康强壮。但如果被猪八戒吃掉,可能会让他变得更加凶猛可怕。


孙悟空是一位身怀绝技的神仙,他喜欢吃人参果来增强自己的力量。

ChatPromptTemplate聊天提示模板

LangChain提供了几个相关的提示模板,以便轻松构建和处理提示。在使用聊天模型时,建议使用这些与聊天相关的提示模板,而不是PromptTemplate,以充分发挥基础聊天模型的潜力。

PromptTemplate创建字符串提示的模板。默认情况下,使用Python的str.format语法进行模板化。而ChatPromptTemplate是创建聊天消息列表的提示模板。

创建一个ChatPromptTemplate提示模板,模板的不同之处是它们有对应的角色。

基本使用

通过from_messages方法,传入简单的聊天列表数据,以此创建提示模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码from langchain.prompts.chat import ChatPromptTemplate

template = "你是一个翻译专家,擅长将 {input_language} 语言翻译成 {output_language}语言."
human_template = "{text}"

chat_prompt = ChatPromptTemplate.from_messages([
("system", template),
("human", human_template),
])
print(chat_prompt)

# 导入LangChain中的ChatOpenAI模型接口
from langchain_openai import ChatOpenAI

# 创建模型实例
model = ChatOpenAI(model_name='gpt-3.5-turbo')
# 输入提示
messages = chat_prompt.format_messages(input_language="英文", output_language="中文", text="I love programming.")
# 得到模型的输出
output = model.invoke(messages)
# 打印输出内容
print(output)
1
python复制代码content='我喜欢编程。' response_metadata={'token_usage': {'completion_tokens': 8, 'prompt_tokens': 48, 'total_tokens': 56}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_b28b39ffa8', 'finish_reason': 'stop', 'logprobs': None} id='run-365c447e-f3d7-4d07-b5f2-7953752d4ba6-0'

进阶使用

LangChain提供不同类型的MessagePromptTemplate.最常用的是AIMessagePromptTemplate、 SystemMessagePromptTemplate和HumanMessagePromptTemplate,分别创建人工智能消息、系统消息和人工消息。

要创建与角色相关联的消息模板,可以使用MessagePromptTemplate。

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
python复制代码# 导入聊天消息类模板
from langchain.prompts import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)

# 模板的构建
system_template = "你是一个翻译专家,擅长将 {input_language} 语言翻译成 {output_language}语言."
system_message_prompt = SystemMessagePromptTemplate.from_template(system_template)

human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)

prompt_template = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])

# 格式化提示消息生成提示
prompt = prompt_template.format_prompt(input_language="英文", output_language="中文",
text="I love programming.").to_messages()

from langchain_openai import ChatOpenAI

# 创建模型实例
model = ChatOpenAI(model_name='gpt-3.5-turbo')
# 得到模型的输出
result = model.invoke(prompt)
# 打印输出内容
print(result)

更直接地构建MessagePromptTemplate,可以在外部创建一个PromptTemplate,然后将其传递进去

1
2
3
4
5
python复制代码prompt=PromptTemplate(
template="你是一个翻译专家,擅长将 {input_language} 语言翻译成 {output_language}语言.",
input_variables=["input_language", "output_language"],
)
system_message_prompt = SystemMessagePromptTemplate(prompt=prompt)

少量样本示例的提示模板

基于LLM模型与聊天模型,可分别使用FewShotPromptTemplate或FewShotChatMessagePromptTemplate,两者使用基本一致。

这里主要使用FewShotPromptTemplate,它是一个复杂的提示模板,它包含多个示例和一个提示。这种模板可以使用多个示例来指导模型生成对应的输出。

使用FewShotPromptTemplate类来创建使用少量样本示例的提示模板,此类要么接受一组示例,要么接受一个ExampleSelector对象。

创建示例集

创建一些提示样本,每个示例都是一个字典,其中键是输入变量,值是输入变量的值。

1
2
3
4
python复制代码examples = [
{"input": "2+2", "output": "4", "description": "加法运算"},
{"input": "5-2", "output": "3", "description": "减法运算"},
]

创建提示模板

配置一个格式化程序,将Few-shot示例格式化为字符串。这个格式化程序应该是一个PromptTemplate对象。

1
2
3
4
5
6
python复制代码from langchain.prompts import PromptTemplate
# 创建提示模板,配置一个提示模板,将一个示例格式化为字符串
prompt_template = "你是一个数学专家,算式: {input} 值: {output} 使用: {description} "
# 这是一个提示模板,用于设置每个示例的格式
prompt_sample = PromptTemplate(input_variables=["input", "output", "description"], template=prompt_template)
print(prompt_sample.format(**examples[0])) # 你是一个数学专家,算式: 2+2 值: 4 使用: 加法运算

创建FewShotPromptTemplate对象

创建一个FewShotPromptTemplate对象。这个对象接受Few-shot示例和Few-shot示例格式化程序

1
2
3
4
5
6
7
8
9
10
python复制代码# 创建一个FewShotPromptTemplate对象
from langchain.prompts.few_shot import FewShotPromptTemplate

prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=prompt_sample,
suffix="你是一个数学专家,算式: {input} 值: {output}",
input_variables=["input", "output"]
)
print(prompt.format(input="2*5", output="10")) # 你是一个数学专家,算式: 2*5 值: 10

使用

初始化大模型,然后调用

1
2
3
4
5
python复制代码from langchain_openai import OpenAI

model = OpenAI(model_name='gpt-3.5-turbo-instruct')
result = model.invoke(prompt.format(input="2*5", output="10"))
print(result) # 使用: 乘法运算

示例选择器

概述

LangChain提供示例选择器来提高效率,避免一次性发送所有示例给模型,同时减少使用的Token数量。

如果有大量示例,可能需要选择要包含在提示中的示例,示例选择器是负责执行此操作的类。

LangChain有几种不同类型的示例选择器。

名称 描述
SemanticSimilarityExampleSelector 使用输入和示例之间的语义相似性来决定选择哪些示例。
MaxMarginalRelevanceExampleSelector 使用输入和示例之间的最大边际相关性来决定选择哪些示例。
LengthBasedExampleSelector 根据一定长度内可以容纳的数量来选择示例
NGramOverlapExampleSelector 使用输入和示例之间的 ngram 重叠来决定选择哪些示例。

这里使用SemanticSimilarityExampleSelector示例选择器,基于少量样本示例的提示模板结合示例选择器进行使用,具体使用参考如下

安装Chroma向量数据库

示例选择器使用向量相似度比较,需要安装向量数据库。这里使用了开源的Chroma。

安装Chroma

1
python复制代码pip install chromadb

定义示例集

1
2
3
4
5
python复制代码# 定义包含的示例
examples = [
{"input": "2+2", "output": "4", "description": "加法运算"},
{"input": "5-2", "output": "3", "description": "减法运算"},
]

创建提示模板

1
2
3
4
5
6
7
python复制代码from langchain.prompts import PromptTemplate

# 创建创建提示模板
prompt_template = "你是一个数学专家,算式: {input} 值: {output} 使用: {description} "
# 这是一个提示模板,用于设置每个示例的格式。
prompt_sample = PromptTemplate(input_variables=["input", "output", "description"], template=prompt_template)
print(prompt_sample.format(**examples[0])) # 你是一个数学专家,算式: 2+2 值: 4 使用: 加法运算

示例选择器

不直接将示例馈送到FewShotPromptTemplate对象中,而是将其馈送到ExampleSelector对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码# 导入FewShotPromptTemplate对象
from langchain.prompts.few_shot import FewShotPromptTemplate
# 导入选择器
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
# 导入向量数据库Chroma
from langchain_community.vectorstores import Chroma
# 导入嵌入模型
from langchain_openai import OpenAIEmbeddings

# 初始化示例选择器
example_selector = SemanticSimilarityExampleSelector.from_examples(
examples,
OpenAIEmbeddings(),
Chroma,
k=1
)

创建FewShotPromptTemplate对象

创建一个FewShotPromptTemplate对象。该对象接受示例选择器和few shot示例的格式化程序。

1
2
3
4
5
6
7
python复制代码# 创建一个使用示例选择器的FewShotPromptTemplate对象
prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=prompt_sample,
suffix="你是一个数学专家,算式: {input} 值: {output}",
input_variables=["input", "output"]
)

使用

1
2
3
4
5
python复制代码# 调用大模型
from langchain_openai import OpenAI

model = OpenAI(model_name='gpt-3.5-turbo-instruct')
print(model.invoke(prompt.format(input="2*5", output="10"))) # 使用: 乘法运算

PipelinePromptTemplate提示模板

概述

LangChain 包含一个抽象 PipelinePromptTemplate,当想要重用部分提示时,它会很有用。

PipelinePrompt 由两个主要部分组成:

1
2
3
复制代码最终提示:返回的最终提示

管道提示:元组列表,由字符串名称和提示模板组成。每个提示模板将被格式化,然后作为具有相同名称的变量传递到未来的提示模板。

最终提示

创建要给最终提示模板,它由多个提示模板构成最终模板。

1
2
3
4
5
6
7
8
9
10
python复制代码from langchain_core.prompts.prompt import PromptTemplate


# 创建一个完整的模板 最终提示
full_template = """{introduction}

{example}

{start}"""
full_prompt = PromptTemplate.from_template(full_template)

多个提示

创建多个提示模板,由这些模板构成最终完整的提示模板,这些单个提示模板可以实现复用的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码# 创建一个介绍模板
introduction_template = """你在冒充 {person}."""
introduction_prompt = PromptTemplate.from_template(introduction_template)

# 创建一个示例模板
example_template = """下面是一个交互示例:

Q: {example_q}
A: {example_a}"""
example_prompt = PromptTemplate.from_template(example_template)

# 创建一个开始模板
start_template = """现在,认真做这件事

Q: {input}
A:"""
start_prompt = PromptTemplate.from_template(start_template)

管道提示

组合单个可复用提示模板成一个管道提示模板

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码from langchain_core.prompts.pipeline import PipelinePromptTemplate

# 元组列表,由字符串名称和提示模板组成
input_prompts = [
("introduction", introduction_prompt),
("example", example_prompt),
("start", start_prompt),
]

# 创建一个管道模板
pipeline_prompt = PipelinePromptTemplate(
final_prompt=full_prompt, pipeline_prompts=input_prompts
)

使用

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码# 提示模板所需的变量名称列表
print(pipeline_prompt.input_variables)

# 输出结果
print(
pipeline_prompt.format(
person="Elon Musk",
example_q="你最喜爱的车辆?",
example_a="特斯拉",
input="你最喜欢的社交媒体网站是什么?",
)
)

执行日志如下

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码['example_q', 'person', 'input', 'example_a']

你在冒充 Elon Musk.

下面是一个交互示例:

Q: 你最喜爱的车辆?
A: 特斯拉

现在,认真做这件事

Q: 你最喜欢的社交媒体网站是什么?
A:

部分提示模板

概述

“部分”提示模板是有意义的,例传递所需值的子集,以创建仅需要剩余值子集的新提示模板。

LangChain通过两种方式支持这一点:

1
2
3
复制代码1.使用字符串值进行部分格式化

2.使用返回字符串值的函数进行部分格式化

基本使用

先使用字符串值部分化提示模板,然后传递部分化的提示模板

1
2
3
4
5
python复制代码from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template("{foo}{bar}")
partial_prompt = prompt.partial(foo="foo")
print(partial_prompt.format(bar="baz"))

在初始化提示模板时,使用字符串值部分化变量

1
2
3
4
python复制代码prompt = PromptTemplate(
template="{foo}{bar}", input_variables=["bar"], partial_variables={"foo": "foo"}
)
print(prompt.format(bar="baz"))

使用返回字符串值的函数进行部分处理,适用于总是想以一种常见的方式获取一个变量时

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码from datetime import datetime


def _get_datetime():
now = datetime.now()
return now.strftime("%m/%d/%Y, %H:%M:%S")

prompt = PromptTemplate(
template="Tell me a {adjective} joke about the day {date}",
input_variables=["adjective", "date"],
)
partial_prompt = prompt.partial(date=_get_datetime)
print(partial_prompt.format(adjective="funny"))

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter 学习 之 DIO40 的封装 一引入插件

发表于 2024-04-22

dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等…

网址在右边 → [dio]

一.引入插件

在 pubspec.yaml 文件下新增 dio(注意空格问题)

1
2
yaml复制代码dependencies:
dio: ^4.0.6

二. 封装DIO

1.创建DioClient单例模式,实现访问方法

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
scss复制代码// 必须是顶层函数
_parseAndDecode(String response) {
return jsonDecode(response);
}

parseJson(String text) {
return compute(_parseAndDecode, text);
}
//继承DioForNavigator(详情见官方文档)
class DioClient extends DioForNative {
//单例模式
static DioClient? _instance;
factory DioClient() => _instance ??= DioClient._init();
//初始化方法
DioClient._init() {
(transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
options = BaseOptions(
//设定一些基础的东西
connectTimeout: 60*1000,//连接超时间
receiveTimeout: 60*1000,//接收超时时间
//除了在这里定义还可以到拦截器中定义
);
//处理访问前的拦截器
interceptors.add(OptionInterceptor());
//处理回来的数据
interceptors.add(RequestInterceptor());
//代理抓包(开发阶段可能用到,正式上线建议关闭)
proxy();
}

///get请求
doGet<T>(path, {queryParameters, options, cancelToken, onReceiveProgress}) {
return get<T>(path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress);
}

///post请求 为了不和继承的DioMixin里面的post方法名冲突所以起名叫doPost
doPost<T>(path,
{queryParameters,
options,
cancelToken,
onSendProgress,
onReceiveProgress}) {
return post<T>(path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress);
}
///上传文件
uploadFile(formData) {
var uploadOptions = Options(contentType: "multipart/form-data");
return doPost(Api.uploadURL, options: uploadOptions, data: formData);
}
///代理抓包测试用
void proxy() {
if (NetworkConfig.proxyEnable) {
if (NetworkConfig.caughtAddress != "") {
(httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
client.findProxy = (Uri uri) {
return 'PROXY ' + NetworkConfig.caughtAddress;
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) {
return true;
};
};
}
}
}
}

2.封装拦截器

dio的请求流程是 请求拦截器 >> 请求转换器 >> 发起请求 >> 响应转换器 >> 响应拦截器 >> 最终结果。

请求拦截器

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
dart复制代码//Option拦截器可以用来统一处理Option信息 可以在这里添加
class OptionInterceptor extends InterceptorsWrapper {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
//在请求发起前修改头部
// options.headers["token"] = "11111";
///请求的Content-Type,默认值是"application/json; charset=utf-8".
/// 如果您想以"application/x-www-form-urlencoded"格式编码请求数据,
options.contentType=Headers.formUrlEncodedContentType;
///如果你的headers是固定的你可以在BaseOption中设置,如果不固定可以在这里进行根据条件设置
options.headers["apiToken"] = "111222154546";
options.headers["user-token"]=CacheUtil().getJson(SPName.userInfo)!["userToken"];
String? mainUrl = CacheUtil().get<String>(SPName.mainUrl);
//修改地址
//如果需要改变主地址可以在这里设置
if (StringUtil.isNotEmpty(mainUrl)) {
options.baseUrl = mainUrl!;
} else {
options.baseUrl = NetworkConfig.baseUrl;
}
//开发阶段可以把地址带参数打印出来方便校验结果
debugPrint(
"request:${options.method}\t url-->${options.baseUrl}${options.path}?${FormatUtil.formattedUrl(options.queryParameters)}");

if (options.queryParameters["hideLoading"] != true) {
EasyLoading.show();
}
// 一定要加上这句话 否则进入不了下一步
return handler.next(options);
}
}

///格式化url,将post和get请求以get链接输出
static String formattedUrl(params) {
var urlParamsStr = "";
if (params?.isNotEmpty ?? false) {
var tempArr = [];
params.forEach((k, v) {
tempArr.add(k + '=' + v.toString());
});
urlParamsStr = tempArr.join('&');
}
return urlParamsStr;
}

响应拦截器

这一部分需要和实际相结合,根据每个后端返回的数据不同灵活配置

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
scss复制代码///拦截器 数据初步处理
class RequestInterceptor extends InterceptorsWrapper {
//请求后 成功走这里
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
EasyLoading.dismiss();
if (response.statusCode == 200) {
//访问正确有返回值的情况
if (response.data is Map) {
//将数据脱壳需要返回自己的数据
ResponseData responseData = ResponseData.fromJson(response.data);
if (responseData.success) {
response.data = responseData.data;
response.statusCode = responseData.respCode;
response.statusMessage = responseData.respDesc;
return handler.resolve(response);
}
return handler.resolve(response);
} else if (response.data is String) {
// {"respCode":403,"respDesc":"非法访问"}
ResponseError model = ResponseError.fromJson(jsonDecode(response.data));
response.statusCode = model.respCode;
if (model.respCode == 403 || model.respCode == 402) {
//做些什么
throwUnauthorizedError(response);
}else{
throwError(response);
}
} else {
throwError(response);
}
} else {
throwError(response);
}
}

@override
void onError(DioError err, ErrorInterceptorHandler handler) {
EasyLoading.dismiss();

throw DioError(
requestOptions: err.requestOptions,
type: err.type,
error: err,
response: err.response);
}

///抛出异常 留给baseModel去统一处理
void throwError(Response<dynamic> response) {
throw DioError(
requestOptions: response.requestOptions,
error: ResponseException(errCode: response.statusCode));
}
}
///鉴权错误
void throwUnauthorizedError(Response<dynamic> response) {
throw DioError(
requestOptions: response.requestOptions,
error: UnauthorizedError(errCode: response.statusCode));
}

上述中用到的类

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
dart复制代码abstract class BaseResponseData{
int? respCode;
String? respDesc;
dynamic attribute;
dynamic data;
bool get success;

BaseResponseData({this.respCode, this.respDesc, this.attribute, this.data});

@override
String toString() {
return 'BaseRespData{code: $respCode, message: $respDesc, data: $attribute}';
}
}

class ResponseData extends BaseResponseData {
@override
bool get success => respCode != null || data != null;

ResponseData.fromJson(Map<String, dynamic> json) {
if (json['respCode'] != null && json['respCode'] is String) {
json['respCode'] = int.parse(json['respCode']);
}
respCode = json['respCode'] ?? json['code'];
respDesc = json['respDesc'] ?? json['message'] ?? json['msg'];
attribute = json['attribute'] ?? json["data"];
if (attribute != null) {
if (attribute is Map && attribute.containsKey("data")) {
data = attribute['data'];
} else {
data = attribute;
}
} else {
data = json;
}
}
}


class ResponseError extends BaseResponseData {

ResponseError.fromJson(Map<String, dynamic> json) {
respDesc = json["respDesc"];
respCode = json["respCode"];
}

Map<String, dynamic> toJson() {
Map<String, dynamic> data = {};
data["respDesc"] = respDesc;
data["respCode"] = respCode;
return data;
}

@override
// TODO: implement success
bool get success => false;
}

class ResponseException implements Exception {
int? errCode;
String? errMsg;

ResponseException({this.errCode});

int? get errorCode => errCode;

//statusCode==200时候返回的data中存在的respCode
String? get errorMessage {
String msg = errMsg ?? "";
switch (errCode) {
default:
}
return msg;
}
@override
String toString() {
return 'RequestException{errorCode: $errorCode, errorMessage: $errorMessage}';
}
}

捕获错误并提示

DioErrorType 分六种 connectTimeout,sendTimeout,receiveTimeout,response,cancel,other,

其实加上刚才我们自定义type 总共可以分成四类 超时的 返回错误的 取消的 和其他

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
ini复制代码  ///格式化Dio返回的Error
///[e] catch到的error
static ErrorMessageModel dioErrorFormat(e) {
int? errorCode;
StateErrorType errorType = StateErrorType.defaultError;
String errMsg = "网络离家出走了~";
//判断一下抛出的异常是不是DIO包裹的异常
if (e is DioError) {
//是不是各种超时
if (e.type == DioErrorType.receiveTimeout ||
e.type == DioErrorType.sendTimeout ||
e.type == DioErrorType.receiveTimeout) {
errorType = StateErrorType.networkTimeoutError;
errMsg = "连接超时了";
} else if (e.type == DioErrorType.response) {
//访问出的各种错 访问中statusCode是400/500代码都会走到这里 如果想详细展示具体是什么错误可以继续细分
errorType = StateErrorType.responseException;
errMsg = _getNumberMeans(e);
} else if (e.type == DioErrorType.cancel) {
//如果是取消访问了走这里
errMsg = e.message;
} else {
//这里是刚才DIOerror包裹的自定义错误
// 这里由于没有定义error.type所以用error的类型判断
dynamic otherError = e.error;
dynamic otherE;
if (otherError is DioError) {
otherE = otherError.error;
}
if (otherE is ResponseException) {
errorType = StateErrorType.responseException;
errMsg = otherE.errorMessage ?? "";
errorCode = otherE.errorCode;
} else if (otherE is SocketException) {
errorType = StateErrorType.networkTimeoutError;
errMsg = "网络无连接,请检查网络设置";
} else {
errorType = StateErrorType.defaultError;
errMsg = "网络无连接,请检查网络设置";
}
}
} else {
errorType = StateErrorType.defaultError;
errMsg = "出问题了~~~";
}
return ErrorMessageModel(
errorType: errorType, message: errMsg, errorCode: errorCode);
}

将获取到的状态码转成中文提示

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
ini复制代码  ///获取到的数值转换成文字
static String _getNumberMeans(DioError e) {
String str;
if (e.response?.statusCode != null) {
switch (e.response?.statusCode) {
case 400:
str = "[${e.response?.statusCode}] 参数有误";
break;
case 402:
str = "[${e.response?.statusCode}] 啊 这是一个非法请求呢";
break;
case 403:
str = "[${e.response?.statusCode}] 服务器拒绝请求";
break;
case 404:
str = "[${e.response?.statusCode}] 访问地址不存在";
break;
case 405:
str = "[${e.response?.statusCode}] 请求方式错误";
break;
case 500:
str = "[${e.response?.statusCode}] 服务器内部出错了";
break;
case 502:
str = "[${e.response?.statusCode}] 无效的请求哦";
break;
case 503:
str = "[${e.response?.statusCode}] 服务器说他在忙";
break;
case 505:
str = "[${e.response?.statusCode}] 不支持的HTTP协议";
break;
default:
str = "[${e.response?.statusCode}] 未知错误";
break;
}
} else {
str = e.message;
}
return str;
}

上面用到的一个类

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
ini复制代码
class ErrorMessageModel {
StateErrorType? errorType;
String? message;
int? errorCode;

ErrorMessageModel({
this.errorType = StateErrorType.defaultError,
this.message = "出错啦,请稍后重试~",
this.errorCode,
});

ErrorMessageModel.fromJson(Map<String, dynamic> json) {
errorType = json['errorType'];
message = json['message'];
errorCode = json['errorCode'];
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {};
data['errorType'] = errorType;
data['message'] = message;
data['errorCode'] = errorCode;
return data;
}
}

配合provider 根据错误不同提示不同页面即可

三 使用

1
2
3
4
5
6
swift复制代码   Future<LoginModel> login(Map<String, dynamic> param) async {
Response<dynamic> response =
await DioClient().doPost(Api.login, queryParameters: param);
LoginModel model =LoginModel.fromJson(response.data);
return model;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Qml 中的那些坑(六)---对象被错误删除,看不见的垃圾回

发表于 2024-04-22

【写在前面】

在 Qml 中,很多时候我们需要动态创建一些 Qml 对象,通常是:createComponent + createObject 或 createQmlObject。

然而,最近工作中却出现了一个相当难以察觉的问题:动态创建的窗口在某些时刻会被莫名其妙的删除,我花了很多时间才定位到关键位置。

其根本原因在于:未给动态创建的对象分配 parent ( 即:没有任何对象持有其引用 ),结果就是,当 Qml 引擎运行垃圾回收时,这些对象会被错误清除掉。


【正文开始】

这里先直接上代码来看清我的意图,main.qml:

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
javascript复制代码import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15

Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Hello World")
color: "#a00000"

Row {
Button {
text: "创建"
onClicked: {
let component = Qt.createComponent("TestWindow.qml");
if (component.status === Component.Ready) {
component.createObject();
}
}
}

Button {
text: "回收"
onClicked: {
qmlApi.collectGarbage();
}
}
}
}

可以看到,创建按钮动态创建了一个窗口,而回收按钮则直接运行 Qml 的垃圾收集器。

这部分需要借助 C++ 来运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c++复制代码class QmlApi : public QObject
{
Q_OBJECT

public:
QmlApi(QQmlEngine *engine, QObject *parent = nullptr)
: QObject(parent)
, m_engine(engine)
{

}

Q_INVOKABLE void collectGarbage() {
if (m_engine)
m_engine->collectGarbage();
}

QQmlEngine *m_engine = nullptr;
};

TestWindow.qml 则只是一个简单的窗口,有一个活动的矩形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
javascript复制代码import QtQuick 2.15
import QtQuick.Window 2.15

Window {
width: 640
height: 480
visible: true
title: qsTr("Test Window")

Rectangle {
width: 100
height: 100
anchors.centerIn: parent
color: "green"

RotationAnimation on rotation {
loops: Animation.Infinite
from: 0
to: 360
}
}
}

运行代码可以看到,一旦我们运行垃圾收集器,刚刚创建出的窗口全部都会被清除:

很明显,这根本不是我们的本意,究其原因,是创建的窗口不可访问:

垃圾收集器将尝试通过定位和处理脚本环境中不再可访问的对象来回收内存。
通常情况下,您不需要调用此函数;当 QJSEngine 决定这样做是明智的时(即,当创建了一定数量的新对象时),垃圾收集器将自动被调用。但是,您可以调用此函数来明确请求应尽快执行垃圾收集。

因此,正确的解决方法是:持有创建后的对象。

例如可以使用变量存储,又或者给这些窗口一个 parent ( 隐式持有 )。


【结语】

实际上,由于 JS( Qml ) 的垃圾回收对开发者是不可见的,并且其运行时机也不确定,所以我们在动态创建对象时需要更加谨慎,来避免潜在可能的问题。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…353637…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%