背景
Google Play 对 APK 大小限制是 100 M,但是游戏稍微重度一点,资源就会很多,包体很容易就超过了这个限制;Google Play 提供了 obb 分包方案,来解决包体问题。
OBB 是 Opaque Binary Blob 的缩写,是一种类型 zip 文件格式,作为安卓应用的扩展数据包。 参考:安卓开发指南和百度百科
游戏多数资源都不需要在启动游戏时,就加载到内存,通常都是在游戏运行期间,需要时再动态去加载;因此可以:
- 将动态的资源分离出来,打包到 obb 文件中;
- 将 obb 文件和 APK 包一起上传到 Google Play;
- 玩家在下载 APK 时,会同时将 obb 文件下载到手机的
/Android/obb/包名/obb文件
路径; - 首次启动游戏后,解压 obb 文件 到指定目录;
- 将 此目录 添加游戏资源的搜索路径中。
根据上面的思路,这里写了一个 demo,供大家参考,cocos creator 版本 2.2.0,过程如下:
一 新建 Demo
- 新建一个 HelloWorld 工程
- 新建 resources 目录,将图片 icon.png 加入到 resources,作为需要动态加载的资源
- 然后再 ui 上新建一个 Sprite,去掉纹理,在代码中动态加载 obb 中的资源
编辑器中设置如图:
4. 打开 HelloWorld 脚本,新建一个 Sprite 变量,关联到刚刚新建的精灵
5. 在 onLoad 方法动动态加载图片
onLoad: function () {
this.label.string = this.text;
// 动态加载资源代码
cc.loader.loadRes("icon", cc.SpriteFrame, ((err, spf)=>{
if (err) {
console.error(err.message || err);
return;
}
this.sp.spriteFrame = spf;
}));
},
这时候可以用网页运行一下:
这一步就是我们平时正常的项目开发流程,前期是不用管obb分包和打包的,只管开发功能就行。等功能开发好之后,才需要进一步的对接渠道,打包,分包等。
二 制作 obb 分包
- 构建 android 工程, 将上一步的 demo 构建 android 工程
- 找到要分包的资源,打开新构建出的 android 工程,刚刚需要动态加载的资源。
- 分离 obb 分包资源,新建一个目录 ‘raw-assets/0c/’,将资源剪切过来,注意是剪切,原来的资源不要了。
- 制作 obb 分包,用压缩软件将 raw-assets 压缩成 zip 文件,然后改名为
main.1.org.cocos2d.helloworld.obb
,命名规则是[main/patch].[versionCode].[packageName].obb
,到这一步 obb 分包就制作完成了。 - 打包 APK,将分离 obb 文件之后工程,打包成 apk。此时如果运行 apk,图片资源是加载不出来的。
找资源,分离资源,打包重命名,过程都很繁琐,建议通过脚本自动化完成
三 上传下载 obb 包
经过上一步构建和分离资源得到了一个 obb 文件,如果是正式上线,需要将分离资源后的工程打包成 apk,然后将 obb 文件 和 apk 一起,提交到 Google Play,玩家下载 apk 时,就会将 obb文件一起下载到手机中的 /Android/obb/包名
中。为了测试,直接把生成的 obb 直接拷贝到这个目录中:
拷贝的方式,不同的模拟器或是手机,方式不一样,请自行搜索。
四 解压 obb 包
obb 文件已经下载到手机之后,游戏首次启动需要先将 obb 文件解压到指定目录。为了能找到资源,还需要将解压的目录,加入到搜索路径中。
- 添加搜索路径,修改 HelloWorld.js ,在加载资源之前,将搜索路径加上。
onLoad: function () {
// 将解压目录添加到资源搜索路径中
if (window.jsb) {
let obbDir = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : "/") + "obb")
jsb.fileUtils.addSearchPath(obbDir, true);
console.log(obbDir);
}
this.label.string = this.text;
cc.loader.loadRes("icon", cc.SpriteFrame, ((err, spf)=>{
if (err) {
console.error(err.message || err);
return;
}
this.sp.spriteFrame = spf;
}));
},
- 解压 obb 资源,在 APPActivity 的 onCreate 方法中,添加如下代码解压资源:
String obbFilepath = Utils.getInstance().getObbFilepath();
String outPath = "/data/data/org.cocos2d.helloworld/files/obb/res";
Utils.unzipFile(obbFilepath, outPath);
- 实现解压方法,在制作 obb 的过程可以知道 obb 实际上就是 zip 文件,因此只需要 zip 文件解压就可以了,这里提供一个 作为参数:
// 获取 obb 文件路径
public String getObbFilepath() {
try {
Cocos2dxActivity context = (Cocos2dxActivity) AppActivity.getContext();
String packageName = context.getPackageName();
int versionCode = context.getPackageManager().getPackageInfo(packageName, 0).versionCode;
return context.getObbDir().getPath()
+ File.separator
+ "main."
+ versionCode
+ "."
+ packageName
+ ".obb";
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
Log.e(TAG, "getFilepath fail");
return "";
}
}
// 解压 zip 文件
public static boolean unzipFile(String zipFilepath, String unzipPath) {
try {
File zipFile = new File(zipFilepath);
if (!zipFile.exists()) {
Log.e(TAG, "no zip file " + zipFilepath);
return false;
}
File outDir = new File(unzipPath);
if (!outDir.exists()) {
if (!outDir.mkdirs()) {
Log.e(TAG, "create unzip dir fail");
return false;
}
}
ZipInputStream zipStream = new ZipInputStream(new FileInputStream(zipFile));
ZipEntry zipEntry;
String entryName;
while ((zipEntry = zipStream.getNextEntry()) != null) {
entryName = zipEntry.getName();
if (zipEntry.isDirectory()) {
entryName = entryName.substring(0, entryName.length() - 1);
File subDir = new File(unzipPath + File.separator + entryName);
if (!subDir.exists() && !subDir.mkdirs()) {
Log.e(TAG, "create unzip sub dir fail " + subDir.getName());
return false;
}
} else {
File subFile = new File(unzipPath + File.separator + entryName);
String parentPath = subFile.getParent();
if (parentPath == null) {
Log.e(TAG, "get sub file parent dir fail" + subFile.getName());
return false;
}
File parentDir = new File(parentPath);
if (!parentDir.exists() || !parentDir.isDirectory()) {
if (!parentDir.mkdirs()) {
Log.e(TAG, "create sub file parent dir fail" + subFile.getName());
return false;
}
}
if (!subFile.createNewFile()) {
Log.e(TAG, "create sub file fail" + subFile.getName());
return false;
}
FileOutputStream out = new FileOutputStream(subFile);
int len;
byte[] buffer = new byte[1024];
while ((len = zipStream.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush();
}
out.close();
}
}
zipStream.close();
return true;
} catch (Exception e) {
Log.e(TAG, "unzipFile fail exception " + zipFilepath + " " + unzipPath);
e.printStackTrace();
return false;
}
}
- 重新打开 apk,然后运行可以看到,现在图片资源显示出来了。
打开我们的解压目录,可以看到解压之后的资源
*** 若是压缩包内容很大,可以考虑用C++实现解压 ***
// 补充CPP版本
FileUtils *_fileUtils;
struct AsyncData
{
std::string zipFile;
std::string outPath;
bool succeed;
};
std::string basename(const std::string& path) const
{
size_t found = path.find_last_of("/\\");
return std::string::npos != found ? path.substr(0, found) : path;
}
bool unzip(AsyncData *data)
{
#define BUFFER_SIZE 8192
#define MAX_FILENAME 512
const std::string rootPath = basename(data->outPath) + "/";
const std::string &zip = data->zipFile;
// Open the zip file
unzFile zipfile = unzOpen(FileUtils::getInstance()->getSuitableFOpen(zip).c_str());
if (! zipfile)
{
CCLOG("unzipFile : can not open zip file %s\n", zip.c_str());
return false;
}
// Get info about the zip file
unz_global_info global_info;
if (unzGetGlobalInfo(zipfile, &global_info) != UNZ_OK)
{
CCLOG("unzipFile : can not read file global info of %s\n", zip.c_str());
unzClose(zipfile);
return false;
}
// Buffer to hold data read from the zip file
char readBuffer[BUFFER_SIZE];
// Loop to extract all files.
uLong i;
for (i = 0; i < global_info.number_entry; ++i)
{
// Get info about current file.
unz_file_info fileInfo;
char fileName[MAX_FILENAME];
if (unzGetCurrentFileInfo(zipfile,
&fileInfo,
fileName,
MAX_FILENAME,
NULL,
0,
NULL,
0) != UNZ_OK)
{
CCLOG("unzipFile : can not read compressed file info\n");
unzClose(zipfile);
return false;
}
const std::string fullPath = rootPath + fileName;
// Check if this entry is a directory or a file.
const size_t filenameLength = strlen(fileName);
if (fileName[filenameLength-1] == '/')
{
//There are not directory entry in some case.
//So we need to create directory when decompressing file entry
if ( !_fileUtils->createDirectory(basename(fullPath)) )
{
// Failed to create directory
CCLOG("unzipFile : can not create directory %s\n", fullPath.c_str());
unzClose(zipfile);
return false;
}
}
else
{
// Create all directories in advance to avoid issue
std::string dir = basename(fullPath);
if (!_fileUtils->isDirectoryExist(dir)) {
if (!_fileUtils->createDirectory(dir)) {
// Failed to create directory
CCLOG("unzipFile : can not create directory %s\n", fullPath.c_str());
unzClose(zipfile);
return false;
}
}
// Entry is a file, so extract it.
// Open current file.
if (unzOpenCurrentFile(zipfile) != UNZ_OK)
{
CCLOG("unzipFile : can not extract file %s\n", fileName);
unzClose(zipfile);
return false;
}
// Create a file to store current file.
FILE *out = fopen(FileUtils::getInstance()->getSuitableFOpen(fullPath).c_str(), "wb");
if (!out)
{
CCLOG("unzipFile : can not create decompress destination file %s (errno: %d)\n", fullPath.c_str(), errno);
unzCloseCurrentFile(zipfile);
unzClose(zipfile);
return false;
}
// Write current file content to destinate file.
int error = UNZ_OK;
do
{
error = unzReadCurrentFile(zipfile, readBuffer, BUFFER_SIZE);
if (error < 0)
{
CCLOG("unzipFile : can not read zip file %s, error code is %d\n", fileName, error);
fclose(out);
unzCloseCurrentFile(zipfile);
unzClose(zipfile);
return false;
}
if (error > 0)
{
fwrite(readBuffer, error, 1, out);
}
} while(error > 0);
fclose(out);
}
unzCloseCurrentFile(zipfile);
// Goto next entry listed in the zip file.
if ((i+1) < global_info.number_entry)
{
if (unzGoToNextFile(zipfile) != UNZ_OK)
{
CCLOG("unzipFile : can not read next file for decompressing\n");
unzClose(zipfile);
return false;
}
}
}
unzClose(zipfile);
后记
obb 分包制作流程就是这样的,但是有几点内容还需要进一步的完成:
- obb 资源分离,需要一个配置文件,来指定哪些资源需要移到 obb 文件中;另外还需要一个自动化脚本,将配置文件中指定的资源,移动到 obb 文件对应的目录,然后压缩重命名;
- 解压 obb 文件,按照 demo 的方法,每次都会解压,需要一个标记,只有首次安装,或者是解压目录不存在时才需要解压
- 热更新,如何用热更新下来的资,覆盖掉obb 文件的资源?其实解压obb文件的,就是为了方便后续做热更新,不然其实可以不用解压,可以有方法直接读取 obb 文件中的内容。