android产品研发(五)-->多渠道打包

国内的Android开发者还是很苦逼的,由于众所周知的原因,google play无法再国内打开,所以android系的应用市场,群雄争霸,而后果就是国内存在着有众多的应用市场,产品在不同的渠道可能有这不同的统计需求,为此android开发人员需要为每个应用市场发布一个安装包,这里就涉及到了android的多渠道打包。

本文主要讲解的就是几种主流的多渠道打包方式,以及其优劣势。

  • 通过配置gradle脚本实现多渠道打包

这种打包方式是使用android Studio的编译工具gradle配合使用的,其核心原理就是通过脚本修改androidManifest.xml中的mate-date内容,执行N次打包签名操作实现多渠道打包的需求,具体实现如下。

(一)在androidmanifest.xml中定义mate-data标签

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="your.package.name">
    <application>    

          <meta-data android:name="UMENG_CHANNEL" android:value="{UMENG}"/>    

    </application>
</manifest>   

这里需要注意的是:上面的value的值要和渠道名所对应,比如wandoujia里面要对应为你豌豆荚的渠道名称

(二)在build.gradle下的productFlavors定义渠道号:

productFlavors {  

        internal {}  

        /*InHouse {}
        pcguanwang {}
        h5guanwang {}
        hiapk {}
        m91 {}
        appchina {}
        baidu {}
        qq {}
        jifeng {}
        anzhi {}
        mumayi {}
        m360 {}
        youyi {}
        wandoujia {}
        xiaomi {}
        sougou {}
        leshangdian {}
        huawei {}
        uc {}
        oppo {}
        flyme {}
        jinli {}
        letv {}*/  

        productFlavors.all { flavor ->
             flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
        }
    }  

同时需要注意的是,这里需要在defaultConfig中配置一个默认的渠道名称

manifestPlaceholders = [UMENG_CHANNEL_VALUE: "channel_name"]  

实现多渠道打包更换mate-data标签中的内容

优势:方便灵活,可以根据自身的需求配置不同的渠道执行不同的逻辑;

劣势:打包速度过慢;

  • 使用第三方打包工具

这种方式就是使用第三方的服务,比如360,百度,友盟等,其原理也是通过修改androidManifest.xml中的mate-data标签内容,然后执行N次打包签名的操作实现多渠道打包的。这里就不在做具体解释说明,免得又做广告的嫌疑,O(∩_∩)O哈哈~。

优势:简单方便,几乎不用自身做什么工作;

劣势:打包速度过慢;

  • 使用美团多渠道打包方式

而这里主要是根据美团客户端打包经验(详见:美团Android自动化之旅—生成渠道包

主要是介绍利用在META-INF目录内添加空文件的方式,实现批量快速打包Android应用。

实现原理

Android应用安装包apk文件其实是一个压缩文件,可以将后缀修改为zip直接解压。解压安装文件后会发现在根目录有一个META-INF目录。如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道。

“采用这种方式,每打一个渠道包只需复制一个apk,在META-INF中添加一个使用渠道号命名的空文件即可。这种打包方式速度非常快,900多个渠道不到一分钟就能打完。”

实现步骤

(一)编写渠道号文件

(二)编写python脚本,实现解压缩apk文件,为META-INF目录添加文件,重新压缩apk文件等逻辑:

# coding=utf-8
import zipfile
import shutil
import os

def delete_file_folder(src):
    ‘‘‘delete files and folders‘‘‘
    if os.path.isfile(src):
        try:
            os.remove(src)
        except:
            pass
    elif os.path.isdir(src):
        for item in os.listdir(src):
            itemsrc=os.path.join(src,item)
            delete_file_folder(itemsrc)
        try:
            os.rmdir(src)
        except:
            pass

# 创建一个空文件,此文件作为apk包中的空文件
src_empty_file = ‘info/empty.txt‘
f = open(src_empty_file,‘w‘)
f.close()

# 在渠道号配置文件中,获取指定的渠道号
channelFile = open(‘./info/channel.txt‘,‘r‘)
channels = channelFile.readlines()
channelFile.close()
print(‘-‘*20,‘all channels‘,‘-‘*20)
print(channels)
print(‘-‘*50)

# 获取当前目录下所有的apk文件
src_apks = [];
for file in os.listdir(‘.‘):
    if os.path.isfile(file):
        extension = os.path.splitext(file)[1][1:]
        if extension in ‘apk‘:
            src_apks.append(file)

# 遍历所以的apk文件,向其压缩文件中添加渠道号文件
for src_apk in src_apks:
    src_apk_file_name = os.path.basename(src_apk)
    print(‘current apk name:‘,src_apk_file_name)
    temp_list = os.path.splitext(src_apk_file_name)
    src_apk_name = temp_list[0]
    src_apk_extension = temp_list[1]

    apk_names = src_apk_name.split(‘-‘);

    output_dir = ‘outputDir‘+‘/‘
    if os.path.exists(output_dir):
        delete_file_folder(output_dir)
    if not os.path.exists(output_dir):
        os.mkdir(output_dir)

    # 遍历从文件中获得的所以渠道号,将其写入APK包中
    for line in channels:
        target_channel = line.strip()
        target_apk = output_dir + apk_names[0] + "-" + target_channel+"-"+apk_names[2] + src_apk_extension
        shutil.copy(src_apk,  target_apk)
        zipped = zipfile.ZipFile(target_apk, ‘a‘, zipfile.ZIP_DEFLATED)
        empty_channel_file = "META-INF/uuchannel_{channel}".format(channel = target_channel)
        zipped.write(src_empty_file, empty_channel_file)
        zipped.close()

print(‘-‘*50)
print(‘repackaging is over ,total package: ‘,len(channels))
input(‘\npackage over...‘)

(三)打包一个正常的apk包

(四)执行python脚本,多渠道打包

(五)android代码中获取渠道号

/**
 * 渠道号工具类:解析压缩包,从中获取渠道号
 */
public class ChannelUtil {
    private static final String CHANNEL_KEY = "uuchannel";
    private static final String DEFAULT_CHANNEL = "internal";
    private static String mChannel;

    public static String getChannel(Context context) {
        return getChannel(context, DEFAULT_CHANNEL);
    }

    public static String getChannel(Context context, String defaultChannel) {
        if (!TextUtils.isEmpty(mChannel)) {
            return mChannel;
        }
        //从apk中获取
        mChannel = getChannelFromApk(context, CHANNEL_KEY);
        if (!TextUtils.isEmpty(mChannel)) {
            return mChannel;
        }
        //全部获取失败
        return defaultChannel;
    }
    /**
     * 从apk中获取版本信息
     *
     * @param context
     * @param channelKey
     * @return
     */
    private static String getChannelFromApk(Context context, String channelKey) {
        long startTime = System.currentTimeMillis();
        //从apk包中获取
        ApplicationInfo appinfo = context.getApplicationInfo();
        String sourceDir = appinfo.sourceDir;
        //默认放在meta-inf/里, 所以需要再拼接一下
        String key = "META-INF/" + channelKey;
        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();
                if (entryName.startsWith(key)) {
                    ret = entryName;
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (zipfile != null) {
                try {
                    zipfile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        String channel = "";
        if (!TextUtils.isEmpty(ret)) {
            String[] split = ret.split("_");
            if (split != null && split.length >= 2) {
                channel = ret.substring(split[0].length() + 1);
            }
            System.out.println("-----------------------------");
            System.out.println("渠道号:" + channel + ",解压获取渠道号耗时:" + (System.currentTimeMillis() - startTime) + "ms");
            System.out.println("-----------------------------");
        } else {
            System.out.println("未解析到相应的渠道号,使用默认内部渠道号");
            channel = DEFAULT_CHANNEL;
        }
        return channel;
    }
}

整个打包的流程就是这样了,打工工具可参考github上的项目:多渠道打包实现

优势:打包速度很快,很方便;

劣势:不够灵活,不能灵活的配置不同的渠道不同的业务逻辑;

问题:

项目中由于使用了友盟统计,以前是在meta-data中保存渠道信息,现在更改了方式之后需要手动执行渠道号的设置代码:

String channel = ChannelUtil.getChannel(mContext);
        System.out.println("启动页获取到的渠道号为:" + channel);
        // 设置友盟统计的渠道号,原来是在Manifest文件中设置的meta-data,现在启动页中设置
        AnalyticsConfig.setChannel(channel);

通过这种打包方式以前需要一个小时的打包工作现在只需要一分钟即可,极大的提高了效率,目前在实际的应用中尚未发现有什么问题,有这种需求的童鞋可以尝试一下。

总结

虽说我们总结了三种打包方式,但是其实通过gradle打包和使用第三方服务打包都是执行了N次的打包签名操作,时间上耗费太多,因此不太推荐,而美团的方式在效率上提高了很多,但是对于那种不同的渠道包执行不同的业务逻辑的需求就无能为例了,只能通过gradle配置,因此大家在选择多渠道打包方式的时候可以根据自身的需求来选择。

另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:

android产品研发(一)–>实用开发规范

android产品研发(二)–>启动页优化

android产品研发(三)–>基类Activity

android产品研发(四)–>减小Apk大小

本文以同步至github中:https://github.com/yipianfengye/androidProject,欢迎star和follow

时间: 2024-10-14 02:54:00

android产品研发(五)-->多渠道打包的相关文章

android产品研发(十五)--&gt;内存对象序列化

转载请标明出处:一片枫叶的专栏 上一篇文章中我们讲解了android app中的升级更新操作,app的升级更新操作算是App的标配了,升级操作就是获取App的升级信息,更新操作是下载,安装,更新app,其中我们既可以使用app store获取应用的升级信息,也可以在应用内通过请求本地服务器获取应用的升级信息,并通过与本地app的版本号对比判断应用是否需要升级. 升级信息是app更新的基础,只有我们的app的升级信息指明需要更新,我们才可以开始后续的更新操作–也就是下载安装更新app.这里强调一点

android产品研发(七)--&gt;Apk热修复

转载请标明出处:一片枫叶的专栏 去年一整年android社区中刮过了一阵热修复的风,各大厂商,逼格大牛纷纷开源了热修复框架,恩,产品过程中怎么可能没有bug呢?重新打包上线?成本太高用户体验也不好,咋办?上热修复呗. 好吧,既然要开始上热修复的功能,那么就得调研一下热修复的原理.下面我将分别讲述一下热修复的原理,各大热修复框架的比较,以及自身产品中热修复功能的实践. 热修复的原理 通过更改dex加载顺序实现热修复 最新github上开源了很多热补丁动态修复框架,大致有: HotFix      

android产品研发(六)--&gt;Apk混淆

前面一篇文章中我们讲解了android里面的多渠道打包,对于大型的app来说,几百个上千个渠道包都是很正常的事,所以效率定制化是一件很重要的事.主要讲解了三种多渠道打包方式,并分析了其各自的利弊,在各自产品多渠道打包的时候,可以根据自身的产品需求选择相应的打包方式. 而本文主要讲解Apk的混淆,这里的混淆分为两种代码混淆和资源文件混淆.实际的产品研发中为了防止自己的劳动成果被别人窃取,混淆代码能有效防止apk文件被反编译,进而查看源代码.说来惭愧,作为互联网创业公司的我们也确实对竞品Apk反编译

android产品研发(十)--&gt;不使用静态变量保存数据

转载请标明出处:一片枫叶的专栏 上一篇文章中我们讲解了Android中的几种常见网络协议:xml,json,protobuf等,以及各自的优缺点,一般而言主要我们的App涉及到了网络传输都会有这方面的内容,具体可根据项目的需求确定各自的网络传输协议.这里可参考android产品研发(九)–>App网络传输协议 而本文讲解的其实并不是一个技术方面,而是一个android产品研发过程中的技巧:尽量不使用静态变量保存核心数据.这是为什么呢?这是因为android的进程并不是安全的,包括applicat

android产品研发(十七)--&gt;Hybird开发

转载请标明出处:一片枫叶的专栏 上一篇文章中我们介绍了android开发中经常会涉及到但又常常被忽视掉的开发者模式.主要讲解了包括如何打开手机的开发者模式,开发者模式中各个菜单的意义和作用,如何清除手机App数据,以及清除手机App数据具体清除那些数据等知识点,具体关于android中开发者模式的知识,可参考我的:android产品研发(十六)–>开发者选项 本文将介绍android中hybird开发相关的知识点.hybird开发实际上是混合开发的意思,这里的混合是H5开发与Native开发混合

android产品研发(十三)--&gt;App轮训操作

转载请标明出处:一片枫叶的专栏 上一篇文章中我们讲解了android app实现长连接的几种方式,各自的优缺点以及具体的实现,一般而言使用第三方的推送服务已经可以满足了基本的业务需求,当然了若是对技术有追求的可以通过NIO或者是MINA实现自身的长连接服务,但是自己实现的长连接服务一来比较复杂耗时比较多,而且可能过程中有许多坑要填,一般而言推荐使用第三方的推送服务,稳定简单,具体管理长连接部分的模块可参考:android产品研发(十二)–>App长连接实现. 而本文将讲解app端的轮训请求服务,

android产品研发--&gt;总结(持续更新中)

转载请标明出处:一片枫叶的专栏 最近的android产品研发系列主要讲解的是android产品研发过程中涉及到的技术,技巧,实践等.前面我们讲解了android源码系列的文章,源码系列的文章东西比较多比较复杂,并且一些东西还没有讲完,这里已经更新了30篇了,后续的东西一定会更新的.考虑一直讲源码系列可能看的比较累,这里就有了产品研发系列的文章.本个系列的文章主要是讲解android产品研发过程中一些需要注意的技术技巧与实践.其主要面对产品研发,对App稳定性,友好型,兼容性要求较高的App. 下

android产品研发(十六)--&gt;开发者选项

转载请标明出处:一片枫叶的专栏 上一篇文章中我们讲解了android中内存对象的序列化方式.由于android开发涉及到不同Activity的数据传递,对于基本数据类型数据的传递是没有问题的,但是一旦涉及到复杂数据类型,就需要将数据序列化以便传输,在文章中我们主要讲解了两种数据序列化的方式:实现Serializable接口和实现Parcelable接口,同时也比较了它们各自的优缺点和实现方式.具体关于内存对象序列化方面的知识可参考:android产品研发(十五)–>内存对象序列化 本文主要介绍A

android产品研发(十四)--&gt;App升级与更新

转载请标明出处:一片枫叶的专栏 上一篇文章中我们讲解了android app中的轮训操作,讲解的内容主要包括:我们在App中使用轮训操作的情景,作用以及实现方式等.一般而言我们使用轮训操作都是通过定时任务的形式请求服务器并更新用户界面,轮训操作都有一定的使用生命周期,即在一定的页面中启动轮操作,然后在特定的情况下关闭轮训操作,这点需要我们尤为注意,我们还介绍了使用Timer和Handler实现轮训操作的实例,更多关于App中轮训操作的信息,可参考我的:android产品研发(十三)–>App轮训