方式一:
转自:http://blog.csdn.net/wei1583812/article/details/44463697

做Android开发一转眼就四年了,以前是用ant打包的,习惯了也没觉得慢。

今年年初加入了新公司,新公司用的是Android studio开发,用的是gradle构建项目。

由于gradle构建每次都是重新编译项目,所以打包时就特别慢了,16个渠道包要打一个小时吧。

然后我们的项目负责人就交给我一个任务,研究下有什么快的打包方法,

并发给我一篇参考文章:http://tech.meituan.com/mt-apk-packaging.html

我一边写代码一边测试,终于找到了一种很快的打渠道包的方法。

因为APK其实就是ZIP的格式,所以,解压apk后,会看到里面有个META-INF目录。

由于META-INF目录并不会影响到APK的签名和运行,所以我们可以在META-INF目录里添加一个空文件,

不同的渠道就添加不同的空文件,文件名代表不同的渠道。

代码是java写的:

public class Tool {    private static final String CHANNEL_PREFIX = "/META-INF/";    private static final String CHANNEL_PATH_MATCHER = "regex:/META-INF/mtchannel_[0-9a-zA-Z]{1,5}";    private static String source_path;    private static final String channel_file_name = "channel_list.txt";    private static final String channel_flag = "channel_";    public static void main(String[] args) throws Exception {        //        if (args.length <= 0) {//            System.out.println("请输入文件路径作为参数");//            return;//        }//        final String source_apk_path = args[0];//main方法传入的源apk的路径,是执行jar时命令行传入的,不懂的往下看。        String source_apk_path = "D:/apk/app.apk";        int last_index = source_apk_path.lastIndexOf("/") + 1;        source_path = source_apk_path.substring(0, last_index);        final String source_apk_name = source_apk_path.substring(last_index, source_apk_path.length());        System.out.println("包路径:" + source_path);        System.out.println("文件名:" + source_apk_name);        ArrayList channel_list = getChannelList(source_path + channel_file_name);        final String last_name = ".apk";        for (int i = 0; i < channel_list.size(); i++) {            final String new_apk_path = source_path + source_apk_name.substring(0, source_apk_name.length() - last_name.length()) //                    + "_" + channel_list.get(i) + last_name;            copyFile(source_apk_path, new_apk_path);            changeChannel(new_apk_path, channel_flag + channel_list.get(i));        }    }    /**     * 修改渠道号,原理是在apk的META-INF下新建一个文件名为渠道号的文件     */    public static boolean changeChannel(final String zipFilename, final String channel) {        try (FileSystem zipfs = createZipFileSystem(zipFilename, false)) {            final Path root = zipfs.getPath("/META-INF/");            ChannelFileVisitor visitor = new ChannelFileVisitor();            Files.walkFileTree(root, visitor);            Path existChannel = visitor.getChannelFile();            Path newChannel = zipfs.getPath(CHANNEL_PREFIX + channel);            if (existChannel != null) {                Files.move(existChannel, newChannel, StandardCopyOption.ATOMIC_MOVE);            } else {                Files.createFile(newChannel);            }            return true;        } catch (IOException e) {            System.out.println("添加渠道号失败:" + channel);            e.printStackTrace();        }        return false;    }    private static FileSystem createZipFileSystem(String zipFilename, boolean create) throws IOException {        final Path path = Paths.get(zipFilename);        final URI uri = URI.create("jar:file:" + path.toUri().getPath());        final Map env = new HashMap<>();        if (create) {            env.put("create", "true");        }        return FileSystems.newFileSystem(uri, env);    }    private static class ChannelFileVisitor extends SimpleFileVisitor<Path> {        private Path channelFile;        private PathMatcher matcher = FileSystems.getDefault().getPathMatcher(CHANNEL_PATH_MATCHER);        public Path getChannelFile() {            return channelFile;        }        @Override        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {            if (matcher.matches(file)) {                channelFile = file;                return FileVisitResult.TERMINATE;            } else {                return FileVisitResult.CONTINUE;            }        }    }    /** 得到渠道列表 */    private static ArrayList getChannelList(String filePath) {        ArrayList channel_list = new ArrayList();        try {            String encoding = "UTF-8";            File file = new File(filePath);            if (file.isFile() && file.exists()) { // 判断文件是否存在                InputStreamReader read = new InputStreamReader(new FileInputStream(file), encoding);// 考虑到编码格式                BufferedReader bufferedReader = new BufferedReader(read);                String lineTxt = null;                while ((lineTxt = bufferedReader.readLine()) != null) {                    // System.out.println(lineTxt);                    if (lineTxt != null && lineTxt.length() > 0) {                        channel_list.add(lineTxt);                    }                }                read.close();            } else {                System.out.println("找不到指定的文件");            }        } catch (Exception e) {            System.out.println("读取文件内容出错");            e.printStackTrace();        }        return channel_list;    }    /** 复制文件 */    private static void copyFile(final String source_file_path, final String target_file_path) throws IOException {        File sourceFile = new File(source_file_path);        File targetFile = new File(target_file_path);        BufferedInputStream inBuff = null;        BufferedOutputStream outBuff = null;        try {            // 新建文件输入流并对它进行缓冲            inBuff = new BufferedInputStream(new FileInputStream(sourceFile));            // 新建文件输出流并对它进行缓冲            outBuff = new BufferedOutputStream(new FileOutputStream(targetFile));            // 缓冲数组            byte[] b = new byte[1024 * 5];            int len;            while ((len = inBuff.read(b)) != -1) {                outBuff.write(b, 0, len);            }            // 刷新此缓冲的输出流            outBuff.flush();        } catch (Exception e) {            System.out.println("复制文件失败:" + target_file_path);            e.printStackTrace();        } finally {            // 关闭流            if (inBuff != null)                inBuff.close();            if (outBuff != null)                outBuff.close();        }    }}

1、新建一个java工程,把上面的代码复制进去。
2、对着这个类点右键,选择Export-java-Runnable JAR file

3、在Launch configuration中,选择你所要导出的类(如果这里不能选择,那么你要run一下你的工程,run成功了才能选择你要导为jar的类),

假设导出的jar的名字是apktool.jar

然后在命令行输入:

java -jar /Users/company/Documents/apk/apktool.jar /Users/company/Documents/apk/test.apk

我用的mac电脑,路径和windows不一样,上面的路径都是拖拽进命令行的。

/Users/company/Documents/apk/apktool.jar 表示jar包所在路径;
/Users/company/Documents/apk/test.apk表示你源apk路径,这个是作为命令行参数传入main方法的。

test.apk就是你已经打包成功的一个apk,就是源apk,在你源apk的基础上生成渠道包。

channel_list.txt一定要和这个源apk在同一个目录下。

比如channel_list.txt里面的数据结构如下:

360xiaomianzhibaidu

运行命令后,你会发现在channel_list.text和源apk目录下,会生成你想要的渠道包。
你可以把扩展名改为.zip,然后解压看看是否在META-INF目录下生成你想要的渠道名文件。

最后,就是读取这个渠道标识了,代码是写在Android工程里的,代码如下:

private static String channel = null;    public static String getChannel(Context context) {        if (channel != null) {            return channel;        }        final String start_flag = "META-INF/channel_";        ApplicationInfo appinfo = context.getApplicationInfo();        String sourceDir = appinfo.sourceDir;        ZipFile zipfile = null;        try {            zipfile = new ZipFile(sourceDir);            Enumeration<?> entries = zipfile.entries();            while (entries.hasMoreElements()) {                ZipEntry entry = ((ZipEntry) entries.nextElement());                String entryName = entry.getName();                if (entryName.contains(start_flag)) {                    channel = entryName.replace(start_flag, "");                    break;                }            }        } catch (IOException e) {            e.printStackTrace();        } finally {            if (zipfile != null) {                try {                    zipfile.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }        if (channel == null || channel.length() <= 0) {            channel = "guanwang";//读不到渠道号就默认是官方渠道        }        return channel;    }

如果你用的友盟统计,可以在主Activity里这么写:AnalyticsConfig.setChannel(“获取到的渠道”);

好了,结束了,有问题留言。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

方式二:

把渠道信息写入APK文件的末尾

这个方法不需要解压缩,直接向apk文件末尾处写数据就可以了。打包速度比上个方法还要快一些(因为少了解压缩和压缩的过程)。

原理探究

写:apk文件打包好了之后,向apk文件的最末位处写入开始写入渠道的标记,再把渠道写到标记之后。这样apk的渠道已经写好了。

读:程序在启动的时候,直接找到apk的存储位置,读流的方式找到之前写的标记位,标记位之后就是渠道了,拿到渠道之后对这个数据做持久化处理就可以了(放sp或者sqlite里都可以),这样就不用每次都读渠道号啦。

/* * Copyright (C) 2014 [email protected] *  * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *  *     http://www.apache.org/licenses/LICENSE-2.0 *  * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package com.example.multichannel2;import java.io.ByteArrayOutputStream;import java.io.Closeable;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.RandomAccessFile;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.nio.channels.FileChannel;import java.security.Key;import java.util.Arrays;import java.util.LinkedHashMap;import java.util.Map;import java.util.zip.ZipFile;import javax.crypto.Cipher;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.DESKeySpec;import javax.crypto.spec.IvParameterSpec;/** * 多渠道打包工具;
* 利用的是Zip文件“可以添加comment(注释)”的数据结构特点,在文件的末尾写入任意数据,而不用重新解压zip文件(apk文件就是zip文件格式);
* 创建时间: 2014-12-16 18:56:29 * @author zhangguojun * @version 1.1 * @since JDK1.6 Android2.2 */
public class MCPTool { /** * 数据结构体的签名标记 */ private static final String SIG = "MCPT"; /** * 数据结构的版本号 */ private static final String VERSION_1_1 = "1.1"; /** * 数据编码格式 */ private static final String CHARSET_NAME = "UTF-8"; /** * 加密用的IvParameterSpec参数 */ private static final byte[] IV = new byte[] { 1, 3, 1, 4, 5, 2, 0, 1 }; /** * 写入数据 * @param path 文件路径 * @param content 写入的内容 * @param password 加密密钥 * @throws Exception */ private static void write(File path, String content, String password) throws Exception { write(path, content.getBytes(CHARSET_NAME), password); } /** * 写入数据(如:渠道号) * @param path 文件路径 * @param content 写入的内容 * @param password 加密密钥 * @throws Exception */ private static void write(File path, byte[] content, String password) throws Exception { ZipFile zipFile = new ZipFile(path); boolean isIncludeComment = zipFile.getComment() != null; zipFile.close(); if (isIncludeComment) { throw new IllegalStateException("Zip comment is exists, Repeated write is not recommended."); } boolean isEncrypt = password != null && password.length() > 0; byte[] bytesContent = isEncrypt ? encrypt(password, content) : content; byte[] bytesVersion = VERSION_1_1.getBytes(CHARSET_NAME); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(bytesContent); // 写入内容; baos.write(short2Stream((short) bytesContent.length)); // 写入内容长度; baos.write(isEncrypt ? 1 : 0); // 写入是否加密标示; baos.write(bytesVersion); // 写入版本号; baos.write(short2Stream((short) bytesVersion.length)); // 写入版本号长度; baos.write(SIG.getBytes(CHARSET_NAME)); // 写入SIG标记; byte[] data = baos.toByteArray(); baos.close(); if (data.length > Short.MAX_VALUE) { throw new IllegalStateException("Zip comment length > 32767."); } // Zip文件末尾数据结构:{@see java.util.zip.ZipOutputStream.writeEND} RandomAccessFile raf = new RandomAccessFile(path, "rw"); raf.seek(path.length() - 2); // comment长度是short类型 raf.write(short2Stream((short) data.length)); // 重新写入comment长度,注意Android apk文件使用的是ByteOrder.LITTLE_ENDIAN(小端序); raf.write(data); raf.close(); } /** * 读取数据 * @param path 文件路径 * @param password 解密密钥 * @return 被该工具写入的数据(如:渠道号) * @throws Exception */ private static byte[] read(File path, String password) throws Exception { byte[] bytesContent = null; byte[] bytesMagic = SIG.getBytes(CHARSET_NAME); byte[] bytes = new byte[bytesMagic.length]; RandomAccessFile raf = new RandomAccessFile(path, "r"); Object[] versions = getVersion(raf); long index = (long) versions[0]; String version = (String) versions[1]; if (VERSION_1_1.equals(version)) { bytes = new byte[1]; index -= bytes.length; readFully(raf, index, bytes); // 读取内容长度; boolean isEncrypt = bytes[0] == 1; bytes = new byte[2]; index -= bytes.length; readFully(raf, index, bytes); // 读取内容长度; int lengthContent = stream2Short(bytes, 0); bytesContent = new byte[lengthContent]; index -= lengthContent; readFully(raf, index, bytesContent); // 读取内容; if (isEncrypt && password != null && password.length() > 0) { bytesContent = decrypt(password, bytesContent); } } raf.close(); return bytesContent; } /** * 读取数据结构的版本号 * @param raf RandomAccessFile * @return 数组对象,[0] randomAccessFile.seek的index,[1] 数据结构的版本号 * @throws IOException */ private static Object[] getVersion(RandomAccessFile raf) throws IOException { String version = null; byte[] bytesMagic = SIG.getBytes(CHARSET_NAME); byte[] bytes = new byte[bytesMagic.length]; long index = raf.length(); index -= bytesMagic.length; readFully(raf, index, bytes); // 读取SIG标记; if (Arrays.equals(bytes, bytesMagic)) { bytes = new byte[2]; index -= bytes.length; readFully(raf, index, bytes); // 读取版本号长度; int lengthVersion = stream2Short(bytes, 0); index -= lengthVersion; byte[] bytesVersion = new byte[lengthVersion]; readFully(raf, index, bytesVersion); // 读取内容; version = new String(bytesVersion, CHARSET_NAME); } return new Object[] { index, version }; } /** * RandomAccessFile seek and readFully * @param raf * @param index * @param buffer * @throws IOException */ private static void readFully(RandomAccessFile raf, long index, byte[] buffer) throws IOException { raf.seek(index); raf.readFully(buffer); } /** * 读取数据(如:渠道号) * @param path 文件路径 * @param password 解密密钥 * @return 被该工具写入的数据(如:渠道号) */ public static String readContent(File path, String password) { try { return new String(read(path, password), CHARSET_NAME); } catch (Exception ignore) { } return null; } /** * Android平台读取渠道号 * @param context Android中的android.content.Context对象 * @param mcptoolPassword mcptool解密密钥 * @param defValue 读取不到时用该值作为默认值 * @return */ public static String getChannelId(Object context, String mcptoolPassword, String defValue) { String content = MCPTool.readContent(new File(getPackageCodePath(context)), mcptoolPassword); return content == null || content.length() == 0 ? defValue : content; } /** * 获取已安装apk文件的存储路径(这里使用反射,因为MCPTool项目本身不需要导入Android的运行库) * @param context Android中的Context对象 * @return */ private static String getPackageCodePath(Object context) { try { return (String) context.getClass().getMethod("getPackageCodePath").invoke(context); } catch (Exception ignore) { } return null; } /** * 加密 * @param password * @param content * @return * @throws Exception */ private static byte[] encrypt(String password, byte[] content) throws Exception { return cipher(Cipher.ENCRYPT_MODE, password, content); } /** * 解密 * @param password * @param content * @return * @throws Exception */ private static byte[] decrypt(String password, byte[] content) throws Exception { return cipher(Cipher.DECRYPT_MODE, password, content); } /** * 加解密 * @param cipherMode * @param password * @param content * @return * @throws Exception */ private static byte[] cipher(int cipherMode, String password, byte[] content) throws Exception { DESKeySpec dks = new DESKeySpec(password.getBytes(CHARSET_NAME)); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); Key secretKey = keyFactory.generateSecret(dks); Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); IvParameterSpec spec = new IvParameterSpec(IV); cipher.init(cipherMode, secretKey, spec); return cipher.doFinal(content); } /** * short转换成字节数组(小端序) * @param stream * @param offset * @return */ private static short stream2Short(byte[] stream, int offset) { ByteBuffer buffer = ByteBuffer.allocate(2); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.put(stream[offset]); buffer.put(stream[offset + 1]); return buffer.getShort(0); } /** * 字节数组转换成short(小端序) * @param data * @return */ private static byte[] short2Stream(short data) { ByteBuffer buffer = ByteBuffer.allocate(2); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.putShort(data); buffer.flip(); return buffer.array(); } /** * nio高速拷贝文件 * @param source * @param target * @return * @throws IOException */ private static boolean nioTransferCopy(File source, File target) throws IOException { FileChannel in = null; FileChannel out = null; FileInputStream inStream = null; FileOutputStream outStream = null; try { File parent = target.getParentFile(); if (!parent.exists()) { parent.mkdirs(); } inStream = new FileInputStream(source); outStream = new FileOutputStream(target); in = inStream.getChannel(); out = outStream.getChannel(); return in.transferTo(0, in.size(), out) == in.size(); } finally { close(inStream); close(in); close(outStream); close(out); } } /** * 关闭数据流 * @param closeable */ private static void close(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException ignore) { } } }// /**// * 简单测试代码段// * @param args// * @throws Exception// */// public static void test() throws Exception {// String content = "abc";// String password = "123456789";// System.out.println("content = " + content);// String contentE = new String(encrypt(password, content.getBytes(CHARSET_NAME)), CHARSET_NAME);// System.out.println("contentE = " + contentE);// String contentD = new String(decrypt(password, contentE.getBytes(CHARSET_NAME)), CHARSET_NAME);// System.out.println("contentD = " + contentD);// // } /** * jar命令行的入口方法 * @param args * @throws Exception */ public static void main(String[] args) throws Exception { args = "-path D:/apk/app.apk -outdir D:/apk/new/ -contents xiaomi;360; -password 12345678".split(" ");//写入// args = "-version".split(" ");// args = "-path D:/apk/app_360.apk -password 12345678".split(" ");//读取 long time = System.currentTimeMillis(); String cmdPath = "-path"; String cmdOutdir = "-outdir"; String cmdContents = "-contents"; String cmdPassword = "-password"; String cmdVersion = "-version"; String help = "用法:java -jar MCPTool.jar [" + cmdPath + "] [arg0] [" + cmdOutdir + "] [arg1] [" + cmdContents + "] [arg2] [" + cmdPassword + "] [arg3]" + "\n" + cmdPath + " APK文件路径" + "\n" + cmdOutdir + " 输出路径(可选),默认输出到APK文件同一级目录" + "\n" + cmdContents + " 写入内容集合,多个内容之间用“;”分割(linux平台请在“;”前加“\\”转义符),如:xiaomi;360; 当没有" + cmdContents + "”参数时输出已有文件中的contents" + "\n" + cmdPassword + " 加密密钥(可选),长度8位以上,如果没有该参数,不加密" + "\n" + cmdVersion + " 显示MCPTool版本号" + "\n例如:" + "\n写入:java -jar MCPTool.jar -path D:/test.apk -outdir ./ -contents googleplay;m360; -password 12345678" + "\n读取:java -jar MCPTool.jar -path D:/test.apk -password 12345678"; if (args.length == 0 || args[0] == null || args[0].trim().length() == 0) { System.out.println(help); } else { if (args.length > 0) { if (args.length == 1 && cmdVersion.equals(args[0])) { System.out.println("version: " + VERSION_1_1); } else { Map argsMap = new LinkedHashMap(); for (int i = 0; i < args.length; i += 2) { if (i + 1 < args.length) { if (args[i + 1].startsWith("-")) { throw new IllegalStateException("args is error, help: \n" + help); } else { argsMap.put(args[i], args[i + 1]); } } } System.out.println("argsMap = " + argsMap); File path = argsMap.containsKey(cmdPath) ? new File(argsMap.get(cmdPath)) : null; String parent = path == null? null : (path.getParent() == null ? "./" : path.getParent()); File outdir = parent == null ? null : new File(argsMap.containsKey(cmdOutdir) ? argsMap.get(cmdOutdir) : parent); String[] contents = argsMap.containsKey(cmdContents) ? argsMap.get(cmdContents).split(";") : null; String password = argsMap.get(cmdPassword); if (path != null) { System.out.println("path: " + path); System.out.println("outdir: " + outdir); if (contents != null && contents.length > 0) { System.out.println("contents: " + Arrays.toString(contents)); } System.out.println("password: " + password); if (contents == null || contents.length == 0) { // 读取数据; System.out.println("content: " + readContent(path, password)); } else { // 写入数据; String fileName = path.getName(); int dot = fileName.lastIndexOf("."); String prefix = fileName.substring(0, dot); String suffix = fileName.substring(dot); for (String content : contents) { File target = new File(outdir, prefix + "_" + content + suffix); if (nioTransferCopy(path, target)) { write(target, content, password); } } } } } } } System.out.println("time:" + (System.currentTimeMillis() - time)); }}

上面MCPTool工具类,直接运行main(),只需改变args的值即可实现:写入(生成多个渠道包,并把渠道号写入清单文件)、读取(读出写入的渠道号)

调取读取的方法如下:

public class Test {    public static void main(String[] args) throws Exception {        String channel = MCPTool.readContent(new File("D:/apk/app_360.apk"), "12345678");        System.out.print(channel);    }}

更多相关文章

  1. 4.0、Android(安卓)Studio配置你的构建
  2. Android中各种Adapter的使用方法
  3. 将 apk无线安装到 android 设备中的四种方式
  4. 采用busybox 代替android 自带的shell
  5. Android(安卓)NDK开发技巧二
  6. Android(安卓)APK加固后如何签名
  7. Android启动过程以及各个镜像的关系
  8. OpenCV4Android(安卓)环境配置(最新详细教程)
  9. Android(安卓)Q之提前适配攻略(五)(存储权限变更)

随机推荐

  1. Android(安卓)JSON数据与实体类之间的相
  2. Android(安卓)getevent命令分析Input事件
  3. android实现接通和挂断电话
  4. Android(安卓)异步加载图片
  5. Android之MediaRecorder进行录影
  6. 带header的GridView(HeaderGridView)
  7. 【Android】Intent中使用Extra传递数据
  8. 【高通SDM660平台 Android(安卓)10.0】(1
  9. Android(安卓)RxJava操作符详解 系列:组合
  10. android监听器(Listener)的使用