Java实现Android APK多渠道打包

时间:2021-07-19 09:28:25

使用Android Studio开发的朋友都知道,Gradle自带多渠道打包的功能,但是此功能较慢,对于我们这种近百个的渠道包来说,打包无疑是种痛苦。对于爱学习的我来说,自己动手写一份多渠道打包程序,那是多么的快乐!

原理:多渠道打包其实非常简单,编译好的APK中包含AndroidManifest.xml文件,基本APK都将渠道号存储在此文件中(本人使用的友盟渠道统计,KEY为UMENG_CHANNEL),将APK文件解压,读取AndroidManifest.xml,找到KEY值,修改保存,最后再将文件重新编译为APK,整个修改渠道就完成了。

PS:不过需要注意一点的是,重新编译好的APK是没有签名的,需要使用JDK下的jarsigner工具重新签名


备工具:jdk(别告诉我你没有)、apktool和aapt是反编译的常用工具,将三个工具添加到环境变量中,本人使用的是mac,将三个路径添加到.bash_profile中就可以了。

知道了原理,那么就不难实现我们的多渠道打包的程序,下面开始动手写程序!注意是JAVA程序,不是Android程序

1.写一个工具类,叫ApkUtil,

import org.w3c.dom.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;

/**
 * Created by monch on 16/3/7.
 */
public class ApkUtil {

    /**
     * 检测文件是否存在
     *
     * @param filePath
     * @return
     */
    public static boolean fileExists(String filePath) {
        return new File(filePath).exists();
    }

    /**
     * 清除解压后生成的文件夹
     *
     * @param filePath
     */
    public static void fileClear(String filePath) {
        final File file = new File(filePath);
        if (!file.exists()) return;
        if (!file.isFile()) {
            final String[] childFilePathArray = file.list();
            for (String childFilePath : childFilePathArray) {
                final File childFile = new File(filePath, childFilePath);
                fileClear(childFile.getAbsolutePath());
            }
        }
        file.delete();
    }

    /**
     * 执行命令
     *
     * @param command
     */
    public static void runCommand(String command) {
        System.out.println("执行:" + command);
        final Runtime runtime = Runtime.getRuntime();
        InputStream inputStream = null;
        InputStreamReader inputStreamReader = null;
        BufferedReader bufferedReader = null;
        try {
            Process process = runtime.exec(command);
            inputStream = process.getInputStream();
            inputStreamReader = new InputStreamReader(inputStream);
            bufferedReader = new BufferedReader(inputStreamReader);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
            int exitValue = process.waitFor();
            System.out.println("Process Exit Value : " + exitValue);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStreamReader != null) {
                try {
                    inputStreamReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 修改渠道号
     *
     * @param value
     */
    public static boolean modifyChannelValue(File file, String value) {
        System.out.println("修改渠道号开始:" + value);
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setIgnoringElementContentWhitespace(true);
        final DocumentBuilder builder;
        Document document = null;
        try {
            builder = factory.newDocumentBuilder();
            document = builder.parse(file);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (document == null) {
            System.out.println("修改渠道号转换失败:" + value);
            return false;
        }
        Node applicationNode = document.getDocumentElement()
                .getElementsByTagName("application").item(0);
        if (applicationNode == null) {
            System.out.println("解析Application节点异常:" + value);
            return false;
        }
        final NodeList applicationNodeArray = applicationNode.getChildNodes();
        final int count = applicationNodeArray.getLength();
        for (int i = 0; i < count; i++) {
            final Node childNode = applicationNodeArray.item(i);
            if (!"meta-data".equals(childNode.getNodeName())) continue;
            final NamedNodeMap M = childNode.getAttributes();
            final Node N = M.getNamedItem("android:name");
            if (N == null || !"UMENG_CHANNEL".equals(N.getNodeValue())) continue;
            final Node NV = M.getNamedItem("android:value");
            NV.setNodeValue(value);
            M.setNamedItem(NV);
            return saveChannelValue(document, file.getAbsolutePath());
        }
        return false;
    }

    /**
     *  保存修改后的XML文件
     * @param document
     * @param filePath
     */
    private static boolean saveChannelValue(Document document, String filePath) {
        TransformerFactory factory = TransformerFactory.newInstance();
        try {
            Transformer transformer = factory.newTransformer();
            DOMSource source = new DOMSource(document);
            StreamResult result = new StreamResult(new File(filePath));
            transformer.transform(source, result);
            System.out.println("保存XML文件成功:" + filePath);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 签名APK
     * @param keystoreFilePath 签名文件路径
     * @param keystoreTag 签名文件别名
     * @param keystorePassword 签名文件密码
     * @param sourceFilePath 源文件,对应未签名的文件
     * @param targetFilePath 目标文件,对应签名后最终的文件
     * @param deleteSourceFile 是否删除源文件
     */
    public static void signApk(final String keystoreFilePath, String keystoreTag, String keystorePassword, String sourceFilePath, String targetFilePath, boolean deleteSourceFile) {
        System.out.println("正在准备签名APK:" + sourceFilePath + "," + targetFilePath);
        runCommand(String.format("jarsigner -verbose -keystore %s -signedjar %s %s %s -storepass %s", keystoreFilePath, targetFilePath, sourceFilePath, keystoreTag, keystorePassword));
        final File targetFile = new File(targetFilePath);
        if (deleteSourceFile && targetFile.exists()) {
            new File(sourceFilePath).delete();
        }
    }

}
工具类很简单,方法上的注释就能清楚的解释此方法是做什么的。如有不懂的朋友,可留言!

下一步,创建一个MakeApk的类,用于执行整个流程

    private static final String[] CHANNEL_NAME_ARRAY = new String[]{
            "渠道名称0",
            "渠道名称1",
            "渠道名称2",
            "渠道名称3",
            "渠道名称4",
            "渠道名称5",
            "渠道名称6",
            "渠道名称7",
            "渠道名称8",
            "渠道名称9",
            "渠道名称10"
    };

    // 文件绝对路径,例:/Users/monch/Dev/apktool/make/
    private static final String POSITION = "/Users/monch/Dev/apktool/make/";
    // APK文件名称,注意,上面的路径中必须存有此文件,不带文件后缀
    private static final String APK_NAME = "app-release";
    // 签名文件绝对路径,例:/Users/monch/Dev//xxx.keystore
    private static final String KEYSTORE_PATH = "/Users/monch/Dev//xxx.keystore";
    // 签名文件别名
    private static final String KEYSTORE_TAG = "创建keystore文件时间别名";
    // 签名文件密码
    private static final String KEYSTORE_PASSWORD = "创建keystore文件时设置的密码";

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        System.out.println("开始时间:" + startTime);
        // 检查文件是否存在
        if (!ApkUtil.fileExists(POSITION + APK_NAME + ".apk")) {
            System.out.println("文件" + POSITION + APK_NAME + ".apk" + "不存在");
            return;
        }
        // 清理文件夹
        System.out.println("执行:清除解压后的文件");
        ApkUtil.fileClear(POSITION + APK_NAME);
        // 解压apk文件
        ApkUtil.runCommand("apktool d " + POSITION + APK_NAME + ".apk -o " + POSITION + APK_NAME);
        // 检查文件是否解压成功
        File manifestFile = new File(POSITION + APK_NAME, "AndroidManifest.xml");
        if (!ApkUtil.fileExists(manifestFile.getAbsolutePath())) {
            System.out.println("解压失败");
            return;
        }
        // 准备开始生成新渠道的APK
        File sourceFile = new File(POSITION + APK_NAME + "/AndroidManifest.xml");
        for (String channelName : CHANNEL_NAME_ARRAY) {
            if (!ApkUtil.modifyChannelValue(sourceFile, channelName)) {
                System.out.println("修改渠道号失败:" + channelName);
                continue;
            }
            final String apkUnSignName = APK_NAME + "_temp_" + channelName + ".apk";
            final String apkSignName = APK_NAME + "_sign_" + channelName + ".apk";
            ApkUtil.runCommand("apktool b " + POSITION + APK_NAME + " -o " + POSITION + apkUnSignName);
            ApkUtil.signApk(KEYSTORE_PATH, KEYSTORE_TAG, KEYSTORE_PASSWORD, POSITION + apkUnSignName, POSITION + apkSignName, true);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("结束时间:" + endTime + ",共计:" + (endTime - startTime));
    }
将上面的配置参数都配置好后,把apk文件放至配置的目录里以后,右键运行,一切就只剩等待完成了!


整个代码很简单,没有任何难以理解的逻辑。下次发布一份使用shell写的,在mac或linux上面运行,将更为简便!如有不懂的地方,或更简便的方法,请留言讨论,感谢!