- 精通Cocos2d-x游戲開發(進階卷)
- 王永寶
- 3213字
- 2020-11-28 22:37:00
3.5 自動打包工具
手寫Manifest文件極其低效且容易出錯,而Cocos2d-x官方又沒有給出對應的工具,所以需要實現一個自動對比差異并打包的小工具來代替手動編輯Manifest文件。
我們希望用新版本的資源覆蓋了舊版本的資源之后,通過工具來自動對比版本差異,為差異的資源自動生成Manifest文件。通過不同的打包方式,可以有兩種增量更新的方法,這兩種方法各有利弊,這里簡單分析一下。
第一種是逐文件更新的方法,把要更新的資源放到服務器上,客戶端每次更新時對比出遠程Manifest文件和本地Manifest文件的差異資源,然后逐個下載。這種方式的好處是,當隔了很久沒有去更新時,不論一個文件被修改了多少次,都只更新一次該文件,并且支持在新版本中刪除資源文件。而缺點也很明顯,如果文件很多,那么就需要下載很多次,一次只能下載一個文件,并且文件沒有經過壓縮,既消耗流量又消耗服務器資源,而且所有的資源都可以被訪問(這也降低了資源的安全性,增加了資源被盜用的風險)。如果修改以圖片為主(這里說的是修改而不是新增),那么可以用這種方式,因為圖片本身的zip壓縮率就很低。
假設游戲引用了名字為1.png~100.png的100張png圖片,首次更新需要更新88.png,那么就把88.png放到服務器的資源目錄下,生成只記錄88.png的Manifest文件。第二次又更新了1.png~50.png,那么將這50張圖片放到服務器的資源目錄下,生成記錄88.png以及1.png~50.png的Manifest文件。服務器的資源目錄會一直保持與客戶端本地的更新目錄一一對應的關系。如果第三次更新刪除了88.png,那么客戶端更新到該版本之后本地更新目錄的88.png也會被刪掉,但安裝包中最老版本的88.png是不會被刪除的。
第二種是打包更新的方法,把本次要更新的資源打包然后放到服務器上,每個小版本都是一個更新包。每次更新時服務器的Manifest文件都會新增這個更新包到資源列表中,如果我們發布了10個版本,那么Manifest文件中會記錄這10個版本的更新包。這種方式的好處是每次更新只需要更新一個文件即可,相對來說會更省流量,服務器的壓力也會更小,但缺點是如果有很多較大的文件是頻繁修改的,如巨大的公共圖集,每次都修改了這個圖集,那么10個zip包中就會有10張圖集,如果隔了很久沒有去更新時,就需要將中間每一個版本的更新包都下載下來,而且服務器的資源目錄和客戶端本地的更新目錄不是一一對應的,無法實現刪除某資源的操作。
對于打包更新的方法,可以通過優化打包的方式來彌補它的缺點,在每次打包時,遍歷之前打好的包,將重復的資源剔除,如果之前的包中只剩下要剔除的資源,則刪除這個包,并更新Manifest文件。這樣既可以保證資源都被壓縮,也可以避免當用戶長期不登錄時,再次登錄時需要下載大量的冗余重復資源。
1.設計自動打包工具
接下來實現這個自動打包的小工具,用這個小工具來自動計算出每次版本更新的差異資源,并生成Manifest文件,而不是手動整理出差異資源并手寫Manifest文件。打包工具的需求如下。
每次要發布新版本的時候,執行一次打包工具,指定資源路徑以及版本號,打包工具自動遍歷所有資源,對比與上一個版本資源的差異??梢赃x擇將差異資源復制到一個資源發布目錄下,或將差異資源打包壓縮再移動到資源發布目錄下,并生成Manifest文件。
在運行打包工具之前,先將資源放到打包工具下的資源目錄下,覆蓋舊的資源。首先需要用一個資源列表文件來記錄最后一次更新時所有文件的狀態,拿到最新版本的資源列表release.assets,如果這是第一個版本,那么直接生成這個版本的資源列表即可,后面所有的變化都是在這個基礎版本的資源上進行變化的。可以遍歷整個資源目錄,將每一個資源的路徑及其MD5碼記錄到資源列表中。
如果獲取到了最新(上一個)版本的資源列表,則遍歷整個資源目錄,生成新版本的資源列表,對比兩個資源列表,將新增以及MD5值不同的資源整理出來,打包成一個zip包,然后將新版本的資源列表保存為release.assets,替換為最新版本的資源列表,最后生成最新版本的Manifest文件。
也可以選擇不生成zip包,實際上不生成zip包就是把最新的資源目錄整個發布出去,然后將完整的資源列表記錄到Manifest文件中。
我們需要解決以下幾個問題。
? 如何遍歷目錄下的所有文件。
? 如何讀寫文件。
? 如何獲取文件的MD5。
? 如何使用JSON編碼和解碼。
? 如何使用zip壓縮文件。
這樣的一個小工具用PHP或Python可以很方便地實現,這里使用PHP來實現。雖然PHP是專門用于實現Web服務器的腳本語言,但也可以用它來實現一些命令行小工具,在命令行中執行PHP解釋器,傳入要執行的PHP腳本即可在命令行中執行PHP, quick-cocos2d-x的很多命令行工具都是這么實現的。
2.使用打包工具
接下來了解一下如何使用打包工具進行打包,首先需要從控制臺進入打包工具的目錄,直接運行腳本不輸入任何參數會彈出介紹說明,如圖3-4所示。如果在Mac下的幫助說明是亂碼,只需要將lib/pack_assets.php的文件編碼轉為UTF-8即可。

圖3-4 打包工具
下面先執行一條命令來生成基礎1.0.0版本。在Windows下執行packassets.bat res res/url http://localhost/test/ m zip命令,Mac下將packassets.bat修改為./packassets.sh,如圖3-5所示,這條指令會將當前的res目錄下的資源進行打包,傳入指定的URL以及打包模式。由于是基礎版本,所以不會生成增量更新包,只是記錄了所有文件的狀態。

圖3-5 生成基礎版本
然后在res目錄中隨意添加新文件,并對舊的文件進行修改,再執行一次剛才輸入的命令,如圖3-6所示,打包工具會自動對比出差異的文件并進行打包。打包的新版本號會自動在舊版本號的最后一位自增一,也可以通過version參數指定版本。默認會將生成的包生成到當前目錄下的release目錄中,也可以通過release參數指定發布路徑。使用m參數可以指定zip打包和file逐文件打包兩種模式。打包參數的作用在PHP腳本中有詳細的介紹,這里不再細述。

圖3-6 生成增量包
執行完命令之后會在指定的release目錄下輸出project.manifest、version.manifest、對應的版本資源以及記錄所有資源詳情的release.assets文件,如圖3-7所示。將release目錄下的文件復制到服務器的下載路徑下(如在nginx的html目錄下,根據指定的URL相對路徑test目錄中),即可發布新版本。

圖3-7 release發布目錄
3.自動打包工具的實現
接下來簡單介紹一下這個小工具是如何實現的。首先需要將Windows和Mac下的PHP程序放到對應的目錄下,然后編寫一個bat和一個shell腳本,這種方式是參考quick-cocos2d-x的命令行工具實現,感興趣的讀者可以下載quick,然后打開quick下的bin目錄學習一下。如不關心打包工具的實現,可以跳過這一節。
pack_assets.bat腳本的代碼如下。
@echo off set DIR=%~dp0 %DIR%win32\php.exe "%DIR%lib\pack_assets.php" %*
pack_assets.sh腳本的代碼如下。
#!/bin/bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" php "$DIR/lib/pack_assets.php" $*
然后在當前目錄的lib目錄下新建pack_assets.php文件,輸入以下代碼。
<?php require_once 'jsbeautifier.php'; define('DS', DIRECTORY_SEPARATOR); /* 自動打包腳本 1.對比差異 2.自動打包 輸入參數: res: 要打包的資源路徑 url: 下載鏈接 release: 發布的路徑 asset: 默認為發布路徑下的version.manifest version: 版本號[默認為1.0.0 或最后一個版本自增0.0.1] 2016-7-5 by寶爺 */ function help() { echo <<<EOT 本工具可用于Cocos2d-x增量更新自動打包 生成增量更新包之前需要生成基礎版本資源列表(用于對比差異) 有兩種情況會生成基礎版本資源列表: 1.首次運行或找不到最新資源列表時會自動生成 2.輸入新的大版本號時,如2.0.0 之后修改資源目錄,再次運行本工具即可生成更新資源包 如果是file模式,生成了新的version和project文件之后,只需要將所有資源放到發布 目錄下即可 大版本更新需要先清空release目錄,此操作并不頻繁,且需要慎重,所以本工具不進行自 動處理 輸入參數: res: 要打包的資源路徑 release: 發布的路徑[默認為當前目錄下的release目錄] version: 版本號[默認為1.0.0或最后一個版本自增0.0.1] m: 打包模式[默認為zip模式打包zip,可選file模式不打包] url: 下載鏈接[用于寫入version和project Manifest文件中] 注意,上述的路徑均為當前目錄的相對路徑 Example: pack_assets res ./res/ release../mygame/release/ m zip url http://localhost/ test/ version 1.1.0 EOT; } $options = array(); #檢查輸入參數 function checkArgs() { global $argc, $argv, $options ; $argsCheck = array( "res", "release", "m", "version", "url" ); $argcCheck = count($argsCheck); # 獲取輸入的參數 for($idx = 1; $idx < $argc; $idx++) { if($idx + 1 < $argc) { for($i = 0; $i < $argcCheck; ++$i) { if($argv[$idx] == $argsCheck[$i]) { $options[$argv[$idx] ] = $argv[++$idx]; print($options[$argv[$idx] ]); break; } } } } # 設置默認參數 if(! array_key_exists("res", $options) || ! array_key_exists("url", $options)) { help(); return false; } if(! array_key_exists("release", $options)) { $options["release"] = $options["res"] . "/../release/"; } if(! array_key_exists("m", $options)) { $options["m"] = "zip"; } return true; } # 獲取最后一次發布的所有文件狀態 function getLastRelease(array & $lastRelease) { global $options ; if(file_exists($options["release"] . "release.assets")) { $jsonFile = file_get_contents($options["release"] . "release.assets"); $lastRelease = json_decode($jsonFile, true); return true; } return false; } # 獲取版本,輸入的版本應該以a.b.c的格式 function getVersion($lastVersion) { global $options ; if(array_key_exists("version", $options)) { return $options["version"]; } if($lastVersion ! = null) { $verArray = explode('.', $lastVersion); if(count($verArray) > 0) { $verArray[count($verArray) -1] += 1; return implode('.', $verArray); } } return "1.0.0"; } # 檢查一個版本字符串是不是一個大版本,即類似3.0.0這樣的 function isLargeVersion($version) { if($version == null) return false; $verArray = explode('.', $version); $arrayCount = count($verArray); if($arrayCount > 0) { for($idx = 1; $idx < $arrayCount; ++$idx) { if($verArray[$idx] ! = 0) return false; } } return true; } # 遍歷目錄,找出指定目錄下的所有文件 function findFiles($dir, array & $files) { $dir = rtrim($dir, "/\\"); $dh = opendir($dir); if ($dh == false) { print("\nopen dir error\n"); return; } while (($file = readdir($dh)) ! == false) { if ($file == '.' || $file == '..') { continue; } $path = $dir . '/' . $file; if (is_dir($path)) { findFiles($path, $files); } elseif (is_file($path)) { $files[] = $path; } else { print("error find " . $path); } } closedir($dh); } # 生成一個文件數組對應的MD5數組 function genMD5(array & $files) { $filemd5s = array(); $length = count($files); for($idx = 0; $idx < $length; $idx++) { $filemd5s[$files[$idx]] = md5_file($files[$idx]); } return $filemd5s; } # 生成一個新的Manifest數組 function genVersionManifest($version, $url) { #$url = http://localhost/; $manifest = array( "packageUrl"=> $url, "remoteManifestUrl" => $url . "project.manifest", "remoteVersionUrl" =>$url . "version.manifest", "version" => $version ); return $manifest; } # 將當前版本的更新追加到VersionManifest文件中 function appendVersionManifest(array & $manifest, $subversion) { $manifest["version"] = $subversion; if(array_key_exists("groupVersions", $manifest)) { $count = count($manifest["groupVersions"]) + 1; $manifest["groupVersions"][(string)$count] = $subversion; } else { $manifest["groupVersions"] = array("1" => $subversion); } } # 將當前版本的更新追加到ProjectManifest文件中 function appendProjectManifest(array & $manifest, $subversion, $file, $searchPath) { if(! array_key_exists("assets", $manifest)) { $manifest["assets"] = array(); $manifest["assets"][$subversion] = array(); } $manifest["assets"][$subversion]["path"] = "release" . $subversion . ".zip"; $manifest["assets"][$subversion]["md5"] = md5_file($file); $manifest["assets"][$subversion]["compressed"] = true; #$manifest["assets"][$subversion]["group"] = $group; if($searchPath ! = null) { if(! array_key_exists("searchPaths", $manifest)) { $manifest["searchPaths"] = array ($searchPath); } else { $manifest["searchPaths"][] = $searchPath; } } } # 將文件添加到ProjectManifest文件中 function appendResToProjectManifest(array & $manifest, $file) { global $options ; # 去掉頭部 $path = str_replace($options["res"], "", $file); if(! array_key_exists("assets", $manifest)) { $manifest["assets"] = array(); } if(! array_key_exists($path, $manifest["assets"])) { $manifest["assets"][$path] = array(); } $manifest["assets"][$path]["md5"] = md5_file($file); } # 將指定的文件壓縮到指定的release目錄下的release + 版本號.zip文件 如release1.0.0.zip function genZip($zipfile, array & $files) { global $options ; $zip = new ZipArchive(); echo "compress to " . $zipfile . "\n"; if (! $zip->open($zipfile, ZIPARCHIVE::OVERWRITE | ZIPARCHIVE:: CM_STORE)) { return false; } foreach ($files as $path => $md5) { # 保留res下的相對路徑 $file = str_replace($options["res"], "", $path); echo "\ncompress file " . $file . "\n"; $zip->addFile($path, $file); } $zip->close(); return true; } #保存Json文件 function saveJsonFile($filePath, $manifest) { # $options = new BeautifierOptions(); $beautifier = new JSBeautifier($options); $file = fopen($filePath, "w"); $jsonStr = $beautifier->beautify(json_encode($manifest)); fwrite($file, str_replace("\\", "", $jsonStr)); fclose($file); } # 自動生成包 function releaseVersion() { global $options ; # 檢查參數 if(! checkArgs()) return false; # 生成更新包 $lastRelease = array(); if(getLastRelease($lastRelease) && ! isLargeVersion($options["version"])) { $versionManifest = json_decode(file_get_contents($options ["release"]. "version.manifest"), true); if($versionManifest == null) { echo "decode " . $options["release"] . "version.manifest" . " faile\n"; return false; } $projectManifest = json_decode(file_get_contents($options ["release"] . "project.manifest"), true); if($projectManifest == null) { echo "decode " . $options["release"] . "project.manifest" . "faile\n"; return false; } # 檢查版本更新 $files = array(); findFiles($options["res"], $files); $files = genMD5($files); $diffFiles = array_diff_assoc($files, $lastRelease); print("diff files is: \n"); print_r($diffFiles); # 沒有差異,無須打包 if(count($diffFiles) == 0) return true; # 獲得新版本號 $version = getVersion($versionManifest["version"]); echo "version is " . $version . "\n"; # zip模式打包 if($options["m"] == "zip") { # 生成壓縮包 $zippkg = $options["release"] . "release" . $version . ".zip"; genZip($zippkg, $diffFiles); # 更新VersionManifest和ProjectManifest appendVersionManifest($versionManifest, $version); saveJsonFile($options["release"] . "version.manifest", $versionManifest); appendVersionManifest($projectManifest, $version); appendProjectManifest($projectManifest, $version, $zippkg, null); saveJsonFile($options["release"] . "project.manifest", $projectManifest); } # file模式打包 elseif($options["m"] == "file") { # 創建新的VersionManifest和ProjectManifest $versionManifest = genVersionManifest($version, $options["url"]); saveJsonFile($options["release"] . "version.manifest", $versionManifest); # 把所有文件寫入project.manifest foreach ($files as $path => $md5) { appendResToProjectManifest($versionManifest, $path); } saveJsonFile($options["release"] . "project.manifest", $versionManifest); } # 保存最新的版本資源MD5信息 saveJsonFile($options["release"] . "release.assets", $files); } # 生成基礎版本(不發布) else { $files = array(); findFiles($options["res"], $files); $files = genMD5($files); @mkdir($options["release"] , 0700); # 保存最新的版本資源MD5信息 saveJsonFile($options["release"] . "release.assets", $files); $version = getVersion(null); echo "version is " . $version; # 生成VersionManifest和ProjectManifest $versionManifest = genVersionManifest($version, $options["url"]); saveJsonFile($options["release"] . "version.manifest", $versionManifest); saveJsonFile($options["release"] . "project.manifest", $versionManifest); } } releaseVersion(); echo "\nbuild version success ! ! ! \n" ?>