由于国内APP大都不需要发布到Google Play, 所以关于Google Play APK扩展文件的中文资料比较少。不过好在Google官方文档已经介绍的很详细了,本文部分内容也来自该文档。
官方参考链接:https://developer.android.com/google/play/expansion-files.html
关于Google Play的说明:Google Play有两种含义,一种指的是Google提供的应用发布平台,另一种指的是手机上的安装的Google Play商店这个APP。本文使用Google Play大多数情况下都是指的第一种含义,有时也会用作第二种含义,需要加以区分。
Google Play目前对apk文件的大小限制为100M,也就是说开发者在Google Play上发布app时,只能上传小于100M的apk文件。100M对一般的应用来说也足够了,但是对于游戏来说就显得有点太小了,很多游戏动辄几百M,甚至几个G的体积根本无法作为一个apk文件发布到Google Play上。 这时就需要应用在打包时做分包处理,核心的逻辑代码编译到apk文件中,脚本及资源部分则放到几个扩展文件中。然后分别将apk文件和扩展文件上传到Google Play。当app运行时,读取扩展文件内容,执行其中的代码和加载资源。
目前Google Play支持一到两个扩展文件,每个扩展文件最大为2GB,也就是说整个app包括扩展文件在内最大不能超过4.1GB(两个2GB的扩展文件 + 一个100MB的apk文件)。Google Play对扩展文件的最小字节数没有限制, 只要不上传空文件就行,也不要求apk文件和扩展文件加起来要大于100MB,因此,上传一个1MB的apk文件和一个1KB的扩展文件也是可以的。
Google Play将两个扩展文件分别称为main扩展文件和patch扩展文件,第一次上传的扩展文件作为main扩展文件,之后可以追加patch扩展文件。虽然从名字上看,main扩展文件是一个主要的文件,而patch扩展文件则是一个补丁文件,但实际上main扩展文件和patch扩展文件对Google Play来说只是两个不同文件名的文件,它们的地位是完全一样的。Google Play对这两个的功能和使用没有做任何限制,完全由开发者自己决定哪些资源要放到main扩展文件中,哪些要放到patch扩展文件中。
两个扩展文件下载下来后的文件名是固定的,main扩展文件的文件名是main.
Google Play对扩展文件的格式没有任何要求,可以是任意类型的文件。不过由于最多只能上传两个扩展文件,所以一般都会使用压缩文件的格式,以zip格式居多。
apk扩展文件下载到手机后放置的位置同样是固定的,始终在
// 代码来源于后文提到的Downloader Library,可以直接使用Downloader Library中的相关方法。
// 不需要自己实现,这里只是展示如何拼接起扩展文件的下载路径。
public static final String EXP_PATH = File.separator + "Android"
+ File.separator + "obb" + File.separator;
static public String getSaveFilePath(Context c) {
File root = Environment.getExternalStorageDirectory();
String path = root.toString() + EXP_PATH + c.getPackageName();
return path;
}
完整的APK扩展文件开发流程包含以下几个部分。
Unity3D已经提供了分包的机制,使用也很方便,只需要在导出时勾选一个选项即可。
参考链接:http://docs.unity3d.com/Manual/android-OBBsupport.html
其他不提供分包的机制的引擎或原生app,需要自己实现分包机制。简单的做法就是将资源文件夹(assets)压缩成zip格式的文件作为扩展文件,剔除assets文件夹后的剩余部分打包成apk文件。Android提供了一个jobb的工具,可以完成类似的工作。
参考链接:https://developer.android.com/studio/command-line/jobb.html
直接在Google Play开发者控制台中上传即可。需要注意的是APK扩展文件上传之后可以被多个APK版本所复用,也就是说可以只更新APK,不更新APK扩展文件。但是反过来不行,如果要更新APK扩展文件,一定要同时更新APK文件。由于Google Play更新APK文件时要求每次的版本号都要比上次的大, 所以即使APK文件自身没有任何变化,也需要修改AndroidManifest.xml中的版本号,然后重新打包生成APK文件上传。
大多数情况下,当用户从Google Play上下载应用时,Google Play会自动将APK文件和扩展文件同时下载下来, 应用启动时就可以直接使用扩展文件了。但是Google Play并不总是保证一定会下载扩展文件, 官方文档中的原文是”Most of the time, Google Play downloads and saves your expansion files at the same time it downloads the APK to the device. However, in some cases Google Play cannot download the expansion files.”,至于具体是哪些cases下Google Play无法下载扩展文件并没有说明,此外即使Google Play正确的下载了扩展文件,但是由于扩展文件存放的目录是可以被用户和其他应用访问的,下载下来的扩展文件有可能会被用户或其他应用删除。为此,开发者必须在应用中自行实现扩展文件完整性检查和下载的机制。
Google提供了一个辅助APK扩展文件下载的库Downloader Library来简化扩展文件检查和下载的逻辑,如果要实现下载扩展文件,还需要结合Google Play提供的另一个库License Verification Library (LVL) 。当APP中调用Downloader Library的接口来下载扩展文件时,Downloader Library会调用LVL的接口,LVL则会调用Google Play的进程间接口来下载,所以最终执行下载的其实还是Google Play。这两个库都可以通过Android SDK Manager来下载,如图所示。
这两个库下载后分别保存在android sdk的/extras/google/play_apk_expansion和/extras/google/play_licensing中。
这两个库下载完成后,参数官方文档中给出的流程一步步实现下载过程即可。
https://developer.android.com/google/play/expansion-files.html#Downloading
值得注意的是,这两个库可能很久都没有维护了,所以是存在一些bug的。有两个bug必须改掉。
在com.google.android.vending.licensing.LicenseChecker类的checkAccess()中,有这样一段代码
boolean bindResult = mContext.bindService(new Intent(new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))),
this, // ServiceConnection.
Context.BIND_AUTO_CREATE);
这段在代码在Android5.0上会抛出IllegalArgumentException,需要替换成如下代码。
Intent serviceIntent = new Intent(new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")));
serviceIntent.setPackage("com.android.vending");
在com.google.android.vending.expansion.downloader.DownloaderClientMarshaller类的connect()方法中,有这样一段代码。
if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) {
if ( Constants.LOGVV ) {
Log.d(Constants.TAG, "Service Unbound");
}
} else {
mBound = true;
}
这里会导致绑定的服务在某些情况下无法启动,服务不启动,IDownloaderClient接口的onServiceConnected()方法就不会执行,mRemoteService为null,从而导致NullPointerException。虽然在使用mRemoteService前增加对其是否为null的判断可以避免crash,但是下载过程仍然无法监控,无法得到下载的结果。需要将这段代码替换成如下代码。也就是将BIND_DEBUG_UNBIND替换成BIND_AUTO_CREATE。
if ( !c.bindService(bindIntent, mConnection, Context.BIND_AUTO_CREATE) ) {
if ( Constants.LOGVV ) {
Log.d(Constants.TAG, "Service Unbound");
}
} else {
mBound = true;
}
当上述下载逻辑实现完成后还需要对代码进行测试,看是否能够正确的下载APK扩展文件。测试流程可以参照官方文档中给出的步骤来进行。
https://developer.android.com/google/play/expansion-files.html#Testing
这里需要注意的是,要想能够成功下载APK扩展文件,执行测试的设备需要满足如下条件。
很多时候下载不了APK扩展文件,可能并不是实现方式不对,而是需要检查下测试设备是不是满足了下载的条件。
APK扩展文件的使用通常需要由开发者自行实现,扩展文件一般都是一个zip格式的压缩包,可以解压后再使用。不过Android SDK中附带了一个APK Expansion Zip Library(在SDK的/extras/google/google_market_apk_expansion/zip_file/目录中),可以直接读取zip文件中的内容,官方建议是不解压,直接用APK Expansion Zip Library来访问zip文件中包含的其他文件。此外,即使需要解压,也不要解压到扩展文件所在的目录中,而是放到getExternalFilesDir()所在的目录中。