42

背景

我有一个应用程序(此处),除其他功能外,它还允许共享 APK 文件。

为此,它通过访问 packageInfo.applicationInfo.sourceDir 的路径(此处的文档链接)来访问文件,并仅共享文件(在需要时使用 ContentProvider,正如我在此处使用的那样)。

问题

这在大多数情况下都可以正常工作,尤其是从 Play 商店或独立 APK 文件安装 APK 文件时,但是当我使用 Android-Studio 本身安装应用程序时,我在此路径上看到多个 APK 文件,但没有一个是可以安装和运行没有任何问题的有效的。

这是此文件夹内容的屏幕截图,在尝试了来自“Alerter”github repo的示例后 :

在此处输入图像描述

我不确定这个问题是什么时候开始的,但至少在我的 Nexus 5x 和 Android 7.1.2 上确实发生了。甚至可能更早。

我发现了什么

这似乎只是因为在 IDE 上启用了即时运行,因此它可以帮助更新应用程序而无需重新构建它:

在此处输入图像描述

禁用后,我可以看到只有一个 APK,就像过去一样:

在此处输入图像描述

您可以看到正确的 APK 和拆分的 APK 之间的文件大小差异。

此外,似乎有一个 API 可以获取所有拆分 APK 的路径:

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

问题

共享必须拆分为多个 APK 的最简单方法应该是什么?

真的需要以某种方式合并它们吗?

根据文档似乎有可能:

零个或多个拆分 APK 的完整路径,当与 sourceDir 中定义的基本 APK 组合时,形成一个完整的应用程序。

但是正确的方法是什么,有没有一种快速有效的方法呢?也许没有真正创建文件?

是否有一个 API 可以从所有拆分的 APK 中获取合并的 APK?或者也许这样的APK已经存在于其他路径中,并且不需要合并?

编辑:刚刚注意到我尝试过的所有第三方应用程序都应该共享已安装应用程序的 APK 在这种情况下无法这样做。

4

4 回答 4

18

我是 Android Gradle 插件的技术负责人@Google,假设我了解您的用例,让我尝试回答您的问题。

首先,一些用户提到您不应该共享您启用 InstantRun 的构建,他们是正确的。基于应用程序构建的 Instant Run 针对您要部署到的当前设备/模拟器映像进行了高度定制。所以基本上,假设您为运行 21 的特定设备生成启用 IR 的应用程序构建,如果您尝试在运行 23 的设备上使用完全相同的 APK,它将惨遭失败。如有必要,我可以进行更深入的解释,但可以说我们生成了在 android.jar 中找到的 API 上定制的字节码(当然是特定于版本的)。

所以我认为共享这些 APK 没有意义,您应该使用禁用 IR 的构建或发布构建。

现在了解一些细节,每个切片 APK 包含 1+ 个 dex 文件,所以理论上,没有什么能阻止您解压缩所有这些切片 APK,取出所有 dex 文件并将它们塞回 base.apk/rezip/resign 和它应该可以工作。但是,它仍将是一个启用 IR 的应用程序,因此它将启动小型服务器以侦听 IDE 请求等……我无法想象这样做的充分理由。

希望这可以帮助。

于 2017-04-13T04:02:40.693 回答
1

将多个拆分 apk 合并到一个 apk 可能有点复杂。

这里建议直接分享拆分的apk,让系统来处理合并和安装。

这可能不是问题的答案,因为它有点长,我在这里发布作为“答案”。

框架新的 APIPackageInstaller可以处理monolithic apksplit 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是调用框架api ,我们可以在这里PackageInstaller查看源代码

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 studioRun输出中获得了列表)

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

下面的代码只是上面脚本的另一种变体,如果你的应用程序有平台签名或设备已root,我认为没问题。我没有测试环境。

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

同时,简单的方法,你可以试试framework 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依赖项。

于 2017-03-18T11:32:19.403 回答
0

这些被称为拆分 apk。主要用于PlayStore。您可能知道,PlayStore 仅在与设备兼容的情况下才向用户显示应用程序。在这种情况下也是如此。拆分文件因设备而异。就像您为不同的设备使用不同的资源一样,这会使应用程序非常繁重。通过进行拆分,只需下载可用的拆分 apk,即可为用户节省下载和运行空间。

是否可以将它们合并到单个 apk 中? 是的。我使用了一个名为 Anti Split 的应用程序,它允许这样做。Plus Apk Editor Ultra 具有相同的功能。

我们可以将其保存到单个文件中吗? 是的你可以。与 Anti Split 一样,您必须先备份应用程序。就像在这种情况下,您必须将其备份为apks文件或在Android Studio中称为捆绑应用程序的xapk。我为此创建了一个库。它对我来说非常有效。我正在使用它将应用程序备份到xapk中,以后可以使用SIA应用程序或XAPK 安装程序安装,或者我们可以使用xapk文件合并它并制作apk

于 2021-11-25T16:33:45.023 回答
-4

对我来说,即时运行是一场噩梦,2-5 分钟的构建时间,而且经常令人抓狂的是,最近的更改并未包含在构建中。我强烈建议禁用即时运行并将这一行添加到 gradle.properties:

android.enableBuildCache=true

对于大型项目,首次构建通常需要一些时间(1-2 分钟),但在缓存之后,后续构建通常会很快(<10 秒)。

从 reddit 用户 /u/QuestionsEverythang那里得到了这个提示,它为我省去了即时运行的很多麻烦!

于 2017-03-15T22:48:35.487 回答