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

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

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

1、介紹

lua相對於c++開發的優勢之一是代碼能夠在運行的時候才加載,基於此咱們不只能夠在編寫的時候熱更新代碼(針對開發過程當中的熱更新將在另一篇文章中介紹),也能夠在版本已經發布以後更新代碼。html

2、熱更新模塊

cocos2dx的熱更新已經有不少篇文章介紹了,這裏主要是基於 quick-cocos2d-x的熱更新機制實現(終極版2)(更新3.3版本)基礎上修改。node

一、launcher模塊(lua更新模塊)

launcher模塊的具體介紹能夠看原文,不過這裏的更新邏輯是稍微修改了的。
先請求服務器的launcher模塊文件,若是本地launcher模塊文件和服務器不一樣則替換新的模塊再從新加載。
具體更新邏輯如流程圖所示:python

原文是把文件md5和版本信息都放在同一個文件裏面,這裏把版本信息和文件md5分紅兩個文件,這樣的好處是不用每次都把完整的md5文件列表下載下來。除此以外還增長了程序版本號判斷,優化了一些邏輯。具體代碼見最後的資源連接。android

二、版本文件/文件md5信息生成

原文的md5信息(flist)是經過lua代碼調用引擎模塊生成,可是鑑於工程太大不利於分享(其實目的只是要生成文件md5信息),因此這裏把代碼改爲python版本的了。ios

注意,若是你也想要嘗試把lua改爲其餘語言實現,你可能會發現生成的md5和lua版本的不一樣,這是由於lua版本將字節流轉換成大寫的十六進制來生成md5的。c++

#lua 版本
local function hex(s)
    s=string.gsub(s,"(.)",function (x) return string.format("%02X",string.byte(x)) end)
    return s
end

#python 版本
def toHex(s):
    return binascii.b2a_hex(s).upper()

具體的python腳本代碼(仍是基於上個教程的腳本增長代碼)git

#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 copySingleFile(sourceFile, targetFile):
    if os.path.isfile(sourceFile): 
        if not os.path.exists(targetFile) or(os.path.exists(targetFile) and (os.path.getsize(targetFile) != os.path.getsize(sourceFile))):  
            open(targetFile, "wb").write(open(sourceFile, "rb").read()) 

def copyFiles(sourceDir,  targetDir, isAll): 
    for file in os.listdir(sourceDir): 
        sourceFile = os.path.join(sourceDir,  file) 
        targetFile = os.path.join(targetDir,  file) 
        if os.path.isfile(sourceFile): 
            if not isAll:
                extName = file.split('.', 1)[1] 
                if IgnoreCopyExtFileDic.has_key(extName):
                    continue
            if not os.path.exists(targetDir):
                os.makedirs(targetDir)
            if not os.path.exists(targetFile) or(os.path.exists(targetFile) and (os.path.getsize(targetFile) != os.path.getsize(sourceFile))):  
                open(targetFile, "wb").write(open(sourceFile, "rb").read()) 
        if os.path.isdir(sourceFile): 
            First_Directory = False 
            copyFiles(sourceFile, targetFile, isAll)

def toHex(s):
    return binascii.b2a_hex(s).upper()

def md5sum(fname):

    def read_chunks(fh):
        fh.seek(0)
        chunk = fh.read(8096)
        while chunk:
            yield chunk
            chunk = fh.read(8096)
        else: #最後要將遊標放回文件開頭
            fh.seek(0)
    m = hashlib.md5()
    if isinstance(fname, basestring) and os.path.exists(fname):
        with open(fname, "rb") as fh:
            for chunk in read_chunks(fh):
                m.update(toHex(chunk))
    #上傳的文件緩存 或 已打開的文件流
    elif fname.__class__.__name__ in ["StringIO", "StringO"] or isinstance(fname, file):
        for chunk in read_chunks(fname):
            m.update(toHex(chunk))
    else:
        return "" 

    return m.hexdigest()


def calMD5ForFolder(dir):
    md5Dic = []
    folderDic = {}
    for root, subdirs, files in os.walk(dir):
        #get folder
        folderRelPath = os.path.relpath(root, dir)
        if folderRelPath != '.' and len(folderRelPath) > 0:
            normalFolderPath =  folderRelPath.replace('\\', '/') #convert to / path
            folderDic[normalFolderPath] = True

        #get md5
        for fileName in files:
            filefullpath = os.path.join(root, fileName)
            filerelpath = os.path.relpath(filefullpath, dir)
            size = os.path.getsize(filefullpath)
            normalPath =  filerelpath.replace('\\', '/') #convert to / path

            if IgnoreMd5FileDic.has_key(fileName): #ignode special file
                continue

            print normalPath
            md5 = md5sum(filefullpath)
            md5Dic.append({'name' : normalPath, 'code' : md5, 'size' : size})


    print 'MD5 figure end'  
    return md5Dic, folderDic

#-------------------------------------------------------------------
def initEnvironment():
    #注意:複製的資源分兩種
    #第一種是加密的資源,從packres目錄複製到APP_RESOURCE_ROOT。加密資源的類型在PackRes.php的whitelists定義。
    #第二種是普通資源,從res目錄複製到APP_RESOURCE_ROOT。IgnoreCopyExtFileDic定義了不復制的文件類型(一、加密資源,如png文件;二、無用資源,如py文件)
    
    global ANDROID_APP_VERSION
    global IOS_APP_VERSION
    global ANDROID_VERSION
    global IOS_VERSION
    global BOOL_BUILD_APP #是否構建app

    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 IgnoreCopyExtFileDic #不從res目錄複製的資源
    global IgnoreMd5FileDic #不計算md5的文件名

    global APP_BUILD_USE_JIT #是否使用jit

    global PHP_NAME #php
    global SCRIPT_NAME #scriptsName

    global BUILD_PLATFORM #生成app對應的平臺


    BOOL_BUILD_APP = True

    IgnoreCopyExtFileDic = {
        'jpg' : True,
        'png' : True,
        'tmx' : True,
        'plist' : True,
        'py' : True,
    }

    IgnoreMd5FileDic = {
        '.DS_Store' : True,
        'version' : True,
        'flist' : True,
        'launcher.zip' : True,
        '.' : True,
        '..' : True,
    }

    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:
        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 #ios not use jit

        if BOOL_BUILD_APP:
            APP_RESOURCE_ROOT = APP_ROOT + "/Resources" 
            APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res"
        else:
            APP_RESOURCE_ROOT = APP_ROOT + "/server/game/cocos2dx/udp"
            APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT

    else:
        APP_BUILD_USE_JIT = True

        if BOOL_BUILD_APP:
            APP_RESOURCE_ROOT = APP_ANDROID_ROOT + "/assets" #default build android
            APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res"
        else:
            APP_RESOURCE_ROOT = APP_ROOT + "/server/game/cocos2dx/udp"
            APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT

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

def svnUpdate():
    print "1:svn update"
    try:
        args = ['svn', 'update']
        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


def packRes():
    print "2:pack res files"

    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

def copyResourceFiles():
    print "3:copy resource files"

    print "remove old resource files"
    removeDir(APP_RESOURCE_ROOT)

    if not os.path.exists(APP_RESOURCE_ROOT):
        print "create resource folder"
        os.makedirs(APP_RESOURCE_ROOT)

    if BOOL_BUILD_APP:  #copy all resource 
        print "copy config"
        copySingleFile(APP_ROOT + "/config.json", APP_RESOURCE_ROOT + "/config.json")
        copySingleFile(APP_ROOT + "/channel.lua", APP_RESOURCE_ROOT + "/channel.lua")
        
        print "copy src"
        copyFiles(APP_ROOT + "/scripts/",  APP_RESOURCE_ROOT + "/src/", True)

    print "copy res"
    copyFiles(APP_ROOT + "/res/",  APP_RESOURCE_RES_DIR, False)

    print "copy pack res"
    copyFiles(APP_ROOT + "/packres/",  APP_RESOURCE_RES_DIR, 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 compileFile():
    print "4:compile script file"

    compileScriptFile("game.zip", "src", "xxtea_zip") #--->代碼加密
    compileScriptFile("launcher.zip", "pack_launcher", "xxtea_zip") #--->更新模塊加密

def writeFile(fileName, strArr):
    if os.path.isfile(fileName):
        print "Remove old file!"
        os.remove(fileName)

    #write file
    f = file(fileName, 'w') 

    for _, contentStr in enumerate(strArr):
        f.write(contentStr)

    f.close()

def genFlist():
    print "5: generate flist"
    # flist文件格式 lua table
    # key
    #  --> dirPaths 目錄
    #  --> fileInfoList 文件名,md5,size
    folderPath = APP_RESOURCE_RES_DIR

    md5Dic, folderDic = calMD5ForFolder(folderPath)

    #sort md5
    sortMd5Dic = sorted(md5Dic, cmp=lambda x,y : cmp(x['name'], y['name']))  

    #convert folder dic to arr
    folderNameArr = []

    for folderName, _ in folderDic.iteritems():
        folderNameArr.append(folderName)

    #sort folder name
    sortFolderArr = sorted(folderNameArr, cmp=lambda x,y : cmp(x, y)) 

    #str arr generate
    strArr = []

    strArr.append('local flist = {\n')

    #dirPaths
    strArr.append('\tdirPaths = {\n')

    for _,folderName in enumerate(sortFolderArr):
        strArr.append('\t\t{name = "%s"},\n' % folderName)

    strArr.append('\t},\n')

    #fileInfoList
    strArr.append('\tfileInfoList = {\n')
    
    for index, md5Info in enumerate(sortMd5Dic):
        name = md5Info['name']
        code = md5Info['code']
        size = md5Info['size']
        strArr.append('\t\t{name = "%s", code = "%s", size = %d},\n' % (name, code, size))

    strArr.append('\t},\n')
    strArr.append('}\n')
    strArr.append('return flist\n')

    writeFile(folderPath + "/flist", strArr)

def genVersion():
    print "6: generate version"
    folderPath = APP_RESOURCE_RES_DIR
    #str arr generate
    strArr = []
    strArr.append('local version = {\n')

    strArr.append('\tandroidAppVersion = %d,\n' % ANDROID_APP_VERSION)
    strArr.append('\tiosAppVersion = %d,\n' % IOS_APP_VERSION)
    strArr.append('\tandroidVersion = "%s",\n' % ANDROID_VERSION)
    strArr.append('\tiosVersion = "%s",\n' % IOS_VERSION)

    strArr.append('}\n')
    strArr.append('return version\n')

    writeFile(folderPath + "/version", strArr)

if __name__ == '__main__': 
    print 'Pack App start!--------->'
    isInit = initEnvironment()

    if isInit == True:
        #若不更新資源則直接執行copyResourceFiles和compileScript
        
        svnUpdate() #--->更新svn

        packRes() #--->資源加密(若資源如圖片等未更新則此步可忽略)

        copyResourceFiles() #--->複製res資源
        
        compileFile() #--->lua文件加密

        genFlist() #--->生成flist文件

        ANDROID_APP_VERSION = 1 #app 更新版本才須要更改
        IOS_APP_VERSION = 1 #app 更新版本才須要更改
        ANDROID_VERSION = "1.0.1"
        IOS_VERSION = "1.0.1"

        genVersion() #--->生成version文件

    print '<---------Pack App end!'

注意:這個腳本是集成代碼加密、資源加密、熱更新文件生成的。具體使用的時候確定會遇到不少坑的github

  • 坑1:項目使用luajit。
    熱更新和luajit有點不完美適應,由於iOS的luajit是2.1beta的(iOS的坑),而其餘平臺是使用的是舊版本luajit,這意味着它們的更新文件不能通用,iOS和android下載服務器的加密代碼要區分開,固然若是項目沒有用luajit的話就沒有這個煩惱了。shell

  • 坑2: 資源文件的位置。
    android/iOS的文件引用時注意不要把未加密的代碼複製進去了,上面的pyhton腳本已經幫你作了部分操做了,可是還有一些須要本身手動去改。
    iOS:xcode工程注意要把原來的資源引用換成加密的資源(Mac下執行腳本會把假面資源拷貝到Resource目錄下)
    Android:若是你是用build_apk、build_native、build_native_release來編譯的話,注意把proj.android裏面的build_native_release腳本的資源複製刪除語句屏蔽掉
    windows:由於windows是開發的時候才用,因此是直接引用源代碼的。不過你要發佈windows版本的話,須要自行替換加密資源了。

  • 坑3:檢查腳本是否放在正確的位置。
    python腳本/PackRes.php放在工程根目錄(res、src同級目錄);FilesPacker.php/pack_files.php放在引擎相應目錄;

  • 坑4:檢查QUICK_ROOT是否已經設置。
    由於腳本要用到引擎自帶的加密腳本,注意Mac使用的shell命令(.sh文件)有權限執行

  • 坑5:檢查參數是否正確設置。
    python腳本中,APP_BUILD_USE_JIT是否使用luajit加密腳本,BOOL_BUILD_APP是否打包apk仍是熱更新(複製的目錄不一樣)


三、引擎修改

由於代碼已經加密,並且加入了熱更新模塊,因此lua的加載入口須要修改。

首先找到AppDelegate.cpp文件,加入初始化資源搜索路徑initResourcePath方法,而後增長更新文件和加密文件判斷。
這裏有三種狀況。
1:更新模式(發佈版本使用)
2:加密模式(無更新,windows版本使用)
3:普通模式(無更新和無加密,開發時候使用)

void AppDelegate::initResourcePath()
{
    FileUtils* sharedFileUtils = FileUtils::getInstance();
    std::string strBasePath = sharedFileUtils->getWritablePath();
    #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)|| (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
        sharedFileUtils->addSearchPath("res/");
    #else
        sharedFileUtils->addSearchPath("../../res/");
    #endif
    sharedFileUtils->addSearchPath(strBasePath + "upd/", true);
}

bool AppDelegate::applicationDidFinishLaunching()
{
    #if CC_TARGET_PLATFORM == CC_PLATFORM_WIN32
        initRuntime();
    #elif (COCOS2D_DEBUG > 0 && CC_CODE_IDE_DEBUG_SUPPORT > 0)
        // NOTE:Please don't remove this call if you want to debug with Cocos Code IDE
        if (_launchMode)
        {
            initRuntime();
        }
    #endif
    //add resource path
    initResourcePath();
    // initialize director
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();    
    if(!glview) {
        Size viewSize = ConfigParser::getInstance()->getInitViewSize();
        string title = ConfigParser::getInstance()->getInitViewName();
    #if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 || CC_TARGET_PLATFORM == CC_PLATFORM_MAC)
        extern void createSimulator(const char* viewName, float width, float height, bool isLandscape = true, float frameZoomFactor = 1.0f);
        bool isLanscape = ConfigParser::getInstance()->isLanscape();
        createSimulator(title.c_str(),viewSize.width,viewSize.height, isLanscape);
    #else
        glview = cocos2d::GLViewImpl::createWithRect(title.c_str(), Rect(0, 0, viewSize.width, viewSize.height));
        director->setOpenGLView(glview);
    #endif
        director->startAnimation();
    }
   
    auto engine = LuaEngine::getInstance();
    ScriptEngineManager::getInstance()->setScriptEngine(engine);
    lua_State* L = engine->getLuaStack()->getLuaState();
    lua_module_register(L);

    // use Quick-Cocos2d-X
    quick_module_register(L);

    LuaStack* stack = engine->getLuaStack();

    stack->setXXTEAKeyAndSign("ilovecocos2dx", strlen("ilovecocos2dx"), "XXTEA", strlen("XXTEA"));

    stack->addSearchPath("src");
    
    FileUtils *utils = FileUtils::getInstance();

    //1: try to load launcher module
    const char *updateFileName = "code/launcher.zip";
    std::string updateFilePath = utils->fullPathForFilename(updateFileName);

    bool isUpdate = false;

    if (updateFilePath.compare(updateFileName) != 0) //check if update file exist
    {
        printf("%s\n", updateFilePath.c_str());
        isUpdate = true;
        engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str());
    }

    if (!isUpdate) //no update file
    {
        //2: try to load game script module
        const char *zipFilename ="code/game.zip";
        
        std::string zipFilePath = utils->fullPathForFilename(zipFilename);

        if (zipFilePath.compare(zipFilename) == 0) //no game zip file use default lua file
        {
            engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str());
        }
        else
        {
            //3: default load game script
            stack->loadChunksFromZIP(zipFilename);
            stack->executeString("require 'main'");
        }
    }
    return true;
}

四、加入新的main入口(配合更新模塊)

對於熱更新,遊戲執行後首先執行main.lua的代碼,main.lua再調用launcher模塊的代碼,launcher根據版本狀況決定接下來的邏輯。
這裏的main.lua放在script目錄裏,執行python腳本後main.lua會複製到對應的src目錄下

//main.lua
function __G__TRACKBACK__(errorMessage)
print("----------------------------------------")
print("LUA ERROR: " .. tostring(errorMessage) .. "\n")
print(debug.traceback("", 2))
print("----------------------------------------")
end

local fileUtils = cc.FileUtils:getInstance()
fileUtils:setPopupNotify(false)
-- 清除fileCached 避免沒法加載新的資源。
fileUtils:purgeCachedEntries()

cc.LuaLoadChunksFromZIP("code/launcher.zip")

package.loaded["launcher.launcher"] = nil
require("launcher.launcher")

五、代碼地址

https://github.com/chenquanjun/Cocos2dxEncyptAndUpdate

相關文章
相關標籤/搜索