首先,对于传统的Android开发领域来讲,分包指的是MultiDex,即将单个dex拆分红多个以突破函数数目瓶颈的技术。而这里的分包(Apk Expansion Files)指的是将Apk文件和大容量的资源文件分开打包,大容量的资源文件包括高清大图,音频文件,视频文件等,这些文件最终都会压缩到统一的.obb文件里。注意,抽出到obb的内容不包括运行时代码。因此开发者须要保证在缺乏.obb文件的状况下,程序依然能正常运行(不会Crash)。java
在分包以前,开发者须要明确项目中的大容量资源文件到底是什么,大多数状况下,他们指的是assets目录下的资源以及raw下的文件,若是drawable和mipmap目录下有超过1M的文件,也能够考虑将其进行分包处理,这种状况下须要开发者将该资源的引用方式从直接使用资源id:R.drawable.xxx改成从文件中解析。android
全部的资源文件将被压缩为obb文件,最终上传到GooglePlay供用户下载。数组
什么是obb文件,obb全称是Opaque Binary Blob,翻译过来是不透明的二进制对象,再进一步解析就是具备访问权限的二进制文件。看到这个定义很容易联想到另一种文件格式——zip压缩包文件。因此,从本质上来讲,obb文件和zip文件是同样的,它们只是在不一样领域上不一样解释罢了。而在Android分包领域,obb还有本身的一些规则。浏览器
obb的命名规则以下:bash
[main/patch].[versionCode].[packageName].obb
复制代码
main.16.com.example.obbtest.obb
复制代码
方法一:并发
官方工具法,Google官方提供了Jobb工具用来生成obb文件,工具能够在 Android\sdk\tools\bin文件夹下找到。这是一个命令行工具,具体用法和参数以下:app
$ jobb -d [全部资源的路径] -o [生成的obb名称(请遵循上述命名规则)] -k [打包密码] -pn [包名] -pv [versionCode(跟obb名称的versionCode一致)]
复制代码
也可使用该工具对obb文件进行解压:ide
$ jobb -d [输出路径] -o [obb文件名] -k [打包所用的密码]
复制代码
方法二:函数
压缩工具法,直接使用Windows或者Mac上的打包工具,将文件压缩成zip包后,更改文件名便可。 工具
main.16.com.example.obbtest.obb
复制代码
方法三:
gradle打包法,即经过在build.gradle中添加压缩脚本的方式,将须要打入obb的资源集体打包的方法。该方法会在后文中进行详细介绍。
本地测试 本地测试的原理是模仿Google Play下载,将obb文件复制到相应的目录。经过Google Play下载的obb文件存放的路径为:
/Android/obb/App包名/
复制代码
因此,经过在/Android/obb/下建立[app包名 如com.example.obbtest]文件夹,并将obb文件复制到该目录下便可模拟Google Play安装App。
线上测试
登陆Google Play Console开发者帐号,打开应用列表,选择须要测试的App:
左边控制栏选择 Release managerment ,而后选择 App Release,最后选择Internal test 的MANAGE INTERNAL TEST发布内部测试版本。
在内部测试里建立新的发布版本:将GooglePlay版本的Apk上传,上传完毕后,点击Apk右侧添加更多按钮,将obb文件提交上去,注意obb文件的命名版本号必须与上传的apk的版本号一致,不然会收到提交版本失败的错误。推荐你们使用不可能用在线上版本的versionCode进行测试,好比手机号码、女友生日等,以避免后续提交正式版本时版本号被占用(不知道为何GooglePlay的内部测试和正式发布的版本号居然不能重复)。
填写剩下内容并发。,回到内部测试管理界面,选择管理测试者,将须要测试的Google帐号提交上去,并将“Opt-in URL”的地址复制下来。
在测试机上登陆测试帐号,在浏览器里打开刚刚的“Opt-in URL”地址,便可加入内测,并能够经过Google Play App下载测试版本的App。
下载完成后,能够在/Android/obb/App包名/下看到一份崭新的obb文件。
解压
第一次安装完app后,须要将obb文件进行解压并将解压后的文件存储到咱们定义的文件夹里(能够是data/data/包名/files/也能够是内置存储下自定义的项目文件夹)。要想解压obb文件,第一步是获取obb文件的本地路径,具体代码以下:
public static String getObbFilePath(Context context) {
try {
return Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/Android/obb/"
+ context.getPackageName()
+ File.separator
+ "main."
+ context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode
+ "."
+ context.getPackageName()
+ ".obb";
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}
复制代码
拿到obb文件路径后,能够开始进行解压了:
public static void unZipObb(Context context) {
String obbFilePath = getObbFilePath(context);
if (obbFilePath == null) {
return;
} else {
File obbFile = new File(obbFilePath);
if (!obbFile.exists()) {
//下载obb文件
} else {
File outputFolder = new File("yourOutputFilePath");
if (!outputFolder.exists()) {
//目录未建立 没有解压过
outputFolder.mkdirs();
unZip(obbFile, outputFolder.getAbsolutePath());
} else {
//目录已建立 判断是否解压过
if (outputFolder.listFiles() == null) {
//解压过的文件被删除
unZip(obbFile, outputFolder.getAbsolutePath());
}else {
//此处可添加文件对比逻辑
}
}
}
}
}
复制代码
谷歌官方有提供解压obb文件的库供开发者使用,叫作APK Expansion Zip Library,感兴趣的小伙伴能够在一下路径下查看。
<sdk>/extras/google/google_market_apk_expansion/zip_file/
复制代码
笔者不推荐使用该库,缘由是这个库已经编写了有一些年头了,当时编译的sdk版本比较低,有一些兼容性的bug须要开发者修改代码后才能使用。因此这里使用的upzip方法是用最普通的ZipInputStream和FileOutputStream解压zip包的方式来实现的:
//这里没有添加解压密码逻辑,小伙伴们能够本身修改添加如下
public static void unzip(File zipFile, String outPathString) throws IOException {
FileUtils.createDirectoryIfNeeded(outPathString);
ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFile));
ZipEntry zipEntry;
String szName;
while ((zipEntry = inZip.getNextEntry()) != null) {
szName = zipEntry.getName();
if (zipEntry.isDirectory()) {
szName = szName.substring(0, szName.length() - 1);
File folder = new File(outPathString + File.separator + szName);
folder.mkdirs();
} else {
File file = new File(outPathString + File.separator + szName);
FileUtils.createDirectoryIfNeeded(file.getParent());
file.createNewFile();
FileOutputStream out = new FileOutputStream(file);
int len;
byte[] buffer = new byte[1024];
while ((len = inZip.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush();
}
out.close();
}
}
inZip.close();
}
public static String createDirectoryIfNeeded(String folderPath) {
File folder = new File(folderPath);
if (!folder.exists() || !folder.isDirectory()) {
folder.mkdirs();
}
return folderPath;
}
复制代码
解压完成后,就能够经过输出文件的路径来访问到咱们须要访问的大容量资源了,文件的读取在这里就不展开了。
下载obb
从Google Play下载和安装App有必定几率会下载到不包含obb文件的apk,或者obb文件被人为删除了。这种状况下,须要开发者到谷歌提供的下载地址处下载相应的obb文件。但是要怎么获取到下载地址呢,这里使用了官方的Downloader Library。
这个库能够经过Android Sdk Manager下载到,打开manager后勾上Google Play Licensing Library package和Google Play APK Expansion Library package点下载便可。但是在我兴高采烈准备大干一场的时候,发现它居然编译不过[捂脸]。这个库和上面说的APK Expansion Zip Library同样,因为年代久远又年久失修,基本不能使用了。折腾了一些时间后,魔改了一个版本,才终于可使用。 这里提供一个编译好的jar包google_apk_expand_helper。具体代码以下:
//随机byte数组,随便填就好
private static final byte[] salt = new byte[]{18, 22, -31, -11, -54, 18, -101, -32, 43, 2, -8, -4, 9, 5, -106, -17, 33, 44, 3, 1};
private static final String TAG = "Obb";
public static void getObbUrl(Context context, String publicKey) {
final APKExpansionPolicy aep = new APKExpansionPolicy(
context,
new AESObfuscator(salt,
context.getPackageName(),
Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID)
));
aep.resetPolicy();
final LicenseChecker checker = new LicenseChecker(context, aep, publicKey);
checker.checkAccess(new LicenseCheckerCallback() {
@Override
public void allow(int reason) {
Log.i(TAG, "allow:" + reason);
if (aep.getExpansionURLCount() > 0) {
//这里就是获取到的地址
String url = aep.getExpansionURL(0);
}
}
@Override
public void dontAllow(int reason) {
Log.i(TAG, "dontAllow:" + reason);
}
@Override
public void applicationError(int errorCode) {
Log.i(TAG, "applicationError:" + errorCode);
}
});
}
复制代码
上述方法中须要提供参数publicKey,这个publicKey能够在GooglePlayConsole中找到。
小结
掌握了上述的方法咱们就已经完成了Apk分包的主要流程了,如下内容将举例说明若是经过配置gradle文件进行多渠道打包,如何在每次打包的时候自动将大容量资源文件压缩成obb等。
例子
假设咱们如今须要发布一个超过100M的安装包到GooglePlay以及应用宝,对于GooglePlay来讲,咱们须要生成小于100M的apk文件和obb文件,而对于应用宝来讲,只须要生成一个完整的apk便可。
那么问题来了,咱们不可能说在打包GooglePlay的时候将资源文件手动移除并修改资源引用的相关逻辑,而后再在打包应用宝的时候将他们放回来,这样作会大大增长开发者的工做量而且增大出错的可能性。那有没有办法在单个工程项目下既能打包GooglePlay的包又能够打包应用宝的包呢?答案是有的,build.gradle中的sourceSets就能够解决这样的问题。
利用sourceSets隔离渠道资源和资源引用代码
假设咱们有一个splash.mp4文件,在应用宝中渠道包中,它被放在了res/raw/目录下。而在googlePlay渠道包中,它被放置在obb文件里,咱们能够这么处理。
首先,在src目录下建立两个新的目录googlePlay和tencent,并在他们的目录下新建java,res和assest文件夹。
在app级别的build.gradle文件中添加GooglePlay和应用宝的渠道信息:
android {
flavorDimensions "default"
productFlavors {
GooglePlay { dimension "default" }
Tencent { dimension "default" }
/** 在AndroidManifest.xml中加入
<meta-data android:name="Channel"
android:value="${CHANNEL_NAME}" />
**/
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [CHANNEL_NAME: name]
}
}
}
复制代码
紧接其后添加sourceSets配置,指定不一样渠道的资源和代码地址,其中main为共有资源和代码,其他的为对应渠道包的资源和代码:
sourceSets {
main {
java.srcDirs = ['src/main/java']
assets.srcDirs = ['src/main/assets']
res.srcDirs = ['src/main/res']
}
GooglePlay {
java.srcDirs = ['src/googlePlay/java']
res.srcDirs = ['src/googlePlay/res']
assets.srcDirs = ['src/googlePlay/assets']
}
Tencent {
java.srcDirs = ['src/tencent/java']
res.srcDirs = ['src/tencent/res']
assets.srcDirs = ['src/tencent/assets']
}
}
复制代码
将splash.mp4放到tencent/res/raw/文件夹下,并为不一样渠道的java文件夹新建包名文件夹以及ResourcesHelper.java,完成后的目录结构以下:
有两点须要注意的地方:
一是java包下必须建立包名文件夹,不然会没法引用到项目下的类。该例子中就是com.example.obbtest包。
二是AndroidStudio中能够经过左下角的Build Variants窗口选择当前须要编译的渠道包类型,当选择GooglePlay时会发现tencent下的java文件失效了。因此,若是须要修改某渠道下的java文件,请先经过Build Variants切换到指定渠道。
最后,针对不一样渠道的ResourcesHelper.java采用不一样的资源获取方式:
GooglePlay版本:
public class ResourcesHelper {
public static void playSplashVideoResource(VideoView videoView){
String filePath = ObbHelper.getCurrentObbFileFolder()+"raw/"+"splash.mp4";
videoView.setVideoPath(filePath);
}
}
复制代码
tencent版本:
public class ResourcesHelper {
public static void playSplashVideoResource(VideoView videoView) {
int resource = R.raw.splash;
String uri = "android.resource://" + videoView.getContext().getApplicationContext().getPackageName() + "/" + resource;
videoView.setVideoURI(Uri.parse(uri));
}
}
复制代码
经过sourceSets隔离渠道资源和资源引用代码在这里就完成了,针对更加复杂的场景,就须要小伙伴根据实际状况进行扩展和修改了。下面咱们来看一下如何在构建时自动将资源打包成obb文件。
构建时生成obb文件
要在构建时生成obb文件就必须经过添加gradle脚原本实现。咱们先在项目目录下新建一个脚本文件flavour.gradle。
而后,要想打包obb文件,就必须知道如今构建的是哪一个渠道的包,那要怎么拿到如今的渠道呢,请看代码:
def String getCurrentFlavor() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern;
if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find())
return matcher.group(1).toLowerCase()
else {
println "NO MATCH FOUND"
return ""
}
}
复制代码
咱们知道obb的本质就是zip文件,因此只要在flavour.gradle中添加压缩文件的方法,就能够达到生成obb的效果了。因为笔者的Groovy语言不精通,因此这里使用java代码来解决,在flavour.gradle中添加:
import java.util.regex.Matcher
import java.util.regex.Pattern
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
ext {
zipObb = this.&zipObb
getCurrentFlavor = this.&getCurrentFlavor
}
//外部压缩方法入口,参数是全部须要压缩文件的目录以及输出路径,一样没有添加压缩密码逻辑,小伙伴们须要的本身添加吧
def static zipObb(File[] fs, String zipFilePath) {
if (fs == null) {
throw new NullPointerException("fs == null");
}
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFilePath)));
for (File file : fs) {
if (file == null || !file.exists()) {
continue;
}
compress(file, zos, file.getName());
}
zos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(zos != null){
try {
zos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//内部递归压缩方法
def static compress(File sourceFile, ZipOutputStream zos, String name) throws Exception {
byte[] buf = new byte[2048];
if (sourceFile.isFile()) {
// 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字
zos.putNextEntry(new ZipEntry(name));
// copy文件到zip输出流中
int len;
FileInputStream inputStream = new FileInputStream(sourceFile);
while ((len = inputStream.read(buf)) != -1) {
zos.write(buf, 0, len);
}
// Complete the entry
zos.closeEntry();
inputStream.close();
} else {
File[] listFiles = sourceFile.listFiles();
if (listFiles == null || listFiles.length == 0) {
// 须要保留原来的文件结构时,须要对空文件夹进行处理
zos.putNextEntry(new ZipEntry(name + "/"));
// 没有文件,不须要文件的copy
zos.closeEntry();
} else {
for (File file : listFiles) {
compress(file, zos, name + "/" + file.getName());
}
}
}
}
def String getCurrentFlavor() {
........
}
复制代码
咱们已经在flavour.gradle中添加了获取当前渠道和压缩文件的方法了,如今回到app下的build.gradle文件中,经过判断当前渠道是否GooglePaly,对须要压缩的全部文件进行压缩,并输出到googlePlay渠道包apk的同级目录下:
apply from: "../flavour.gradle"
//添加到文件最后
//自动打包扩展文件obb
task zipObb(type: JavaExec) {
//判断是否GooglePlay渠道包,获取渠道包的时候作了小写处理
if (getCurrentFlavor().equals("googleplay")) {
//获取debug仍是release模式输出到不一样地址
String outputFilePath
if(gradle.startParameter.taskNames.toString().contains("Debug")){
outputFilePath = "app/build/outputs/apk/GooglePlay/debug/main." + android.defaultConfig.versionCode + ".com.example.testobb.obb"
}else{
outputFilePath = "app/GooglePlay/release/main." + android.defaultConfig.versionCode + ".com.example.testobb.obb"
}
File file = new File('app/src/tencent/res/raw/splash.mp4')
//此处添加更多文件 也能够经过配置文件的方式输入须要打包obb的全部资源文件
File[] files = new File[]{file}
zipObb(files, outputFilePath)
}
}
复制代码
至此,咱们的多渠道打包和自动化生成obb就实现了。
如发现任何错误或有不明白的地方能够留言。