42 votes

Comment partager les APK divisés créés lors de l'utilisation de l'exécution instantanée, dans Android même ?

Contexte

J'ai une application ( ici ) qui, entre autres, permet de partager des fichiers APK.

Pour ce faire, il atteint le fichier en accédant au chemin de packageInfo.applicationInfo.sourceDir (lien docs ici ), et partage simplement le fichier (en utilisant ContentProvider si nécessaire, comme j'ai utilisé ici ).

Le problème

Cela fonctionne bien dans la plupart des cas, en particulier lors de l'installation de fichiers APK à partir du Play Store ou d'un fichier APK autonome, mais lorsque j'installe une application en utilisant Android-Studio lui-même, je vois plusieurs fichiers APK sur ce chemin, et aucun d'eux n'est valide et peut être installé et exécuté sans aucun problème.

Voici une capture d'écran du contenu de ce dossier, après avoir essayé un échantillon provenant de "Dépôt github "Alerter :

enter image description here

Je ne sais pas quand ce problème a commencé, mais il se produit au moins sur mon Nexus 5x avec Android 7.1.2. Peut-être même avant.

Ce que j'ai trouvé

Cela semble être dû uniquement au fait que l'exécution instantanée est activée dans l'IDE, ce qui permet de mettre à jour l'application sans avoir à la reconstruire entièrement :

enter image description here

Après l'avoir désactivé, je constate qu'il n'y a qu'un seul APK, comme c'était le cas dans le passé :

enter image description here

Vous pouvez voir la différence de taille du fichier entre l'APK correct et celui qui a été divisé.

Il semble également qu'il existe une API permettant d'obtenir les chemins d'accès à tous les APK divisés :

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

La question

Quel est le moyen le plus simple de partager un APK qui doit être divisé en plusieurs exemplaires ?

Est-il vraiment nécessaire de les fusionner d'une manière ou d'une autre ?

Il semble que ce soit possible selon les docs :

Chemins d'accès complets vers zéro ou plusieurs APK fractionnés qui, une fois combinés avec l'APK de base défini dans le APK de base défini dans sourceDir, forment une application complète.

Mais quelle est la bonne façon de procéder, et existe-t-il un moyen rapide et efficace de le faire ? Peut-être sans vraiment créer de fichier ?

Il y a peut-être une API pour obtenir un APK fusionné à partir de tous les APK divisés ? Ou peut-être un tel APK existe déjà de toute façon dans un autre chemin, et il n'y a pas besoin de fusionner ?

EDIT : je viens de remarquer que toutes les applications tierces que j'ai essayées et qui sont censées partager l'APK d'une application installée ne le font pas dans ce cas.

18voto

Jerome Dochez Points 706

Je suis le responsable technique @Google pour le plugin Android Gradle. Je vais essayer de répondre à votre question en supposant que je comprends votre cas d'utilisation.

Tout d'abord, certains utilisateurs ont mentionné que vous ne devriez pas partager votre build activé par InstantRun et ils ont raison. Les builds InstantRun d'une application sont hautement personnalisés en fonction de l'image du dispositif/émulateur sur lequel vous déployez l'application. Ainsi, si vous générez une compilation de votre application pour un appareil particulier fonctionnant sous 21, vous échouerez lamentablement si vous essayez d'utiliser les mêmes APKs sur un appareil fonctionnant sous 23. Je peux entrer dans une explication beaucoup plus profonde si nécessaire, mais il suffit de dire que nous générons des codes d'octet personnalisés sur les API trouvées dans Android.jar (qui est bien sûr spécifique à la version).

Je ne pense donc pas que le partage de ces APKs ait un sens, vous devriez soit utiliser une build désactivée par l'IR, soit une release build.

Maintenant pour quelques détails, chaque APK de tranche contient 1+ fichier(s) dex, donc en théorie, rien ne vous empêche de dézipper tous ces APK de tranche, de prendre tous les fichiers dex et de les remettre dans le base.apk/rezip/resign et cela devrait fonctionner. Cependant, il s'agira toujours d'une application activée par IR, donc elle démarrera le petit serveur pour écouter les requêtes IDE, etc, etc... Je ne peux pas imaginer une bonne raison pour faire cela.

J'espère que cela vous aidera.

1voto

alijandro Points 3693

Fusionner plusieurs apks divisés en un seul apk peut être un peu compliqué.

Voici une suggestion pour partager directement les apks divisés et laisser le système s'occuper de la fusion et de l'installation.

Ce n'est peut-être pas une réponse à la question, mais comme c'est un peu long, je le poste ici comme une "réponse".

Nouveau cadre API PackageInstaller peut gérer monolithic apk ou split apk .

Dans l'environnement de développement

  • pour monolithic apk en utilisant adb install single_apk

  • pour split apk en utilisant adb install-multiple a_list_of_apks

Vous pouvez voir ces deux modes ci-dessus à partir d'Android studio Run La sortie dépend de votre projet a Instant run activer ou désactiver.

Pour la commande adb install-multiple nous pouvons voir le code source ici il appellera la fonction install_multiple_app .

Et ensuite, effectuez les procédures suivantes

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

Qu'est-ce que le pm est en fait d'appeler l'api du cadre PackageInstaller nous pouvons voir le code source ici

runInstallCreate
runInstallWrite
runInstallCommit

Ce n'est pas mystérieux du tout, j'ai juste copié quelques méthodes ou fonctions ici.

Le script suivant peut être invoqué à partir de adb shell pour installer tous les split apks au dispositif, comme adb install-multiple . Je pense que cela pourrait fonctionner de manière programmatique avec Runtime.exec si votre appareil est enraciné.

#!/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

Dans mon exemple, les apks divisés comprennent (j'ai obtenu la liste à partir d'Android studio) Run sortie)

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

Utilisation de adb push tous les apks et le script ci-dessus dans un répertoire public accessible en écriture, par exemple /data/local/tmp/slices et exécutez l'install script, il s'installera sur votre appareil comme suit adb install-multiple .

Le code ci-dessous est juste une autre variante du script ci-dessus, si votre application a la signature de la plateforme ou que l'appareil est enraciné, je pense que ce sera ok. Je n'ai pas eu l'environnement pour tester.

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();
                }
            }
        }
    }
}

Entre-temps, la manière la plus simple est d'essayer l'api du framework. Comme l'exemple de code ci-dessus, cela pourrait fonctionner si l'appareil est enraciné ou si votre application a une signature de plate-forme, mais je n'ai pas eu d'environnement fonctionnel pour le tester.

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();
        }
    }
}

Afin d'utiliser l'api cachée IIntentSender J'ajoute la bibliothèque jar Android-hidden-api comme le provided dépendance.

-4voto

landonmutch Points 42

Pour moi, l'exécution instantanée était un cauchemar, des temps de construction de 2 à 5 minutes et, souvent, les changements récents n'étaient pas inclus dans les constructions. Je recommande vivement de désactiver l'exécution instantanée et d'ajouter cette ligne à gradle.properties :

android.enableBuildCache=true

La première construction prend souvent un certain temps pour les grands projets (1 à 2 minutes), mais une fois qu'elle est mise en cache, les constructions suivantes sont généralement très rapides (<10 secondes).

J'ai trouvé ça. conseil de l'utilisateur reddit /u/QuestionsEverythang ce qui m'a évité bien des tracas avec la course instantanée !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X