Comment partager des APK divisés créés lors de l’utilisation de l’instantané, au sein même d’Android?

Contexte

J’ai une application ( ici ) qui, entre autres fonctionnalités, permet de partager des fichiers APK.

Pour ce faire, il accède au 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 je l’ai utilisé ici ).

Le problème

Cela fonctionne très 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 à l’aide d’Android-Studio, plusieurs fichiers APK sont présents sur ce chemin. celles valides pouvant être installées et exécutées sans problème.

Voici une capture d’écran du contenu de ce dossier, après avoir essayé un extrait du repository github “Alerter” :

entrer la description de l'image ici

Je ne sais pas quand ce problème a commencé, mais cela 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û au fait que l’exécution instantanée est activée sur l’EDI, de sorte que cela pourrait aider à mettre à jour l’application sans avoir besoin de la reconstruire tous ensemble:

entrer la description de l'image ici

Après l’avoir désactivée, je peux voir qu’il y a un seul APK, comme auparavant:

entrer la description de l'image ici

Vous pouvez voir la différence de taille de fichier entre le fichier APK correct et le fichier partagé.

En outre, il semble qu’il y ait une API pour obtenir les chemins d’access à tous les APK divisés:

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

La question

Quel devrait être le moyen le plus simple de partager un APK à diviser en plusieurs?

Est-il vraiment nécessaire de les fusionner?

Il semble que ce soit possible selon les docs :

Les chemins d’access complets à zéro ou plusieurs fichiers APK fractionnés qui, combinés à l’APK de base défini dans sourceDir, forment une application complète.

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

Existe-t-il une API pour obtenir un APK fusionné parmi tous les split? Ou peut-être un tel APK existe-t-il déjà dans un autre chemin, et il n’y a pas besoin de fusionner?

EDIT: il suffit de remarquer que toutes les applications tierces que j’ai essayées sont supposées partager l’APK d’une application installée dans ce cas.

Je suis le leader technologique @Google pour Android Gradle Plugin, laissez-moi essayer de répondre à votre question en supposant que je comprends votre cas d’utilisation.

Tout d’abord, certains utilisateurs mentionnés ne doivent pas partager votre version activée pour InstantRun et ils sont corrects. L’instantané construit sur une application est hautement personnalisé pour l’image de périphérique / émulateur actuelle que vous déployez. Donc, fondamentalement, disons que vous générez une version compatible IR de votre application pour un périphérique particulier exécutant 21, cela échouera lamentablement si vous essayez d’utiliser exactement les mêmes APKs sur un périphérique fonctionnant par exemple. il suffit de dire que nous générons des codes d’octets personnalisés sur les API trouvées dans android.jar (qui est bien sûr spécifique à la version).

Donc, je ne pense pas que le partage de ces fichiers APK ait du sens, vous devez soit utiliser une version désactivée IR ou une version release.

Maintenant, pour certains détails, chaque slice APK contient 1+ fichier (s) dex, donc en théorie, rien ne vous empêche de décompresser tous ces fichiers APK, de prendre tous les fichiers dex et de les replacer dans base.apk / rezip / resign et ça devrait juste marcher. Cependant, il s’agira toujours d’une application compatible IR, elle démarrera donc le petit serveur pour écouter les requêtes IDE, etc., etc. Je ne peux pas imaginer une bonne raison pour cela.

J’espère que cela t’aides.

Fusionner plusieurs apks séparé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 gérer la fusion et l’installation.

Ce n’est peut-être pas une réponse à la question, car c’est un peu long, je poste ici comme une “réponse”.

Framework nouvel API PackageInstaller peut gérer PackageInstaller monolithic apk ou split apk .

En 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 de Android Studio. La sortie de Run dépend de votre projet a Instant run enable ou disable.

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

Et puis 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 fait réellement est d’appeler le framework API PackageInstaller , nous pouvons voir le code source ici

 runInstallCreate runInstallWrite runInstallCommit 

Ce n’est pas du tout mystérieux, je viens de copier quelques méthodes ou fonctions ici.

Le script suivant peut être adb shell partir de l’environnement adb shell pour installer tous les périphériques split apks sur le périphérique, comme adb install-multiple . Je pense que cela pourrait fonctionner par programmation 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 

I mon exemple, les apks divisés incluent (j’ai obtenu la liste de sortie de studio Android)

 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 

En utilisant adb push tous les apks et le script ci-dessus vers un répertoire accessible en écriture, par exemple /data/local/tmp/slices , et exécutez le script d’installation, il sera installé sur votre appareil comme adb install-multiple .

Le code ci-dessous est juste une autre variante du script ci-dessus, si votre application a la signature de la plate-forme ou si l’appareil est enraciné, je pense que ça ira. Je n’avais pas l’environnement à 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())); Ssortingng l; Pattern sessionIDPattern = Pattern.comstack(".*(\\[\\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(); } SsortingngBuilder pmInstallWriteBuilder = new SsortingngBuilder(); 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.toSsortingng()); 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.toSsortingng()); 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())); Ssortingng 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(); } } } } } 

Pendant ce temps, de manière simple, vous pouvez essayer l’API du framework. Comme l’exemple de code ci-dessus, cela peut fonctionner si le périphérique est enraciné ou si votre application possède une signature de plate-forme, mais je n’ai pas obtenu 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, Ssortingng s, IIntentReceiver iIntentReceiver, Ssortingng 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(); } } } 

Pour utiliser l’api caché IIntentSender , j’ajoute la bibliothèque jar android-hidden-api comme dépendance provided .

Pour moi, la course instantanée était un cauchemar, des temps de construction de 2 à 5 minutes, et, souvent, les changements récents n’ont pas été inclus dans les versions. Je recommande fortement de désactiver l’exécution instantanée et d’append cette ligne à gradle.properties:

 android.enableBuildCache=true 

La première version prend souvent un certain temps pour les gros projets (1-2 minutes), mais après sa mise en cache, les versions suivantes sont généralement rapides (moins de 10 secondes).

Vous avez cette astuce de reddit utilisateur / u / QuestionsEverythang qui m’a sauvé beaucoup de soucis avec l’exécution instantanée!