【Quick 3.3】資源腳本加密及熱更新(二)資源加密

【Quick 3.3】資源腳本加密及熱更新(二)資源加密

注:本文基於Quick-cocos2dx-3.3版本編寫php

1、介紹

在前一篇文章中介紹了代碼加密,加密方式是XXTEA。對於資源文件來講,一樣可使用XXTEA來加密,所以以前那套加密模塊能夠通用了。python

加密腳本:compile_pack_files.bat、compile_pack_files.sh
使用方法和前一篇的腳本差很少android

考慮到不是否是全部的資源都須要加密,因此這裏不使用這個腳本。ios

2、資源加密

一、加密參數文件

涉及文件目錄:項目根目錄git

首先咱們定義一個php文件,裏面定義了加密所須要的參數github

<?php
    ini_set('memory_limit','1024M');
    return array(
        'src'      => 'res', 
        'output'   => 'packres',
        'prefix'   => '',
        'excludes' => '',
        'pack'  => 'files',//files or zip
        'key'      => 'ilovecocos2dx',
        'sign'     => 'XXTEA',
        'whitelists' => 'jpg,png,tmx,plist',
    );
?>
  • src 需加密的腳本所在目錄
  • output 加密後文件輸出目錄
  • pack files、zip 分開文件加密仍是打包成zip加密
  • sign 簽名,用來識別代碼、文件是否是加密的,通常填爲"XXTEA"便可
  • key 密鑰,簡單來講就是加密和解密的鑰匙,確保只有你本身知道
  • whitelists 須要加密的文件格式,根據文件的擴展名。

注意,whitelists這個參數是官方版本沒有的shell

將文件保存爲PackRes.php,放在項目根目錄(res、src同級目錄)windows

二、修改加密資源腳本的文件

涉及文件目錄:引擎目錄/quick/bin/lib、引擎目錄/quick/bin/lib/quickxcode

由於增長了參數whitelists,因此這裏須要修改加密的腳本。
涉及的文件有兩個,/quick/bin/lib/pack_files.php和/quick/bin/lib/quick/FilesPacker.phpapp

  • pack_files.php文件

能夠看到array裏面定義了全部參數,往array的最後加上新增的參數whitelists

//pack_files.php
<?php
    //...

$options = array(
    //...
    //...
    array('c',   'config',     1,      null,        'load options from config file'),
    array('q',   'quiet',      0,      false,       'quiet'),
    array('w',   'whitelists',   1,      null,        'whitelists extension, use "," to splite array, example "jpg,png"'),
);

    //...
  • FilesPacker.php文件

定位到validateConfig方法,加入對whitelist的解析

//FilesPacker.php
<?php

//...

class FilesPacker
{
    //...

    function validateConfig()
    {

        //...

        if (!empty($this->config['excludes']))
        {
            $excludes = explode(',', $this->config['excludes']);
            array_walk($excludes, function($value) {
                return trim($value);
            });
            $this->config['excludes'] = array_filter($excludes, function($value) {
                return !empty($value);
            });
        }
        else
        {
            $this->config['excludes'] = array();
        }

        //--------------add code begin--------------

        if (!empty($this->config['whitelists']))
        {
            $whitelists = explode(',', $this->config['whitelists']);
            array_walk($whitelists, function($value) {
                return trim($value);
            });
            $this->config['whitelists'] = array_filter($whitelists, function($value) {
                return !empty($value);
            });
        }

        //----------add code end--------------

        if ($this->config['pack'] != self::COMPILE_ZIP
            && $this->config['pack'] != self::COMPILE_FILES
            && $this->config['pack'] != self::COMPILE_C
            )
        {
            printf("ERR: invalid pack mode %s\n", $this->config['pack']);
            return false;
        }

定位到prepareForPack方法,加入whitelist的文件過濾

protected function prepareForPack(array $files)
    {

        //...

        foreach ($this->config['excludes'] as $key => $exclude)
        {
            if (substr($moduleName, 0, strlen($exclude)) == $exclude)
            {
                unset($files[$key]);
                $skip = true;
                break;
            }
        }

        if ($skip) continue;

        //--------------add code begin--------------

        if (!empty($this->config['whitelists'])) {
            $isDirty = false;
            foreach ($this->config['whitelists'] as $key => $whitelist)
            {

                if (end(explode('SPLIT_CHAR', $moduleName)) == $whitelist)
                {
                    $isDirty = true;
                    break;
                }
            }

            if (!$isDirty) {
                unset($files[$key]);
                continue;
            }
        }

        //--------------add code end--------------

        $bytesName = 'lua_m_' . strtolower(str_replace(array('.', '-'), '_', $moduleName));

        //...

三、生成加密資源腳本

仍是上個教程的腳本,此次加了packRes方法

#coding=utf-8
#!/usr/bin/python
import os 
import os.path 
import sys, getopt  
import subprocess
import shutil 
import time,  datetime
import platform
from hashlib import md5
import hashlib  
import binascii

def removeDir(dirName):
    if not os.path.isdir(dirName): 
        return
    filelist=[]
    filelist=os.listdir(dirName)
    for f in filelist:
        filepath = os.path.join( dirName, f )
        if os.path.isfile(filepath):
            os.remove(filepath)
        elif os.path.isdir(filepath):
            shutil.rmtree(filepath,True)

def initEnvironment():
    global APP_ROOT #工程根目錄 
    global APP_ANDROID_ROOT #安卓根目錄
    global QUICK_ROOT #引擎根目錄
    global QUICK_BIN_DIR #引擎bin目錄
    global APP_RESOURCE_ROOT #生成app的資源目錄
    global APP_RESOURCE_RES_DIR #資源目錄

    global APP_BUILD_USE_JIT #是否使用jit

    global PHP_NAME #php
    global SCRIPT_NAME #執行腳本文件名

    global BUILD_PLATFORM #生成app對應的平臺

    SYSTEM_TYPE = platform.system() #當前操做系統

    APP_ROOT = os.getcwd() #當前目錄
    APP_ANDROID_ROOT = APP_ROOT + "/frameworks/runtime-src/proj.android"
    QUICK_ROOT = os.getenv('QUICK_V3_ROOT')

    if QUICK_ROOT == None: #quick引擎目錄未指定,可手動指定路徑或者運行引擎目錄下相應腳本
        print "QUICK_V3_ROOT not set, please run setup_win.bat/setup_mac.sh in engine root or set QUICK_ROOT path"
        return False

    if(SYSTEM_TYPE =="Windows"): 
        QUICK_BIN_DIR = QUICK_ROOT + "quick/bin"
        PHP_NAME = QUICK_BIN_DIR + "/win32/php.exe" #windows
        BUILD_PLATFORM = "android" #windows dafault build android
        SCRIPT_NAME = "/compile_scripts.bat"
    else:
        PHP_NAME = "php"
        BUILD_PLATFORM = "ios" #mac default build ios
        QUICK_BIN_DIR = QUICK_ROOT + "/quick/bin" #mac add '/'
        SCRIPT_NAME = "/compile_scripts.sh"


    if(BUILD_PLATFORM =="ios"):
        APP_BUILD_USE_JIT = False
        APP_RESOURCE_ROOT = APP_ROOT + "/Resources" 
        APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res"

    else:
        APP_BUILD_USE_JIT = True
        APP_RESOURCE_ROOT = APP_ANDROID_ROOT + "/assets" #default build android
        APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res"

    print 'App root: %s' %(APP_ROOT)
    print 'App resource root: %s' %(APP_RESOURCE_ROOT)
    return True

def compileScriptFile(compileFileName, srcName, compileMode):
    scriptDir = APP_RESOURCE_RES_DIR + "/code/"
    if not os.path.exists(scriptDir):
        os.makedirs(scriptDir)
    try:
        scriptsName = QUICK_BIN_DIR + SCRIPT_NAME
        srcName = APP_ROOT + "/" + srcName
        outputName = scriptDir + compileFileName
        args = [scriptsName,'-i',srcName,'-o',outputName,'-e',compileMode,'-es','XXTEA','-ek','ilovecocos2dx']

        if APP_BUILD_USE_JIT:
            args.append('-jit')

        proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
        while proc.poll() == None:  
            outputStr = proc.stdout.readline()
            print outputStr,
        print proc.stdout.read(),
    except Exception,e:  
        print Exception,":",e

def packRes():
    removeDir(APP_ROOT + "/packres/") #--->刪除舊加密資源

    scriptName = QUICK_BIN_DIR + "/lib/pack_files.php"
    try:
        args = [PHP_NAME, scriptName, '-c', 'PackRes.php']
        proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
        while proc.poll() == None:  
            print proc.stdout.readline(),
        print proc.stdout.read()
    except Exception,e:  
        print Exception,":",e

if __name__ == '__main__': 
    isInit = initEnvironment()

    if isInit == True:
        #加密資源
        packRes()

        #將src目錄下的全部腳本加密打包成game.zip
        compileScriptFile("game.zip", "src", "xxtea_zip")

使用方法:

  1. 保存爲compileScript.py文件
  2. 放到項目根目錄(res、src同級目錄)
  3. 運行命令行工具,cd到該目錄,執行python compileScript.py
  4. 若屏幕輸出 "create output files in xx/xx/packres ." 說明執行成功

四、修改引擎文件

因爲涉及到的資源格式比較多,每一個涉及到的類修改讀取文件的方式太麻煩。而如今cocos2dx3.x版本統一了資源讀取的接口,因此只須要修改讀取文件的接口便可。

具體步驟:

  • 加入解密XXTEA文件
    一、把frameworks/cocos2d-x/external/xxtea文件夾裏面的xxtea.h和xxtea.cpp複製到frameworks/cocos2d-x/cocos/base目錄中

二、引用文件,也就是讓編譯器知道xxtea文件在哪

  • windows

打開vs,打開lubcocos2d項目,展開base,把xxtea兩個文件拖到base裏面便可

  • android

打開frameworks/cocos2d-x/cocos/Android.mk文件
在LOCAL_SRC_FILES中加入xxtea.cpp文件

#Android.mk

#...

LOCAL_SRC_FILES := \
cocos2d.cpp \

#...

base/ObjectFactory.cpp \
base/xxtea.cpp \ #加入此項
renderer/CCBatchCommand.cpp \

#...
  • mac

打開xcode,打開cocos2dlib.xcodeproj,展開base,把xxtea兩個文件拖到base裏面便可

  • 文件讀取 CCFileUtils

由於避免和引擎舊的接口混淆,因此這裏是新增兩個方法而不是修改舊的方法

頭文件CCFileUtils.h加入兩個public方法聲明

//CCFileUtils.h

//...
class CC_DLL FileUtils
{
public:

    //...
    static unsigned char* getDecryptFileData(const char* pszFileName, const char* pszMode, unsigned long * pSize);

    static Data getDecryptDataFromFile(const std::string& filename);

protected:
    //...

Cpp文件CCFileUtils.cpp中加入頭文件和方法定義

//CCFileUtils.cpp

//...
#include "xxtea/xxtea.h" 

//..
Data FileUtils::getDecryptDataFromFile(const std::string &filename)
{
    unsigned long sz;
    unsigned char * buf = FileUtils::getDecryptFileData(filename.c_str(), "rb", &sz);
    if (!buf) {
        return Data::Null;
    }
    Data data;
    data.fastSet(buf, sz);
    return data;
}

unsigned char* FileUtils::getDecryptFileData(const char* pszFileName, const char* pszMode, unsigned long * pSize)
{
    ssize_t size;
    unsigned char* buf = FileUtils::getInstance()->getFileData(pszFileName, pszMode, &size);
    if (NULL == buf || size<1) return NULL;

    const char *xxteaKey = "ilovecocos2dx";
    int xxteaKeyLen = strlen(xxteaKey);
    const char *xxteaSign = "XXTEA";
    int xxteaSignLen = strlen(xxteaSign);
    unsigned char* buffer = NULL;

    bool isXXTEA = true;
    for (int i = 0; isXXTEA && i<xxteaSignLen && i<size; ++i) {
        isXXTEA = buf[i] == xxteaSign[i];
    }

    if (isXXTEA) { // decrypt XXTEA
        xxtea_long len = 0;
        buffer = xxtea_decrypt(
            buf + xxteaSignLen,
            (xxtea_long)size - (xxtea_long)xxteaSignLen,
            (unsigned char*)xxteaKey,
            (xxtea_long)xxteaKeyLen,
            &len);
        delete[]buf;
        buf = NULL;
        size = len;
    }
    else {
        buffer = buf;
    }

    if (pSize) *pSize = size;
    return buffer;
}
  • 資源讀取
    涉及加密的資源有圖片文件(png、jpg),xml文件(plist、tmx)

圖片文件

//CCImage.cpp
//目錄:frameworks/cocos2d-x/cocos/platform/

//...
bool Image::initWithImageFile(const std::string& path)
{
    //...
        SDL_FreeSurface(iSurf);
    #else
        //修改此處便可
        Data data = FileUtils::getInstance()->getDecryptDataFromFile(_filePath);

        if (!data.isNull())
        {

    //...
}

xml文件

在windows/android平臺下,xml解析使用CCSAXParser,在ios平臺下則是用系統類NSDictionary來解析,因此這裏要修改兩處地方

對於windows/android平臺

//CCSAXParser.cpp
//目錄:frameworks/cocos2d-x/cocos/platform/

//...
bool SAXParser::parse(const std::string& filename)
{
    bool ret = false;
    //修改此處便可
    Data data = FileUtils::getInstance()->getDecryptDataFromFile(filename);
    if (!data.isNull())
    {
        ret = parse((const char*)data.getBytes(), data.getSize());
    }

    return ret;
}
//...

對於ios平臺

//CCFileUtils-apple.mm
//frameworks/cocos2d-x/cocos/platform/apple

//...

ValueMap FileUtilsApple::getValueMapFromFile(const std::string& filename)
{
    std::string fullPath = fullPathForFilename(filename);

    //註釋下面兩句代碼
//    NSString* path = [NSString stringWithUTF8String:fullPath.c_str()];
//    NSDictionary* dict = [NSDictionary dictionaryWithContentsOfFile:path];
    
    //------add code begin-------
    unsigned long fileSize = 0;
    unsigned char* pFileData = FileUtils::getDecryptFileData(fullPath.c_str(), "rb", &fileSize);
    NSData *data = [[[NSData alloc] initWithBytes:pFileData length:fileSize] autorelease];
    delete []pFileData;
    NSPropertyListFormat format;
    NSString *error;
    NSMutableDictionary *dict = (NSMutableDictionary *)[
                                                         NSPropertyListSerialization propertyListFromData:data
                                                         mutabilityOption:NSPropertyListMutableContainersAndLeaves
                                                         format:&format
                                                         errorDescription:&error];

    //------add code end-----------

    ValueMap ret;

    if (dict != nil)
    {
        for (id key in [dict allKeys])
        {
            id value = [dict objectForKey:key];
            addValueToDict(key, value, ret);
        }
    }
    return ret;
}

3、總結

至此資源加密已經完成,整體來講分兩個步驟,一個是資源加密生成,另外一個是資源加密讀取。因爲涉及跨平臺文件讀取,因此修改後須要在各個平臺測試資源是否能正確讀取

資源文件見:https://github.com/chenquanjun/Cocos2dxResourceEncrypt

相關文章
相關標籤/搜索