Android开发之旅-实用工具之多渠道打包工具

AndroidStudio实现多渠道打包速度慢,且公司渠道多大54个(后面还会追加/(ㄒoㄒ)/~~),一到上线就需要各个渠道的apk,着实是慢,现在用自己编写的jar包来实现多渠道打包。

先看多渠道打包成果。


image.png

现在我们来实现多渠道的jar包吧。
1、首先新建一个Java Application,创建一个PackagingTool 工具类,添加如下代码。

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class PackagingTool {

    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 final String CHANNEL_LIST_TXT = "channel_list";
    private static String source_path;

    public static void main(String[] args) throws Exception {
        if (args.length < 3) {
            System.out.println("参数不足,请重新输入...");
            return;
        }
        final String source_apk_path = args[0];
        System.out.println("接收第一个参数源apk:"+args[0]);
        
        int last_index = source_apk_path.lastIndexOf("/") + 1;
        source_path = source_apk_path.substring(0, last_index);
        System.out.println("源apk所在路径:" + source_path);
        
        final String source_apk_name = source_apk_path.substring(last_index,
                source_apk_path.length());
        System.out.println("源apk名称:" + source_apk_name);
        
        final String last_name = ".apk";
        
        String channelName = args[1];
        System.out.println("接收到的渠道名称:" + channelName);
        
        String channelVersion = args[2];
        System.out.println("接收到的渠道版本:" + channelVersion);
        
        //如果接收到的是all(就是打所有渠道的apk)
        if ("all".equals(channelName)) {
            System.out.println("开始生成所有渠道包...");

            ArrayList<String> allChannels = getChannelList(source_path+CHANNEL_LIST_TXT);
            if (allChannels==null||allChannels.size()==0) {
                System.out.println("生成所有渠道包失败,渠道信息为空");
                return;
            }
            ChannelJson channelJson = null;
            for(String str:allChannels){
                
                if ("hsz".equals(str)) {
                    System.out.println("无需生成红手指渠道apk...");
                    continue;
                }else{
                    channelJson = new ChannelJson();
                    channelJson.setChannelName(str);
                    channelJson.setChannelID("com.redfinger.app."+str);
                    channelJson.setChannelVersion(channelVersion);
                    
                    String new_apk_path = source_path
                            + source_apk_name.substring(0, source_apk_name.length()
                                    - last_name.length()) + "_channel_" + str +"_v"+channelVersion+ last_name;
                    copyFile(source_apk_path, new_apk_path);
                    changeChannel(new_apk_path, "channel", channelJson.toString());
                    System.out.println("生成渠道包成功,渠道:"+str+"...");
                }
            }
            
        }else if("hsz".equals(channelName)){
            System.out.println("无需生成红手指渠道apk...");
            return;
        }else{
            String new_apk_path = source_path
                    + source_apk_name.substring(0, source_apk_name.length()
                            - last_name.length()) + "_channel_" + args[1] +"_v"+channelVersion+ last_name;
            System.out.println("将生成渠道包,名称为:" + new_apk_path);
            
            System.out.println("开始生产渠道apk:"+new_apk_path+"...");
            
            copyFile(source_apk_path, new_apk_path);
            
            System.out.println("完成生成渠道apk:"+new_apk_path+"...");
            
            ChannelJson channelJson = new ChannelJson();
            channelJson.setChannelName(channelName);
            channelJson.setChannelID("com.redfinger.app."+channelName);
            channelJson.setChannelVersion(channelVersion);
            System.out.println("生成的Json数据内容:"+channelJson.toString());
            
            System.out.println("开始打入渠道信息json:"+channelJson.toString());
            changeChannel(new_apk_path, "channel", channelJson.toString());
        }
        System.out.println("生成渠道包完毕...");
    }

    public static class ChannelJson{
        private String channelName="hsz";
        private String channelID="com.redfinger.app";
        private String channelVersion="2.1.15";
        public String getChannelName() {
            return channelName;
        }
        public void setChannelName(String channelName) {
            this.channelName = channelName;
        }
        public String getChannelID() {
            return channelID;
        }
        public void setChannelID(String channelID) {
            this.channelID = channelID;
        }
        public String getChannelVersion() {
            return channelVersion;
        }
        public void setChannelVersion(String channelVersion) {
            this.channelVersion = channelVersion;
        }
        
        public String toString(){
            StringBuffer buffer = new StringBuffer();
            buffer.append("{");
            buffer.append("\"channelName"+"\":"+"\""+getChannelName()+"\",");
            buffer.append("\"channelID"+"\":"+"\""+getChannelID()+"\",");
            buffer.append("\"channelVersion"+"\":"+"\""+getChannelVersion()+"\"");
            buffer.append("}");
            return buffer.toString();
        }
    }
    
    /**
     * 修改渠道号,原理是在apk的META-INF下新建一个文件名为渠道号的文件
     */
    public static boolean changeChannel(final String zipFilename,
            String channel, String jsonstr) {
        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 {
                Path path = Files.createFile(newChannel);
                if (path != null) {
                    BufferedWriter writer = Files.newBufferedWriter(path,
                            StandardCharsets.UTF_8, StandardOpenOption.APPEND); // 追加
                    writer.write(jsonstr);
                    writer.close();
                }
            }
            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;
        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<String> getChannelList(String filePath) {
        ArrayList<String> channel_list = new ArrayList<String>();
        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();
        }
    }
}```
先run下这个Java Application工程,右键项目选择export->java->Runnable jar file->导出到D盘中,然后在D:盘根目录下生成文件channelList文件,添加渠道列表内容。

youku
yxfw
jdy
jbjl
sy
zfwl
nmzs
xxfz
hnyx
mmy
wy
azyxlt
ayx
bsj
jy
vip_tg

将自己原apk文件放置于如上channelList文件相同目录。我这里放到D盘。CMD窗口运行D:\>java -jar apkTool.jar RedFingerClient.apk all 2.1.15就能打出所有渠道包。生成一个渠道就需将all 修改成渠道名称即可。

D:>java -jar apkTool.jar RedFingerClient.apk all 2.1.15
接收第一个参数源apk:RedFingerClient.apk
源apk所在路径:
源apk名称:RedFingerClient.apk
接收到的渠道名称:all
接收到的渠道版本:2.1.15
开始生成所有渠道包...
无需生成红手指渠道apk...
生成渠道包成功,渠道:hsz_uat...
生成渠道包成功,渠道:youku...
生成渠道包成功,渠道:yxfw...
生成渠道包成功,渠道:jdy...
生成渠道包成功,渠道:jbjl...
生成渠道包成功,渠道:sy...
生成渠道包成功,渠道:zfwl...
生成渠道包成功,渠道:nmzs...
生成渠道包成功,渠道:xxfz...
生成渠道包成功,渠道:hnyx...
生成渠道包成功,渠道:mmy...
生成渠道包成功,渠道:wy...
生成渠道包成功,渠道:azyxlt...
生成渠道包成功,渠道:ayx...
生成渠道包成功,渠道:bsj...
生成渠道包成功,渠道:jy...
生成渠道包成功,渠道:vip_tg...
生成渠道包完毕...

现在就可以在新生产的渠道apk中查看重要的渠道文件了。
我这里把apk文件格式改成.zip格式方便查看。
![image.png](http://upload-images.jianshu.io/upload_images/1914079-883436dd373c3f9a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我们看看内容channel文件内容:
![channel文件内容](http://upload-images.jianshu.io/upload_images/1914079-64b9c5415d03ce34.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


现在接下来就需通过apk文件去获取渠道名称和渠道ID了,详细代码如下:

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.util.Log;

public class ChannelUtil {

private String channelName = null;
private String channelId = null;
private String channelVersion = null;

private Context context;
private static ChannelUtil channelUtil;

/**
 * 获取渠道名称
 * @return
 */
public String getChannelName() {
    return channelName;
}

/**
 * 获取渠道ID
 * @return
 */
public String getChannelId() {
    return channelId;
}

/**
 * 获取渠道版本
 * @return
 */
public String getChannelVersion() {
    return channelVersion;
}

public static ChannelUtil getInstant(Context context) {
    if (channelUtil==null) {
        channelUtil = new ChannelUtil(context);
    }
    return channelUtil;
}

private ChannelUtil(Context context) {
    this.context = context;
}

public ChannelUtil init() {
    final String start_flag = "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)&&!entry.isDirectory()) {
                InputStream inputStream = null;
                try {
                    inputStream = new BufferedInputStream(zipfile.getInputStream(entry));
                } catch (IOException e) {
                    e.printStackTrace();
                    Log.e("ChannelUtil", "Read channel failure");
                }
                if (inputStream!=null) {
                    String jsonStr = getInputStreamTxt(inputStream);
                    JSONObject jsonObject = new JSONObject(jsonStr);
                    channelName = jsonObject.getString("channelName");
                    channelId = jsonObject.getString("channelID");
                    channelVersion = jsonObject.getString("channelVersion");
                }
                break;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        e.printStackTrace();
    } finally {
        if (zipfile != null) {
            try {
                zipfile.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    if (channelName==null||channelId==null||channelVersion==null) {
        channelName = "com.redfinger.app";
        channelId = "com.redfinger.app";
        channelVersion = "3.1.14";
    }
    return this;
}

/**
 * 读取传入输入流的内容
 * @param iStream
 * @return 内容
 * @throws IOException 会跑出IO异常
 */
public String getInputStreamTxt(InputStream iStream) throws IOException{
    StringBuffer out = new StringBuffer();
    byte[] b = new byte[1024];
    int n;
    while ((n = iStream.read(b)) != -1) {
        out.append(new String(b, 0, n));
    }
    return out.toString();
}

}

//获取渠道名称和渠道ID
ChannelUtil util = ChannelUtil.getInstant(this);
TextView tv = (TextView) findViewById(R.id.channel);
tv.setText("渠道名:"+util.getChannelName()+"\n"+"渠道ID:"+util.getChannelID());

![image.png](http://upload-images.jianshu.io/upload_images/1914079-efba74f39fc7a79c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


至此,多渠道打包完毕,万圣节不快乐....
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,734评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,628评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,773评论 6 342
  • 这一章主要针对项目中可以用到的一些实用功能来介绍Android Gradle,比如如何隐藏我们的证书文件,降低风险...
    acc8226阅读 7,572评论 3 25
  • 当清晨的第一拢阳光射进窗户,朦胧的睁开了眼睛,心里隐约着要发生一些事情。“今天你结婚,快准备吧”正在看似忙碌又不...
    溪水旁的小树苗1阅读 137评论 0 0