一、前言
Cocos Creator 打包后的素材资源,如:图片,声音等。默认是保持原始格式,只要遇到破解党,那么他们极有可能很简单就直接获取到这部分素材资源。
针对这个问题,大部分同学都会有一种资源加密的需求,即对打包后的资源进行加密,让破解党 不那么容易 获取到资源。
由此,引出了很多关于资源加密一些诉求,包括但不限于:
- https://forum.cocos.org/t/creator/46017
- https://forum.cocos.org/t/topic/90094
- https://forum.cocos.org/t/cocos-creator/58620
- https://forum.cocos.org/t/web/89123
- ...
但是,你可能发现,通篇下来,可能并没有一个完整的流程方案。
- 或是过于注重怎么加解密
- 或是仅支持图片资源,其他资源没涉及
- 或是仅涉及到部分平台,比如:只关注了原生平台
- 或是需要侵入到项目代码,需要使用指定的资源加载器
(鉴于目前的搜索结果)总结下来,开源出来的好像还没有一整套Cocos Creator资源加解密流程方案的样子。
二、Cocos Creator Build Encrypt 方案介绍
这次为大家带来一种 Cocos Creator 「无侵入」 「全资源支持」 「跨平台」 「资源处理流」 方案。
以下为我关于上述四个加粗特性的定义:
- 「无侵入」:使用此方案,开发者只需要针对 Cocos Creator 构建后的输出目录 进行执行指定命令,即完成资源加密。 实际游戏项目不需要加入/删除/修改代码等其他操作,全程无侵入。
- 「全资源支持」:此方案可以对 Cocos Creator 引擎本身所支持的 所有资源文件(如:.txt,.json, .png, .mp3, .fnt等等)进行加密 ,并且依旧「无侵入」。
- 「跨平台」:此方案可以针对不同版本的 Cocos Creator 进行单独适配,并且可以对每个 Cocos Creator 支持发布的所有平台进行单独适配。
- 「资源处理流」:使用此方案,你可以对资源进行包括但不限于 加密 、 压缩 等处理流。
当然,作为一种方案,目前上述四个特性可能还过于抽象,因此,为了支撑整个方案,我准备了一个示例 CocosCreator-Build-Encrypt ,作为大家的参考 :
三、Cocos Creator Build Encrypt 方案实现原理
3.1 原始资源加载 VS 加密资源加载
原始资源的加载,其实可以简化为如下步骤:
运行时:读取原始资源 -> 生成资源对象
而加密资源的加载,则是在上述基础上,加入额外的步骤,整个流程大概如下:
运行前:读取原始资源 -> 生成加密内容 -> 保存到加密文件
运行时:读取加密文件 -> 解密加密内容 -> 生成资源对象
原理大家可能都有所了解,但是怎么做呢?具体一点,在 Cocos Creator 上应该怎么做呢?
为此,我们需要先大概了解一下 Cocos Creator 的资源加载流程。
3.2 Cocos Creator 的资源加载流程
- 后续说明将以 Cocos Creator 2.3.3 为例进行说明,2.3.3 和 2.4.0 的资源加载是不一样的
- 建议大家下载 Cocos Creator 2.3.3 的 引擎源码去使用VSCode辅助理解
git clone [email protected]:cocos-creator/engine.git cd engine git checkout 2.3.3 code ../engine
事实上,Cocos Creator 的所有资源加载都是通过 cc.loader
去进行的,无论是动态加载,静态加载(场景)等,都是通过 cc.loader
去进行加载的。
而 cc.loader
内部的加载实现是一个 pipe 管道工作流。每次加载一份资源由最基础的 3 个工作流去负责整个加载流程。
- assetLoader
- downloader
- loader
这部分理解,可以参阅官方文档 loader 的说明
3.3 Cocos Creator Build Encrypt 「全资源支持」「资源处理流」原理
从上述文档中,我们知道有 3 个管道工作流,但是每个 loader 具体是干什么的,我们并不知晓,为了知道这 3 个工作流的实际作用,我们需要翻阅源码
- 再次建议大家下载 Cocos Creator 2.3.3 的 引擎源码去使用VSCode辅助理解
git clone [email protected]:cocos-creator/engine.git cd engine git checkout 2.3.3 code ../engine
经过翻阅,可以发现整个资源加载的内容都在引擎源码的 cocos2d/core/load-pipeline
目录下:
通过上面的图,我们找到了3个 loader 的初始化入口了,在依次翻阅 assetloader,downloader,loader 的相关实现之后,我们可以发现突破口 downloader.js
总结一下:
在 cc.loader 的 pipe 管道流的第二个 loader ,即 downloader 中,Cocos 会根据不同资源文件的后缀名,去调用不同的 download 函数,并生成内存对象。
比如:
上述截图中,png 后缀的文件会调用 downloadImage
的函数,该函数最终会通过 callback(error, data)
函数返回结果
- 如果图片加载成功过,那么会返回一个 Image 对象
callback(null, img);
- 如果图片加载失败,那么会返回 Error 对象
callback(error);
再总结一下:
首先,defaultMap
对象中,罗列了很多资源文件后缀名,并且都有对应的 download 函数。而我们通过了解 png 的加载逻辑,更是了解到,实际上 downloadImage
函数的作用是 将文件下载并读取转换为内存对象 。
那如果我们的文件是加密后的文件,那么我们只需要在 downloadImage
函数中,读取文件之后,先进行解密,然后在生成 Image 对象返回,那就可以 解决运行时解密的问题(读取加密文件->解密加密内容 -> 生成内存对象) 了。
再往外看一下 defaultMap
对象 ,感觉 Cocos Creator 支持的所有的资源,我们都可以这样子来弄。而这就是我们 Cocos Creator Build Encrypt
方案的特性—— 「全资源支持」 的理论支撑!
那么,新的问题来了,我们应该怎么替换不同资源的 download 函数呢?
在搜索过程中,发现了很久以前的一个 帖子 ,帖子中 panda 大大就已经罗列了一个范例:
cc.loader
已经为我们提供了 addDownloadHandlers
了,因此,我们可以很方便地替换我们的资源加载方式。
我们以 Cocos Creator 2.3.3 原生平台加载的 png 资源为例:
- 假设我们对 Cocos Creator 2.3.3 构建原生平台后的输出目录的所有 png 图片进行加密,仅仅是转换为 Base64字符串,并且存放到原png图片所在目录的话,参考代码如下:(具体代码见 CocosCreator-Build-Encrypt 仓库)
handle(): void {
// 获取图片文件
let imgFilePaths: string[] = [];
// 获取指定目录的所有图片文件路径
this.collectImageFilePaths(buildOutputResDirPath, imgFilePaths);
// 加密图片文件
console.log(`图片处理:找到 ${imgFilePaths.length} 张原始图片`);
imgFilePaths.forEach((filePath: string) => {
// 读取原图片内容
let fileBuffer: Buffer = fs.readFileSync(filePath);
// 删除原图片资源
fs.unlinkSync(filePath);
// 原图片文件内容转换为 Base64 字符串,写入到同目录文件名的 .xxpng 文件中
fs.writeFileSync(filePath.replace(".png", ".xxpng"), fileBuffer.toString("base64"));
});
console.log(`图片处理:${imgFilePaths.length} 张原始图片已加密完成`);
}
- 那么,对应的解密代码就可以这样子写:先读取 .xxxpng 文本,然后将文本解密转换为 Image 对象,返回 Image 对象即可:
if (CC_JSB) {
function downloadText(item) {
var url = item.url;
var result = jsb.fileUtils.getStringFromFile(url);
if (typeof result === "string" && result) {
return result;
} else {
return new Error("Download text failed: " + url);
}
}
cc.loader.addDownloadHandlers({
png: function (item, callback) {
// 从定向原图片地址 为 加密后的文件
item.url = item.url.replace(".png", ".xxpng");
let text = downloadText(item);
if (text instanceof Error) {
callback(text, null);
} else {
// 将图片文件的文本转换为Image;
let img = new Image();
img.src = "data:image/png;base64," + text;
img.onload = function (info) {
callback(null, img);
};
img.onerror = function (event) {
callback(new Error("load image fail:" + img.src), null);
}; // Don't return anything to use async loading.
}
},
});
}
当然,细品上面代码之后,你可能会有一些问题,比如:
Q:第2步中, downloadText 和 处理 Image 函数,你是怎么知道这样子写的,我看的源码中(cocos2d/core/load-pipeline
)文本和图片的处理并不是这样子的!
A:在 Cocos Creator 2.3.3 打包原生平台后,会生成一个 jsb-adapter
文件夹,在 jsb-adapter/jsb-engine.js
中,可以看到在原生平台上,引擎是重写了资源加载的方式以适配原生平台,参考这里面 png 和 txt 的加载方式,就可以写出第2步中的代码了—— 在原生平台上读取文件,并转换为内存对象。
至此,整个流程上,我们已经实现了资源加解密的流程了,并且理论上可以支持所有的资源哦~
当然,你完全也可以处理png图片的时候
- 先进行加密
- 对加密内容压缩在写入文件(此压缩主要用于减少文件体积)
解密时
- 先解压文件
- 在解密内容
流式处理,你想怎么处理就怎么处理。
而这个也是 Cocos Creator Build Encrypt 的「资源处理流」原理——不仅仅可以加密,还可以压缩等等。
3.4 Cocos Creator Build Encrypt 「跨平台」原理
有了上个章节的流程后,我们可以开始考虑这个方案的适用性,比如:这个方案能跨平台吗?能跨平台到什么程度呢?
先回顾一下整个运行时解密的过程:
运行时:读取加密文件 -> 解密加密内容 -> 生成资源对象
这个过程里面的难点在哪里呢?
- 解密加密内容:这一步不是难点所在,那怕是在不同平台上,我们也能比较好处理,毕竟加密代码是我们自己写的加密代码,对应的解密代码不是问题
- 生成资源对象:在不同平台上,也不难,可以参考源码
那么剩下的就是 读取加密文件 这一步了,在跨平台上,它可能难在哪里呢?
- 文件可能存放在(手机,PC)设备本地,可以能存放在网络上
- 在不同平台上,读取本地文件和网络文件的方式可能是不同。比如:
- 原生Android,iOS上可能是调用不同的原生读本地文件接口去读取文件内容,调用不同的请求接口去加载网络文件
- 微信小游戏上可能又是不一样的微信接口方式
- ...
那么,这些可能的难点要怎么解决呢?
实际上,在上个章节的问题环节中,我们已经初步涉及到这问题:怎么读取不同平台(Android,iOS,微信小游戏等等)的资源?
在上个章节中,我们提及到一个关键点:
在 Cocos Creator 2.3.3 打包原生平台后,会生成一个
jsb-adapter
文件夹
在这个理解下,只要我们在使用 Cocos Creator 打包指定平台后,检查一下其是否存在相应的适配代码(一般是 jsb-adapter 文件夹),参考其中的适配代码的实现就可以完成适配该平台的读取文件的实现了。
而这就是 Cocos Creator Build Encrypt 「跨平台」 原理的理论支撑!
当然,对应到不同版本的 Cocos Creator ,你也可以这样子参考其输出的构建目录去进行适配。
3.5 Cocos Creator Build Encrypt 「无侵入」原理
到这里,我们在回顾一下上面的流程,还缺了一个很重要的点没讲:
cc.loader.addDownloadHandlers({
png: function (item, callback) {
// ...
}
}
在什么时候执行上面这段代码注入呢?
- 不能在引擎代码之前注入,否则脚本不生效
- 不能在场景加载后才能注入,否则可能没有覆盖所有资源加载
那么,剩下答案就是 插件脚本 了 ,在 Cocos Creato 中,存在几种类型脚本,它们之间的加载顺序 如下:
那么,我们只需要将解密的代码放入到我们的项目,并弄成插件脚本就可以了,Easy~
至此,我们整个加解密流程是已经跑起来了,但是完美了吗?还没有。
在将解密代码导入为插件脚本这一步上,我们并没有做得很好,理由有几个点:
- 每个项目都需要导入同一份插件脚本,侵入了项目,并且重复的工作很乏味
- 换个同学来开发,肯定会对这份插件脚本的代码有疑问,因为里面只有解密代码,没有加密部分(加密部分我们另外写了),代码不完整,容易带来一些疑问,沟通成本
那么,对应这些问题,最好的解决方案就是「无侵入」,让开发的依旧按照他的开发,他看不到也不需要看到整个资源加解密过程,只需要提供构建目录,我们对构建目录进行一次加密即可。那么又该如何做?
这里需要我们去了解一些基本的问题:
Q1. 插件脚本在构建后放在哪里?
Q2. 引擎是怎么控制脚本加载顺序呢?具体一点,插件脚本是怎么控制在项目代码之前加载的呢?
带着这两个疑问,我们依旧以 Cocos Creator 2.3.3 打包原生这个作为例子去了解。
A1:从下图可以看到我们构建后的插件脚本是放在 src/assets/scripts
目录下
A2:在打包的输出目录中
- 入口是
main.js
(这一点不解释了) - 在
require
一堆 js 后,进入到window.boot
函数,这里注意一下,jsb-engine.js
是先于window.boot
reqiure 的 - 在
window.boot
函数中,所有的脚本都在jsList
数组变量中,并且插件脚本是先于我们的项目脚本的
当我们了解到这些之后,我们就可以自己去对 Cocos Creator 构建输出后的目录加入额外的插件脚本了,步骤如下:
- 复制解密插件脚本到项目构建输出目录的
src/assets
中 - 修改
main.js
,让我们的插件脚本先加入到jsList
变量的前面即可
当然,这两步操作,其实我们也可以通过额外的脚本自动化,释放人力,具体自动化代码可以参考我的示例仓库 CocosCreator-Build-Encrypt
而这,就是我们 Cocos Creator Build Encrypt 方案的 「无侵入」 原理了。
三、总结
好了,方案完了,这应该是一篇长文,需要反复琢磨,细节的地方可能需要在复习一下。
需要强调一下,这依旧是一个方案。作为一个方案,它可以改动的点太多了,比如:
- 上述例子,我们只是以 Cocos Creator 2.3.3 进行说明,并没有对大改后的 2.4.0 进行说明,但是根据 2.4.0 loader
的文档和上述理论,但是相信你现在已经可以自行适配了 - 上述例子,我们将 .png 改为了 .xxpng 的密文格式,其实你完全可以直接覆盖在 .png 上,无需额外弄 .xxpng 后缀(毕竟小游戏平台上支持的文件后缀是有限制的)
- 上述例子,加解密部分只是为了方便讲解,大家完全可以自己重写这部分
- ...
而我们的例子 CocosCreator-Build-Encrypt 支持的还比较少,但是作为一种方案,它理论上能支持我上面所说的特性:
- 「无侵入」
- 「全资源支持」
- 「跨平台」
- 「资源处理流」
它依旧需要大家去根据自己的需求去做完善,但是相信现在的你已经知道怎么去弄了。
四、参考资料
- https://forum.cocos.org/t/creator/46017/5
- https://docs.cocos.com/creator/2.3/api/zh/classes/loader.html
- https://docs.cocos.com/creator/2.3/api/zh/classes/LoadingItems.html
- https://github.com/cocos-creator/engine/tree/2.3.3/cocos2d/core/load-pipeline