Android 多渠道打包方式详解

面试的时候,如果面试官突然问到:你们渠道包是怎么打的?如果你说是用gradle一个一个编译的,然后他很鄙视的说这个效率太低啦,你们写过什么脚本自己打渠道包没?你肯定心里想,卧槽,这么狂炫吊炸天,自己写脚本打包?!其实这个根本也不是太难啦!!今天就来聊聊多渠道打包的原理以及如何自己DIY多渠道打包的工具!

渠道包出现

当一个产品到发版的时候,我们搞Android的就会面临一个超级尴尬的问题:国内这么多的渠道,渠道统计是必须做滴,那么十多个主要渠道再加无限量的地推渠道包就成了一个巨坑了!这一块耗费的时间是一个无底洞啊!!!

方式一览

这里一共会介绍三种渠道包的实现方式,分别是:
1、使用gradle配置直接编译出不同的渠道包。
2、通过反编译修改对应的渠道号。
3、META-INF里面新加一个文件。

Gradle方式

不管是用友盟统计还是其他什么的,首先肯定都是要有一些准备工作的,由于本人就比较了解友盟的,所以就用友盟统计来举例啦!
友盟统计提供了两种渠道统计策略,其实就是一个自动挡的一个手动挡的。

    <meta-data
        android:name="UMENG_APPKEY"
        android:value="xxxxxxxx"/>
    <meta-data
        android:name="UMENG_CHANNEL"
        android:value="${GRADLE_CHANNEL_VALUE}"/>

在对应的build.gradle里面配置对应的信息:

productFlavors.all { flavor ->
    flavor.manifestPlaceholders = [GRADLE_CHANNEL_VALUE: name]
}
productFlavors {
    dev {
    }
    baidu {
        minSdkVersion 18
        applicationId "com.test.michat"
    }
}

如果手动去设置对应的渠道号的话,就在程序入口处调用以下方法:

MobclickAgent. startWithConfigure(UMAnalyticsConfig config)

UMAnalyticsConfig(Context context, String appkey, String channelId)

UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType)

UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType,Boolean isCrashEnable)

那么怎么获取到对应的渠道号呢?!这个方法在之后的所有方式中都要使用滴,其实不管是哪种方式,最后都会调用这个方法去读相关数据的!!

    private String getChannel(Context context) {
    try {
        PackageManager pm = context.getPackageManager();
        ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
        return appInfo.metaData.getString("CHANNEL_VALUE");
    } catch (PackageManager.NameNotFoundException ignored) {
    }
    return "";

}
buildVariants.png

当然你也可以使用命令行:gradlew assemble 组装出所有的渠道包!!

反编译方式

gradle方式用着也挺不错的,为什么还要去搞什么反编译这么麻烦的东西呢?因为它有一个很大的问题,那就是每一个包都是要去编译打包的!这是相当的耗时!time is 加班啊!谁也不想加班打渠道包咯!!反编译的方式就是节省了每个渠道包都去编译的时间,而是编译好一个渠道包之后就使用该渠道包,通过反编译动态修改AndroidManifest.xml里面的信息,然后再重新打包签名!

说到反编译,那么这里就不得不提大名鼎鼎的apktool.jar了!纳尼,你说你从未听说过?!没事儿,以前没有听过,现在会用了就行了!!

然后总结一下接下来的一系列套路:
解包->修改相关参数->打包->签名->Zipalign优化

  • 1、解包

    apktool d your_original_apk build
    

你没有看错,就是这样的!因为我们是站在巨人的肩膀上工作的嘛,所以好多工作就不同自己搞了!
执行以上命令之后,如果不出什么意外,你就会得到一个文件夹:

反编译.png

相关代码:

try {
        brut.apktool.Main.main(new String[]{"d", "-f", apkFilePath, "-o", outPath});
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        callback("解包失败 !!!!!\r\n" + e.getMessage());
    }
  • 2、修改对应的参数
    打开对应的AndroidManifest.xml,你没有看错,什么都在里面,直接修改就好了!等等,xml解析你不会?!没有关系,这里有dom4j.jar给你使用啦!!

修改反编译之后的AndroidManifest文件相关代码

try {
        File androidManifestFile = new File(appFolderName + File.separator + "AndroidManifest.xml");
        Document document = new SAXReader().read(androidManifestFile);//使用dom4j的sax解析
        Element element = document.getRootElement().element("application");
        List<Element> list = element.elements("meta-data");//获取到所有的“meta-data”
        List<MetaData> metaData = manifest.getMetaData();
        boolean isUpdate = false;
        for (MetaData data : metaData) {
            String name = data.getName();
            String value = data.getValue();
            callback(" meta-data name='" + name + "' value='" + value + "'");
            for (Element s : list) {
                Attribute attribute = s.attribute("name");
                //更新相关渠道号
                 if ( "UMENG_CHANNEL".equals(name)&&"UMENG_CHANNEL".equals(attribute.getValue())) {//更换相关的渠道号
                    s.attribute("value").setValue(value);
                    isUpdate = true;
                    callback("更新1 AndroidManifest.xml meta-data name='" + attribute.getValue() + "' value='" + value + "'");
                   break;
                 }

            }
        }
        if(isUpdate){//更新后重新写入
            XMLWriter writer = new XMLWriter(new FileOutputStream(androidManifestFile));
            writer.write(document);
            writer.close();
            callback("更新 AndroidManifest.xml 完成 ~ ");
        }
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
  • 3、打包

    apktool b build your_unsigned_apk
    

还是这么简单:

try {
        brut.apktool.Main.main(new String[]{"b", buildApkFolderPath, "-o", buildApkOutPath});
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        callback("打包失败 !!!!!\r\n" + e.getMessage());
    }
  • 4、签名

    jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias
    

这个是jdk里面直接提供了的,只要你的环境变量配置好了的,就没有什么问题啦!

重新签名相关代码

executeCommand("jarsigner", "-verbose", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore", keystoreFilePath, apkFilePath, alias, "-storepass", password);


/**
 * 执行命令
 *
 * @param command 命令
 */
private synchronized boolean executeCommand(String... command) {
    Process process = null;
    BufferedReader reader = null;
    try {
        ProcessBuilder builder = new ProcessBuilder();
        builder.command(command);
        builder.redirectErrorStream(true);
        process = builder.start();
        reader = new BufferedReader(new InputStreamReader(process.getInputStream(),"UTF-8"));
        String line;
        while ((line = reader.readLine()) != null) {
            callback(line);
            if (line.contains("Exception") || line.contains("Unable to open")) {
                return false;
            }
        }
        return true;
    } catch (IOException e) {
        e.printStackTrace();
        callback(e.getMessage());
    } finally {
        close(reader);
        if (process != null) {
            process.destroy();
        }
    }
    return false;
}
  • 5、Zipalign优化
Zipalign.png

如图所示,sdk/build-tools里面每个版本都是有这个东西的,加到环境变量中就好了!!!

zipalign 优化处理相关代码

 * 需要安装并Android SDK并配置环境变量Build Tools路径
 * 优化apk文件,这个需要Android Build Tools 中的zipalign程序文件
 *
 * @param apkFilePath 要优化的apk文件路径
 * @param outFilePath 优化后的apk存放文件路径
 */
public boolean zipalign(String apkFilePath, String outFilePath) {
    return executeCommand("zipalign", "-f", "-v", "4", apkFilePath, outFilePath);
}

美团方式

上面说的反编译要各种解包,打包,签名,相对也比较繁琐,然后我们可以发现,apk其实都是一个压缩包,我们直接在这个压缩包里添加对应的文件作为渠道号标记是不是又能省去上面繁琐的步奏呢?!打开一个APK文件之后你会看到META-INF这个文件夹!

apk压缩包.png

META-INF.png

美团的方式就是在这里面直接再添加一个文件,然后通过这个文件的名称来指定对应的渠道号!

话不多说,直接上代码!!

public static void addUmengChannel(String filepath, String channel) {
    String channel_title = "umengchannel_";
    if(filepath.substring(filepath.lastIndexOf(".") + 1).toLowerCase().equals("apk")) {
        String path2 = "";
        if(filepath.lastIndexOf(File.separator) >= 0) {
            path2 = filepath.substring(0, filepath.lastIndexOf(File.separator) + 1);//得到父路径
        }

        if(path2.length() != 0) {
            File s = new File(filepath);//原始的apk
            File t = new File(filepath.substring(0, filepath.lastIndexOf(".")) + "_" + channel + ".apk");//目标apk
            if(!t.exists()) {//不存在就创建
                try {
                    t.createNewFile();
                } catch (IOException var12) {
                    var12.printStackTrace();
                }
            }

            Utils.fileChannelCopy(s, t);//拷贝原始apk到目标apk
            File addFile = new File(path2 + channel_title + channel);//需要添加的渠道文件 
            if(!addFile.exists()) {
                try {
                    addFile.createNewFile();
                } catch (IOException var11) {
                    var11.printStackTrace();
                }
            }

            try {
                Utils.addFileToExistingZip(t, addFile);//将新加的渠道文件添加到目标apk文件中
                addFile.delete();
            } catch (IOException var10) {
                var10.printStackTrace();
            }

        }
    }
} 


public static void addFileToExistingZip(File zipFile, File file) throws IOException {
        File tempFile = File.createTempFile(zipFile.getName(), (String)null);
        tempFile.delete();
        boolean renameOk = zipFile.renameTo(tempFile);//拷贝
        if(!renameOk) {
            throw new RuntimeException("could not rename the file " + zipFile.getAbsolutePath() + " to " + tempFile.getAbsolutePath());
        } else {
            byte[] buf = new byte[1024];
            ZipInputStream zin = new ZipInputStream(new FileInputStream(tempFile));
            ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));

            for(ZipEntry entry = zin.getNextEntry(); entry != null; entry = zin.getNextEntry()) {
                String in = entry.getName();
                if(in.contains("umengchannel")) {//如果有重复的就不复制回去了!
                    continue;
                }

                out.putNextEntry(new ZipEntry(in));

                int len1;
                while((len1 = zin.read(buf)) > 0) {
                    out.write(buf, 0, len1);
                }
            }

            zin.close();
            FileInputStream in1 = new FileInputStream(file);
            out.putNextEntry(new ZipEntry("META-INF/" + file.getName()));//创建对应的渠道文件

            int len2;
            while((len2 = in1.read(buf)) > 0) {
                out.write(buf, 0, len2);
            }

            out.closeEntry();
            in1.close();
            out.close();
            tempFile.delete();
        }
    }
渠道包完成.png

最后送上读取相关的方法:

public static String getChannel(Context context) {
        ApplicationInfo appinfo = context.getApplicationInfo();
        String sourceDir = appinfo.sourceDir;
        String ret = "";
        ZipFile zipfile = null;
        try {
            zipfile = new ZipFile(sourceDir);
            Enumeration<?> entries = zipfile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = ((ZipEntry) entries.nextElement());
                String entryName = entry.getName();
                    //这里需要替换成你的那个key
                if (entryName.startsWith(YOUR_CHNANNEL_NAME)) {
                    ret = entryName;
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (zipfile != null) {
                try {
                    zipfile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        String[] split = ret.split("_");
        if (split != null && split.length >= 2) {
            return ret.substring(split[0].length() + 1);

        } else {
            return "";
        }
    }

当然你肯定要手动设置了,这个没法直接在清单文件中去配置了!!

小结

主要的方式就是这三种了!可以说一个比一个快,一个比一个的定制也要高,效率提高了,灵活性似乎就会下降的,至于到底使用哪种方式,还是根据实际情况灵活选择吧,反正到现在,这些方案都是很成熟的,没有什么坑!一不小心又说了几句废话啊!

参考文档及Demo

1、美团Android自动化之旅—生成渠道包
2、GRADLE构建最佳实践 gradle也是很厉害的!
3、Android逆向之旅---反编译利器Apktool和Jadx源码分析以及错误纠正


4、github-AndroidUmengMultiChannelBuildTool
5、github-ApkCustomizationTool
6、github-AndroidMultiChannels

如有问题,欢迎拍砖 ~~~
---- Edit By Joe ----

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

推荐阅读更多精彩内容