如何分享在Android本身内使用即时运行时创build的拆分APK?

背景

我有一个应用程序( 在这里 ),除了其他function,允许共享APK文件。

为了做到这一点,它通过访问packageInfo.applicationInfo.sourceDir(文档链接这里 )的path到达文件,并共享文件(在需要时使用ContentProvider,就像我在这里使用的那样)。

问题

这在大多数情况下工作正常,特别是从Play商店或独立的APK文件安装APK文件时,但是当我使用Android-Studio本身安装应用程序时,我看到这个path上有多个APK文件,都不是有效的,可以安装和运行没有任何问题。

下面是这个文件夹的内容截图,在从“Alerter”github repo中尝试一个示例之后:

在这里输入图像说明

我不确定这个问题何时开始,但至less在Android 7.1.2的Nexus 5x上会发生。 甚至可能在之前。

我发现了什么

这似乎只是由于在IDE上启用了即时运行的事实而引起的,因此它可以帮助更新应用程序而无需重新构build它们:

在这里输入图像说明

禁用后,我可以看到有一个APK,就像以前一样:

在这里输入图像说明

您可以在正确的APK和分割的文件之间看到文件大小的差异。

另外,似乎有一个API可以获取所有分割的APK的path:

https://developer.android.com/reference/android/content/pm/ApplicationInfo.html#splitPublicSourceDirs

这个问题

共享一个APK的最简单的方法是什么应该被拆分成多个?

是否真的需要以某种方式合并它们?

看来这是可能的根据文档

与零个或多个拆分APK的完整path,当与在sourceDir中定义的基本APK结合时,将形成一个完整的应用程序。

但是,做到这一点的正确方法是什么?有没有一种快速有效的方法呢? 也许没有真正创build一个文件?

是否有一个API可以从所有拆分的APK中获取合并的APK? 或者,也许这样一个APK已经存在了在其他path,并且没有必要合并?

编辑:只是注意到,我已经尝试所有第三方应用程序应该共享已安装的应用程序的APK在这种情况下没有这样做。

我是Android Gradle Plugin的技术负责人@Google,让我试着回答你的问题,假设我理解你的用例。

首先,有些用户提到你不应该分享你已经启用了InstantRun的版本,他们是正确的。 即时运行在应用程序上构build,针对您正在部署的当前设备/仿真器映像进行高度自定义。 所以基本上,假设你为一个运行21的特定设备生成了一个启用了IR的应用程序版本,如果你试图在运行23的设备上使用完全相同的APK,那么它将会惨败。我可以进行更深入的解释,只要我们生成在android.jar中find的API(这当然是特定于版本的)上定制的字节代码即可。

所以我不认为分享这些APK是有道理的,你应该使用禁用IR的构build或发布构build。

现在有些细节,每一个切片APK都包含1个以上的dex文件,所以理论上来说,没有任何东西可以阻止你将所有的切片APK解压缩,取出所有的dex文件并把它们放回到base.apk / rezip / resign中。它应该只是工作。 但是,它仍然是一个启用IR的应用程序,所以它将启动小型服务器来侦听IDE请求等等,我无法想象这样做的好理由。

希望这可以帮助。

合并多个分裂的apks到单个apk可能有点复杂。

这里有一个build议,直接共享拆分apks,让系统来处理合并和安装。

这可能不是问题的答案,因为这有点长,我在这里作为“答案”发帖。

框架新的API PackageInstaller可以处理monolithic apksplit apk

在开发环境中

  • 对于monolithic apk ,使用adb install single_apk

  • split apk ,使用adb install-multiple a_list_of_apks

你可以从android studio中看到上述两种模式Run输出取决于你的项目是否启用或禁用了Instant run

对于命令adb install-multiple ,我们可以在这里看到源代码,它会调用函数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实际上做的是调用框架api PackageInstaller ,我们可以在这里看到源代码

 runInstallCreate runInstallWrite runInstallCommit 

这并不神秘,我只是在这里复制了一些方法或function。

可以从adb shell环境中调用以下脚本,以便将所有split apks安装到设备,如adb install-multiple 。 我认为如果您的设备是根源的,它可能与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 

我的例子,分裂apks包括(我从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所有脚本和脚本adb push送到公共可写目录(例如/data/local/tmp/slices ,然后运行安装脚本,它将像adb install-multiple一样安装到设备中。

下面的代码只是上面的脚本的另一种变体,如果你的应用程序有平台签名或设备是根源,我认为它会好的。 我没有环境来testing。

 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。 像上面的示例代码一样,如果设备已经植入或者您的应用程序具有平台签名,它可能会工作,但是我没有得到一个可行的环境来testing它。

 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依赖项。

对我来说,即时运行是一场噩梦,build造时间为2-5分钟,令人疯狂的是,最近的变化并未包含在构build中。 我强烈build议禁用即时运行并将此行添加到gradle.properties中:

 android.enableBuildCache=true 

第一次构build通常需要一些时间用于大型项目(1-2分钟),但是在caching之后,后续构build通常是快速的(<10秒)。

从reddit用户/ u / QuestionsEverythang得到了这个技巧,它已经救了我很多,即时运行hassling!