如何在Android内部共享使用instant run创建的拆分APK? 背景

如何在Android内部共享使用instant run创建的拆分APK? 背景,android,apk,android-instant-run,Android,Apk,Android Instant Run,我有一个app(),除其他功能外,它允许共享APK文件 为此,它通过访问packageInfo.applicationInfo.sourceDir(docs链接)的路径到达文件,并只共享文件(如我所用,在需要时使用ContentProvider) 问题 这在大多数情况下都可以正常工作,尤其是从Play Store或独立APK文件安装APK文件时,但当我使用Android Studio本身安装应用程序时,我会在该路径上看到多个APK文件,并且没有一个是可以安装和运行的有效文件,不会出现任何问题 以

我有一个app(),除其他功能外,它允许共享APK文件

为此,它通过访问packageInfo.applicationInfo.sourceDir(docs链接)的路径到达文件,并只共享文件(如我所用,在需要时使用ContentProvider)

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

以下是试用以下示例后该文件夹内容的屏幕截图:

我不确定这个问题是什么时候开始的,但至少在我的Nexus5x上,安卓7.1.2版上出现过。也许以前

我发现了什么 这似乎只是因为IDE上启用了instant run,因此它可以帮助更新应用程序,而无需一起重新构建:

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

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

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

问题 共享一个需要拆分为多个APK的APK最简单的方法是什么

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

根据以下情况,这似乎是可能的:

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

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

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


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

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

android.enableBuildCache=true

对于大型项目来说,第一次构建通常需要一些时间(1-2分钟),但在缓存之后,后续构建通常会非常快(将多个拆分的apk合并到单个apk可能有点复杂

下面是一个直接共享拆分的APK并让系统处理合并和安装的建议

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

框架新API
PackageInstaller
可以处理
单片apk
拆分apk

在开发环境中

  • 对于
    单片apk
    ,使用
    adb安装单片apk

  • 对于
    splitapk
    ,使用
    adb安装多个apk列表

您可以从android studio
Run
查看以上两种模式,输出取决于您的项目是否具有
即时运行
启用或禁用

对于命令
adb install multiple
,我们可以看到源代码,它将调用函数
install\u multiple\u 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
,我们可以看到源代码

这一点也不神秘,我只是在这里复制了一些方法或函数

可以从
adb shell
环境调用以下脚本,将所有
split-apk
安装到设备,如
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包括(我从androidstudio
Run
output获得了列表)

使用
adb将上述所有APK和脚本推送到公共可写目录,例如
/data/local/tmp/slices
,并运行安装脚本,它将安装到您的设备上,就像
adb install multiple
一样

下面的代码只是上面脚本的另一个变体,如果你的应用程序有平台签名或者设备是根目录的,我认为它可以。我没有要测试的环境

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 Gradle插件的技术负责人@Google,假设我了解您的用例,让我试着回答您的问题

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

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

现在了解一些细节,每个slice APK包含1+dex文件,因此理论上,没有什么可以阻止您解压缩所有这些slice APK,获取所有dex文件并将其填充回base.APK/r
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();
                }
            }
        }
    }
}
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();
        }
    }
}