Android Studio Gradle多渠道打包

news2024/11/24 5:39:47

 原理使用Android Studio打一次渠道包,用反编译工具反编译后,修改渠道信息重新编译

准备文件

分渠道配置文件:channel.txt ↓

# 多渠道配置里“统计平台”、“市场名称”、“渠道编号”分别代表什么意思?
# 统计平台:即android name,应用中集成的数据分析sdk的公司名称,例:umeng_channel(下拉列表里提供了若干选项);
# 市场名称:各大安卓应用分发市场(下拉列表里提供了Top20的市场供选择),以帮助开发者区分不同渠道包特征上传相对应市场;
# 渠道编号:即android value,一般填写相关channel id。用户可自行定义区分各大市场的关键字,尽量避免使用特殊字符。
BaiduMobAd_CHANNEL yingyonghui yingyonghui
BaiduMobAd_CHANNEL oppo oppo
BaiduMobAd_CHANNEL 360 360
BaiduMobAd_CHANNEL baidu baidu
BaiduMobAd_CHANNEL xiaomi xiaomi
BaiduMobAd_CHANNEL huawei huawei
BaiduMobAd_CHANNEL lianxiang lianxiang
BaiduMobAd_CHANNEL yingyongbao yingyongbao
BaiduMobAd_CHANNEL aliyun aliyun
BaiduMobAd_CHANNEL sanxing sanxing
BaiduMobAd_CHANNEL vivo vivo
BaiduMobAd_CHANNEL honor honor

(反编译+对齐+签名)文件:↓

// 可以在Android SDK目录里面找到D:\Android\sdk\build-tools\30.0.3\lib

apksigner.jar

// Mac就找【zipalign】,windows就找【zipalign.exe】

zipalign

zipalign.exe

// 官网:下载Apktool官网

apktool_2.9.3.jar

app build.gradle文件中这样配置

 下面需要自己自行调整

要打多渠道包时点击运行那个绿色的小三角

核心文件:release.gradle

import java.nio.file.Files
import java.util.regex.Matcher
import java.util.regex.Pattern
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

ext {
    SIGN_JAR = rootDir.getPath() + "/tools/apksigner.jar"
    ZIPALIGN = rootDir.getPath() + "/tools/zipalign"
    APKTOOL_JAR = rootDir.getPath() + "/tools/apktool_2.9.3.jar"

    storeFile = rootDir.getPath() + "/app.keystore" //密钥路径
    storePassword = "dmx1234567890" //密钥密码
    keyAlias = "Alias" //密钥别名
    keyPassword = "dmx1234567890"//别名密码
    // 反编译目录
    unApkPath = buildDir.getPath() + '/outputs/release/unapk'
    // 渠道Apk输出路径
    channel_apks_path = buildDir.getPath() + '/outputs/release/channels/'
    // 保存渠道配置
    CHANNEL_CONFIG = rootDir.getPath() + "/channel.txt"
}
/**
 * Apk渠道类
 */
class ApkChannel {
    /**
     * 统计平台,即 android name
     */
    String name;
    /**
     * 市场名称,即应用分渠道包时,会加上这个名称 列:app_1.0.0_5_{market}_sign.apk
     */
    String market;
    /**
     * 统计渠道,即 android value
     */
    String value;

    public ApkChannel(String name, String market, String value) {
        this.name = name;
        this.market = market;
        this.value = value;
    }

}

static def isWindows() {
    return org.gradle.internal.os.OperatingSystem.current().isWindows()
}

static def isMacOsX() {
    return org.gradle.internal.os.OperatingSystem.current().isMacOsX()
}
// 这个在MacOS上有时候会删除不到,会导致编译出来的包存在.DS_Store文件,懂的可以自己研究处理一下
def deleteDS_Store() {
    if (isMacOsX()) {
        exec {
            commandLine "/bin/sh", "-c", "find ${unApkPath} -name '.DS_Store' -depth -exec rm {} +"
        }
    }
}

static def String readXml(File xmlFile) {
    String result = null
    try (InputStream stream = new FileInputStream(xmlFile)) {
        // 创建byte数组
        byte[] buffer = new byte[stream.available()]
        // 将文件中的数据读到byte数组中
        stream.read(buffer)
        result = new String(buffer, "UTF-8")
    } catch (Exception e) {
        throw new Exception(e)
    }
    return result
}

static def boolean writeXml(File xmlFile, String xmlString) {
    boolean isWriteXml = false
    // 写入文件
    try (BufferedWriter writer = new BufferedWriter(new FileWriter(xmlFile))) {
        writer.write(xmlString);
        isWriteXml = true
    } catch (IOException e) {
        throw new Exception(e)
    }
    return isWriteXml
}

/**
 * 根据统计平台名称匹配,是否存在该统计平台的 meta-data
 * @param xmlString
 * @param channelName
 * @return
 */
private static boolean isExistsMetaData(String xmlString, String channelName) {
    String metaDataReg = "\\<meta-data android:name=\\\"" + channelName + "\\\" android:value=\".*?\"/\\>"
    Pattern pattern = Pattern.compile(metaDataReg)
    return pattern.matcher(xmlString).find()
}
/**
 * 替换指定的统计平台的 meta data
 * @param xmlString
 * @param channelName
 * @param metaData
 */
private static String replaceMetaData(String xmlString, String channelName, String metaData) {
    String metaDataReg = "\\<meta-data android:name=\\\"" + channelName + "\\\" android:value=\".*?\"/\\>"
    Pattern pattern = Pattern.compile(metaDataReg)
    Matcher matcher = pattern.matcher(xmlString)
    if (matcher.find()) {
        return xmlString.replace(matcher.group(), metaData)
    }
    return xmlString
}
/**
 * 生成 meta data
 * @param channelName
 * @param channelValue
 * @return
 */
private static String generateMetaData(String channelName, String channelValue) {
    return String.format("<meta-data android:name=\"%s\" android:value=\"%s\"/>", channelName, channelValue)
}

def List<ApkChannel> initChannels() {
    List<ApkChannel> channels = new ArrayList<ApkChannel>()
    println("并初始化channel.txt文件...")
    File channelFile = new File(CHANNEL_CONFIG)
    if (!channelFile.exists()) {
        throw new FileNotFoundException(channelFile.getPath() + "文件不存在!")
    }
    try (BufferedReader reader = new BufferedReader(new FileReader(channelFile))) {
        String line
        while ((line = reader.readLine()) != null) {
            // 处理每一行数据
            if (line.startsWith("#")) {
                println(line.replace("# ", ""))
            } else {
                String[] arr = line.split(" ")
                channels.add(new ApkChannel(arr[0], arr[1], arr[2]))
            }
        }
        println("初始化成功,渠道数:" + channels.size())
    } catch (Exception e) {
        e.printStackTrace()
    }
    return channels
}

/**
 * 反编译Apk
 * @param inApkFile 要反编译的Apk文件
 * @return 返回反编译后的文件目录
 */
def File decompileApk(File inApkFile) {
    println "*************** apktool decompile start ***************"
    File outDecompileFile = new File(unApkPath, inApkFile.name.replace(".apk", ""))
    if (outDecompileFile.exists()) {
        if (!delete(outDecompileFile)) {
            throw new RuntimeException("delete apktoolOutputDir failure!")
        }
    }
    if (!outDecompileFile.mkdirs()) {
        throw new RuntimeException("make apktoolOutputDir failure!")
    }
    println("apktool decompile out file: " + outDecompileFile)
    // 解压APK命令
    String unApkCommand = String.format(
            "java -jar %s d -o %s %s -f -s",
            APKTOOL_JAR,
            outDecompileFile.getPath(),
            inApkFile.getPath()
    )

    exec {
        if (isWindows()) {
            commandLine "powershell", unApkCommand
        } else if (isMacOsX()) {
            commandLine "/bin/sh", "-c", unApkCommand
        } else {
            throw new RuntimeException("Please confirm your platform command line!")
        }
    }
    deleteDS_Store()
    println "*************** apktool decompile finish ***************"
    return outDecompileFile
}
/**
 * 编译Apk
 * @param inDecompileApkFileDir 输入反编译后的文件目录
 * @param outCompileApkFileDir 输出编译Apk文件存储目录
 * @param outFileName 编译后的Apk文件名
 * @return
 */
def File compileApk(File inDecompileApkFileDir, File outCompileApkFileDir, String outFileName) {
    println "*************** apktool compile start ***************"
    if (!inDecompileApkFileDir.exists()) {
        throw new FileNotFoundException("no " + inDecompileApkFileDir.getPath() + " has found!")
    }
    if (!outCompileApkFileDir.exists()) {
        outCompileApkFileDir.mkdirs()
    }
    File outCompileApkFile = new File(outCompileApkFileDir, outFileName)
    String buildApkCommand = String.format("java -jar %s b %s -o %s",
            APKTOOL_JAR,
            inDecompileApkFileDir.getPath(),
            outCompileApkFile.getPath())
    exec {
        if (isWindows()) {
            commandLine "powershell", buildApkCommand
        } else if (isMacOsX()) {
            commandLine "/bin/sh", "-c", buildApkCommand
        } else {
            throw new RuntimeException("Please confirm your platform command line!")
        }
    }
    println "*************** apktool compile finish ***************"
    return outCompileApkFile
}

/**
 * 对齐Apk
 * @param inApkFile 要对齐的Apk文件
 * @return 返回对齐后的Apk文件
 */
def File zipalignApk(File inApkFile) {
    println "*************** zipalign optimize start ***************"
    String zipalignApkFilename = inApkFile.name.replace(".apk", "_unsigned.apk")
    File outZipalignApkFile = new File(inApkFile.getParent(), zipalignApkFilename)
    exec {
        if (isWindows()) {
            // zipalign.exe -p -f -v 4 infile.apk outfile.apk
            String alignApkCommand = String.format(
                    "%s.exe -p -f -v 4 %s %s",
                    ZIPALIGN,
                    inApkFile.getPath(),
                    outZipalignApkFile.getPath()
            )
            commandLine "powershell", alignApkCommand
        } else if (isMacOsX()) {
            // zipalign -p -f -v 4 infile.apk outfile.apk
            String alignApkCommand = String.format(
                    "%s -p -f -v 4 %s %s",
                    ZIPALIGN,
                    inApkFile.getPath(),
                    outZipalignApkFile.getPath()
            )
            commandLine "/bin/sh", "-c", alignApkCommand
        } else {
            throw new RuntimeException("Please confirm your platform command line!")
        }
    }
    println "*************** zipalign optimize finish ***************"
    return outZipalignApkFile
}
/**
 * 签名Apk
 * @param inApkFile 要签名的Apk文件
 * @return 返回签名后的Apk文件
 */
def File signerApk(File inApkFile) {
    println "*************** start apksigner ***************"
    File outSignerApkFile = new File(inApkFile.getPath().replace("_unsigned.apk", "_signed.apk"))
    String apksignerCommand = String.format(
            "java -jar %s sign -verbose --ks %s --v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled false --ks-key-alias %s --ks-pass pass:%s --key-pass pass:%s --out %s %s",
            SIGN_JAR,
            storeFile,
            keyAlias,
            storePassword,
            keyPassword,
            outSignerApkFile.getPath(),
            inApkFile.getPath()
    )
    exec {
        if (isWindows()) {
            commandLine "powershell", apksignerCommand
        } else if (isMacOsX()) {
            commandLine "/bin/sh", "-c", apksignerCommand
        } else {
            throw new RuntimeException("Please confirm your platform command line!")
        }
    }
    println "*************** finish apksigner ***************"
    return outSignerApkFile
}

private def processingChannel(File decompileApkFile, String channelMarket) {
    // 删除恶心的.DS_Store
    deleteDS_Store()
    // 编译
    File compileApkFile = compileApk(decompileApkFile, new File(channel_apks_path), decompileApkFile.name + "_" + channelMarket + ".apk")
    // 对齐
    File zipalignApkFile = zipalignApk(compileApkFile)
    if (zipalignApkFile.exists()) {
        delete(compileApkFile)
    }
    // 签名
    File signerApkFile = signerApk(zipalignApkFile)
    if (signerApkFile.exists()) {
        delete(zipalignApkFile)
    }
}

task assemblePackageChannel() {
    dependsOn('assembleAdRelease')
    doLast {
        if (isMacOsX()) {
            exec { commandLine "/bin/sh", "-c", "chmod +x " + SIGN_JAR }
            exec { commandLine "/bin/sh", "-c", "chmod +x " + ZIPALIGN }
            exec { commandLine "/bin/sh", "-c", "chmod +x " + APKTOOL_JAR }
        }
        List<ApkChannel> channels = initChannels()
        if (channels.size() <= 0) {
            throw new Exception("没有渠道信息!")
        }
        FilenameFilter filter = new FilenameFilter() {
            @Override
            boolean accept(File dir, String name) {
                return name.endsWith(".apk")
            }
        }
        // 获得release包地址,暂时固定死
        String sourceApkDir = buildDir.getPath() + "/outputs/apk/ad/release"
        File inSourceApkFile = new File(sourceApkDir).listFiles(filter).first()
        if (inSourceApkFile == null || !inSourceApkFile.exists()) {
            throw new FileNotFoundException("no apk files has found!")
        }
        File decompileApkFile = decompileApk(inSourceApkFile)
        // 检测AndroidManifest.xml是否存在
        File xmlFile = new File(decompileApkFile, "AndroidManifest.xml")
        if (!inSourceApkFile.exists()) {
            throw new FileNotFoundException("no AndroidManifest.xml files has found!")
        }
        String xmlString = readXml(xmlFile)
        if (xmlString == null || xmlString.length() <= 0) {
            throw new NullPointerException("read AndroidManifest.xml is null.")
        }
        // 多渠道处理
        for (ApkChannel channel : channels) {
            // 检测是否存在多个平台
            if (channel.name.split("\\|").length > 1) {
                String[] channelNames = channel.name.split("\\|")
                String[] channelValues = channel.value.split("\\|")
                for (int i = 0; i < channelNames.length; i++) {
                    String channelName = channelNames[i]
                    String channelValue = channelValues[i]
                    if (isExistsMetaData(xmlString, channelName)) {
                        // 替换渠道信息
                        xmlString = replaceMetaData(xmlString, channelName, generateMetaData(channelName, channelValue))
                        // 写入渠道信息
                        writeXml(xmlFile, xmlString)
                        processingChannel(decompileApkFile, channel.market)
                    }
                }
            } else {
                if (isExistsMetaData(xmlString, channel.name)) {
                    // 替换渠道信息
                    xmlString = replaceMetaData(xmlString, channel.name, generateMetaData(channel.name, channel.value))
                    // 写入渠道信息
                    writeXml(xmlFile, xmlString)
                    processingChannel(decompileApkFile, channel.market)
                }
            }
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1983629.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 后端接收HTML等标签数据,到后端标签丢失

文章目录 前言一、修改Xss配置总结 前言 一开始以为是接收参数出了问题&#xff0c;后面看了RequestBody注解并不会改变参数&#xff0c; 最后发现是xss的配置问题。 一、修改Xss配置 把enabled: true改成false就好了 #xss配置,防止xss攻击 xss:#过滤开关&#xff1a;enable…

简单的docker学习 第10章 docker管理监控平台

第10章 Docker管理监控平台 当 Docker引擎中管理的镜像、容器、网络等对象数量变得越来越多时&#xff0c;通过简单的 docker命令来管理已经显得使人力不从心了。于是就出现了很多的 Docker 可视化管理平台。我们这里对现在较流行的、使用较多的几种平台进行介绍。 10.1 Dock…

【Python】torch.nn模块中函数详解和示例(一)

前言 在深度学习日益成为解决复杂问题重要工具的今天&#xff0c;PyTorch凭借其灵活性和易用性&#xff0c;成为了众多研究者与开发者的首选框架。本系列博客 将对torch中的nn模块中186个函数进行介绍&#xff0c;以函数首字母从a到z的排序开展&#xff0c;包含函数原理、原型…

【css】 CSS3+JS做一个酷炫的仪表进度条3d进度条

创建一个动态进度环组件 在现代网页设计中&#xff0c;进度环是一种常见的视觉元素&#xff0c;用于展示任务的完成度或加载状态。本文将介绍如何使用Vue.js和Less创建一个动态进度环组件&#xff0c;该组件不仅具有美观的视觉效果&#xff0c;还能够根据用户输入动态改变颜色…

「链表」Floyd判环法(弗洛伊德判圈法|龟兔赛跑法)/ LeetCode 141(C++)

给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;…

【C++】C++11的新特性 — function 包装器 , bind包装器

有些人的生活&#xff0c;可以轻轻松松&#xff0c;有些人的生活就是奥运会&#xff0c;生下来就在跑道上&#xff0c;如果不去全力奔跑&#xff0c;注定会被淘汰&#xff0c;更何况&#xff0c;即使努力奔跑&#xff0c;也未必能战胜很多人。 -- 傅首尔 -- C11的新特性 1 fun…

判断字符串是否接近:深入解析及优化【字符串、哈希表、优化过程】

本文将详细解析解决这个问题的思路&#xff0c;并逐步优化实现方案。 问题描述 给定两个字符串 word1 和 word2&#xff0c;如果通过以下操作可以将 word1 转换为 word2&#xff0c;则认为它们是接近的&#xff1a; 交换任意两个现有字符。将一个现有字符的每次出现转换为另…

SQL进阶技巧:多行转列问题中如何保证不同字段内容有序性及一一对应?【collect_list函数有序性保证问题】

目录 0 问题描述【小红书面试题】 1 数据准备 2 问题分析 3 小结 0 问题描述【小红书】 有如下需求,需要将左边的表变换成右边的表,注意字段内容的顺序及对应内容的一致性。 第一个字段为name,第二个字段为subject,第三个字段为score,变换后要求subject按照语文、数学…

android13 禁止某个app接口某个广播 禁止应用接受开机广播 禁止应用接收广播

总纲 android13 rom 开发总纲说明 目录 1.前言 2.问题分析 3.代码更改 4.彩蛋 1.前言 我们在定制系统的过程中,有时候,有些客户的应用的一些表现,并不能满足需求。例如应用接收了开机广播,然后做了一些事情,起调了某些activity。或者接受了某个广播,做了一些操作等…

网络安全第一次作业(ubuntuan安装nginx以及php部署 and sql注入(less01-08)))

ubuntuan安装nginx以及php部署 1.安装依赖包 rootadmin123-virtual-machine:~# apt-get install gcc libpcre3 libpcre3-dev zliblg zliblg-dev openssl libssl-dev2.安装nginx 到https://nginx.org/en/download.html下载nginx 之后将压缩包通过xtfp传输到ubuntu的/usr/loc…

Android:Uniapp平台中接入即构RTC+相芯美颜

0 前言 前阵子使用Uniapp平台开发了一个跨平台app&#xff0c;并且接入了即构RTC后&#xff0c;今天想进一步丰富app的直播功能。之前有相芯美颜的开发经验&#xff0c;打算将相芯美颜接入即构RTC. **在DCloud插件市场找到了在即构RTC接入相芯美颜插件&#xff0c;https://ex…

Golang | Leetcode Golang题解之第324题摆动排序II

题目&#xff1a; 题解&#xff1a; func wiggleSort(nums []int) {n : len(nums)x : (n 1) / 2target : quickSelect(nums, x-1)transAddress : func(i int) int { return (2*n - 2*i - 1) % (n | 1) }for k, i, j : 0, 0, n-1; k < j; k {tk : transAddress(k)if nums[t…

STM32之GPIO(General Purpose Input/Output,通用型输入输出)

文章目录 前言一、GPIO简介二、GPIO结构2.1 GPIO基本结构2.2 GPIO位结构2.2.1 输入部分2.2.1 输出部分 四、GPIO模式4.1 浮空/上拉/下拉输入4.2 模拟输入4.3 开漏/推挽输出4.4 复用开漏/推挽输出 前言 提示&#xff1a;本文主要用作在学习江协科大STM32入门教程后做的归纳总结…

【数据结构-前缀哈希】力扣523. 连续的子数组和

给你一个整数数组 nums 和一个整数 k &#xff0c;如果 nums 有一个 好的子数组 返回 true &#xff0c;否则返回 false&#xff1a; 一个 好的子数组 是&#xff1a; 长度 至少为 2 &#xff0c;且 子数组元素总和为 k 的倍数。 注意&#xff1a; 子数组 是数组中 连续 的部…

SpringBoot快速学习

目录 SpringBoot配置文件 多环境配置 SpringBoot整合junit SpringBoot整合mybatis 1.在创建时勾选需要的模块 2.定义实体类 3.定义dao接口 4.编写数据库配置 5.使用Druid数据源 SpringBoot 是对 Spring 开发进行简化的。 那我们先来看看SpringMVC开发中的一些必须流程…

C++ | Leetcode C++题解之第324题摆动排序II

题目&#xff1a; 题解&#xff1a; class Solution { public:int partitionAroundPivot(int left, int right, int pivot, vector<int> &nums) {int pivotValue nums[pivot];int newPivot left;swap(nums[pivot], nums[right]);for (int i left; i < right; …

【Buffer Pool】定长内存池的实现

创建一个大块的内存内存 1.内存的类型是什么&#xff1f; char* 方便有多少字节就乘以多少字节 2.如何还回来内存&#xff1f;可以将换回来的小块的内存块链接起来&#xff0c;使用freeList 3.如何链接起来? 让上一个内存块的数据存下一个内存块的地址即可 4.如果内存块的…

Mybatis-plus乐观锁

为什么要用锁 原因是当两个线程并发修改同一条数据时候 例如有条数据 id 1 count(金额/数量) 500 有两个线程都在查询数据库 查出来都是 1 500 现在两个线程都要修改这条数据 在原来基础上20 和30 那么理论来讲应该是550 可是实际有可能是530 原…

点双联通分量和边双联通分量如何选择?

先讲一下 &#xff0c;双联通分量 一定是用于 无向图 考虑什么时候需要用边双联通分量呢&#xff1f;&#xff0c;考虑给你的是一个一般图&#xff0c;需要你把联通的点都缩起来&#xff0c;视作一个点的情况&#xff0c;就是说割点可以反复访问&#xff0c;就是说割点和其他点…

鸿蒙应用服务开发【华为支付服务】 服务端

介绍 华为支付云侧接口 Java SDK Sample。 官方 Java 语言开发库pay-java由 core 和 service 组成&#xff1a; core 为基础库。包含自动签名和验签的 HTTP 客户端、回调处理、加解密库。service 为业务服务。基于业务场景提供不同的业务类&#xff0c;其下的方法为对应的ht…