iOS swiftui布局

可以在position之后设置overlay吗?那可以在offset之后设置overlay吗?

在 SwiftUI 中,position 和 offset 对布局的影响不同,因此在它们之后设置 overlay 会导致不同的行为。下面具体分析两种情况,以及是否可以在这些修饰符之后使用 overlay:

1. 在 position 之后设置 overlay

​ • 行为

​ • position 改变了视图的实际布局坐标系,指定的是视图的中心点

​ • overlay 的绘制依赖于视图的边界框(frame),而 position 不会改变视图的原始边界框的大小,只改变其位置。

​ • 结果

​ • overlay 将基于视图的原始边界框绘制,而不是移动后的显示位置。

​ • 如果你在 position 之后设置 overlay,你会发现 overlay 的位置不符合预期。

​ • 示例问题代码

1
2
3
4
5
6
7
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
.position(x: 150, y: 150) // 移动中心点
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
) // 边框不在预期位置

​ • 解决方法

​ • 不要在 position 之后设置 overlay。

​ • 把 overlay 放在 position 之前,或者使用 ZStack 来包装视图和装饰内容。

1
2
3
4
5
6
7
8
ZStack {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
.frame(width: 100, height: 50)
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
}
.position(x: 150, y: 150)

2. 在 offset 之后设置 overlay

​ • 行为

​ • offset 仅影响视图的视觉显示位置,不会改变视图的布局边界框(frame)。

​ • 因此,overlay 仍然会基于视图的原始边界框绘制,而不是移动后的显示位置。

​ • 结果

​ • overlay 不会随着 offset 的视觉位置变化,导致错位。

​ • 示例问题代码

1
2
3
4
5
6
7
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
.offset(x: 50, y: 100) // 视觉位置偏移
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
) // 边框位置基于原始frame,不正确

​ • 解决方法

​ • 把 overlay 放在 offset 之前。

1
2
3
4
5
6
7
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
)
.offset(x: 50, y: 100) // 最后移动

​ • 如果必须在 offset 后设置 overlay,需要手动调整 overlay 的偏移量:

1
2
3
4
5
6
7
8
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
.offset(x: 50, y: 100)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
.offset(x: 50, y: 100) // 手动同步偏移
)

总结

在 position 之后设置 overlay:

​ • 不推荐,position 改变了视图的坐标系,但不会改变边界框,导致 overlay 错位。

​ • 解决方法:使用 ZStack 或将 overlay 放在 position 之前。

在 offset 之后设置 overlay:

​ • 可以使用,但 overlay 基于原始边界框绘制,可能导致错位。

​ • 解决方法:将 overlay 放在 offset 之前,或者手动调整 overlay 的偏移量。

最佳实践

避免在 position 或 offset 之后设置 overlay,优先通过 ZStack 或调整修饰符顺序解决问题。

网络接口数据安全的【攻与防大全】

前言

网络最重要的是安全,防抓包。其次是数据加密。

Charles抓包原理:简单来说 Charles 是做了一个中间人的角色。

客户端和 Charles 建立 SSL 连接,然后 Charles 和服务端建立 SSL 连接。

客户端会验证 Charles 发来的证书(因为我们在手机上将 Charles 的根证书加入到信任区之后,Charles 签发的所有证书都会被客户端认为是可信的,则客户端就不会对 Charles 返回的数据进行怀疑,直接使用,这也是我们在 Charles 上修改 HTTPS 返回数据的理论基础。),而 Charles 会验证 SSL 服务端发来的证书。

iOS 禁用抓包实践

1、禁止代理抓包/路由直连:通过设置connectionProxyDictionary为空字典,可以禁止代理抓包工具如Charles和Fiddler等

2、检查是否有代理:缺点使用正常的VPN也会被禁止。

一、对数据加密的概述

1、安全的数据传输是什么样的

数据传输:都是使用密文(客户端提供加密的参数,服务端提供加密的结果)。

【客户端提供加密的参数】:需要在客户端加密后,上传到服务端,并在服务端对其进行解密,以确认传过来的参数。

【服务端提供加密的结果】:需要在服务端加密后,返回给客户端,并在客户端对其进行解密,以确认传过来的结果。

数据安全传输(AES)

若要了解加密方式可查看下文【附1:加密的方式有哪些

2、一端的AESKey怎么【安全的】传给另一端 / 为什么要对AES的密钥进行RSA公钥加密后传输??

答:为了安全,一端的AESKey必须先加密,然后才能传给另一端,这样才能防止用户从请求中抓取到AESKey。同时另一端需要对传过来的数据进行解密得到AESKey原值,后续两端才能都通过AESkey原值进行两边数据的对称加解密。即数据加密采用AES,而把AES的加密秘钥用RSA加密。(附:为什么AESKey不是使用md5方式加密?答:因为另一端要解密来使用原值。)。

问题:为什么要对AES的密钥进行RSA公钥加密后传输?

AES是对称加密算法,加密和解密的密钥都是同一个,为了防止被别人恶意获取到该密钥,然后对我们的业务请求参数进行解密,我们需要将AES密钥进行非对称加密后再进行传输。

2.1、密钥来源于客户端(类似Https)

数据安全传输(AES+RSA)

上图和Https原理有点相像。

2.1.1、客户端密钥保存的安全问题

如果密钥是来源于客户端,那如何在客户端安全的保存密钥?请查看下文中的【附2:如何在客户端安全的保存密钥AESKey

2.2、密钥来源于服务端(为了密钥能够像版本那样进行控制)

实际过程中,可能你的AESKey为了能够像版本那样进行控制,可能你的AESKey是由服务端分发的,即增加服务端的AESKey不能直接给客户端,而应该用客户端的RSA加密后再传递给客户端。则其过程可能如下:

数据安全传输(AES+RSA)

空 ==> getServicePublicKey ==> servicePublicKey

clientPublicKey(使用ServicePublicKey加密) ==> sendClientPublicKey ==> 使用servicePrivateKey解密得到clientPublicKey

空 ==> getServiceAESKey ==> aesKey(使用clientPublicKey加密)

参考文章:

最终网络数据传输,可能就只剩下类似下面一样,只有一个参数和

1
2
3
4
5
只剩一个参数:
encryptedParametersString: xxxx

只剩一个返回:
encryptedResultString: yyyy

二、被破解后,篡改参数值,进行请求攻击,怎么办?(二次加密:签名)

如果上面的加密过程被破解了,用户就能自己还原原始参数和原始返回值了。这时候如果他篡改参数值进行请求攻击怎么办?

1、请求参数值被篡改的场景

购物车多商品结算时候,本需共100美元的商品。在生成订单的时候,携带了所要支付的各个商品的id,以及总费用。结果总费用被篡改成以0.01美元的价格生成了订单Id。而后服务端却没有校验这些商品是不是0.01美元,从而导致平台损失。

以购物车结算为例,传统的传给服务器的参数一般是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
goods: [
{
id: xxx
title:
price:
},
{
id: yyy
title:
price:
}
],
totoalPrice: 100

当用户破解了我们之前的加密算法后,其就能还原原始参数结果,并篡改参数值,然后再利用破解所得的加密算法得到一个新的加密参数如下:

1
2
3
4
5
篡改前所传参数:
encryptedParametersString: xxxx1

篡改后所传参数:
encryptedParametersString: xxxx2

之后,如果服务器接收和处理的就是被修改过的值了。

那这种情况下怎么办法呢?答:再加一层加密算法,即签名。

2、请求参数值被篡改的解决/防篡改方案

为了防篡改,通常有以下三个步骤:

1、我们通过上述原始参数计算得出一个签名值。然后将签名值添加一起传给服务端。

1
2
3
4
5
6
7
8
9
篡改前所传参数:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

篡改后所传参数:
encryptedParametersString: xxxx2
signatureValue: xxxx1的签名值(用户不知道此签名值的计算方法)
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

由于用户不知道签名值的计算方法,所以即使其篡改了原原始参数中的部分参数值并生成加密参数,其也仍需要再破解出这个签名值的计算方法,如果其不修改签名值,而是把签名值直接上传到服务端,则服务端会进行以下操作。

2、然后服务端接收到所传的参数(含签名值)后,也按照和客户端一样的方式对除签名值外的其他参数再计算一遍签名值。

3、如果所得的签名值和客户端传过来的签名值不一样,则是参数被篡改了,服务端放弃该请求。(注:为了保证正常情况下计算出的客户端和服务端的签名一样,我们会对请求参数进行字典排序后再签名。

所以,防篡改的解决办法是客户端加签,服务端验签。

2.1、加签的方法

对上述的 encryptedParametersString: xxxx1 进行md5、sha256或其他加密算法。

2.1.1、签名算法的防猜测

如果签名的加密算法被猜到是用md5进行的签名。则用户可以也跟着对encryptedParametersString: xxxx1 进行md5加密即可得到签名会被服务端认证通过的请求了。那是不是md5不能用呢?如果用怎么用?答:**md5加盐,进一步增加破解的难度(盐的值不一定也要写在请求参数中,可前后端约定一致即可)**。

为什么加盐?
避免用户直接使用参数值进行md5加密,发现签名值就是由参数值md5加密而来。所以为了让MD5码更加安全 ,我们现在都采用加盐,盐要越长越乱。

1
2
3
4
5
加盐前:
digest = hash(input)

加盐后:用户拿参数值直接md5发现和签名值匹配不上,则一般就不会发现是用md5加密了。
digest = hash(salt + input)

这样加盐后,当用户再拿参数值直接进行md5后,发现和签名值匹配不上,则一般就不会想到算法是用md5加密了。

这个salt可以看作是一个额外的“认证码”,可以是固定的值,也可以是动态的值,如每个用户自己的userid。

更多MD5及其加盐可查看下文中的【[附3:签名 –> md5(加盐)](#附3:签名 –> md5(加盐))】

2.1.2、签名算法关注的内容

除了请求参数外,签名算法还关注请求的接口和方法,即其一般是对含参数在内、外加其他值(如URL、Method)一起进行签名。

参考文章:

三、不破解,但进行二次请求/API重放攻击(Replay Attacks)怎么办?

加密后,破解难度增加了,但是如果用户不破解,而是通过API重放攻击你的服务器呢,怎么办?

1、重放攻击(Replay Attacks)概述及危害介绍

API重放攻击(Replay Attacks)又称为重播攻击、回放攻击。它的原理就是把之前窃听到的数据原封不动的重新发送给接收方。

1.1、即使有幂等性情况下,重放请求的危害

追问:服务端不是应该有做幂等性了吗,为什么还需要处理防重放问题?

答:虽然如果一个系统操作是幂等的,即使攻击者重放了一个请求,系统也不会因为重复执行而产生不同的结果或状态。在一定程度上可以减少重放攻击的潜在损害(例如,一个幂等的HTTP GET请求,无论请求多少次,服务器都会返回相同的结果,而不会对服务器的状态产生影响)。但是即使请求本身是幂等的,也需要防止恶意用户发送大量重放请求来耗尽系统资源,从而实现拒绝服务攻击(DoS)。

若要了解幂等性知识,可查看

1.2、没有幂等性情况下,重放请求的危害

如果服务端没有幂等性,则假设有一个在线投票系统,用于选举或表决,每个用户只能投票一次。那么攻击者可能会尝试重放投票请求,进行重复投票,以增加他们支持的候选人或选项的票数。

2、重放攻击(Replay Attacks)的解决方案

防止重放攻击必须要保证请求仅一次有效。需要通过在请求体中携带当前请求的唯一标识,并且进行签名防止被篡改。所以,防止重放攻击需要建立在添加签名且防止签名被篡改的基础之上。

2.1、措施一:添加请求的时效性参数 Timestamp

即某个请求,其请求时间戳Timestamp,和服务端的当前时间在规定时间内(如1分钟内)则为合法请求,反之,则视为无效请求。

服务端时间戳的获取时机和服务端的RSA公钥一致,你可理解为网络库的初始化阶段(虽然这是真正初始化之后的请求的”二次初始化”)。

数据安全传输_防重放_1加timestamp

客户端与服务端的时间戳差值 diff = 服务器返回的时间戳 - 客户端请求结束的时间戳

校准服务端后的客户端的时间戳 = 当前客户端时间戳 + 客户端与服务端的时间戳差值 diff

2.2、措施二:添加一个唯一的随机数Nonce

2.2.1、Nonce的核心思路:

调用者每次调用时:

​ A:调用者生成并带上一个随机数Nonce(可以考虑用用户id+当前时间戳+接口url)

​ B:服务端该随机数是否已出现,有则拒绝,无则存储该随机数并放过请求。

则请求参数变为如下:

1
2
3
4
5
6
7
8
9
10
11
未加 timestamp 和 nonce 前:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

加了 timestamp 和 nonce 后:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值,📢:要把timestamp和nonce也包括进去,避免它们被篡改
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)
nonce: xxx1的nonce
timestamp: xxx1的timestamp

这里注意:计算xxxx1的签名值要把timestamp和nonce也包括进去,避免它们被篡改。

2.2、Nonce的优化思路:

这里服务端要保证Nonce唯一,就得存储已经用过的Nonce,但长期保持会带来两个问题

​ (1)存储成本增加,日积月累,这里要存储的Nonce会越来越多,需要的存储空间就越大

​ (2)碰撞概率增加,正常服务被拒绝概率增大;这里随着生成Nonce值越来越多,碰撞的概率一定越来越大,若通过增加Nonce值的长度,有增加存储成本。

​ 那么,另一个可行的办法,就是调用者每次请求时带上当前请求时间点Timestamp,然后由服务端限制请求的时效性。

​ 如此,上面提到的Nonce值存储成本可能比较大的问题,在结合Timestamp后,可大大降低存储成本,如Timestamp=1min,则仅需存储1min内的请求Nonce值即可,大大减少存储的量级。

至此,客户端发送的参数为:

1
2
3
4
5
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值,📢:要把timestamp和nonce也包括进去,避免它们被篡改
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)
nonce: xxx1的nonce
timestamp: xxx1的timestamp

3、扩展:客户端自己如何防止接口重复请求?

1、界面添加 loading

2、进行节流(立即执行:按钮点击)和防抖(最后执行:搜索框输入)。

1. 使用 GCD 实现节流(Throttle)

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
// 节流
class Throttler {
private var lastExecutionTime: DispatchTime = .now() // 记录上次执行时间,如果两次调用间隔小于 1.0s,则不执行。
private let queue: DispatchQueue
private let interval: TimeInterval
init(interval: TimeInterval, queue: DispatchQueue = DispatchQueue.main) {
self.interval = interval
self.queue = queue
}

func execute(action: @escaping () -> Void) {
let now = DispatchTime.now()
let deadline = lastExecutionTime + interval

if now >= deadline {
lastExecutionTime = now
queue.async {
action()
}
}
}
}

// 使用示例:限制按钮点击间隔
let throttler = Throttler(interval: 1.0)
@objc func buttonTapped() {
throttler.execute {
print("按钮点击事件触发")
}
}

2. 使用 GCD 实现防抖(Debounce)

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
// 防抖
class Debouncer {
private var workItem: DispatchWorkItem? // 每次调用 execute 方法,都会取消之前的任务,重新延迟执行。
private let queue: DispatchQueue
private let delay: TimeInterval
init(delay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) {
self.delay = delay
self.queue = queue
}

func execute(action: @escaping () -> Void) {
workItem?.cancel()
workItem = DispatchWorkItem(block: action)
if let workItem = workItem {
queue.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
}

// 使用示例:搜索框输入防抖
let debouncer = Debouncer(delay: 0.5)
func textDidChange(text: String) {
debouncer.execute {
print("用户输入完成后请求接口:\(text)")
}
}

3、请求队列:类似SDWebImage加载相同地址的图片。

如果重复请求的话,客户端的优化:

1、缓存

其他可参考:功能问题:如何防止接口重复请求?


附1:加密的方式有哪些

对称加密 非对称加密
概念 加密密钥和解密密钥相同 加密密钥和解密密钥不相同
算法 DES、AES、RC2、RC4、RC5 等 RSA、ECC 等
缺点 速度快,安全性低。使用同一个密钥,如果一方密钥泄露,数据就不安全了 非对称加密的安全性更高,但加密速度较慢。

注意:非对称加密的安全性更高,但加密速度较慢。一般我们在项目中采用的是 ”结合对称和非对称加密,数据加密采用AES,而把AES的加密秘钥用RSA加密,这样兼顾速度及安全性“https://juejin.cn/post/7002058547395559438

1.1、对称加密

加密方式_对称

1、李雷想要给韩梅梅发送消息,他们约定使用对称加密的方式把消息进行加密

2、李雷用密钥把消息加密然后发送给韩梅梅

3、韩梅梅用同一个密钥解密,然后看到李雷发送给自己的消息

1.2、非对称加密

加密方式_非对称

1、李雷想要给韩梅梅发送消息,他们约定使用非对称加密的方式把消息进行加密

2、李雷首先要得到韩梅梅的公钥

2、李雷用韩梅梅的公钥把消息加密然后发送给韩梅梅

3、韩梅梅用自己的私钥解密,然后看到李雷发送给自己的消息

韩梅梅给李雷发送消息同理。

附2:如何在客户端安全的保存密钥AESKey

如果密钥是来源于客户端,那如何在客户端安全的保存密钥?

1、密钥在代码上(内部)

1.1、存放密码的常见的代码

1
2
3
4
#define kSecret "abcd1234"

// 或者
const char* kSecret = "abcd1234";

这是非常危险的,因为常量会被直接编译到可执行文件的data段,只要对生成的可执行文件使用stringsotool等命令就可以dump出原始字符串。

1.2、对密码加密

仍然存

1、类的代码层面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方法1:密码在可执行文件的data段,且是明文
#define kSecret "abcd1234"
void getSecret {
return kSecret
}

// 方法2:密码在可执行文件的data段,虽是明文,但取到的值要再经过一层你自己知道的方法解密
#define kSecret "\x7e\x77\x64\x3c\xa7\xd4\x6d\x46\x29\x8b\xe3\x23\x9f\x1a\x5c\xdb"; // 使用aes加密后的
#define kAESKey "abcdefgh12345678";
char* getSecret() {
return decryptSecretByAESKey(kSecret, kAESKey);
}
// 方法2的优化:内部代码使用额外包在宏(kAppSecret)上,由于宏没有明确的入口,使得静态分析相比直接调用函数形式的getSecret更加不容易被破解。
#define kAppSecret \
({ \
char* buf = decryptSecretByAESKey(kSecret, kAESKey); \
[NSString stringWithUTF8String:buf]; \
})

附:带返回值的宏的写法 见**宏定义的两个技巧:**

1
2
3
4
#define SOME_MACRO \ 
({ \
expression; \
})

最后一个表达式的值就是宏的返回值,使用时更像函数的返回值。

2、类本身(可包含类中的函数名)

因为objc代码的动态性,编译器会在binary中留下类名、函数名等信息,这些信息是可以被class-dump-z等工具提取的,友好的命名让程序猿更方便,但同时也方便了破解者。对安全相关的重要模块类,可以故意混淆类名,让人不容易轻易联想到该的真实目的。

PS、更多混淆知识可看我写的另一篇文章:iOS APP安全加固方案(一、代码混淆CodeObfuscation)

2、密钥保存在外部

2.1、密钥保存在Keychain

密钥保存在Keychain并不安全,iOS越狱后可以导出Keychain的内容。应该尽量避免存放重要信息(如:token、用户名、密码等)在Keychain中,即使要存放,也一定要加密后存放。参考:iOS安全攻防(九):使用Keychain-Dumper导出keychain数据

2.2、密钥保存在文件

保存在app bundle、plist等配置文件更不安全,但可以使用隐写术等方式迷惑hackers。

2.2.1、隐写术

隐写术的载体:相比视频、音乐等文件,通常选择使用图片这种占用空间小的文件,但因为Xcode打包时会对png图片做特殊处理,如果将密码携带在png中,可能会在使用的时候无法复原。所以若非确定的隐写方法,则建议用bmp图片。

本节点【如何防止客户端被破解/密钥的保护】部分内容摘自/参考:如何防止客户端被破解

LSB算法:LSB全称为 Least Significant Bit(最低有效位),是一种常被用做图片隐写的算法。为了避免我们对图片进行的处理可能会在压缩的过程中被破坏,建议在无压缩的bmp图片上实现。(png图片是无损压缩,jpg图片是有损压缩。)

steganography_lbs图片来源于:《网络加密.graffle》中的【隐写术】

bmp的隐写,相比lsb隐写,bmp简单的多,并且达到了无损隐写。

bmp了解:BMP位图隐写

ctf杂项中图片隐写拿到一张图片常见的解题方法

附3:签名 –> md5(加盐)

md5是哈希函数。SHA-256或SHA-3也是哈希函数,相比md5更安全。

Mac:在终端上实现将abcd1234进行MD5(Message Digest algorithm 5 ,信息摘要算法)加密。

1
2
3
echo -n 'abcd1234' | md5

得到结果:e19d5cd5af0378da05f63f891c7467af

使用MD5就是将一串字符串通过某特定的算法来将其变成另一种形式,这样子就在外观上起到了加密的效果,但是由于背后的算法(md5是一种常见的hash算法)是固定的,所以每一个字符串都有固定的MD5格式。(反过来,一个hash值是否只能是一个值才可能?答:不是)

因为每一个字符串就只有一种特定的MD5格式,所以md5的破解(从c=hash(m)逆向得到原始明文m)容易想到的方式有:

方法 优缺点 推荐程度
暴力破解法 时间成本太高 ⭐️
字典法 提前构建一个“明文->密文”对应关系的一个大型数据库,破解时通过密文直接反查明文。但存储一个这样的数据库,空间成本是惊人的 ⭐️⭐️⭐️
构建彩虹表 在字典法的基础上改进,以时间换空间。是现在破解哈希常用的办法。
彩虹表:彩虹表记录了几乎所有字符串的MD5对照表。
⭐️⭐️⭐️⭐️⭐️

md5在线解密网站

md5在线解密

因为已经有了类似彩虹表的破解方法,所以如果不**’加盐’**,则当用户知道是 e19d5cd5af0378da05f63f891c7467af 时候,就很容易知道原值是 ‘abcd1234’了。

所以Md5加密方式的进化之路小结:

密码存储的种方式
1 直接存储密码明文m 明文存储,无安全性可言。
2 存储密码明文的哈希值hash(m) 虽然是入侵者得到的是hash值,但由于彩虹表的存在,也很容易批量还原出密码明文来。
3 存储密码明文的加盐哈希 hash(m+salt)。
这里的salt可以是用户名,手机号等,但必须保证每个用户的salt都不一样才是安全的。
相对前两种安全

网路本身的安全

HTTPS是应用层的安全协议。TCP是传输层的协议,但是它不安全,因为它是明文传输的,所以SSL的诞生就是给TCP加了一层保险,使HTTPS和TCP之间使用加密传输。而TLS只是SSL的升级版,他们的作用是一样的。

以下内容摘自HTTPS、SSL/TLS、TCP之间错综复杂的情感纠葛

SSL(Secure Sockets Layer) 安全套接层:为网络通信提供安全及数据完整性的一种安全协议。

TLS(Transport Layer Security)安全传输层协议:用于在两个通信应用程序之间提供保密性和数据完整性。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake),是更新、更安全的SSL版本。

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。**==> 类似 RSA**

但是,这里有两个问题。

(1)如何保证公钥不被篡改?

解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。

(2)公钥加密计算量太大,如何减少耗用的时间?

解决方法:每一次对话(session),客户端和服务器端都生成一个”对话密钥”(session key),用它来加密信息。由于”对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密”对话密钥”本身,这样就减少了加密运算的消耗时间。 ==> 类似 AES

HTTPS(超文本传输安全协议)是建立在HTTP(超文本传输协议)之上,通过SSL/TLS(安全套接字层/传输层安全协议)进行加密的安全协议。在HTTPS的握手过程中,SSL/TLS握手会先于HTTP握手发生。以下是HTTPS连接建立的基本步骤:

  1. TCP握手:客户端首先通过TCP(传输控制协议)与服务器建立一个普通的连接。这个过程包括三次握手,确保双方都准备好进行数据传输。
  2. SSL/TLS握手:一旦TCP连接建立,SSL/TLS握手就开始了。这个过程包括:
  3. 应用层协议:一旦SSL/TLS握手完成,客户端和服务器就可以使用这个安全的连接来传输加密的数据。这时,HTTP请求和响应就会在SSL/TLS层之上进行传输。

因此,HTTPS的连接建立过程中,是先进行TCP握手,然后进行SSL/TLS握手,最后才开始HTTP通信。

其他参考文章

End

妙用注解

前言

一、 为什么要使用注解开发

在日常开发过程中,我们经常会面临以下挑战:大量重复的模板代码、臃肿的代码降低可读性,以及JSON序列化和反序列化问题。为应对这些问题,各种编程语言都有自己的解决方案。在Dart语言中,采用了注解这一高效手段来解决这些问题。

注解就像是一把神奇的钥匙,为我们解锁了许多高效开发的大门。以下是使用注解的一些原因:

  1. 自动生成代码:注解就像会魔法的小精灵,能够在编译时自动生成代码,减轻了我们手动编写重复代码的负担,让开发变得更加轻松。
  2. 简化代码:注解可以减少重复代码,提高代码质量。
  3. 提高可读性:注解可以将元数据与实际代码分离,使代码更易于理解。
  4. 扩展功能:通过注解和注解处理器,我们可以在编译时执行一些额外操作,例如代码检查、代码优化等

二、什么是注解?

注解(Annotation)是一种编程语言特性,它不属于特定的架构或框架,而是可以在多种编程语言和开发框架中使用的一种工具。注解提供了一种方式,允许开发者在代码中添加元数据,这些元数据可以由编译器、运行时环境或其他工具在不同阶段使用。

在Dart语言中,注解是一种特殊的语法,用于在编译时或运行时向代码添加额外的元数据。注解以@符号开头,后跟一个编译时常量表达式。这种元数据可以用于指导工具(如静态分析器、编译器和构建器)执行特定操作,例如代码生成、静态检查和优化。注解不会影响程序的执行过程,但可以在编译时或运行时被工具和库访问,以实现各种目的。

三、注解实践:使用注解为每个页面类添加页面描述信息,埋点中经常需要

1、iOS:使用宏定义实现注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义宏,用于创建页面信息注解
#define CreatePageInfoAnnotation(pageClass, pageKey, pageDes) \
@interface pageClass (PageInfo) \
@property (nonatomic, readonly) NSString *pageKey; \
@property (nonatomic, readonly) NSString *pageDescription; \
@end \
@implementation pageClass (PageInfo) \
- (NSString *)pageKey { \
return pageKey; \
} \
- (NSString *)pageDescription { \
return pageDes; \
} \
@end


// LoginViewController.m
@PageInfoAnnotation(LoginViewController, @"login_page", @"登录页面") // 调用宏,为类添加注解
@implementation LoginViewController
@end

2、Flutter:使用source_gen库生成文件实现注解

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
// page_info_generator.dart
import 'package:source_gen/source_gen.dart';
class TestGenerator extends GeneratorForAnnotation<CJPageInfoMetadata> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
String className = element.name;
String? pageKey = annotation.read("pageKey");
String? pageDesc = annotation.read("pageDesc");
return """
extension ${className}APTExtension on ${className} {
String? apt_pageKey() {
return ${pageKey};
}
String? apt_pageDesc() {
return ${pageDesc};
}
}
""";
}
}

// TestModel.dart
@CJPageInfoMetadata("我是页面描述", 'home_page')
class TestModel {
// 属性
int age;
int bookNum;

// 方法
void fun1() {}
void fun2(int a) {}
}

结果就会生成一堆dart文件。

四、进入页面前的登录判断

其他参考文章:自定义iOS注解

1、最差做法

1
2
3
4
5
6
7
8
9
10
11
12
// HomeViewController.m
- (void)goMineHomePage {
// 进入个人主页前,需要先判断是否登录
if (!UserManager.isLogin) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
return;
}

MineHomeViewController *viewController = [[MineHomeViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
}

2、统一到路由中优化:在路由中拦截+拦截器(可选)

所有跳转使用 RouterManger,在RouterManager中限制未登录允许的页面。

2.1、常规路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// HomeViewController.m
- (void)goMineHomePage {
[RouterManger push:RouterNames.mine_home_page argument:nil];
}

// RouterManger.m
- (void)push:(String *)pageName argument:(NSDictionary *)argument {
if (!UserManager.isLogin) { // 进入个人主页前,需要先判断是否登录
if ([RouterNames.mine_home_page].contains(pageName)) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[topVC pushViewController:viewController animated:YES];
return;
}
}

getVCHandle = [HandleManager.handleMap objectForKey:pageName];
UIViewController *viewController = getVCHandle(argument:argument);
[topVC pushViewController:viewController animated:YES];
}

2.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
// HomeViewController.m
- (void)goMineHomePage {
[RouterManger push:RouterNames.mine_home_page argument:nil];
}

// RouterManger.m 优化为拦截器
- (void)push:(String *)pageName argument:(NSDictionary *)argument {
for (var interceptor in interceptors) {
bool canPush = [interceptor canPushPageName:pageName arguments: arguments];
if (!canPush) return;
}

getVCHandle = [HandleManager.handleMap objectForKey:pageName];
UIViewController *viewController = getVCHandle(argument:argument);
[topVC pushViewController:viewController animated:YES];
}

// LoginInterceptor.m
- (BOOL)canPushPageName:(NSString *)pageName arguments:(NSDictionary *)arguments {
if (!UserManager.isLogin) { // 进入个人主页前,需要先判断是否登录
if ([RouterNames.mine_home_page].contains(pageName)) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[topVC pushViewController:viewController animated:YES];
return NO;
}
}
return YES;
}

3、在调用原始方法之前插入额外的执行逻辑(不推荐,不使用)

《在调用原始方法之前插入额外的执行逻辑》

三、常见的注解示例

以下内容摘自:Flutter 注解开发

  1. @override:这个注解表示一个方法覆盖了父类的方法。它可以帮助我们检查是否正确地实现了方法覆盖,如果没有正确实现,编译器会给出警告。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Animal {
    String makeSound() {
    return "makeSound";
    }
    }

    class Dog extends Animal {
    @override
    void makeSound() {
    print('makeSound');
    }
    }
  2. @JsonSerializable():这个注解就是我们项目中使用到的json_annotation库的@JsonSerializable()注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @JsonSerializable()
    class ConversationListEntity {
    String? nextSeq;
    late List<ConversationListResList> resList;

    ConversationListEntity();

    factory ConversationListEntity.fromJson(Map<String, dynamic> json) => $ConversationListEntityFromJson(json);

    Map<String, dynamic> toJson() => $ConversationListEntityToJson(this);

    @override
    String toString() {
    return jsonEncode(this);
    }
    }
  3. 我们项目中的@RootView(jumpId: "1005"):用来为订单详情页面生成路由代码。

    1
    2
    3
    4
    5
    6
    7
    @RootView(jumpId: "1005")
    class OrderDetail extends BaseWidget {
    OrderDetail({Key? key}) : super();

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

附1、iOS注解原理介绍

1、在oc中实现注解一样的东西肯定是通过宏来实现的。

2、在oc中实现注解主要有4个方向:代码中注解,类注解,属性注解,方法注解。

其中代码中注解的形式应该是最常见的。你说没见过?libextobjc中那一套@weakify和@strongify你们总用得很爽吧。拆开看看这就是做代码中注解的标准形式了。而类注解,考虑了很久暂时还是误解状态,因此也没法讲了。因此实现属性注解和方法注解是关键。

3、既然是宏实现的,因此注解宏展开后应该是实际能够在对应的段落实际有效的语法才对。另外为了和面向对象的oc类型进行关联,因此在oc中可以随便乱写的c代码当然很难办了。因此我们可以做的宏很快就限定下来了,在属性中宏展开新的属性,在方法中宏展开新的方法。

在oc中实现注解一样的东西

实现

首先实现一下方法注解。由于我们知道我们需要展开方法,因此我们很快就能写出这样的宏:

1
2
3
4
5
6
7
8
// __COUNTER__ 这个宏每次使用都会自动+1
// ## 是宏的直接串接
#define path(x) \
- (id)__klmurl_path_##__COUNTER__() { \
return x ;\
}

所以结果是 __klmurl_path_1

@是哪儿来的,毕竟宏里面本来是不允许有这样的符号的。原来强项在展开的内容前加了无用的带@的表达式:参考 @weakify 其也只是宏定义weakify

1
2
3
4
5
6
#define weakify(...) \
rac_keywordify \
metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

#define rac_keywordify autoreleasepool {}
#define rac_keywordify try {} @catch (...) {}

这样才能很自然的串一个@。

优化考虑有什么带@的而且没有什么卵用的标示呢。@compatibility_alias

这一段展开后的代码是:

1
2
3
4
5
6
7
8
9
// __COUNTER__ 这个宏每次使用都会自动+1
// ## 是宏的直接串接
#define path(x) \
compatibility_alias _KLMURL_0 KLMURL;
- (id)__klmurl_path_##__COUNTER__() { \
return x ;\
}

所以结果是 __klmurl_path_1

附1、Flutter 注解

参考文章:Flutter 注解处理及代码生成

1、注解的调用和执行结果

对 TestModel 类进行注解

1
2
3
4
5
6
7
8
9
10
@CJPageInfoMetadata("我是页面描述", 'home_page')
class TestModel {
// 属性
int age;
int bookNum;

// 方法
void fun1() {}
void fun2(int a) {}
}

以要生成如下 TestModel.g.dart 文件为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// CJPageInfoMetadata
// **************************************************************************

extension TestModelAPTExtension on TestModel {
String? apt_pageKey() {
return "home_page";
}
String? apt_pageDesc() {
return "我是页面描述";
}
}

2、注解内部的实现

2.1、注解及生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 注解
class CJPageInfoMetadata {
final String pageDesc;
final String pageKey;

const CJPageInfoMetadata(this.pageDesc, this.pageKey);
}

// 生成器
import 'package:source_gen/source_gen.dart';
class TestGenerator extends GeneratorForAnnotation<CJPageInfoMetadata> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
String className = element.name;
String? pageKey = annotation.read("pageKey");
String? pageDesc = annotation.read("pageDesc");
return """
extension ${className}APTExtension on ${className} {
String? apt_pageKey() {
return ${pageKey};
}
String? apt_pageDesc() {
return ${pageDesc};
}
}
""";
}
}

2.2、使用注解生成代码的运行结果

命令执行成功后将会生成一个新的文件:TestModel.g.dart 其内容:

最后生成的文件为

2.2、生成器中 Element element, ConstantReader annotation, BuildStep buildStep 各值

2.2.1、Element element:要注解的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
element.toString: class TestModel
element.name: TestModel // 要注解的对象的【名】:TestModel
element.metadata: [@CJPageInfoMetadata("我是页面描述", 'home_page')]
element.kind: CLASS // 元素类型有 CLASSFIELDFUNCTION 等,
element.displayName: TestModel
element.fields: [int age, int bookNum] // 要注解的对象的【属性】:[int age, int bookNum]
element.methods: [fun1() → void, fun2(int a) → void] // 要注解的对象的【方法名】:[fun1() → void, ...]
element.documentationComment: null
element.enclosingElement: flutter_annotation|lib/demo_class.dart
element.hasAlwaysThrows: false
element.hasDeprecated: false
element.hasFactory: false
element.hasIsTest: false
element.hasLiteral: false
element.hasOverride: false
element.hasProtected: false
element.hasRequired: false
element.isPrivate: false
element.isPublic: true
element.isSynthetic: false
element.nameLength: 9
element.runtimeType: ClassElementImpl
...
2.2.2、ConstantReader annotation:对注解对象添加的注解信息
1
2
3
4
annotation.runtimeType: _DartObjectConstant
annotation.read("pageDesc"): '我是页面描述'
annotation.read("pageKey"): 'home_page'
annotation.objectValue: CJPageInfoMetadata (pageDesc = String ('我是页面描述'); pageKey = String ('home_page'))
2.2.3、BuildStep buildStep :提供的是该次构建的输入输出信息:
1
2
3
4
5
6
7
8
9
buildStep.runtimeType: BuildStepImpl
buildStep.inputId.path: lib/demo_class.dart
buildStep.inputId.extension: .dart
buildStep.inputId.package: flutter_annotation
buildStep.inputId.uri: package:flutter_annotation/demo_class.dart
buildStep.inputId.pathSegments: [lib, demo_class.dart]
buildStep.expectedOutputs.path: lib/demo_class.g.dart
buildStep.expectedOutputs.extension: .dart
buildStep.expectedOutputs.package: flutter_annotation

3、使用注解生成代码前的准备

注解的生成器 Generator 的执行需要 Builder 来触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1、使用Generator 创建一个Builder
Builder testBuilder(BuilderOptions options) => LibraryBuilder(TestGenerator());

// 2、创建 build.yaml 文件,将我们要执行的 builder 配置进去
builders:
testBuilder:
import: "package:flutter_annotation/test.dart"
builder_factories: ["testBuilder"]
build_extensions: {".dart": [".g.part"]}
auto_apply: root_package
build_to: source

// 3、运行 Builder,命令执行成功后将会生成一个新的文件:TestModel.g.dart 其内容:
flutter packages pub run build_runner build

Flutter注解的Builder的原理很像使用shell或者python脚本遍历文件的方式。

已知@Description是注解,其作用为提取 @Description 的方法描述到一个文件。

请说说@Description注解的内部实现(使用shell或者python脚本遍历文件的方式,不在本次讨论中)

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
// my_class.dart
class MyClass {
@Description('This method does something important.') // 注解
void myImportantMethod() {
print('Doing something important.');
}
}

// description_generator.dart
import 'package:source_gen/source_gen.dart';
import 'package:code_builder/code_builder.dart';
import 'package:annotations/annotations.dart'; // 引入上面创建的注解库

class DescriptionGenerator extends GeneratorForAnnotation<Description> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { {
// 检查注解是否应用在方法上
if (element.isMethod) {
final description = annotation.read('value').stringValue;
final method = element.asMethod();

// 创建一个方法调用的描述文本
final code = Block.of([
Directive.code("print('Method ${method.name} description: $description');")
]);

// 返回生成的代码
return code.toString();
}

// 如果注解没有应用在方法上,返回空字符串
return '';
}
}

其他参考文章

https://pub.dev/packages/mustache_template

mustache_template 是一个用于Flutter的Dart模板库,它支持dart2jsdart2native

End

灰度系统

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

一、灰度接口

1、灰度开关接口

1、是否进行功能的灰度使用的开关

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

接口名定义:

1
function getGrayscaleSwitchConfig(userid, deviceId)

接口结果的json示例如下:

1
2
3
4
5
6
7
8
{
"logFuture": {
"needOpen": 1,
},
"cacheOptimize": {
"needOpen": 1
}
}

End

请求规范_JS

一、JS请求结果结构规范(JSResponseModel )

json举例

1
2
3
4
5
{
"resultCode": 0,
"message": "成功",
"result": map 或 array
}

点击跳转到附文查看:JSResponseModel 代码示例

二、JS调用规范

1、规范标准

1、js需要回传值时候,回传值方法统一由h5的callbackMethod告知app(而非app写死)

2、js需要回传值时候,一定要有回传值,且回传值结果必须如《本文》中的【一、JS请求结果结构】所示

2、JS调用示例

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
/// 调起微信小程序去支付
static JavascriptChannel pay(
BuildContext context, {
required WebViewController? Function() webViewControllerGetBlock,
}) {
return BJJavascriptChannel(
name: 'h5CallBridgeAction_pay',
onMessageReceived: (JavaScriptMessage message) async {
Map<String, dynamic>? map = json.decode(message.message.toString());
if (map == null) {
return;
}

Map<String, dynamic> jsCallbackMap = JSResponseModel.initMap();
if (map['payType'] == null || map['payType'].isEmpty) {
jsCallbackMap = JSResponseModel.errorMap(message: '缺少 payType 参数');
} else {
MiniProgramPayBean payParams = MiniProgramPayBean.fromJson(map);
bool isSuccess = false;
if (map['payType'] == 'wx_mp') {
isSuccess = await WXMPPay.jumpWechatMP2Pay(payParams);
} else {
isSuccess = await WXMPPay.jumpWechatMP2Pay(payParams);
}
jsCallbackMap = JSResponseModel.successMap(isSuccess: isSuccess);
}

String? jsMethodName = map["callbackMethod"];
if (jsMethodName == null) {
WebViewController? webViewController = webViewControllerGetBlock();
WebViewWays.runJavaScriptMap(
jsMethodName!,
params: jsCallbackMap,
controller: webViewController,
);
}
},
);
}

三、测试JS的JS接口

描述 JS方法 其他
测试 h5 调用 app 方法,并将返回值回调给 h5 h5CallBridgeAction_test_h5CallAppAndCallBackToH5
测试浏览器中的链接显示,且链接可打开app的_原生页面 h5CallBridgeAction_test_openBrowser
原生调用h5提供的显示回传值的方法 bridgeCallH5Action_showCallbackJsonString

四、公共的JS接口

1、app设备相关

描述 JS方法 其他
获取app的公共信息,并将返回值回调给 h5 h5CallBridgeAction_getFixedAppInfo 改用user-agent
获取埋点monitor的公共信息,并将返回值回调给 h5 h5CallBridgeAction_getFixedMonitorInfo
更新状态栏颜色 h5CallBridgeAction_updateAppStatusBarStyle
是否是模拟器 h5CallBridgeAction_isSimulator

2、用户相关

描述 JS方法 其他
获取用户token(用于安全的告知h5用户信息),并将返回值回调给 h5 h5CallBridgeAction_getCurrentUserToken
退出登录 h5CallBridgeAction_logout

3、基础提示相关

描述 JS方法 其他
显示app的toast样式 h5CallBridgeAction_showAppToast

4、路由相关

描述 JS方法 其他
跳转到app指定页面_Url h5CallBridgeAction_jumpAppPageUrl
跳转到app指定页面_Name h5CallBridgeAction_jumpAppPageName

5、分享相关

描述 JS方法 其他
分享内容到指定的分享方式(微信聊天页面等) h5CallBridgeAction_share

6、网页相关

描述 JS方法 其他
刷新网页(到新地址) h5CallBridgeAction_reloadAppWebView
关闭webview h5CallBridgeAction_closeWebView

五、业务相关的JS接口

入参 JS方法 其他
发送IM的各种信息 h5CallBridgeAction_sendIMText

六、JS安全

附1:JSResponseModel 代码示例

class JSResponseModel 示例:

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
// h5CallBridge_responseModel.dart
class JSResponseModel {
final int resultCode;
final String? message;
final dynamic result;

JSResponseModel({
this.resultCode = 0,
this.message,
this.result,
});

// js默认的map(默认是成功)
static Map<String, dynamic> initMap({
String? message,
}) {
return JSResponseModel(
resultCode: 0,
).toMap();
}
// js失败时候的map
static Map<String, dynamic> errorMap({
String? message,
}) {
return JSResponseModel(resultCode: 1, message: message).toMap();
}
// js成功时候的map
static Map<String, dynamic> successMap({
required bool isSuccess,
dynamic result,
}) {
return JSResponseModel(
resultCode: isSuccess ? 1 : 0,
result: result,
).toMap();
}

// Model 转 map 以回传结果给h5
Map<String, dynamic> toMap() {
Map<String, dynamic> responseMap = {};

responseMap.addAll({"resultCode": resultCode});
if (message != null) {
responseMap.addAll({"message": message});
}
if (result != null) {
responseMap.addAll({"result": result});
}
return responseMap;
}

bool get isSuccess => resultCode == 0;
}

End

性能监控:BUGLY崩溃问题汇总

Bugly

一、iOS 崩溃

1、崩溃信息

【全部】崩溃信息截图如下:

image-20230526150407119

2、分析

image-20230526163957707
问题id 问题描述 发生次数
#1038 SIGSEGV 腾讯播放器问题 236
其他 其他问题 7

腾讯播放器占比超95%,已修复,待上线。

二、Android 崩溃

1、崩溃信息

image-20230526150921436

2、分析

image-20230526164207063

模拟器相关 43+/72 > 60%

即至少能再降低60%崩溃率。

三、iOS Flutter错误

1、错误信息

image-20230526151515956

1、分析

image-20230526165153853

与游戏webView相关的异常错误达 7000+/8000 > 87.5%。

原因:app调用游戏webView提供的js,h5那边返回了不支持的类型。

影响:只是异常错误,实际无业务影响。

即处理后能降低 87.5%,预计为 5.08% = 12.5% * 40.66%

四、Android Flutter 错误

1、错误信息

image-20230526165814931

2、分析

image-20230526170106312

已处理 524+ / 1200 ≈ 43.7%

五、小结

描述 新百分比 旧百分比 旧主要问题 旧非主要问题
时间 05-31 -> 10.31 2023-05-26 2023-05-26 2023-05-26
iOS崩溃 0.419%->5.09‱ 2.70 % 2.565%:95%腾讯播放器 0.135%
Android崩溃 0.478%->3.60‱ 1.51% 0.910%:60%+模拟器 0.6%
iOS错误 40.66% 34.58%:> 87.5% webView游戏 5.08%
Android错误 4.93% 10.59% 4.530%:类型或退出异常,已处理43.7% 6.06%

End

游戏通讯协议的选择TCP?UDP?HTTP?WebSocket?

二、协议选择

通过以上对协议特性分析,我想大家心里已经有低儿了。根据小编的工作经验以及对游戏理解,有以下建议:**
**

1、对于弱联网类游戏,必须消除类的,卡牌类的,可以直接HTTP协议,考虑安全的话直接HTTPS,或者对内容体做对称加密;

2、对于实时性,交互性要求较高,且team有过相关经验,可以优先选择websocket,其次TCP协议;

3、对于实时性要求极高,且可达性要求一般可以选择UDP协议;

4、局域网对战类,×××类,直接来UDP协议吧(公网对战,P2P的UDP还得“打洞”处理,后面分享会讲到)

其他无关

#8030 SIGTRAP(TRAP_BRKPT)

image-20230524132453348

主要原因:Webview渲染线程导致APP崩溃

发生次数:4

#8002 SIGTRAP #4002 SIGABRT #6040 SIGABRT #4028 SIGABRT #2038 SIGABRT

分析原因:用户使用的是Android模拟器,具体版本为Android模拟器Android 7.1.2,level 25,CPU架构为x86、x86_64,设备ROOT占比100%

发生次数:84+5+8

#5008 SIGABRT

分析原因:在分析崩溃日志时,发现每次发生此项崩溃是在用户从后台返回前台时

发生次数:11 (只在 redmi note 8 pro机型有上报此项错误)

#3032 SIGABRT

分析原因:SIG是信号名的通用前缀。ABRT是abort program的缩写。当操作系统发现不安全的情况时,它能够对这种情况进行更多的控制,必要的话,它能要求进程进行清理工作。在调试造成此信号的底层错误时,并没有什么妙招。 如 cocos2d 或 UIKit 等框架通常会在特定的前提条件没有满足或一些糟糕的情况出现时调用 C 函数 abort (由它来发送此信号)。

发生次数:7

#10 java.lang.NullPointerException

Attempt to invoke virtual method ‘io.flutter.plugin.common.PluginRegistry$RequestPermissionsResultListener com.lyokone.location.FlutterLocationService.getServiceRequestPermissionsResultListener()’ on a null object reference

分析原因:百度定位库在调用了空对象的方法,引发空指针问题。

发生次数:5

#3034 SIGSEGV(SEGV_MAPERR)

#03 pc 0000000000129d48 /vendor/lib64/egl/libRBGLESv2_adreno.so (rb_perform_resolve+616) [arm64-v8a::d49019399b6d1ce7075020c1e441c844]

分析原因:这是一个与SIGSEGV (Segmentation Fault)错误相关的错误消息。它指示在运行时出现了一个段错误,也称为内存访问错误。在您提供的错误消息中,指出错误发生在libRBGLESv2_adreno.so库的rb_perform_resolve函数中。libRBGLESv2_adreno.so是Qualcomm Adreno GPU驱动程序中的库文件。Adreno是Qualcomm开发的一系列图形处理器(GPU),广泛用于移动设备和嵌入式系统中。该库提供了OpenGL ES 2.0(Embedded Systems)的实现,允许应用程序在使用Adreno GPU的设备上进行图形渲染和加速。

在bugly日志上显示出现上述错误线程为Chrome_InProcGp线程,Chrome_InProcGp 线程是Webview其中一个处理渲染和绘图任务的线程。它负责管理和执行网页内容的呈现、布局和绘制操作,包括处理 HTML、CSS 和 JavaScript,解析页面内容并生成可视化的渲染结果。

发生次数:8 影响设备:2

#4102 SIGSEGV(SEGV_MAPERR)

分析原因:发生崩溃线程为Chrome_InProcGpuThreadChrome_InProcGpuThread 是 Chrome 浏览器中的一个线程,它是与 GPU 相关的线程之一。在 Chrome 的多进程架构中,GPU 线程用于处理图形渲染和加速,以提供流畅的图形显示和动画效果。

发生次数:5 影响设备:1 系统版本:Android 6.0.1,level 23

问题编号 发送次数 影响设备 备注
#12004 23 14 已修复
#9075 7 1 模拟器
#8088 6 1 模拟器
#2038 4 2 模拟器
#3034 3 1 模拟器
#2110 2 1
#5008 2 1
#8020 2 2
#10057 1 1
#10 1 1
其他 21 - 其他发送次数为1,影响设备为1的问题
汇总 72
模拟器 288->20 27.7%
已修复 23 31.9%
其他 29 40.2%

iOS bugly 崩溃

img

img

img

img

这个问题已于 4.20 提交工单,工单多次反馈已修复问题,多次升级sdk均未解决问题。

5.9重新提交工单,并投诉。自行在愿望屋app1.2.0版本hook仓储,对此方法添加容错处理,未生效。

img

工单记录

img

img

img

image-20230524132704989

上架-提审前自检

参考文档:

前言

1、常见问题类型

  • 隐私、权限的使用或声明
  • 登录
  • 版本检查
  • 黑名单
  • 无关h5网页
  • 内购

2、常见问题举例

背景/场景 被拒原因 解决
1.1 Android用户点击同意协议 同意用户协议前,提前获取了应用的设备信息、初始化了其他sdk 所有三方信息及sdk的初始化都放在同意协议之后
1.2 提供同城附近功能 提前使用位置 在调用的时候,才去申请位置权限
1.3 剪切板 未声明 已声明,误判,申述
1.4 获取安装列表 数美、同盾风控SDK调用 移除获取
2 使用微信、一键登录等三方登录 有三方,则一定要有苹果登录 审核期间隐藏三方登录入口
3 添加了蒲公英的版本检查 内部出现了,跳转到pgyer.com的功能 移除pgyer相关内容
4 app中的用户可创建内容,并进行朋友圈式传播 A mechanism for users to block abusive users 提供①拉黑用户;②内容举报功能,并告知其入口位置
5.1 app中需要嵌入一个h5 游戏,用来给app拉新。 提示游戏与app无太大关系。 审核期间隐藏各种入口,重提一个包
5.2 为推广,app推广图里,加入了游戏视图,而游戏本身被拒 app里隐藏了游戏, 告知其确实误操作上传了一张截图,其中的内容还没有上线。
5.3 app隐藏了游戏,游戏的获利,却出现在了流水功能中。 需告知①怎么获取,②怎么使用 告知是脏数据
6.1 app提供会员功能,会员需要充值 充值不是使用in-app purchase 为该账号重置,告知其已调整为免费使用

原图为 开发流程-提审前自检
开发流程-提审前自检

3、审核失败的报告示例

一、隐私协议

修改建议:请确保应用内包含的所有SDK均已在应用内的隐私政策/在AppGallery Connect上提交的隐私政策内逐一罗列明示,

说明SDK收集使用的个人信息以及使用目的。请排查应用内包含的所有SDK,井在隐私政策内进行规范化的说明,以保证隐私检
測准确性。

1、微信SDK

Android接入指南 中的 《微信Open SDK个人信息处理规则》 提到如下内容:

1.1.4 前述关于收集、使用个人信息的内容适用于iOS及Android系统。

1.1.5 特别说明,在Android系统中我们会验证用户设备上安装微信APP的状态,用于确认该设备是否能够实现Open SDK的相关能力,如设备上未安装微信APP,用户将无法使用微信Open SDK能力所实现的相关功能。

所以,微信SDK也要补充获取应用的权限。

2、风控SDK

问题1:

经检测发现,您的应用中集成了”com.ishumei(数美;数美反作弊)”等SDK,但未在应用内的隐私政策/在AppGallery Connect
上提交的隐私政策内容中进行明示,不符合华为应用市场审核标准。

解决:补充隐私协议

问题2:

1.您的应用首次运行时,在展示使用权限对应的柏关产品或服务之前,提前向用户弹奇申请开后【已安装应用列表】权限,不符台华为应用市场南核标准。
测试步骤:首次打开APP,进行功能换作前,应用弹奇丰请开启【己安装应用列表】权限。
修改建议:应用如需丰请权限,应在用户使用对应业务功能时丰请,不得提前弹窗丰清权限,旦权限丰请行为需与实际使用目的一致。

解决:升级或配置来移除获取安装列表的功能

3、支付SDK

经检测发现,您的应用中集成了“com.alipay(支付宝;mPaaS;阿里乘车码;阿里芝麻信用实名认证;芝麻认证), com.sensorsdata.analytics(SensorsAnalytics;神策)”等SDK,但未在应用内的隐私政策/在AppGallery Connect上提交的隐私政策内容中进行明示,不符合华为应用市场审核标准。

解决:申述:

已跟技术排查应用内包含的所有SDK,核实愿望屋应用中并未集成com.alipay SDK。可能相关的两个地方:
1、支付宝支付,是用H5的方式调用支付宝客户端进行支付,并未获取用户任何权限和用户隐私。
2、阿里云金融级实人认证SDK,均已在应用内的隐私政策/在AppGallery Connect上提交的隐私政策内逐一罗列明示,并说明SDK收集使用的个人信息以及使用目的。
符合《审核指南》第7.2相关审核要求:
7.2 应用的隐私政策链接指向的隐私政策应当包含应用程序(包括委托的第三方或嵌入的第三方代码、插件)收集和使用个人信息的目的、方式和范围。
麻烦审核人员进行复合上线。 如有问题,随时沟通。谢谢~

4、剪切板权限

经检测发现,您的应用存在收集用户的个人信息或权限的行为(获取剪切板个人信息),但未在应用内的隐私政策/在AppGallery Connect上提交的隐私政策网址中进行说明。
修改建议:请在应用内的隐私政策/在AppGallery Connect上提交的隐私政策网址中,对应用所收集个人信息的目的、方式、范国进行说明。

解决:

5、AndroidID

容易出现的地方:网络库中的header添加deviceId

二、登录、审核账号

您的应用提供的测试帐号和密码无法正常登录应用,应用内实际只有微信登录方式(提示密码错误),不符合华为应用市场审
核标准,
修改建议:请提交应用时,在”应用审核信息”选项中提供有效的测试帐号和密码。
请参考《审核指南》 第1.21相关审核要求:https:/developer.huawei.com/consumerlcn/doc/50104

原因:风控限制(一台设备只能登录三个账号)、白名单未真的白,还存在某些限制

解决:白名单真的白。

三、版本检查

1、pgyer.com

2.1、Specifically, your app uses the itms-services URL scheme to connect to pgyer.com, which may allow for installations or updating of the app.

Guideline 2.5.2 - Performance - Software Requirements

During review, your app installed or launched executable code, which is not permitted on the App Store. Specifically, your app uses the itms-services URL scheme to connect to pgyer.com, which may allow for installations or updating of the app.

The next submission of this app may require a longer review time, and this app will not be eligible for an expedited review until this issue is resolved.

Next Steps

- Remove any reference to itms-services URL schemes from your app.
- Review the Software Requirements section of the App Store Review Guidelines.
- Ensure your app is compliant with all sections of the App Store Review Guidelines and the Terms & Conditions of the Apple Developer Program.
- Once your app is fully compliant, resubmit your app for review.

Submitting apps designed to mislead or harm customers or evade the review process may result in the termination of your Apple Developer Program account. Review the Terms & Conditions of the Apple Developer Program to learn more about our policies regarding termination.

2.2、Guideline 2.5.2 - Performance - Software Requirements

During review, your app installed or launched executable code, which is not permitted on the App Store. Specifically, your app uses the itms-services URL scheme to connect to pgyer.com, which may allow for installations or updating of the app.

The next submission of this app may require a longer review time.

Next Steps

- Remove any reference to itms-services URL schemes from your app.

- Review the Software Requirements section of the App Store Review Guidelines.

- Ensure your app is compliant with all sections of the App Store Review Guidelines and the Terms & Conditions of the Apple Developer Program.

- Once your app is fully compliant, resubmit your app for review.

Submitting apps designed to mislead or harm customers or evade the review process may result in the termination of your Apple Developer Program account. Review the Terms & Conditions of the Apple Developer Program to learn more about our policies regarding termination.

四、UGC黑名单

3、Guideline 1.2 - Safety - User Generated Content

Guideline 1.2 - Safety - User Generated Content

We found in our review that your app includes user-generated content but does not have all the required precautions. Apps with user-generated content must take specific steps to moderate content and prevent abusive behavior.

Next Steps

To resolve this issue, please revise your app to implement the following precautions:

- A mechanism for users to block abusive users

五、h5 game

Guideline 4.7 - Design - HTML5 Games, Bots, etc.

We noticed that your app offers HTML5-based games, but the games appear to be an incidental feature that do not deeply enhance or enrich the user’s experience.

Next Steps

To resolve this issue, please remove any HTML5-based games from your app that are not directly related to your app’s core functionality.

Please see attached screenshots for details.

背景:

app中需要嵌入一个h5 游戏,用来给app拉新。

被拒原因:

提示游戏与app无太大关系。

六、内购(会员)

3、in-app purchase

** Guideline 3.1.1 - Business - Payments - In-App Purchase**

We noticed that your app includes or accesses paid digital content, services, or functionality by means other than in-app purchase, which is not appropriate for the App Store. Specifically:

- The VIP can be purchased in the app using payment mechanisms other than in-app purchase.

回复:

尊敬的审核人员您好: 根据上一次反馈的审核意见,我们已经做出了整改 1、涉嫌App内购的会员付费业务,我们目前调整为上线免费赠送,不提供购买入口 2、涉嫌跳转其他App的问题我们也已经做出了修正,目前不需要其他App也可正常完整的使用愿望屋App 以上,请审核人员审核,致谢

七、绕过审核

如果一个页面被苹果拒回。那怎么通过审核。(参考金融类APP提交苹果审核,怎么避开5.2.1)

答:

1.第一种是有后台控制壳什么时候出现。移动端传一个版本号给后台,后台根据版本号判断App否在审核,在审核就显示壳,不在审核就显示原生界面。

2.直接由移动端来切换界面。此时App需要登录才能看到主界面,对给苹果审核人员的测试账号做一个判断,如果是测试账号,就显示壳,非测试账号显示原生界面。

其中:

第一种方式用户进来就可以看到主界面,更有利于吸引自然流量,但是如果苹果后期复查,发现了用户使用的App跟审核的不一样,会被下架。

第二种方式不利于吸引自然流量,但是被下架的风险很小,因为苹果的审核团队在美国,不可能自己去注册账号,复查也没关系,除非有人举报你的App。

其他

1、icon 和 launch 图

2、允许ipad的话 icon 和 Launch 也需要图,另外应用在ipad上默认支持多任务,不需要的时候

设置 UIRequiresFullScreen 为 YES 全屏。

3、截屏(截屏尺寸应为:1242 × 2688px、2688 × 1242px、1284 × 2778px 或 2778 × 1284px)

iPHone:推荐用 iPhone12 Pro Max 即可,去掉横屏的支持。

设备型号 竖屏截图 (px) 横屏截图 (px)
iPhone 11 Pro Max / XS Max 1242 × 2688 2688 × 1242
iPhone 12 Pro Max / 13 Pro Max 1284 × 2778 2778 × 1284

iPad:推荐用iPad Pro 12.9 英寸

End

请求规范

一、各种示例

1、request的HEADER公共入参(固定+可变的)示例

1
2
3
4
5
6
7
8
9
10
11
12
13
- HEADER(REQUEST):
{
"version":"1.16.30",
"buildNumber":"16301610",
"platform":"iOS",
"appType":"monkey",
"appFeatureType":"dev",
"packageChannel":"general",
"trace_id":"1683158692451101/2023-05-04 08:04:52/_sy_]onw^w^",
"Authorization":"bearer clientApp247deea3-da67-47bc-94f7-04b83ca0fb07",
"content-type":"application/json",
"content-length":"28"
}

参数详情介绍见下文《公共入参介绍》。

2、request的BODY公共入参(固定+可变的)示例

示例可详见 《埋点规范》–【埋点入参】–固定的公共入参 。附:其实改部分也可考虑放在HEADER中

3、response的返回结构示例

1
2
3
4
5
6
7
8
{
"status": 200,
"message": "请求成功,但是业务处理失败",
"errorCode": 1001,
"errorMessage": "参数name不能为空",
"data": null,
"timestamp": "2023-05-04T09:00:00Z"
}

二、设置request的HEADER公共入参(固定不变7个+可变N个)

网络库初始化所需的header参数

header形参 header形参名 描述 示例
必填 Map headerCommonFixParams header 中公共但【不变】的参数 如上
可选 block headerCommonChangeParamsGetBlock header 中公共但【会变】的参数 { ‘trace_id’: TraceUtil.traceId() }
可空 String headerAuthorization header 中公共但会变的参数
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
// 1、初始化网络库的时候定义
required Map<String, dynamic> headerCommonFixParams, // header 中公共但不变的参数
String? headerAuthorization, // header 中公共但【基本不会变】的参数--Authorization
Map<String, dynamic> Function()? headerCommonChangeParamsGetBlock, // header 中公共但会变的参数
// 示例:
headerCommonChangeParamsGetBlock: () {
return {
'trace_id': TraceUtil.traceId(),
};
},
// 1.2、初始化网络库的时候使用 headerCommonFixParams
// headers 的所有参数处理
Map<String, dynamic> headers = {};
// ① header 中公共但【固定不变】的参数
headers.addAll(headerCommonFixParams);
// ② header 中公共但【基本不会变】的参数--Authorization--策略:因为这只是一次性取值,后面的没法自动变化,需要调用update接口
if (headerAuthorization != null && headerAuthorization.isNotEmpty) {
headers.addAll({'Authorization': headerAuthorization});
}
// 将 header 参数设置到网络单例中
dio!.options = dio!.options.copyWith(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
contentType: contentType,
headers: headers,
);

// 2、具体发起网络请求里进行: headers 的所有参数处理
if (options.headers != null) {
if (optionsHeaderCommonChangeParamsGetBlock != null) {
Map<String, dynamic> customHeaders =
optionsHeaderCommonChangeParamsGetBlock();
options.headers!.addAll(customHeaders);
}
}

1、【固定的】公共请求参数 headerCommonFixParams(7个)

获取方法形如:

1
2
3
4
headerCommonFixParams: await CommonParamsHelper.commonHeaderParams(
appFeatureType: appFeatureType,
channel: channelName,
);

参数介绍如下:

序号 类型 字段 值示例 用途
1 平台系统 platform iOS/Android
2 app的唯一标识 appType 区分出同一公司的多个app
3 版本号(1.月.日) version 1.04.30
4 版本编译号(月日时分)
安卓隔年需加+12
buildNumber 04301610
5 功能类型 appFeatureType dev、inner、formal 区分出内测、正式功能
6 渠道 packageChannel appstore
7 唯一设备码 deviceId 未登录时的关闭检查更新

1.1、平台(iOS/Android)

1.2、app类型(同一公司多个app)/App的唯一标识

1.3、版本号(1.月.日)

1.4、版本编译号(月日时分)

1.5、功能类型(蒲公英dev、内测inner、正式formal)

1.6、渠道

平台 应用商店 渠道名
iOS App Store general
Android 华为应用市场 huawei
Android 小米应用商店 Xiaomi
Android Vivo应用商店 vivo
Android ColorOS应用商店 oppo
Android 应用宝 yingyongbao
Android 360手机助手 360
Android 百度手机助手 baidu
Android 酷安应用市场 kuan
Android 阿里应用市场 alibaba
Android 官网包 general

1.7、DeviceId 唯一设备码

Android 端主要取 Android ID

iOS 端先尝试获取 IDFA,如果获取不到,则取 IDFV

2、【变化的】公共请求参数 headerCommonChangeParamsGetBlock

获取方法形如:

1
2
3
4
5
6
headerCommonChangeParamsGetBlock: () {
return {
'trace_id': TraceUtil.traceId(),
'Authorization': headerAuthorization,
};
},

参数介绍如下:

序号 类型 字段 值示例 其他
1 用户token Authorization bearer clientApp247deea3-da67-47bc-94f7-04b83ca0fb07
2 跟踪id trace_id 1683158692451101/2023-05-04 08:04:52/sy]onw^w^

2.1、用户 Authorization

该值为登录时候,后台接口返回。且需要在用户状态变更的时候同步更新。

2.2、跟踪 trace_id

组成部分:时间戳/时间字符串/10位随机字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'dart:math';
import 'package:flutter/foundation.dart';

class TraceUtil {
static String traceId() {
String timeStamp = DateTime.now().microsecondsSinceEpoch.toString();
String timeString = DateTime.now().toString().substring(0, 19);
String randomString = TraceUtil.generateRandomString(10);
String traceId = "$timeStamp/$timeString/_$randomString";
return traceId;
}

// Declare a fucntion for reusable purpose
static String generateRandomString(int len) {
final _random = Random();
final result = String.fromCharCodes(
List.generate(len, (index) => _random.nextInt(33) + 89));
return result;
}
}

三、设置request的BODY公共入参(当不放header时候)

详见 《埋点规范》–【埋点入参】–固定的公共入参 其实可考虑放在HEADER中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1、初始化网络库的时候定义
required Map<String, dynamic> bodyCommonFixParams, // body 中公共但不变的参数
Map<String, dynamic> Function()? bodyCommonChangeParamsGetBlock, // body 中公共但会变的参数(一般可考虑放在header中)

// 2、具体发起网络请求里进行: body 的所有参数处理
// ① body 中公共但不变的参数
Map<String, dynamic> allParams = Map.from(_bodyCommonFixParams);
// ② body 中公共但会变的参数
if (_bodyCommonChangeParamsGetBlock != null) {
allParams.addAll(_bodyCommonChangeParamsGetBlock!());
}
// ③ body 中自定义的其他参数
if (customParams != null && customParams.isNotEmpty) {
allParams.addAll(customParams);
}

四、response返回结构介绍

为了准确判断业务是否成功,通常需要同时判断status和errorCode两个字段。

1
2
3
4
5
6
7
8
{
"status": 200,
"message": "请求成功,但是业务处理失败",
"errorCode": 1001,
"errorMessage": "参数name不能为空",
"data": null,
"timestamp": "2023-05-04T09:00:00Z"
}
  1. 状态码(status code):表示请求的处理结果,常见的状态码包括200(请求成功)、400(请求参数错误)、401(未授权)、403(禁止访问)、404(请求的资源不存在)等。
  2. 消息(message):对请求处理结果的简要描述,可以用于前端展示错误信息或者提示。
  3. 数据(data):请求处理成功后返回的数据,例如json格式的数据、图片、文件等。
  4. 时间戳(timestamp):表示服务器处理请求的时间,可以用于前端调试和日志记录。
1
2
3
4
5
6
7
8
9
10
if (response.status >= 200 && response.status < 300) {
// 请求成功,继续处理业务逻辑
if (response.errorCode) {
// 业务逻辑处理失败,根据errorCode和errorMessage进行相应的处理
} else {
// 业务逻辑处理成功,根据data字段获取返回的数据
}
} else {
// 请求失败,根据错误码和错误信息进行相应的处理
}

附1:不建议把status和errorCode用同一个字段表示

将状态码(status code)和业务错误码(error code)合并成一个字段可能会带来一些问题,建议不要采用这种方法。

首先,将状态码和业务错误码合并在一个字段中会导致客户端处理逻辑复杂化。客户端需要在处理请求结果时先判断状态码,然后再解析业务错误码,这会增加客户端代码的复杂度,降低代码的可维护性。

其次,将状态码和业务错误码合并在一个字段中还会带来一些歧义。状态码是HTTP协议定义的标准字段,具有固定的含义,例如200表示请求成功,400表示请求参数错误,500表示服务器内部错误等。而业务错误码是根据具体业务定义的,具有一定的灵活性。将这两种含义合并在一起会导致状态码的含义变得模糊,不利于代码的可读性和维护性。

因此,建议在response结构中分别使用状态码和业务错误码两个字段来表示请求的处理结果。这样可以使客户端处理逻辑更加清晰,并且可以使状态码和业务错误码的含义保持清晰和统一。

五、埋点的入参

详见埋点规范

五、请求接口的整理规范

背景:为了能够将请求接口的用途和页面关联起来。

目的:①便于省去后端频繁询问该页面该位置调用的是什么接口(虽然已有日志,但是仍有部分后端人员懒到不愿去自己排查)。

②完成前端依赖服务关系的梳理,并一直到后台各级服务-数据层。

序号 参数 必填 描述与其目的 purpose 策略
1.1 caller_page 必填 接口调用者(页面、管理器等)
①多页面调用接口:告知后台,以对业务进行逻辑区分处理
②日志使用:方便查看与统计每个页面调用的接口
可全局记录获当前页面
eg: goods_detail_page
1.2 caller_view 可空 接口调用者(视图)
同上(有时候一个page下有多个tab)
必须请求单独设置
eg: sku_choose_window
2 caller_times 必填 调用场景
①页面打开:open_page
②页面输入:input
②页面点击:click
open_page|input|click
3 purpose 必填 接口用途中文
①日志使用:便于快速/直接筛选出问题根源接口
eg: 商品搜索
4 byApper 必填 前台具体开发者的id eg:byQian
5 byApier 必填 后端具体开发者的id eg:byApier2

方案:

将 purpose 添加到 body 的参数中

1
_params.addPurpose(caller_page: "商城页", caller_time:open_page|click, purpose: "商品搜索").byQian.byApier2;

请求接口的整理产出效果示例如下:

请求接口整理

该图来源于脑图: 请求接口的整理规范.xmind

附:多处页面调用的公共接口提取出,描述在页面中调用的公共接口的部分添加颜色或者其他标记来标识使用的是哪个部分的公共接口。

六、网络库的加强

1、网络日志加强

目的:查看日志的时候能清晰的知道接口的中文含义,以及调用其的页面或者其他类或实例。

方法:每个接口请求时候添加对应页面。

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
NetworkUtil.requestApi1(caller:aPage, belongLoadPage:true)

requestUrl(caller, belongLoadPage) {
if (belongLoadPage != true) {
return;
}

PageRequestManager.
map[caller]["apiStartCount"] = map[caller]["apiStartCount"] + 1
dio.request({
url: url,
completeCallback: () {
int callerApiStartCount = map[caller]["apiStartCount"];
int callerApiEndCount = map[caller]["apiEndCount"];
callerApiEndCount++;
map[caller]["apiEndCount"] = callerApiEndCount;
if (callerApiEndCount == callerApiStartCount) {
_pageLoadingEnd();
}
},
);
}


End