web-dev-qa-db-ja.com

Android自体で、インスタントランを使用中に作成された分割APKを共有する方法は?

バックグラウンド

私はアプリを持っています( here )。他の機能の中でもAPKファイルを共有できます。

そのためには、packageInfo.applicationInfo.sourceDirのパスにアクセスしてファイルに到達します(ドキュメントリンク here ) 、ファイルを共有するだけです(必要に応じてContentProviderを使用します。これは here を使用したためです)。

問題

これはほとんどの場合、特にPlayストアまたはスタンドアロンAPKファイルからAPKファイルをインストールする場合に正常に機能しますが、Android-Studio自体を使用してアプリをインストールすると、このパスに複数のAPKファイルが表示されますが、いずれもありません問題なくインストールして実行できる有効なもの。

"Alerter" github repo からサンプルを試した後の、このフォルダーのコンテンツのスクリーンショットを次に示します。

enter image description here

この問題がいつ始まったかはわかりませんが、少なくともNexus 5xでAndroid 7.1.2。おそらく以前でも。

私が見つけたもの

これは、IDEでインスタントランが有効になっていることだけが原因であると思われるため、アプリをまとめて再ビルドすることなくアプリを更新できます。

enter image description here

無効にした後、以前のように、APKが1つあることがわかります。

enter image description here

正しいAPKと分割されたAPKのファイルサイズの違いを確認できます。

また、分割されたすべてのAPKへのパスを取得するAPIがあるようです。

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

質問

複数のAPKに分割されたAPKを共有する最も簡単な方法は何ですか?

どういうわけかそれらをマージすることが本当に必要ですか?

the docs に従って可能であるようです:

SourceDirで定義されたベースAPKと組み合わせて完全なアプリケーションを形成する、ゼロ個以上の分割APKへのフルパス。

しかし、それを行う正しい方法は何ですか?それを行うための高速で効率的な方法はありますか?たぶん実際にファイルを作成せずに?

分割されたすべてのAPKからマージされたAPKを取得するAPIがありますか?それとも、そのようなAPKが他のパスに既に存在し、マージする必要がないのでしょうか?

編集:ちょうど私が試みたすべてのサードパーティのアプリがインストールされたアプリのAPKを共有することになっていることに気づいただけこの場合そうすることはできません。

42

私はAndroid Gradleプラグインの@Googleテクニカルリードです。ユースケースを理解していると仮定して、質問に答えてみましょう。

最初に、一部のユーザーは、InstantRunが有効なビルドを共有するべきではないと述べましたが、それらは正しいものです。アプリケーション上のインスタントランビルドは、デプロイ先の現在のデバイス/エミュレーターイメージ用に高度にカスタマイズされています。基本的に、21を実行している特定のデバイス用にアプリのIR対応ビルドを生成するとします。23を実行しているデバイスなどでまったく同じAPKを使用しようとすると悲惨に失敗します。 Android.jar(もちろんバージョン固有)にあるAPIでカスタマイズされたバイトコードを生成すると言うだけで十分です。

したがって、これらのAPKを共有することは理にかなっていないと思うので、IR無効化ビルドまたはリリースビルドを使用する必要があります。

詳細については、各スライスAPKに1つ以上のdexファイルが含まれているため、理論上、これらすべてのスライスAPKを解凍し、すべてのdexファイルを取得してbase.apk/rezip/resignに戻すことを妨げるものはありません。うまくいくはずです。ただし、それはまだIR対応アプリケーションであるため、小さなサーバーを起動してIDEリクエストなどなど)をリッスンします。これを行う正当な理由は想像できません。

お役に立てれば。

18
Jerome Dochez

複数の分割apkを単一のapkにマージするのは少し複雑かもしれません。

分割apkを直接共有し、システムでマージとインストールを処理できるようにするための提案があります。

これは質問に対する回答ではないかもしれません。少し長いので、ここに「回答」として投稿します。

フレームワークの新しいAPI PackageInstallermonolithic apkまたはsplit apkを処理できます。

開発環境で

  • monolithic apkの場合、adb install single_apkを使用

  • split apkの場合、adb install-multiple a_list_of_apksを使用

上記の2つのモードは、Android studio Runの出力はプロジェクトがInstant runを有効または無効にするかどうかに依存します]から確認できます。

コマンドadb install-multipleについては、ソースコード here を見ることができます。関数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を呼び出すことです。ソースコードを確認できます here

runInstallCreate
runInstallWrite
runInstallCommit

まったく不思議ではありません。ここでいくつかのメソッドまたは関数をコピーしました。

次のスクリプトを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

私の例では、分割apkが含まれています(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上記のすべてのapkとスクリプトを使用して、書き込み可能なパブリックディレクトリ(例:/data/local/tmp/slices)にインストールスクリプトを実行すると、adb install-multipleと同様にデバイスにインストールされます。

以下のコードは、上記のスクリプトのもう1つのバリアントです。アプリにプラットフォームシグネチャがある場合、またはデバイスがルート化されている場合は、大丈夫だと思います。テストする環境がありませんでした。

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-apiprovided依存関係として追加します。

1
alijandro