风控相关

前言

1、相关文档

问1:同盾和数美的风控为什么返回的是各个不同的加密blackBox,而不是其解密后都对应到的统一deviceId?

答:已知:同一设备同一用户账号,7月1号使用时候,app取到风控sdk返回的设备指纹1;7月2号使用时候,app取到风控sdk返回的设备指纹2;

作用1:由于后台请求都是使用设备指纹,如果设备指纹1和设备指纹2是一样的,则会导致某个指纹过期了,却一直在用。其实可以简单理解为该设备指纹使用blackBox,等价于app中的accesstoken

一、属性列表

deviceLabels

序号 描述 属性名
1 PC模拟器 fake_device.b_pc_emulator
2 adb开启 device_suspicious_labels.b_adb_enable

二、属性介绍

1、PC模拟器:b_pc_emulator

image-20230524134912885

2、adb开启 b_adb_enable

image-20230524134833287

device_suspicious_labels

image-20230524134739399

相关文档

以下来自三方参考

image-20230526170924306

image-20230526171036378

image-20230526171756442

image-20230526172239451

image-20230526174526595

image-20230526174620225

分享业务规范

一、分享的链接/口令值获取时机:不适合提前获取再拼接而成,而是需要时候再请求接口

  • 推荐:需要请求接口。(目的是为了省得分享时候再请求)

  • 不推荐:提前给值,然后自己去拼接。(即启动后的某个时刻,请求全局分享配置信息,后续分享的时候使用该信息拼接。)

    好处:分享时候,完全不需要前端逻辑,而是完全请求接口,且不用去担心接口内部读库的频次操作。

    不推荐的原因/方案缺点:

    ①不利于后期维护。如要在大部分(但不是全部)分享链接中添加之前遗漏的分享者uid。

    ②特殊场景,无法达到省请求的目的。如当最终分享链接还要经过后台(如动态短链)时,原本希望不用再请求的最后分享值还是得再请求。

    不推荐的原因/方案缺点举例说明如下:

    可能形式①:固定的域名链接 + “固定属性参数(常为分享人uid)”+ 其他属性参数

    示例:http://www.company.com/app/shareGoods?uid=123&objectType=goodsDetail&objectId=goodsId

    可能形式②:动态短链(背景:以防被误封)

    示例:https://my5353.com/no3Er

二、分享请求接口的参数与返回值结构规范

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
29
// request
{
"shareScene": "shareGoodsInGoodsDetail", // 分享对象的类型及场景(能知道分享的具体是什么种类的对象的id、可能在多个地方都有分享商品)
"objectId": goodsId, // 分享对象的id
"senderId": userId, // 分享来源人的唯一标识符userId(📢不要💊使用token,因为token会过期)(有时候业务需要名字、头像等其他值,可通过id后台自己获取)
"requiredTyps": ["WebPageUrl", "PasswordText"], // 需要给什么值,默认都给。( WebPageUrl:分享链接(包括短链)、PasswordText:口令)
"shareInfoInCellParams": { // 可选参数。要显示在微信聊天页面的UI信息,不提供时候,后台就得通过传入的objectType和objectId获取了
"title": 标题,
"description": 描述,
"thumbnailUrl": 缩略图,
}
}

// response
{
"shareInfoInCellParams": { // 要显示在微信聊天页面的UI信息
"title": 标题,
"description": 描述,
"thumbnailUrl": 缩略图,
},
"shareWebPageUrl": "https://wwww.baidu.com?a=123&b=456", // 点击消息要打开的网页链接,📢这里有时候是短链
"shareWebPageMinimumUI": { // 保底UI:用于避免shareWebPageUrl网页能访问但界面布局未实现时候而一片空白,导致无法进行点击打开app的闭环操作
"title": 标题(一般用显示在微信聊天页面的UI信息里的标题),
"bgImageUrl": 背景图(一般用显示在微信聊天页面的UI信息里的图片),
"buttonText": 按钮文案(一般为打开app)
},
"sharePosterQrCodeUrl": "https://wwww.baidu.com?a=123&b=456", // 海报二维码的链接
"sharePasswordText": "{appName}{shareWebPageUrl} {randomString} {ui_title}\n{ui_shareFlag}" //"【淘宝】https://m.tb.cn/h.Us94Snr?tk=VFCpd9FVHPy CZ0001 「花花公子官网正品手表男士全自动机械表十大品牌防水男款学生腕表」\n点击链接直接打开 或者 淘宝搜索直接打开" //分享的口令火星文等,有些口令甚至内部还包含分享链接
}

代码可点击查看: 分享流程代码示例

2、request 结构可选入参是否从前端直接提供的衡量依据

分享时候,建议从前端直接提供的参数,以分享商品为例

可选参数 描述 是否从前端取 原因
goodsTitle 商品名称 建议 直接从前端取,可省去接口通过id去内部获取各种数据
goodsImageUrl 缩略图 建议 直接从前端取,可省去接口通过id去内部获取各种数据
goodsPrice 商品加个 不建议 价格不稳定,从前端取容易过时,即重要且动态的都不应从前端取

三、复制分享的口令回APP

1、复制分享的口令回APP的流程图

share_watchword_flow1

图片来源:《分享相关(含微信、粘贴板).graffle》中的【粘贴板】

附1、口令介绍

实际口令举例:

1
2
3
{
"sharePasswordTextFormatter": "【淘宝】https://m.tb.cn/h.Us94Snr?tk=VFCpd9FVHPy CZ0001 「花花公子官网正品手表男士全自动机械表十大品牌防水男款学生腕表」\n点击链接直接打开 或者 淘宝搜索直接打开",
}

理论口令格式(文案为火星文等,且有些口令有内部包含分享链接的情况):

1
2
3
{ 
"sharePasswordTextFormatter": "{appName}{shareWebPageUrl} {randomString} {ui_title}\n{ui_shareFlag}",
}

实际口令格式(文案为火星文等,且有些口令有内部包含分享链接的情况):

1
2
3
{ 
"sharePasswordTextFormatter":"【淘宝】{shareWebPageUrl} {randomString} {ui_title}\n点击链接直接打开 或者 淘宝搜索直接打开",
}

2、查找哪些SDK使用了粘贴板

详见:查找哪些SDK使用了粘贴板

四、微信内网页跳转 APP 功能

微信官方文档:微信内网页跳转 APP 功能

五、未上架应用的微信每日分享100次限制及分享域名被屏蔽的解决方案

100次限制规定见官方文档:分享与收藏功能

官方对分享的其他问题,请看: 分享与收藏接口

其他解决方案的相关搜索文档:

一个不建议用的方案(未落地过):《分享相关(含微信、粘贴板).graffle》中的【未上架应用的微信每日分享100次限制及分享域名被屏蔽的解决方案】

对于iOS开发中未上架应用的微信分享限制问题,微信平台规定若移动应用未上架,则每天的分享量受到限制为100次,这包括分享到会话和朋友圈,主要用于满足调试需求 。针对分享域名被屏蔽的问题,可以采取以下几种解决方案:

  1. 域名备案:确保分享链接的域名已经完成备案,并且加入微信白名单,避免因备案问题导致域名被封 。
  2. 使用中转页跳转:设置中转页进行跳转,同时去除来源,保证即使中转页面被封,流量不会流失 。
  3. 分散域名访问:通过分散和切换域名来降低单一域名的曝光率,减少被封风险 。
  4. 域名监控系统:使用域名监控系统自动检测域名是否被封,避免使用有不良记录的域名 。

请注意,这些方法可以在一定程度上缓解域名被封的情况,但无法完全避免。开发者应持续关注微信的相关政策更新,以确保分享活动符合规定 。

附1、分享流程代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ShareUtil {
// 1、弹出分享弹窗
static showGoodsDetailShareView({
required String h5Title,
required String goodsID,
required String goodsName,
required String goodsThumbnailUrl,
WeChatScene? scene,
}) async {
Map<String, dynamic>? params = {
"h5Title": h5Title,
};
ShareWebUrlModel shareUrlModel = await _requestShareUrl(objectType: "GoodsDetail",
objectId: "124",
params: params,
requiredTyps: ["WebPageUrl"]); // "PasswordText"

}

附:分享网页的代码如下

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
class ShareInfoRequestAndSendUtil {
// 1、从服务端请求获取分享信息 shareUrlModel
static Future<ShareWebUrlModel> requestShareUrl({
required String objectType,
required String objectId,
Map<String, dynamic>? params
}) async {
params ??= {};
params.addAll({
"objectType": objectType,
"objectId": objectId,
});

return AppNetworkUtil.post(Url, params).then((ResponseModel responseModel) {
....
ShareWebUrlModel shareWebUrlModel = xxxx;
return shareWebUrlModel;
});
}

// 2、分享从服务端得到的分享信息 shareUrlModel
static shareWebPage({
required ShareWebUrlModel shareUrlModel,
WeChatScene? scene,
}) async {
ShareUtil.shareWebPageUrl(
shareUrlModel.shareWebPageUrl, {
title: shareUrlModel.shareInfoInCellParams.title
description: shareUrlModel.shareInfoInCellParams.description,
thumbnailUrl: shareUrlModel.shareInfoInCellParams.thumbnailUrl,
shareTo: scene?.toString() ?? "WeChatScene.SESSION",
});
}
}

class ShareUtil {
// 3、分享任意分享信息
static shareWebPageUrl({
required String webPage,
String shareTitle = "",
String? shareDescription,
String shareThumbnailUrl = '',
String shareTo = "WeChatScene.SESSION",
}) async {
var isInstalled = await isWeChatInstalled;
if (!isInstalled) {
ToastUtil.showMessage("未安装微信");
return;
}
var shortUrl = await _getShortUrl(webPage);

WeChatScene scene = WeChatScene.SESSION;
if (shareTo == "WeChatScene.TIMELINE") {
scene = WeChatScene.TIMELINE;
} else if (shareTo == "WeChatScene.FAVORITE") {
scene = WeChatScene.FAVORITE;
} else {
scene = WeChatScene.SESSION;
}

var model = WeChatShareWebPageModel(
shortUrl,
title: shareTitle,
description: shareDescription,
thumbnail: WeChatImage.network(shareThumbnailUrl),
scene: scene,
);
shareToWeChat(model);
}
}

附2(不用看):分享时候,完全不需要请求接口,而是完全由前端拼接出完整分享的返回结构值

1.1、本地入参1:启动/前后台切换得到的全局Gobal信息中的分享配置模型 objectShareConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Map<String, dynamic> objectShareConfig = {
// 要显示在微信聊天页面的不会随objectId值变的固定UI信息(有些场景objectId值变了,它们也不变,且可由后台配置)
"shareInfoUIFixedParams": {
"title": "不随objectId值变的标题(eg:快来XXX app ,和我一起玩)",
"description": "不随objectId值变的描述(eg:点击链接,就能我和一起玩啦)",
"thumbnailUrl": "不随objectId值变的缩略图地址(eg:游戏缩略图)",
},
// 点击分享消息要打开的h5地址的公共路径path部分(有其他参数拼接在此path后)(此值不一定是真实固定值,可为动态用于防封)
"shareWebPageUrlFixedPath": "点击分享消息要打开的h5地址的[固定]公共路径path部分(有其他参数拼接在此path后)",
// 点击分享消息要打开的h5地址的公共参数params部分(用于拼接在公共路径path后,来作为组成完整分享路径的一部分)
"shareWebPageUrlFixedParams": {
"objectIdKey/objectType": "分享对象使用的id/分享对象的类型,因为需要知道分享的是什么种类(分享商品详情:goodsDetail、分享订单详情:orderDetail、推荐名片:recommendUser、邀请好友:inviteCode等),同时为了解耦,省去前端和后台要硬编码匹配这个值"
},
// 不随object变的保底UI:用于避免shareWebPageUrl网页能访问但界面布局未实现时候而一片空白,导致无法进行点击打开app的闭环操作
"shareWebPageFixedMinimumUI": {
"title": "一般等同于 shareInfoUI 中的 title",
"bgImageUrl": "一般等同于 shareInfoUI 中的 thumbnailUrl",
"buttonText": "打开app"
},

// 口令分享格式
"sharePasswordTextFormatter":"【淘宝】{shareWebPageUrl} {randomString} {ui_title}\n点击链接直接打开 或者 淘宝搜索直接打开"
}
};

1.2、本地入参2:为具体object选择分享时候,提供的入参模型 dynamicShareParams

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Map<String, dynamic> dynamicShareParams = {
// 分享到哪
"shareTo": "微信/朋友圈等",
// 要显示在微信聊天页面的会随objectId值变的动态UI信息(有些场景objectId值变标题就得变,eg:商品名)
"shareInfoUIDynamicParams": {
"title": "要自定义的标题",
"description": "要自定义的描述",
"thumbnailUrl": "要自定义的缩略图地址",
},
// 点击分享消息要打开的h5地址的非公共参数部分(用于拼接在公共部分path后,来作为完整分享路径的一部分)
"shareWebPageUrlDynamicParams": {
"objectId": "分享对象的id值(分享商品goodsId值、分享订单orderId值、推荐名片userId值、邀请好友inviteCode值)",
"senderId": "分享人的用户id(以用来确定分享是谁分享的,邀请码的时候一定会用,其他需求也有要显示分享人的可能)"
},
// 会随object变的保底UI:用于避免shareWebPageUrl网页能访问但界面布局未实现时候而一片空白,导致无法进行点击打开app的闭环操作
"shareWebPageDynamicMinimumUI": {
"title": "一般等同于 shareInfoUI 中的 title",
"bgImageUrl": "一般等同于 shareInfoUI 中的 thumbnailUrl",
"buttonText": "打开app"
}
};

1.3、计算得到最终分享信息的结构模型(=本地入参1+本地入参2)

1.3.1、计算过程如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"shareTo": dynamicShareParams["shareTo"], // 分享到哪

Map<String, dynamic> lastShareParams = {
// 要显示在微信聊天页面的UI信息(有些场景objectId值变标题就得变,eg:商品名;而有些场景objectId值变标题也不用变,eg:这个游戏很好玩,推荐给你)
"shareInfoUIParams": {
"title": ui_title,
"description": dynamicShareParams["shareInfoUIDynamicParams"]["description"] ?? objectShareConfig["shareInfoUIFixedParams"]["description"],
"thumbnailUrl": dynamicShareParams["shareInfoUIDynamicParams"]["thumbnailUrl"] ?? objectShareConfig["shareInfoUIFixedParams"]["thumbnailUrl"],
},
// 点击分享消息要打开的h5地址
"shareWebPageUrl": shareWebPageUrl,
// 保底UI:用于避免shareWebPageUrl网页能访问但界面布局未实现时候而一片空白,导致无法进行点击打开app的闭环操作
"shareWebPageMinimumUI": {
"title": dynamicShareParams["shareWebPageDynamicMinimumUI"]["title"] ?? objectShareConfig["shareWebPageFixedMinimumUI"]["title"],
"bgImageUrl": dynamicShareParams["shareWebPageDynamicMinimumUI"]["bgImageUrl"] ?? objectShareConfig["shareWebPageFixedMinimumUI"]["bgImageUrl"],
"buttonText": dynamicShareParams["shareWebPageDynamicMinimumUI"]["buttonText"] ?? objectShareConfig["shareWebPageFixedMinimumUI"]["buttonText"]
},
// 口令
"sharePasswordText": "【淘宝】${shareWebPageUrl} ${randomString} ${ui_title}\n点击链接直接打开 或者 淘宝搜索直接打开",
};
附,上述各种分享都必备的值,放于临时变量供取及加工,示例如下:
1
2
3
4
5
6
7
8
// 各种分享都必备的值
// 各种分享都必备的值-标题(口令中也需要)
String ui_title = dynamicShareParams["shareInfoUIDynamicParams"]["title"] ?? objectShareConfig["shareInfoUIFixedParams"]["title"];
// 各种分享都必备的值-要打开的分享链接(口令中也需要)
String shareWebPageUrl = objectShareConfig["shareWebPageUrlFixedPath"] +
dynamicShareParams["shareWebPageUrlDynamicParams"]["objectId"] +
dynamicShareParams["shareWebPageUrlDynamicParams"]["senderId"] +
objectShareConfig["shareWebPageUrlFixedParams"]["objectType"];
口令分享值的获取过程如下:
1
2
3
// 口令分享需要的值;
String randomString = "CZ0001";
String sharePasswordText ="【淘宝】{shareWebPageUrl} {randomString} {ui_title}\n点击链接直接打开 或者 淘宝搜索直接打开";

结束语

发布业务规范

一、流程

1、方案

参考:

问题:为了优化上传耗时,我们将上传所需的图片压缩提前到用户选择完图片之后。那么当点击上传的时候,需要做哪些处理?

答:1、需要先判断之前的压缩任务是否

publish_main_flowchart

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
class BaseAssetModel {
AssetEntity? assetEntity;
}


mixin AssetEntityCompressProtocol {
/// 🚗: 要压缩的文件 assetEntity 是一个公共的属性,其他功能也需要使用到。
/// 🚗: 所以此类为了能够使用 assetEntity ,将在使用时【通过方法的参数传入】,以避免和其他功能冲突重复定义该属性,
/// 🚗: 并在使用后额外将其赋值给 compressAssetEntity 变量,方便主控制器通过判断该值非空来得出有在处理本功能业务。
AssetEntity? compressAssetEntity;
// 1、选择完图片的时候就处理,避免进行上传的时候才去压缩导致时长增加
Future<void> checkAndBeginCompress(AssetEntity assetEntity) async {
compressAssetEntity = assetEntity;
.......
}

// 2、选择完显示时候调用,用于获取要显示在列表上的图片
ImageProvider? get compressedImageOrVideoThumbnailProvider {
......
}

// 3、点击发布的时候(进行上传文件、创建内容单)
// 获取最后要上传的图片文件(会自动等待前面的压缩结束)
Future<String?> lastUploadImagePath() async {
_log("image choose bean hashCode = $hashCode");
// 草稿里的图片已有压缩数据
if (compressInfoProcess == CompressInfoProcess.finishCompress) {
File resultFile = imageCompressResponseBean?.reslut;
return resultFile.path;
}

await _compressCompleter.future; // 优化压缩流程,上传时候未完成压缩会自动等待,并在完成压缩后,自动继续
File resultFile = imageCompressResponseBean?.reslut;
return resultFile.path;
}

结束语

Android打包_多渠道

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看是否已安装apktool
apktool --version

# 1.1未安装的话,即出现
`zsh: command not found: apktool`
# 则执行以下命令,进行安装
brew install apktool

# 1.2未正确安装,即出现
`/usr/local/bin/apktool: line 3: /Users/qian/@@HOMEBREW_JAVA@@/bin/java: No such file or directory`
`/usr/local/bin/apktool: line 3: exec: /Users/qian/@@HOMEBREW_JAVA@@/bin/java: cannot execute: No such file or directory`
# 则执行以下命令,进行安装
HOMEBREW_BOTTLE_DOMAIN= brew reinstall kafka

结束语

WebView优化

脑图与概览

点击查看 webView.xmind

参考文章:

概念

1、当App首次打开时,默认是并不初始化浏览器内核的;

只有当创建WebView实例的时候,才会创建WebView的基础框架。

所以与浏览器不同,App中打开WebView的第一步并不是建立连接,而是启动浏览器内核

先上结论

阶段 优化方案
(初次/重复)打开时,加载慢 webview提前(尽可能多的)初始化+全局化
加载中,加载慢 拦截加载自定义图片(webview_flutter暂不支持)

一、(初次/重复)打开时,加载慢:webview提前(尽可能多的)初始化+全局化

背景:有一款基于 cocos2d 的app游戏,其每次启动都很慢。(游戏端说是需要加载游戏引擎,而且引擎本身又比较消耗时间。)

请问1:如何提高初次打开的加载速度?

请问2:如何做到频繁退出进入游戏,不用都走一遍重新加载的流程呢?

序号 问题 解决
1 初次打开,加载慢 1、webview的在冷启动过后不久的提前初始化
2、初始化的时候多多进行可以初始化的部分,如尝试游戏引擎的加载。
2 重复打开,加载慢 webview设为全局变量,重复使用

优化前后对比小结:

方法 存在的问题 VS 改进的好处
未优化 每次进入游戏都开启页面,退出游戏都销毁页面。 使用上述方法,每次退出再进入该页面都会因为需要重新加载引擎,而导致游戏主页内容的显示很慢,从而严重影响app的使用体验。
优化后 1、游戏使用一个全局GameWebView(其他WebView不用全局)
2、游戏显示使用将webView添加到vc或者window上。
全局GameWebView不会被销毁,能够重复使用,提升加载速度。
①加载过的游戏,下次再进入不用重新重新加载引擎,有效的跳过loading的过程,极大的改善了用户体验。

附1:游戏引擎能否直接在原生上提前加载?

答:常见做法:构建出含Cocos的原生工程,然后将自己的功能添加上去。见 Cocos Creator 上原生平台二次开发指南

存在问题:不能自己在原生上添加游戏引擎。 Cocos Creator 引擎定制工作流程 也只是将其自己编译器创建的项目的引擎定制。

最终结论:游戏引擎没办法在原生上提前自己加载。

背景:如何将 Cocos Creator 构建出来的工程用正确的姿势接入Flutter 工程

附2:一个视图实例被添加后,再被添加到其他地方后的效果

此需求背景:《页面跳转相关(路由)》中【四、游戏与app跳转交互优化调研】

测试过程见附录 附1:测试一个视图实例被添加后,再被添加到其他地方后的效果

被使用的方式 使用的结果
1 一个视图实例添加到一个vc上的两个container上 视图只显示在后面设置的位置
2 一个视图实例添加到不同viewController 视图只显示在后面设置的位置。则代表回来的时候需要重新更新视图位置。

二、加载中,加载慢

方案1: 进行 Scheme 拦截,利用 WKURLSchemeHandler

解决:加载自定义图片,将webView的图片数据改为app的图片数据(Flutter现有的webview_flutter暂时不支持,如要需要自己修改)

参考文档:

h5中

1
<h3 id="photo"> 自定义图片:canineschool-avatar://photo  </h3>

app中

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
let configuration = WKWebViewConfiguration()
configuration.setURLSchemeHandler(MyCustomSchemeHandler(), forURLScheme: "myScheme")
let webView = WKWebView(frame: .zero, configuration: configuration)
// 当WKWebView在加载一个使用"myScheme://"的资源时,就会调用我们的 MyCustomSchemeHandler 来获取数据。


class MyCustomSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
var data: Data?
var mimeType = "application/octet-stream" // 默认MIME类型

let url = urlSchemeTask.request.url!
// 判断URL的文件扩展名,如果是图片才进行特殊处理,即将webView的图片数据改为app的图片数据
if (["png", "jpg", "jpeg"].contains(url.pathExtension.lowercased()) {
data = SDWebImageManager.shared.imageData(forKey: url.absoluteString, options: [])
mimeType = url.pathExtension.lowercased() == "png" ? "image/png" : "image/jpeg"
} else if url.path.hasSuffix(".css") {
let localPath = getOfflinePath(for: url)//映射到/var/.../offline_packs/css/common.css
let data = try? Data(contentsOf: URL(fileURLWithPath: localPath))

} else { // 对于非图片类型,使用默认响应
}

// 创建URL响应,向WKWebView提供响应和数据
let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data: validData)
urlSchemeTask.didFinish()
}

func webView(_ webView: WKWebView, stop urlSchemTask: WKURLSchemeTask) {

}
}

[WKURLSchemeHandler 拦截https时引起网络请求也被拦截了,如何重定向](#WKURLSchemeHandler 拦截https时引起网络请求也被拦截了,如何重定向)

方案2: 下载并解压zip,app加载本地静态文件

在WebView中加载本地资源:使用file协议在WebView中加载沙盒目录中的资源。

跨域问题:

方法1:在后端设置CORS(跨源资源共享)策略,允许来自特定来源的请求。

方法2:资源通过js来app中获取再回到web中显示。

方案3:在app中内置一个 WebServer

其他离线方案:

iOS离线静态资源包技术方案分析

Flutter下实现WebView拦截加载离线资源

思考:小程序

小程序并不是一个简单的、优化过的网页,而是一个混合架构,可以理解为:

1
小程序 = Native原生容器 + WebView渲染 + JS引擎 + 离线包机制

小程序的页面栈管理,像小程序一样支持多页面:

  • 每个页面独立的WebView
  • 前进/后退时切换WebView而不是重新加载
  • 保存每个页面的滚动位置和表单状态

小程序容器的核心精髓:

核心是要先下载离线包zip,然后用webview加载远程Url映射成用webview加载本地离线包中的html。html中的css、js、图片等资源也映射成本地?然后变成一个路由栈管理加载多个本地的html了?

三、打开后,白屏

1、白屏的原因及原理

  • 深入理解WKWebView白屏

    iOS设备上造成白屏的真正原因只有两个:内存使用过度、进程通信机制出现错误。

2、白屏的解决

webview_blank_flow

理想情况(不一定白屏都会调用到):

1
2
3
4
//进程终止(内存消耗过大导致白屏)
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
[webView reload];
}

其他处理方式:

白屏的像素判断法:iOS WKWebView白屏的像素检测方法iOS WKWebview 白屏的像素检测实现

webview截图 ==> 缩放图片,为了减少待会遍历的像素点 ==> 遍历像素点,判断白色像素占比超过95%则认定为白屏

1
2
3
4
5
totalCount++;
if (red == 255 && green == 255 && blue == 255) {
whiteCount++;
}
// 占比 proportion = (float)whiteCount/totalCount; 如果 占比 >0.95 则认为白屏

白屏的 webView的子视图WKCompositingView 不存在的判断法:白屏时 WKCompositingView 为空

附:WKCompositingView 也是我们在做 WebView同层渲染 的时候经常会遇到的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 判断是否白屏
- (BOOL)isBlankView:(UIView*)view { // YES:blank
Class wkCompositingView = NSClassFromString(@"WKCompositingView");
if ([view isKindOfClass:[wkCompositingView class]]) {
return NO;
}
for(UIView*subView inview.subviews) {
if (![self isBlankView:subView]) {
return NO;
}
}
return YES;
}

其他解决方案:

白屏判断点 参考文章
webView的 url titledocument.body.innerHTML 为空 WKWebView 白屏处理

四、使用中的交互

1、常规

WKWebView 注入js代码: 001-UIKit-CQDemo-Flutter 中的 flutter_webview_kit

2、全局webView下如何正确返回前一页

见 《webview的一生.graffle》中的【全局webView下如何正确返回前一页】版面

五、WebView拦截请求的问题

阿里云官方文档: WKWebView使用私有API进行注册拦截请求

1、WKURLSchemeHandler 拦截https时引起网络请求也被拦截了,如何重定向

当使用WKURLSchemeHandler拦截https的图片时候,其自然的也会把网络请求也拦截了,需要将那些网络请求重定向。

为了区分出不同的https,可以通过获取url地址的扩展名,如 jpg、png、txt、MP4等。

1
2
3
4
5
6
7
8
9
10
11
12
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
// 调用 _didPerformRedirection:newRequest: 执行重定向
// 拆成 _didPerf ormRe dire ctio n:n ewRe que st: 后倒序得到如下
NSArray *privateSelStrArr = @[@"st:", @"que", @"ewRe", @"n:n", @"ctio", @"dire", @"ormRe", @"_didPerf"];
NSString *selName = [[[privateSelStrArr reverseObjectEnumerator] allObjects] componentsJoinedByString:@""];
SEL sel = NSSelectorFromString(selName);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.schemeTask performSelector:sel withObject:response withObject:request];
#pragma clang diagnostic pop
completionHandler(request);
}

六、WebView同层渲染

原理:

同层组件的目标是将原生组件渲染在与其他 Web 组件同一层级中。

在 iOS 中,我们使用 WKWebView 来创建 Web 视图,通常 WKWebView 内核会将多个组件共同渲染到同一个 WKCompositingView 上,这个 View 是一个原生的 UIView 子类。但是如果某个 HTML 标签的 style 设置了 overflow: scroll 属性,并且内容超出容器的大小,WKWebView 就会为其单独的创建一个 WKChildScrollView,因此我们可以找到这些 View,并和对应的 Web 组件一一关联起来,就可以将原生的组件渲染到这个 View 中,从而实现同层渲染。

理解 iOS 端的 WebView 同层组件

1、同层渲染需求背景:

Webview 资源离线化之后不需要从后端拉取资源,省去了资源加载耗时减少等待时间,用户体验得到了明显提升,但受限于 Webview 的天生限制在某些场景下仍然能感受到与原生明显的体验落差。比如输入框弹出键盘时的页面卡顿、图片较多的页面内存占用过从后台回来前台时页面被重新加载、WebP 图片支持碎片化、图片不能跨页共享内存、视频播放组件体验不佳等问题。在 Webview 本身受限的情况下,如何在体验上向原生靠近是我们不得不面对的问题。如果能将影响用户体验的关键可交互组件用原生组件进行替换可以从根本上解决体验不如原生的问题。这部分内容我称为: WebView原生化

Webview 原生化是指把 Webview 内部分占位 DOM 元素用原生组件进行替换,原生组件与原始 Webview 混合便得到了用户看到的最终界面。

组件替换有两种方式,
蒙层方案:在 Webview 的直接上层添加原生视图蒙层,把原生组件添加到蒙层上占位 DOM 对应的位置。

缺点:当一个原生组件被添另到 Webview 上时它永远处顶层,不会被 Webview 内的弹窗覆盖,在滚动时原生组件会盖住原本应被显示的区域。

同层渲染方案:另一种是将原生组件添加到 Webiview 渲染时与占位 DOM 对应的合成层(独立出来的原生视图)上,下面会对比两种方案的差异。

以上引用内容摘自:微信小程序『同层渲染』技术是怎么回事? 强烈建议点开原文再看一遍。

2、同层渲染实现示例

关键问题:如何映射 DOM 到原生视图?如何在原生视图树中查找 「DOM」

「同层渲染」则是将原生组件直接渲染到 WebView 层级上。

2.1、入门简单实现

实现步骤:

1、创建一个可以生成WKChildScrollView原生组件的 DOM 节点

2、传递给客户端查找到该 DOM 节点对应的 WKChildScrollView 原生组件的必要信息

3、客户端根据传来的信息找到对应的原生组件,并将原生组件挂载到该 WKChildScrollView 节点上

2.2、微信小程序实现

Skyline 渲染引擎 /概览 /介绍

( 微信 Skyline 渲染引擎类似 Flutterr 的 SkyEngine)

微信小程序 原生组件说明

image-20240903234945043

image-20240903234704466

3、其他参考文档

【必读】前端需要懂的 APP 容器原理

容器 URL 统一化建设(一个 URL 地址可按需动态配置渲染成 H5、Native 等页面),小程序容器建设,Flutter 容器建设,容器的快照缓存、预渲染、混合渲染等优化与能力相关建设

其他

优化待处理的文章

APP中WebView性能优化

iOS WebView/H5 调试新姿势

iOS WebView/H5 调试新姿势

附1:测试一个视图实例被添加后,再被添加到其他地方后的效果

测试代码:详见 AppCommonJSCollect 中的 SharedNormalView 、 SharedWebView

1.1、测试一个视图实例添加到一个vc上的两个container上

ViewAddInOneViewController

1.2、测试一个视图实例添加到不同viewController

阶段1:进入新页面的时候,使用之前共享的视图

阶段1结果:虽然第二页复用了第一页中的视图,但是从第二页回来的时候,第一页的视图不见了。

ViewAddInDiffViewController1

阶段2:在阶段1的基础上,补充回来的时候(viewWillAppear),需要重新更新视图的布局

阶段2结果:视图显示正确了

ViewAddInDiffViewController2

End

H5与APP的交互

一、app内的h5调用app的网页

第1.1节:h5js(app内的h5与app互相调用JS方法及传值)

二、app外的h5直接打开app

第1.2节:h5 open app(app外的h5直接打开app)

三、通知(推送/站内信)打开app

1、站内信

接收到通知后,

  • 调用

外部与app交互脑图

相关脚本的本地测试方法

本地代码测试:

将文件放置到 /Library/WebServer/Documents/ 下,比如将 dvlp_h5_open_app_app_route_url_demo.html

放置:/Library/WebServer/Documents/h5_native_interacte/h5_open_app/dvlp_h5_open_app_app_route_url_demo.html

1、基本访问测试

访问:http://localhost/h5_native_interacte/h5_open_app/dvlp_h5_open_app_app_route_url_demo.html

2、参数携带测试

访问:http://localhost/h5_native_interacte/h5_open_app/dvlp_h5_open_app_app_route_url_demo.html?fileUrl=dvlp_h5_open_app_browser_url_demo.json

3、更多参数使用测试可参考 h5_open_apph5js

小程序容器技术

小程序容器技术

React Native 的 Text 组件转换为 iOS 的原生 UILabel ,是怎么实现这一过程的?

小程序容器技术对比分析

1
2
3
<view class="container">
<text class="title">Hello, {{name}}!</text>
</view>
1
2
3
4
5
6
7
8
9
10
11
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}

.title {
font-size: 24px;
color: #333;
}
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
import UIKit

class MiniProgramContainerViewController: UIViewController {

var nameLabel: UILabel!

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white

// 创建一个简单的视图来显示文本
nameLabel = UILabel()
nameLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(nameLabel)

// 设置文本标签的位置
NSLayoutConstraint.activate([
nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])

// 模拟从WXML解析来的数据
let data = ["name": "World"]

// 模拟WXML模板解析
let template = """
<view class="container">
<text class="title">Hello, {{name}}!</text>
</view>
"""

// 简单的模板字符串替换
在实际的小程序容器中,你需要一个完整的模板引擎来解析WXML,并将其转换为iOS的UI组件
你还需要一个样式引擎来解析WXSS,并应用样式到对应的UI组件上
let resultString = template.replacingOccurrences(of: "{{name}}", with: data["name"] ?? "Visitor")

// 将解析后的字符串设置为标签的文本
nameLabel.text = resultString

// 应用WXSS样式(这里只是简单地设置样式,实际上需要更复杂的样式解析和应用逻辑)
nameLabel.font = UIFont.systemFont(ofSize: 24)
nameLabel.textColor = UIColor.black
}
}

在实际的小程序容器中,你需要一个完整的模板引擎来解析WXML,并将其转换为iOS的UI组件。你还需要一个样式引擎来解析WXSS,并应用样式到对应的UI组件上。

滴滴开源小程序框架Mpx

End

详解Exception异常处理

好文分享:

一、异常的捕获

同步异常通过 try-catch 机制捕获

1
2
3
4
5
6
7
8
// 使用 try-catch 捕获同步异常
xxx() {
try {
throw StateError('This is a Dart exception.');
} catch (e) {
print(e);
}
}

异步异常采用 Future 提供的 catchError 语句捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用 catchError 捕获异步异常
Future<Map<String, dynamic>> simulate_get(String url) async {
Future.delayed(Duration(seconds: 1)).then((value) {
throw StateError('This is a Dart exception in Future.');
return {
'statusCode': 200,
'msg': '"${url}执行成功"',
};
}).catchError((onError) {
print(onError);
//rethrow; // 这里不能rethrow. 因为A rethrow must be inside of a catch clause.
});
}

二、异常的上抛

异步异常的上抛

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
 // 使用上文中自己建的异步方法 await simulate_get(url) 来模拟
Future<dynamic> requestBaidu1() async {
var url = "https://www.baidu.com/";
try {
dynamic response = await simulate_get(url);

if (response['statusCode'] == 200) {
return response.data;
} else {
throw Exception('后端接口出现异常');
}
} catch (e) {
print('网络错误:======>url:$url \nbody:${e.toString()}');
// 这里只有执行了rethrow 或 throw,外层才能继续 .catchError((onError) {}
rethrow;
// throw Exception('网络错误:======>url:$url \nbody:${e.toString()}');
}
}



// 使用网络请求库 dio 中的异步方法 await dio.get(url) 来模拟
Future<dynamic> requestBaidu2() async {
var url = "https://www.baidu.com/";
try {
Dio dio = new Dio();
Response response = await dio.get(url);

if (response.statusCode == 200) {
return response.data;
} else {
throw Exception('后端接口出现异常');
}
} catch (e) {
// throw Exception('网络错误:======>url:$url \nbody:${e.toString()}');
print('网络错误:======>url:$url \nbody:${e.toString()}');
rethrow;
}
}

进行如上的上抛操作后,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// xxxPage.dart	
getData() {
Api.requestBaidu2().then((dynamic data) {
if (data.isSuccess) {
Toast.show("请求成功,有数据 isSuccess = true", context);
updateWidgetType(WidgetType.SuccessWithData);
} else {
Toast.show("服务器失败 isSuccess = false", context);
}
}).catchError((onError) {
print('网络问题');
Toast.show("catchError网络问题111", context);
updateWidgetType(WidgetType.ErrorNetwork);
});
}

判断网络异常

1
2
3
4
5
6
7
checkNetwork() async {
try {
final result = await InternetAddress.lookup('baidu.com'); //尝试连接baidu
} on SocketException catch (_) {
Toast.show("catchError网络问题222333", context);
}
}