Как использовать разделенные APK, созданные при использовании мгновенного запуска, в самом Android?

Задний план

У меня есть приложение ( здесь ), которое среди других функций позволяет обмениваться файлами APK.

Чтобы сделать это, он достигает файла, обращаясь к пути packageInfo.applicationInfo.sourceDir (ссылка docs здесь ) и просто делится файлом (используя ContentProvider при необходимости, как я здесь использовал).

Проблема

Это прекрасно работает в большинстве случаев, особенно при установке файлов APK из Play Маркета или из автономного файла APK, но когда я устанавливаю приложение с помощью самой Android-Studio, я вижу несколько файлов APK на этом пути, и ни один из них не является Которые могут быть установлены и запущены без каких-либо проблем.

Вот скриншот содержимого этой папки, попробовав образец из «Alerter» github repo :

Введите описание изображения здесь

Я не уверен, когда эта проблема началась, но это происходит, по крайней мере, на моем Nexus 5x с Android 7.1.2. Может, и раньше.

Что я нашел

Это, по-видимому, вызвано только тем фактом, что мгновенный запуск включен в среде IDE, так что он может помочь обновить приложение без необходимости его повторной сборки:

Введите описание изображения здесь

После его отключения я вижу, что есть один APK, как и раньше:

Введите описание изображения здесь

Вы можете увидеть разницу в размере файла между правильным APK и разделенным.

Кроме того, кажется, что существует API для получения путей для всех разделенных APK:

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

Вопрос

Какой должен быть самый простой способ поделиться APK, который должен быть разделен на несколько?

Действительно ли это необходимо для их слияния?

Кажется, это возможно в соответствии с документами :

Полный путь до нуля или более разделенных APK, которые в сочетании с базовым APK, определенным в sourceDir, образуют полное приложение.

Но каков правильный способ сделать это, и есть ли быстрый и эффективный способ сделать это? Может быть, без создания файла?

Может быть, есть API, чтобы получить объединенный APK из всех разделенных? Или, может быть, такой APK уже существует в любом другом пути, и нет необходимости в слиянии?

EDIT: только что заметил, что все сторонние приложения, которые я пытался, должны совместно использовать APK установленного приложения, в этом случае не могут этого сделать.

Solutions Collecting From Web of "Как использовать разделенные APK, созданные при использовании мгновенного запуска, в самом Android?"

Я технический руководитель @Google для Android Gradle Plugin, позвольте мне попытаться ответить на ваш вопрос, предполагая, что я понимаю ваш случай использования.

Во-первых, некоторые пользователи упомянули, что вы не должны делиться своей поддержкой InstantRun, и они верны. Мгновенный запуск на основе приложения сильно настраивается для текущего образа устройства / эмулятора, к которому вы развертываете. Таким образом, в основном, скажем, вы создаете сборку приложений с поддержкой IR, предназначенную для определенного устройства, работающего на компьютере 21, она потерпит неудачу, если вы попытаетесь использовать те же самые APK, что и устройство, работающее на компьютере 23. В случае необходимости я могу сделать гораздо более глубокое объяснение Достаточно сказать, что мы генерируем байт-коды, настроенные на API, найденные в android.jar (что, конечно, зависит от версии).

Поэтому я не думаю, что обмен этими APK имеет смысл, вы должны либо использовать сборку с отключенным IR, либо сборку релиза.

Теперь для некоторых деталей каждый фрагмент APK содержит 1 + dex-файл (ы), поэтому теоретически ничто не мешает вам распаковывать все эти APK-фрагменты, принимать все файлы dex и снова загружать их обратно в base.apk / rezip / resign и Он должен просто работать. Тем не менее, он по-прежнему будет включенным приложением IR, поэтому он запустит небольшой сервер для прослушивания запросов IDE и т. Д. И т. Д. … Я не могу представить себе вескую причину для этого.

Надеюсь это поможет.

Слияние нескольких split apks с одним apk может быть немного сложным.

Вот предложение по совместному использованию split apks и позволить системе обрабатывать слияние и установку.

Это не может быть ответом на вопрос, так как он немного длинный, я публикую здесь как «ответ».

Framework new 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 мы можем видеть исходный код здесь , он вызовет функцию 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 , – это вызов framework api PackageInstaller , мы можем видеть исходный код здесь

 runInstallCreate runInstallWrite runInstallCommit 

Это совсем не таинственно, я просто скопировал некоторые методы или функции здесь.

Следующий сценарий можно вызвать из среды adb shell для установки всех split apks на устройство, например adb install-multiple . Я думаю, что это может работать программно с Runtime.exec если ваше устройство 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 output)

 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 и вышеприведенный сценарий в общедоступный доступный для записи каталог, например /data/local/tmp/slices , и запустите скрипт установки, он будет установлен на ваше устройство так же, как adb install-multiple .

Код ниже – это еще один вариант сценария выше, если ваше приложение имеет подпись платформы или устройство внедрено, я думаю, что все будет в порядке. У меня не было среды для тестирования.

 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. Как и пример кода выше, он может работать, если устройство коренится или ваше приложение имеет подпись платформы, но я не получил работоспособную среду для его проверки.

 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 library android-hidden-api в качестве provided зависимости.

Для меня мгновенный пробег был кошмаром, 2-5-минутное время сборки и безумно часто, последние изменения не были включены в сборку. Я настоятельно рекомендую отключить мгновенный запуск и добавить эту строку в gradle.properties:

 android.enableBuildCache=true 

Первая сборка часто занимает некоторое время для больших проектов (1-2 минуты), но после того, как они кэшируются, последующие сборки обычно быстро светятся (<10 секунд).

Получил этот совет от пользователя reddit / u / QuestionsEverythang, который спас меня, так много хлопот с мгновенным запуском!