【Android Build】高效批量打渠道包

panpf發表於2019-04-15

做Android開發都會碰到要打渠道包的情況,Gradle通過poroductFlavor可以方便的打任意數量的渠道包,但速度確是其致命問題,經過我的測試一個包要一分鐘,兩個包要1分半,電腦發熱後15個包卻要半小時,這樣的速度實在是不能忍受啊

在檢視了美團的多渠道打包文章後得知修改META-INF資料夾不會破壞APK的整體結構,因此我們可以往META-INF中加入一個標記檔案來標識渠道號,這樣不用編譯,不用簽名,只需複製一個檔案然後往裡面加一個檔案即可,速度槓槓的

知曉後立馬就開工了,讀取渠道號的程式碼一改,然後手工解壓了一個APK,然後往META-INF裡面加了一個檔案,然後再壓縮改成APK就嘗試安裝了。可事實往往是殘酷的,安裝器直接告訴我這是一個無效的APK,百度後也沒有什麼結果,然後就多番嘗試是不是壓縮工具的問題,最終的結果是隻有用好壓壓縮的才能安裝,這就奇了怪了

鬱悶之餘發現了這篇文章分享一種最簡單的Android打渠道包的方法,就是按照這種方法做的,然後重點看了一下,他往包里加檔案的方式並不是解壓再壓縮,而是直接操作FileSystem來改,試了一下這種方法果然可以,下面是我修改後打包好的jar包,下載後可以直接使用

ChannelApkGenerator.jar

建立一個channels.txt檔案要跟ChannelApkGenerator.jar在同一個目錄下,每行一個渠道號,例如:

Official
GooglePlay
MSite
Facebook
SNS2
SNS3
ApkTW
GamerTW
wgun
BBS4
BBS5
OneMarket
AppChina
Market4
Market5
複製程式碼

執行命令打包(需要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;
    }
}
複製程式碼

相關文章