如何在 Android 本身内共享使用 instant-运行 创建的拆分 APK?
How to share split APKs created while using instant-run, within Android itself?
背景
我有一个应用程序 (here),除其他功能外,它还允许共享 APK 文件。
为此,它通过访问 packageInfo.applicationInfo.sourceDir 的路径到达文件(文档 link here), and just shares the file (using ContentProvider when needed, as I've used )。
问题
这在大多数情况下工作正常,尤其是从 Play 商店或独立 APK 文件安装 APK 文件时,但是当我使用 Android-Studio 本身安装应用程序时,我看到多个 APK 文件在这个路径上,其中 none 是可以安装的有效的并且 运行 没有任何问题。
这是此文件夹内容的屏幕截图,在尝试了来自 "Alerter" github repo 的样本后:
我不确定这个问题是什么时候开始的,但至少在我的 Android 7.1.2 的 Nexus 5x 上确实会出现。甚至更早。
我发现了什么
这似乎只是因为在 IDE 上启用了即时 运行,因此它可以帮助更新应用程序而无需一起重新构建它:
禁用后可以看到只有一个APK,和以前一样:
您可以看到正确的 APK 和拆分的 APK 之间的文件大小差异。
此外,似乎有一个 API 来获取所有拆分 APK 的路径:
问题
共享必须拆分为多个 APK 的最简单方法应该是什么?
真的需要以某种方式合并它们吗?
根据the docs看来是可以的:
Full paths to zero or more split APKs that, when combined with the
base APK defined in sourceDir, form a complete application.
但是正确的做法是什么,有没有快速有效的方法呢?也许没有真正创建文件?
是否可以 API 从所有拆分的 APK 中获取合并的 APK?或者这样的 APK 可能已经存在于其他路径中,并且不需要合并?
编辑:刚刚注意到我尝试过的所有第三方应用程序都应该共享已安装应用程序的 APK,但在这种情况下却没有这样做。
对我来说,即时 运行 是一场噩梦,构建时间为 2-5 分钟,而且令人抓狂的是,最近的更改经常没有包含在构建中。我强烈建议禁用即时 运行 并将此行添加到 gradle.properties:
android.enableBuildCache=true
大型项目的首次构建通常需要一些时间(1-2 分钟),但在缓存之后,后续构建通常快如闪电(<10 秒)。
得到这个tip from reddit user /u/QuestionsEverythang,这让我在即时 运行 时省去了很多麻烦!
将多个拆分的 apk 合并为一个 apk 可能有点复杂。
这里建议直接分享拆分的apks,让系统来处理合并安装。
这可能不是问题的答案,因为它有点长,我post在这里作为'answer'。
框架新 API PackageInstaller
可以处理 monolithic apk
或 split apk
.
在开发环境中
对于 monolithic apk
,使用 adb install single_apk
对于 split apk
,使用 adb install-multiple a_list_of_apks
您可以从 android studio Run
输出中看到上面这两种模式,具体取决于您的项目 Instant run
启用或禁用。
对于命令adb install-multiple
,我们可以查看源代码here,它会调用函数install_multiple_app
.
然后执行以下程序
pm install-create # create a install session
pm install-write # write a list of apk to session
pm install-commit # perform the merge and install
pm
其实是调用了框架apiPackageInstaller
,可以看源码here
runInstallCreate
runInstallWrite
runInstallCommit
一点也不神秘,我只是在这里复制了一些方法或函数而已。
可以从 adb shell
环境调用以下脚本将所有 split apks
安装到设备,如 adb install-multiple
。我认为如果您的设备已获得 root 权限,它可能会以编程方式与 Runtime.exec
一起工作。
#!/system/bin/sh
# get the total size in byte
total=0
for apk in *.apk
do
o=( $(ls -l $apk) )
let total=$total+${o[3]}
done
echo "pm install-create total size $total"
create=$(pm install-create -S $total)
sid=$(echo $create |grep -E -o '[0-9]+')
echo "pm install-create session id $sid"
for apk in *.apk
do
_ls_out=( $(ls -l $apk) )
echo "write $apk to $sid"
cat $apk | pm install-write -S ${_ls_out[3]} $sid $apk -
done
pm install-commit $sid
在我的示例中,拆分的 apk 包括(我从 android studio Run
输出中获得列表)
app/build/output/app-debug.apk
app/build/intermediates/split-apk/debug/dependencies.apk
and all apks under app/build/intermediates/split-apk/debug/slices/slice[0-9].apk
使用adb push
所有apks和上面的脚本到public可写目录,例如/data/local/tmp/slices
和运行安装脚本,它会安装到你的设备就像 adb install-multiple
.
下面的代码只是上面脚本的另一种变体,如果你的应用程序有平台签名或者设备是root过的,我想就可以了。我没有环境测试。
private static void installMultipleCmd() {
File[] apks = new File("/data/local/tmp/slices/slices").listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getAbsolutePath().endsWith(".apk");
}
});
long total = 0;
for (File apk : apks) {
total += apk.length();
}
Log.d(TAG, "installMultipleCmd: total apk size " + total);
long sessionID = 0;
try {
Process pmInstallCreateProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallCreateProcess.getOutputStream()));
writer.write("pm install-create\n");
writer.flush();
writer.close();
int ret = pmInstallCreateProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-create return " + ret);
BufferedReader pmCreateReader = new BufferedReader(new InputStreamReader(pmInstallCreateProcess.getInputStream()));
String l;
Pattern sessionIDPattern = Pattern.compile(".*(\[\d+\])");
while ((l = pmCreateReader.readLine()) != null) {
Matcher matcher = sessionIDPattern.matcher(l);
if (matcher.matches()) {
sessionID = Long.parseLong(matcher.group(1));
}
}
Log.d(TAG, "installMultipleCmd: pm install-create sessionID " + sessionID);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
StringBuilder pmInstallWriteBuilder = new StringBuilder();
for (File apk : apks) {
pmInstallWriteBuilder.append("cat " + apk.getAbsolutePath() + " | " +
"pm install-write -S " + apk.length() + " " + sessionID + " " + apk.getName() + " -");
pmInstallWriteBuilder.append("\n");
}
Log.d(TAG, "installMultipleCmd: will perform pm install write \n" + pmInstallWriteBuilder.toString());
try {
Process pmInstallWriteProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallWriteProcess.getOutputStream()));
// writer.write("pm\n");
writer.write(pmInstallWriteBuilder.toString());
writer.flush();
writer.close();
int ret = pmInstallWriteProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-write return " + ret);
checkShouldShowError(ret, pmInstallWriteProcess);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
try {
Process pmInstallCommitProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallCommitProcess.getOutputStream()));
writer.write("pm install-commit " + sessionID);
writer.flush();
writer.close();
int ret = pmInstallCommitProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-commit return " + ret);
checkShouldShowError(ret, pmInstallCommitProcess);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
private static void checkShouldShowError(int ret, Process process) {
if (process != null && ret != 0) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String l;
while ((l = reader.readLine()) != null) {
Log.d(TAG, "checkShouldShowError: " + l);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
同时,简单的方法,你可以试试框架api。就像上面的示例代码一样,如果设备已获得 root 权限或您的应用程序具有平台签名,它可能会工作,但我没有找到一个可行的环境来测试它。
private static void installMultiple(Context context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
try {
final int sessionId = packageInstaller.createSession(sessionParams);
Log.d(TAG, "installMultiple: sessionId " + sessionId);
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
File[] apks = new File("/data/local/tmp/slices/slices").listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getAbsolutePath().endsWith(".apk");
}
});
for (File apk : apks) {
InputStream inputStream = new FileInputStream(apk);
OutputStream outputStream = session.openWrite(apk.getName(), 0, apk.length());
byte[] buffer = new byte[65536];
int count;
while ((count = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, count);
}
session.fsync(outputStream);
outputStream.close();
inputStream.close();
Log.d(TAG, "installMultiple: write file to session " + sessionId + " " + apk.length());
}
try {
IIntentSender target = new IIntentSender.Stub() {
@Override
public int send(int i, Intent intent, String s, IIntentReceiver iIntentReceiver, String s1) throws RemoteException {
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
Log.d(TAG, "send: status " + status);
return 0;
}
};
session.commit(IntentSender.class.getConstructor(IIntentSender.class).newInstance(target));
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
为了使用隐藏的 api IIntentSender
,我添加了 jar 库 android-hidden-api 作为 provided
依赖。
我是 Android Gradle 插件的技术负责人@Google,假设我了解您的用例,让我尝试回答您的问题。
首先,一些用户提到您不应该共享启用 Instant运行 的构建,他们是正确的。 Instant 运行 基于应用程序构建,针对您要部署到的当前 device/emulator 映像高度定制。所以基本上,假设您为特定设备 运行 21 生成了一个启用 IR 的应用程序构建,如果您尝试在设备 运行 23 上使用完全相同的 APK,它将会失败得很惨。我可以如有必要,请进行更深入的解释,但足以说明我们生成了根据 android.jar 中的 API 定制的字节码(这当然是特定于版本的)。
所以我认为共享这些 APK 没有意义,您应该使用禁用 IR 的版本或发布版本。
现在了解一些细节,每个切片 APK 包含 1+ 个 dex 文件,因此理论上,没有什么能阻止您解压缩所有这些切片 APK,取出所有 dex 文件并将它们塞回基地。apk/rezip/resign 它应该可以正常工作。但是,它仍然是一个启用 IR 的应用程序,因此它会启动小型服务器来侦听 IDE 请求,等等……我想不出这样做的充分理由。
希望这对您有所帮助。
这些称为拆分 apk。其中主要用于PlayStore。您可能知道,PlayStore 只会向用户显示与设备兼容的应用程序。在这种情况下相同。拆分文件因设备而异。就像您为不同的设备使用不同的资源一样,这会使应用程序变得非常沉重。通过拆分,它可以节省 space 用于下载,并通过仅下载可用的拆分 apk 为用户节省 运行。
是否可以将它们合并为一个 apk?
是的。我使用了一个名为 Anti Split 的应用程序,它允许这样做。 Plus Apk Editor Ultra 具有相同的内容。
我们可以将它保存到一个文件中吗?
是的你可以。与 Anti Split 一样,您必须先备份应用程序。就像在这种情况下,您必须将其备份为 apks 文件或 xapk ,称为 bundled app在 Android 工作室。我为此创建了一个库。它对我来说很完美。我正在使用它将应用程序备份到 xapk 中,稍后可以使用 SIA 应用程序或 XAPK Installer 或我们可以使用 xapk 文件合并它并制作 apk
背景
我有一个应用程序 (here),除其他功能外,它还允许共享 APK 文件。
为此,它通过访问 packageInfo.applicationInfo.sourceDir 的路径到达文件(文档 link here), and just shares the file (using ContentProvider when needed, as I've used
问题
这在大多数情况下工作正常,尤其是从 Play 商店或独立 APK 文件安装 APK 文件时,但是当我使用 Android-Studio 本身安装应用程序时,我看到多个 APK 文件在这个路径上,其中 none 是可以安装的有效的并且 运行 没有任何问题。
这是此文件夹内容的屏幕截图,在尝试了来自 "Alerter" github repo 的样本后:
我不确定这个问题是什么时候开始的,但至少在我的 Android 7.1.2 的 Nexus 5x 上确实会出现。甚至更早。
我发现了什么
这似乎只是因为在 IDE 上启用了即时 运行,因此它可以帮助更新应用程序而无需一起重新构建它:
禁用后可以看到只有一个APK,和以前一样:
您可以看到正确的 APK 和拆分的 APK 之间的文件大小差异。
此外,似乎有一个 API 来获取所有拆分 APK 的路径:
问题
共享必须拆分为多个 APK 的最简单方法应该是什么?
真的需要以某种方式合并它们吗?
根据the docs看来是可以的:
Full paths to zero or more split APKs that, when combined with the base APK defined in sourceDir, form a complete application.
但是正确的做法是什么,有没有快速有效的方法呢?也许没有真正创建文件?
是否可以 API 从所有拆分的 APK 中获取合并的 APK?或者这样的 APK 可能已经存在于其他路径中,并且不需要合并?
编辑:刚刚注意到我尝试过的所有第三方应用程序都应该共享已安装应用程序的 APK,但在这种情况下却没有这样做。
对我来说,即时 运行 是一场噩梦,构建时间为 2-5 分钟,而且令人抓狂的是,最近的更改经常没有包含在构建中。我强烈建议禁用即时 运行 并将此行添加到 gradle.properties:
android.enableBuildCache=true
大型项目的首次构建通常需要一些时间(1-2 分钟),但在缓存之后,后续构建通常快如闪电(<10 秒)。
得到这个tip from reddit user /u/QuestionsEverythang,这让我在即时 运行 时省去了很多麻烦!
将多个拆分的 apk 合并为一个 apk 可能有点复杂。
这里建议直接分享拆分的apks,让系统来处理合并安装。
这可能不是问题的答案,因为它有点长,我post在这里作为'answer'。
框架新 API PackageInstaller
可以处理 monolithic apk
或 split apk
.
在开发环境中
对于
monolithic apk
,使用adb install single_apk
对于
split apk
,使用adb install-multiple a_list_of_apks
您可以从 android studio Run
输出中看到上面这两种模式,具体取决于您的项目 Instant run
启用或禁用。
对于命令adb install-multiple
,我们可以查看源代码here,它会调用函数install_multiple_app
.
然后执行以下程序
pm install-create # create a install session
pm install-write # write a list of apk to session
pm install-commit # perform the merge and install
pm
其实是调用了框架apiPackageInstaller
,可以看源码here
runInstallCreate
runInstallWrite
runInstallCommit
一点也不神秘,我只是在这里复制了一些方法或函数而已。
可以从 adb shell
环境调用以下脚本将所有 split apks
安装到设备,如 adb install-multiple
。我认为如果您的设备已获得 root 权限,它可能会以编程方式与 Runtime.exec
一起工作。
#!/system/bin/sh
# get the total size in byte
total=0
for apk in *.apk
do
o=( $(ls -l $apk) )
let total=$total+${o[3]}
done
echo "pm install-create total size $total"
create=$(pm install-create -S $total)
sid=$(echo $create |grep -E -o '[0-9]+')
echo "pm install-create session id $sid"
for apk in *.apk
do
_ls_out=( $(ls -l $apk) )
echo "write $apk to $sid"
cat $apk | pm install-write -S ${_ls_out[3]} $sid $apk -
done
pm install-commit $sid
在我的示例中,拆分的 apk 包括(我从 android studio Run
输出中获得列表)
app/build/output/app-debug.apk
app/build/intermediates/split-apk/debug/dependencies.apk
and all apks under app/build/intermediates/split-apk/debug/slices/slice[0-9].apk
使用adb push
所有apks和上面的脚本到public可写目录,例如/data/local/tmp/slices
和运行安装脚本,它会安装到你的设备就像 adb install-multiple
.
下面的代码只是上面脚本的另一种变体,如果你的应用程序有平台签名或者设备是root过的,我想就可以了。我没有环境测试。
private static void installMultipleCmd() {
File[] apks = new File("/data/local/tmp/slices/slices").listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getAbsolutePath().endsWith(".apk");
}
});
long total = 0;
for (File apk : apks) {
total += apk.length();
}
Log.d(TAG, "installMultipleCmd: total apk size " + total);
long sessionID = 0;
try {
Process pmInstallCreateProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallCreateProcess.getOutputStream()));
writer.write("pm install-create\n");
writer.flush();
writer.close();
int ret = pmInstallCreateProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-create return " + ret);
BufferedReader pmCreateReader = new BufferedReader(new InputStreamReader(pmInstallCreateProcess.getInputStream()));
String l;
Pattern sessionIDPattern = Pattern.compile(".*(\[\d+\])");
while ((l = pmCreateReader.readLine()) != null) {
Matcher matcher = sessionIDPattern.matcher(l);
if (matcher.matches()) {
sessionID = Long.parseLong(matcher.group(1));
}
}
Log.d(TAG, "installMultipleCmd: pm install-create sessionID " + sessionID);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
StringBuilder pmInstallWriteBuilder = new StringBuilder();
for (File apk : apks) {
pmInstallWriteBuilder.append("cat " + apk.getAbsolutePath() + " | " +
"pm install-write -S " + apk.length() + " " + sessionID + " " + apk.getName() + " -");
pmInstallWriteBuilder.append("\n");
}
Log.d(TAG, "installMultipleCmd: will perform pm install write \n" + pmInstallWriteBuilder.toString());
try {
Process pmInstallWriteProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallWriteProcess.getOutputStream()));
// writer.write("pm\n");
writer.write(pmInstallWriteBuilder.toString());
writer.flush();
writer.close();
int ret = pmInstallWriteProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-write return " + ret);
checkShouldShowError(ret, pmInstallWriteProcess);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
try {
Process pmInstallCommitProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallCommitProcess.getOutputStream()));
writer.write("pm install-commit " + sessionID);
writer.flush();
writer.close();
int ret = pmInstallCommitProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-commit return " + ret);
checkShouldShowError(ret, pmInstallCommitProcess);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
private static void checkShouldShowError(int ret, Process process) {
if (process != null && ret != 0) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String l;
while ((l = reader.readLine()) != null) {
Log.d(TAG, "checkShouldShowError: " + l);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
同时,简单的方法,你可以试试框架api。就像上面的示例代码一样,如果设备已获得 root 权限或您的应用程序具有平台签名,它可能会工作,但我没有找到一个可行的环境来测试它。
private static void installMultiple(Context context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
try {
final int sessionId = packageInstaller.createSession(sessionParams);
Log.d(TAG, "installMultiple: sessionId " + sessionId);
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
File[] apks = new File("/data/local/tmp/slices/slices").listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getAbsolutePath().endsWith(".apk");
}
});
for (File apk : apks) {
InputStream inputStream = new FileInputStream(apk);
OutputStream outputStream = session.openWrite(apk.getName(), 0, apk.length());
byte[] buffer = new byte[65536];
int count;
while ((count = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, count);
}
session.fsync(outputStream);
outputStream.close();
inputStream.close();
Log.d(TAG, "installMultiple: write file to session " + sessionId + " " + apk.length());
}
try {
IIntentSender target = new IIntentSender.Stub() {
@Override
public int send(int i, Intent intent, String s, IIntentReceiver iIntentReceiver, String s1) throws RemoteException {
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
Log.d(TAG, "send: status " + status);
return 0;
}
};
session.commit(IntentSender.class.getConstructor(IIntentSender.class).newInstance(target));
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
为了使用隐藏的 api IIntentSender
,我添加了 jar 库 android-hidden-api 作为 provided
依赖。
我是 Android Gradle 插件的技术负责人@Google,假设我了解您的用例,让我尝试回答您的问题。
首先,一些用户提到您不应该共享启用 Instant运行 的构建,他们是正确的。 Instant 运行 基于应用程序构建,针对您要部署到的当前 device/emulator 映像高度定制。所以基本上,假设您为特定设备 运行 21 生成了一个启用 IR 的应用程序构建,如果您尝试在设备 运行 23 上使用完全相同的 APK,它将会失败得很惨。我可以如有必要,请进行更深入的解释,但足以说明我们生成了根据 android.jar 中的 API 定制的字节码(这当然是特定于版本的)。
所以我认为共享这些 APK 没有意义,您应该使用禁用 IR 的版本或发布版本。
现在了解一些细节,每个切片 APK 包含 1+ 个 dex 文件,因此理论上,没有什么能阻止您解压缩所有这些切片 APK,取出所有 dex 文件并将它们塞回基地。apk/rezip/resign 它应该可以正常工作。但是,它仍然是一个启用 IR 的应用程序,因此它会启动小型服务器来侦听 IDE 请求,等等……我想不出这样做的充分理由。
希望这对您有所帮助。
这些称为拆分 apk。其中主要用于PlayStore。您可能知道,PlayStore 只会向用户显示与设备兼容的应用程序。在这种情况下相同。拆分文件因设备而异。就像您为不同的设备使用不同的资源一样,这会使应用程序变得非常沉重。通过拆分,它可以节省 space 用于下载,并通过仅下载可用的拆分 apk 为用户节省 运行。
是否可以将它们合并为一个 apk? 是的。我使用了一个名为 Anti Split 的应用程序,它允许这样做。 Plus Apk Editor Ultra 具有相同的内容。
我们可以将它保存到一个文件中吗? 是的你可以。与 Anti Split 一样,您必须先备份应用程序。就像在这种情况下,您必须将其备份为 apks 文件或 xapk ,称为 bundled app在 Android 工作室。我为此创建了一个库。它对我来说很完美。我正在使用它将应用程序备份到 xapk 中,稍后可以使用 SIA 应用程序或 XAPK Installer 或我们可以使用 xapk 文件合并它并制作 apk