原理使用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)
}
}
}
}
}