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

Фон

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

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

Проблема

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

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

enter image description here

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

Что я нашел

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

enter image description here

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

enter image description here

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

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

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

Вопрос

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

Нужно ли как-то сливать их?

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

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

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

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

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

Ответы

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

Новый API API PackageInstaller может обрабатывать monolithic apk или split apk.

В среде разработки

  • для monolithic apk, используя adb install single_apk

  • для split apk, используя adb install-multiple a_list_of_apks

Вы можете видеть, что эти два режима выше с выхода Android-студии 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

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

В adb shell можно вызвать следующий script, чтобы установить все 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

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

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

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

Ответ 3

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

android.enableBuildCache=true

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

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