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


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

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

一、介紹

lua相對於c++開發的優點之一是代碼可以在運行的時候才加載,基於此我們不僅可以在編寫的時候熱更新代碼(針對開發過程中的熱更新將在另外一篇文章中介紹),也可以在版本已經發布之后更新代碼。

二、熱更新模塊

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

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

launcher模塊的具體介紹可以看原文,不過這里的更新邏輯是稍微修改了的。
先請求服務器的launcher模塊文件,如果本地launcher模塊文件和服務器不同則替換新的模塊再重新加載。
具體更新邏輯如流程圖所示:

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

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

原文的md5信息(flist)是通過lua代碼調用引擎模塊生成,但是鑒於工程太大不利於分享(其實目的只是要生成文件md5信息),所以這里把代碼改成python版本的了。

注意,如果你也想要嘗試把lua改成其他語言實現,你可能會發現生成的md5和lua版本的不同,這是因為lua版本將字節流轉換成大寫的十六進制來生成md5的。

#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腳本代碼(還是基於上個教程的腳本增加代碼)

#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定義了不復制的文件類型(1、加密資源,如png文件;2、無用資源,如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!'

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

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

  • 坑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還是熱更新(復制的目錄不同)


3、引擎修改

因為代碼已經加密,而且加入了熱更新模塊,所以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;
}

4、加入新的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")

5、代碼地址

https://github.com/chenquanjun/Cocos2dxEncyptAndUpdate


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM