请选择 进入手机版 | 继续访问电脑版
设为首页收藏本站

houcx

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 5747|回复: 0

Java实现Android APK多渠道打包

[复制链接]

224

主题

226

帖子

1133

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
1133
发表于 2018-7-5 17:02:32 | 显示全部楼层 |阅读模式
使用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上面运行,将更为简便!如有不懂的地方,或更简便的方法,请留言讨论,感谢!



来自:https://blog.csdn.net/monchchen/article/details/50821033


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|houcx ( 京ICP备15004793号  

GMT+8, 2019-7-21 06:28 , Processed in 0.066685 second(s), 24 queries .

Powered by houcx! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表