那么最简单的方法当然是通过添加 local.properties 文件来解决这个问题,可是我看了一眼 Android Studio 生成的 local.properties 文件的内容:
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Thu Mar 22 17:12:39 CST 2018
ndk.dir=/Users/helihua/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/helihua/Library/Android/sdk
这货原本就是 Android Studio 创建 Android 工程时或者说是打开工程时根据当前的系统变量自行生成或者设置的,而且明确告知别把这个文件放到 git 等版本管理控制中去,也就是说别把你本地的一个配置给推送到服务器上去,防止别人从服务器上拉取下来你这个本地配置,这样就可能导致团队中其他的小伙伴在编译的过程中出错。
那么我们自然也不能选择这种方式了,而且 Android Studio 这货非常省心地帮你把这个文件都给放到了工程的 .gitignore 中去了,如下:
Running dex as a separate process.
To run dex in process, the Gradle daemon needs a larger heap.
It currently has 1024 MB.
For faster builds, increase the maximum heap size for the Gradle daemon to at least 2560 MB (based on the dexOptions.javaMaxHeapSize = 2g).
To do this set org.gradle.jvmargs=-Xmx2560M in the project gradle.properties.
For more information see https://docs.gradle.org/current/userguide/build_environment.html
:mergeDebugJniLibFolders UP-TO-DATE
:transformNativeLibsWithMergeJniLibsForDebug
:processDebugJavaRes NO-SOURCE
:transformResourcesWithMergeJavaResForDebug
:validateSigningDebug
:packageDebug
Expiring Daemon because JVM Tenured space is exhausted
Daemon will be stopped at the end of the build after running out of JVM memory
:packageDebug FAILED
BUILD FAILED
Total time: 1 mins 26.532 secs
Expiring Daemon because JVM Tenured space is exhausted
]
exit code: 1
近期我们的游戏 SailCraft 准备上线一个新的版本,在该版本中我们采用了 APK + Obb 文件的方式来实现资源的分发,尽可能减少玩家直接通过 CDN 下载游戏资源文件的概率。这是基于前一段时间我们游戏在 Google Play 中获得了一周的新游推荐之后,从玩家注册转化率数据上发现了在部分地区的玩家转化率明显不正常,跟踪分析之后发现是由于我们客户端的一个错误实现 + 玩家所在地区网络带宽太小这两个原因,致使玩家无法顺畅地进入游戏的事实得出来的判断。由于我们在海外 Android 的分发完全依赖 Google Play,这样就意味着我们所有的用户都能顺畅地从 Google Play 下载安装游戏,依托 Google Play 在全球的网络优化,我们完全可以不用考虑安装包分发的问题。由于时间的原因和我个人在 Obb 上的一些不太愉快的经历,之前确定资源文件分发方案的时候,我们过于理想化地认为只要我们选择最好的 CDN 厂商,应该就能解决我们的问题,所以最终我们在上线时并未考虑使用 Google Play 提供的 Obb 文件机制。
最终事实告诉我们 Google Play 在全球的网络优化和分发能力远不是我们这种小团队可以想象的,而且使用 Obb 文件还有一个额外的好处就是可以大量减少我们使用的 CDN 服务的流量费用。经历了这次 Google Play 全球新游推荐,让我们更多地接触到了平时可能触及不到的一些地区的玩家,也帮我们收集了更多数据更是直接反映出来了很多问题,其中之一就是使用 Obb 文件的必要性。既然箭已在弦上,那就不得不搞了。我们选择的做法是,直接将未打包在 APK 里头的所有资源打包成 Zip 包,作为 Obb 文件上传到 Google Play 后台,下载成功后首次启动游戏时,将 Zip 中文件解压存放到游戏专属的 SD 卡的 data 子目录中,后续的资源更新也是直接将更新资源下载到该 data 子目录中。
确定方案
那么我们先来梳理一下 Google Play 是如何处理 Obb 文件的,先来看看我们为什么要用 Obb 文件:
Google Play 目前对于 API 等级 9 以上的 APK 支持最大文件大小为 100M,对于 API 等级为 8 及以下的 APK 文件大小限制为 50M(那种设备对于我们游戏开发厂商来讲毫无价值,我们也不会支持)。
目前绝大部分游戏厂商的产品最终的文件大小都是超过 100M 的,我们目前线上的版本完整包的大小时 130M,所以我们的做法是将 APK 大小控制在 95M 左右,将其他的 40M 大小的资源文件打包成 Zip 包作为 Obb 文件进行上传。
既然这样的话,那么当我们从 Google Play 中下载安装游戏时,Google Play 会先在 /sdcard/Android/obb 目录下创建一个以游戏包名(例如:com.seabattle.uq)命名的目录,然后将我们上传到后台的 Obb 文件下载到这个目录下。首次启动游戏时,我们可以直接将该 Obb 文件当作 Zip 包进行解压,将解压的资源文件保存到 /sdcard/Android/data/com.seabattle.uq/ 目录下,游戏内读取资源只从 APK 包内部和该目录进行读取即可。如果后续有某个资源文件需要进行更新,可以直接从 CDN 上下载,将更新的资源文件也保存到该目录即可。
Google Play 在下载安装 APK 的时候,未能成功地将 Obb 文件下载下来,这个 Google 官方的说法是这样的:Expansion files are hosted at no additional cost. When possible, Google Play downloads expansion files when apps are installed or updated. In some cases, your app will need to download its expansion files.
那这个时候玩家的设备上都没有 Obb 文件,我们怎么办,解压个鬼啊?别担心,Google 自己也是考虑到了这些情况的,所以 Google 官方是有提供一个完整的 Obb 文件下载解决方案的,就是为了让大家可以快速地集成一个手动从 Google Play 下载 Obb 文件的服务到我们已有的项目里头来的,代码就在[ANDROID_SDK_PATH]/extras/google/目录下,分别是以下三个目录:
Tests whether the file or directory denoted by this abstract pathname exists.
Returns:
true if and only if the file or directory denoted by this abstract pathname exists; false otherwise
Throws:
SecurityException – If a security manager exists and its SecurityManager.checkRead(java.lang.String) method denies read access to the file or directory
那么这个 F_OK 是个什么鬼啊,这就要去看 Linux 中 access(2) 的文档了,因为 Android 实际上最终调用的就是这个方法。
access() checks whether the calling process can access the file
pathname. If pathname is a symbolic link, it is dereferenced.
The mode specifies the accessibility check(s) to be performed, and is
either the value F_OK, or a mask consisting of the bitwise OR of one
or more of R_OK, W_OK, and X_OK. F_OK tests for the existence of the
file. R_OK, W_OK, and X_OK test whether the file exists and grants
read, write, and execute permissions, respectively.
这么看来一线大厂们基于自己多年分发游戏的积累,已经形成了一套非常稳定可靠的资源更新下载的机制,可以不依赖单个分发平台提供的便利机制,而使用自有的资源分发机制,这样可以降低项目的复杂性和不同平台上维护的难度,不失为一种可行的方案。但是对于中小厂商,由于在全球发行上并未积累太多的经验,很大的程度上选择依托 Google Play 这样成熟的平台会更有优势,所以此时可能只能做退一步的选择了,那就是为了尽可能利用 Google Play 提供的 Obb 文件分发机制减少使用 CDN 可能带来的下载问题和流量费用,但是鉴于目前可能存在的 obb 目录读取权限的问题,在目录出现访问权限的问题时主动申请权限,如果在某些设备上刚好运气不错可以直接访问 obb 目录下的内容的话,就可以直接进行解压了,不需要动态申请该权限了。至于这会影响到多少玩家因为游戏主动申请 SD 卡权限而选择放弃这款游戏或者去 Google Play 中给一个差评,这就很难讲了。作为技术执行者,我们能做到的这已经是极致了。