做Android开发都会碰到要打渠道包的情况,Gradle通过poroductFlavor可以方便的打任意数量的渠道包,但速度确是其致命问题,经过我的测试一个包要一分钟,两个包要1分半,电脑发热后15个包却要半小时,这样的速度实在是不能忍受啊
在查看了美团的多渠道打包文章后得知修改META-INF文件夹不会破坏APK的整体结构,因此我们可以往META-INF中加入一个标记文件来标识渠道号,这样不用编译,不用签名,只需复制一个文件然后往里面加一个文件即可,速度杠杠的
知晓后立马就开工了,读取渠道号的代码一改,然后手工解压了一个APK,然后往META-INF里面加了一个文件,然后再压缩改成APK就尝试安装了。可事实往往是残酷的,安装器直接告诉我这是一个无效的APK,百度后也没有什么结果,然后就多番尝试是不是压缩工具的问题,最终的结果是只有用好压压缩的才能安装,这就奇了怪了
郁闷之余发现了这篇文章分享一种最简单的Android打渠道包的方法,就是按照这种方法做的,然后重点看了一下,他往包里加文件的方式并不是解压再压缩,而是直接操作FileSystem来改,试了一下这种方法果然可以,下面是我修改后打包好的jar包,下载后可以直接使用
创建一个channels.txt文件要跟ChannelApkGenerator.jar在同一个目录下,每行一个渠道号,例如:
OfficialGooglePlayMSiteFacebookSNS2SNS3ApkTWGamerTWwgunBBS4BBS5OneMarketAppChinaMarket4Market5
执行命令打包(需要JDK7)
java -jar ChannelApkGenerator.jar /Users/xiaopan/Desktop/channel/app-Origin-release-1.0.0.apk
原始包可以放在任何地方,但需要输入完整路径,新生成的渠道包会跟原始包在同一个目录
使用这种方式打15个渠道包只需3秒,再加上使用gradle打一个原始包的时间耗时共1分3秒,并且渠道包越多效果越明显
Android读取渠道号的代码:
public static String readChannelFromApkMetaInfo(Context context) { String channel = null; String sourceDir = context.getApplicationInfo().sourceDir; final String start_flag = "META-INF/channel_"; 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(); } } } return channel;}
最后在Application的onCreate()方法中设置渠道号(友盟示例如下)
String channel = readChannelFromApkMetaInfo(context);if(channel == null || "".equals(channel.trim())){ channel = "debug";}AnalyticsConfig.setChannel(channel )
其它统计工具的设置方法请自行研究
最后附上打包的源代码:
import java.io.*;import java.net.URI;import java.nio.file.FileSystem;import java.nio.file.*;import java.nio.file.attribute.BasicFileAttributes;import java.util.HashMap;import java.util.LinkedList;import java.util.Map;public class ChannelApkGenerator { private static final String CHANNEL_PREFIX = "/META-INF/"; private static final String CHANNEL_FILE_NAME = "channels.txt"; private static final String FILE_NAME_CONNECTOR = "-"; private static final String CHANNEL_FLAG = "channel_"; public static void main(String[] args) { if (args.length <= 0) { System.out.println("请输入文件路径"); return; } final String apkFilePath = args[0]; File apkFile = new File(apkFilePath); if (!apkFile.exists()) { System.out.println("找不到文件:" + apkFile.getPath()); return; } String existChannel; try { existChannel = readChannel(apkFile); } catch (IOException e) { e.printStackTrace(); return; } if(existChannel != null){ System.out.println("此安装包已存在渠道号:" + existChannel + ",请使用原始包"); return; } String parentDirPath = apkFile.getParent(); if(parentDirPath == null){ System.out.println("请输入完整的文件路径:" + apkFile.getPath()); return; } String fileName = apkFile.getName(); int lastPintIndex = fileName.lastIndexOf("."); String fileNamePrefix; String fileNameSurfix; if (lastPintIndex != -1) { fileNamePrefix = fileName.substring(0, lastPintIndex); fileNameSurfix = fileName.substring(lastPintIndex, fileName.length()); } else { fileNamePrefix = fileName; fileNameSurfix = ""; } LinkedList<String> channelList = getChannelList(new File(apkFile.getParentFile(), CHANNEL_FILE_NAME)); if(channelList == null){ return; } for (String channel : channelList) { String newApkPath = parentDirPath + File.separator + fileNamePrefix + FILE_NAME_CONNECTOR + channel + fileNameSurfix; try { copyFile(apkFilePath, newApkPath); } catch (IOException e) { e.printStackTrace(); break; } if(!changeChannel(newApkPath, CHANNEL_FLAG + channel)){ break; } } } /** * 读取渠道列表 */ private static LinkedList<String> getChannelList(File channelListFile) { if (!channelListFile.exists()) { System.out.println("找不到渠道配置文件:" + channelListFile.getPath()); return null; } if (!channelListFile.isFile()) { System.out.println("这不是一个文件:" + channelListFile.getPath()); return null; } LinkedList<String> channelList = null; BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(new FileInputStream(channelListFile), "UTF-8")); String lineTxt; while ((lineTxt = reader.readLine()) != null) { lineTxt = lineTxt.trim(); if (lineTxt.length() > 0) { if (channelList == null) { channelList = new LinkedList<>(); } channelList.add(lineTxt); } } } catch (IOException e) { System.out.println("读取渠道配置文件失败:" + channelListFile.getPath()); e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return channelList; } /** * 复制文件 */ private static void copyFile(final String sourceFilePath, final String targetFilePath) throws IOException { File sourceFile = new File(sourceFilePath); File targetFile = new File(targetFilePath); if(targetFile.exists()){ //noinspection ResultOfMethodCallIgnored targetFile.delete(); } BufferedInputStream inputStream = null; BufferedOutputStream outputStream = null; try { inputStream = new BufferedInputStream(new FileInputStream(sourceFile)); outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)); byte[] b = new byte[1024 * 8]; int realReadLength; while ((realReadLength = inputStream.read(b)) != -1) { outputStream.write(b, 0, realReadLength); } } catch (Exception e) { System.out.println("复制文件失败:" + targetFilePath); e.printStackTrace(); } finally { if (inputStream != null){ inputStream.close(); } if (outputStream != null){ outputStream.flush(); outputStream.close(); } } } /** * 添加渠道号,原理是在apk的META-INF下新建一个文件名为渠道号的文件 */ private static boolean changeChannel(final String newApkFilePath, final String channel) { try (FileSystem fileSystem = createZipFileSystem(newApkFilePath, false)){ final Path root = fileSystem.getPath(CHANNEL_PREFIX); ChannelFileVisitor visitor = new ChannelFileVisitor(); try { Files.walkFileTree(root, visitor); } catch (IOException e) { e.printStackTrace(); System.out.println("添加渠道号失败:" + channel); e.printStackTrace(); return false; } Path existChannel = visitor.getChannelFile(); if (existChannel != null) { System.out.println("此安装包已存在渠道号:" + existChannel.getFileName().toString() + ", FilePath: " + newApkFilePath); return false; } Path newChannel = fileSystem.getPath(CHANNEL_PREFIX + channel); try { Files.createFile(newChannel); } catch (IOException e) { System.out.println("添加渠道号失败:" + channel); e.printStackTrace(); return false; } System.out.println("添加渠道号成功:" + channel+", NewFilePath:" + newApkFilePath); 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<String, String> env = new HashMap<>(); if (create) { env.put("create", "true"); } return FileSystems.newFileSystem(uri, env); } private static class ChannelFileVisitor extends SimpleFileVisitor<Path> { private Path channelFile; public Path getChannelFile() { return channelFile; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (file.getFileName().toString().startsWith(CHANNEL_FLAG)) { channelFile = file; return FileVisitResult.TERMINATE; } else { return FileVisitResult.CONTINUE; } } } private static String readChannel(File apkFile) throws IOException { FileSystem zipFileSystem; try { zipFileSystem = createZipFileSystem(apkFile.getPath(), false); } catch (IOException e) { e.printStackTrace(); System.out.println("读取渠道号失败:" + apkFile.getPath()); throw e; } final Path root = zipFileSystem.getPath(CHANNEL_PREFIX); ChannelFileVisitor visitor = new ChannelFileVisitor(); try { Files.walkFileTree(root, visitor); } catch (IOException e) { e.printStackTrace(); System.out.println("读取渠道号失败:" + apkFile.getPath()); throw e; } Path existChannel = visitor.getChannelFile(); return existChannel != null ? existChannel.getFileName().toString() : null; }}