一、前言
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 git@github.com: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 git@github.com: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
變量的前面即可
image.png
當然,這兩步操作,其實我們也可以通過額外的腳本自動化,釋放人力,具體自動化代碼可以參考我的示例倉庫 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 支持的還比較少,但是作為一種方案,它理論上能支持我上面所說的特性:
- 「無侵入」
- 「全資源支持」
- 「跨平臺」
- 「資源處理流」
它依舊需要大家去根據自己的需求去做完善,但是相信現在的你已經知道怎么去弄了。