方式一:
转自: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里面的数据结构如下:
360
xiaomi
anzhi
baidu
运行命令后,你会发现在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);
}
}