使用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上面运行,将更为简便!如有不懂的地方,或更简便的方法,请留言讨论,感谢!