当前位置: 首页 > news >正文

Android 多渠道打包

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

     现在Android多渠道打包普遍使用的是gradle设置productFlavor方式,通过gradle aR,可以执行一个命令,打出多个包,但是这种方式每次都要走一遍打包流程,而目前很多包仅仅是渠道号不一致,并不需要重新在走一遍编译,打包流程。

     看了美团的解决方案,他们利用了签名的漏洞,META-INF目录内添加空文件,可以不用重新签名应用,本文介绍了一种用户执行过gradle aR命令,自动运行渠道包生成脚本,打多个渠道包的方式。想要入门gradle脚本,请查看邓凡平大神的博客文章:http://blog.csdn.net/innost/article/details/48228651 。

     以下是打包脚本:

apply plugin: 'com.android.application'

def versionNameString="1.0"
def versionCodeInt=1
def appName="打包测试"   //你的应用的名称

def  releaseApk='app/build/outputs/apk/app-release.apk'

def packageChannel(String versionName,String appName,String releaseApk){
    try {
        def stdout = new ByteArrayOutputStream()
        exec {
            //执行Python脚本
            commandLine 'python',rootProject.getRootDir().getAbsolutePath()+"/app/mulit_channel.py",versionName,appName,releaseApk
            standardOutput = stdout
        }
        return stdout.toString().trim()
    }
    catch (ignored) {
        return "UnKnown";
    }
}


android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "com.ndktest"
        minSdkVersion 14
        targetSdkVersion 22
        versionCode versionCodeInt
        versionName versionNameString

    }
    signingConfigs {
        debug {
            // No debug config
        }

        release {
            storeFile file("../keystore/netstars.keystore")
            storePassword "123456"
            keyAlias "netstars.keystore"
            keyPassword "123456"
        }
    }
    buildTypes {
        release {
            buildConfigField "boolean", "LOG_DEBUG", "false"
            minifyEnabled true
            zipAlignEnabled true
            // 移除无用的resource文件
            shrinkResources true
            signingConfig signingConfigs.release

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled false
            debuggable true
        }
    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

    project.afterEvaluate {
        //在Release执行以后
        tasks.getByName("assembleRelease"){
            it.doLast{
                def rApk=new File(releaseApk);
                if(rApk.exists()){
                    packageChannel(versionNameString,appName,rApk.absolutePath)
                }
            }
        }
    }

}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.0'
}

  Python脚本:

#!/usr/bin/python
# coding=utf-8
import zipfile
import shutil
import os
import datetime
import sys

# 空文件 便于写入此空文件到apk包中作为channel文件
src_empty_file = 'empty.txt'
# 创建一个空文件(不存在则创建)
f = open(src_empty_file, 'w')
f.close()

# 获取渠道列表
channel_file = 'channel.txt'
f = open(channel_file)
lines = f.readlines()
f.close()

src_apk=sys.argv[3]

# file name (with extension)
src_apk_file_name = os.path.basename(src_apk)
print(src_apk_file_name)

# 分割文件名与后缀
temp_list = os.path.splitext(src_apk_file_name)
# name without extension
src_apk_name = temp_list[0]
# 后缀名,包含.   例如: ".apk "
src_apk_extension = temp_list[1]

# 创建生成目录,与文件名相关
output_dir = '../output' + '/'
# 目录不存在则创建
if not os.path.exists(output_dir):
    os.mkdir(output_dir)

# 遍历渠道号并创建对应渠道号的apk文件
for line in lines:
    # 获取当前渠道号,因为从渠道文件中获得带有\n,所有strip一下
    target_channel = line.strip()

    #获取日期
    now = datetime.datetime.now()
    nowTime=now.strftime('%Y-%m-%d')

    # 拼接对应渠道号的apk
    length=len(sys.argv)
    if length>1 :
        target_apk = output_dir +sys.argv[2]+"v"+sys.argv[1]+"_"+nowTime+ "_" + target_channel + src_apk_extension
    else:
        target_apk = output_dir +src_apk_name + "_" + target_channel + src_apk_extension

    # 拷贝建立新apk
    shutil.copy(src_apk,  target_apk)
    # zip获取新建立的apk文件
    zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED)
    # 初始化渠道信息
    empty_channel_file = "META-INF/channel_{channel}".format(channel = target_channel)
    # 写入渠道信息
    zipped.write(src_empty_file, empty_channel_file)
    # 关闭zip流
    zipped.close()

  1.获取到渠道号:

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
import android.text.TextUtils;

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

 /***
 *https://github.com/GavinCT/AndroidMultiChannelBuildTool
 ***/
public class ChannelUtil {
   
   private static final String CHANNEL_KEY = "channel";
   private static final String CHANNEL_VERSION_KEY = "channel_version";
   private static String mChannel;
   /**
    * 返回市场。  如果获取失败返回""
    * @param context
    * @return
    */
   public static String getChannel(Context context){
      return getChannel(context, "");
   }
   /**
    * 返回市场。  如果获取失败返回defaultChannel
    * @param context
    * @param defaultChannel
    * @return
    */
   public static String getChannel(Context context, String defaultChannel) {
      //内存中获取
      if(!TextUtils.isEmpty(mChannel)){
         return mChannel;
      }
      //sp中获取
      mChannel = getChannelBySharedPreferences(context);
      if(!TextUtils.isEmpty(mChannel)){
         return mChannel;
      }
      //从apk中获取
      mChannel = getChannelFromApk(context, CHANNEL_KEY);
      if(!TextUtils.isEmpty(mChannel)){
         //保存sp中备用
         saveChannelBySharedPreferences(context, mChannel);
         return mChannel;
      }
      //全部获取失败
      return defaultChannel;
    }
   /**
    * 从apk中获取版本信息
    * @param context
    * @param channelKey
    * @return
    */
   private static String getChannelFromApk(Context context, String channelKey) {
      //从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[] split = ret.split("_");
        String channel = "";
        if (split != null && split.length >= 2) {
           channel = ret.substring(split[0].length() + 1);
        }
        return channel;
   }
   /**
    * 本地保存channel & 对应版本号
    * @param context
    * @param channel
    */
   private static void saveChannelBySharedPreferences(Context context, String channel){
      SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
      Editor editor = sp.edit();
      editor.putString(CHANNEL_KEY, channel);
      editor.putInt(CHANNEL_VERSION_KEY, getVersionCode(context));
      editor.commit();
   }
   /**
    * 从sp中获取channel
    * @param context
    * @return 为空表示获取异常、sp中的值已经失效、sp中没有此值
    */
   private static String getChannelBySharedPreferences(Context context){
      SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
      int currentVersionCode = getVersionCode(context);
      if(currentVersionCode == -1){
         //获取错误
         return "";
      }
      int versionCodeSaved = sp.getInt(CHANNEL_VERSION_KEY, -1);
      if(versionCodeSaved == -1){
         //本地没有存储的channel对应的版本号
         //第一次使用  或者 原先存储版本号异常
         return "";
      }
      if(currentVersionCode != versionCodeSaved){
         return "";
      }
      return sp.getString(CHANNEL_KEY, "");
   }
   /**
    * 从包信息中获取版本号
    * @param context
    * @return
    */
   private static int getVersionCode(Context context){
      try{
         return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
      }catch(NameNotFoundException e) {
         e.printStackTrace();
      }
      return -1;
   }
}

  友盟SDK中提供了通过代码设置渠道号的功能,结合上述打包脚本和获取脚本信息代码,相信多渠道打包问题基本可以得到解决了。

项目Demo:http://git.oschina.net/fengcunhan/AndroidMulitChannel

转载于:https://my.oschina.net/fengcunhan/blog/506852

相关文章:

  • shell计算工具源码
  • JavaScript之闭包
  • 秒懂sql intersect
  • VUE多项目间跳转保存用户解决方法
  • [Nuget]使用Nuget管理工具包
  • 一名QA的碎碎念
  • [Python] 输入与输出
  • Android Activity生命周期详解
  • Python+Appium自动化环境搭建
  • Unity3D之Legacy动画系统学习笔记
  • 联想关键业务服务器 sysytem X3850 X6 4U机架式服务器
  • mysql 字符集乱码及解决方案
  • android搜索框列表布局,流程及主要步骤思维导图
  • gcc介绍及安装
  • java 中获得 资源文件方法
  • css选择器
  • JSDuck 与 AngularJS 融合技巧
  • LintCode 31. partitionArray 数组划分
  • MySQL常见的两种存储引擎:MyISAM与InnoDB的爱恨情仇
  • Mysql数据库的条件查询语句
  • PV统计优化设计
  • 从 Android Sample ApiDemos 中学习 android.animation API 的用法
  • 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频
  • 基于Dubbo+ZooKeeper的分布式服务的实现
  • 免费小说阅读小程序
  • 名企6年Java程序员的工作总结,写给在迷茫中的你!
  • 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
  • 详解NodeJs流之一
  • postgresql行列转换函数
  • 仓管云——企业云erp功能有哪些?
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • ​力扣解法汇总946-验证栈序列
  • ​油烟净化器电源安全,保障健康餐饮生活
  • # Pytorch 中可以直接调用的Loss Functions总结:
  • #{} 和 ${}区别
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • %3cli%3e连接html页面,html+canvas实现屏幕截取
  • (1/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (Mac上)使用Python进行matplotlib 画图时,中文显示不出来
  • (Python) SOAP Web Service (HTTP POST)
  • (八)光盘的挂载与解挂、挂载CentOS镜像、rpm安装软件详细学习笔记
  • (一)80c52学习之旅-起始篇
  • (一)pytest自动化测试框架之生成测试报告(mac系统)
  • (一)RocketMQ初步认识
  • (一)为什么要选择C++
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (原創) 人會胖會瘦,都是自我要求的結果 (日記)
  • (轉貼)《OOD启思录》:61条面向对象设计的经验原则 (OO)
  • .NET 6 在已知拓扑路径的情况下使用 Dijkstra,A*算法搜索最短路径
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .NET Windows:删除文件夹后立即判断,有可能依然存在
  • .net 反编译_.net反编译的相关问题
  • .NET简谈设计模式之(单件模式)
  • .Net下使用 Geb.Video.FFMPEG 操作视频文件