在Android
应用集成Appsflyer
SDK
后, 可以跟踪App在不同场景下的使用信息.本文对Pre-Install Campaign
配置, 使用自动化程序,批量生成APK包.关于Appsflyer Pre-Install Campaign的配置请参考其官网说明,根据其官网说明, Pre-Install Campaign配置可以有几种不同的方式,可以通过其SDK的API
进行设置,也可以在AndroidManifest.xml
文件中进行配置.本文档的自动化程序处理的是AndroidManifest.xml中的配置情况.
有时候为了某种推广活动,可能会制作很多的包(几百, 或者成千上万), 这些包只是Appsflyer
中的source
不同, 其他功能逻辑都是完全相同的, 如果为每个包生成一个不同的build, 可能执行效率并不高, 而且这些不同的build会有不同的version code
. 这种场景下可以基于一个build, 再生成一系列的包,这些包只是AndroidManifest.xml中Appsflyer的配置的source不同.Pre-Install Campaign的配置有预定义的键:AF_PRE_INSTALL_NAME
, 其source值,根据需求设置.通常可以设置连续的序列.例如, 如果需要生成1000个推广包, 其source
可以设置为从af001
到af1000
. Appsflyer的这个配置是作为AndroidManifest.xml中的meta-data
项,配置在<application>
tag下面的.
自动化程序的核心思想是调用apktool
工具将一个基准APK解包,然后将Appsflyer配置数据写入AndroidManifest.xml中, 再调用apktool工具生成APK包. 根据需求,自动化程序可以生成批量的包, 配置不同的Appsflyer source. 自动化处理程序以scala
语言实现,编译为class
的jar
包, 并且在shell
文件中调用执行.该执行环境的目录结构如下:
首先将基准APK包放到data
目录中, 然后运行run
脚本即可.该脚本的实现为:
#!/usr/bin/env bash
java -cp ".:scala-library.jar:repack-af-config_2.12-0.1.jar" MainPar "$@"
因为程序是以scala
程序实现的, 如果以java
命令运行,需要在classpath
变量中指定相关的scala类库, 这里只需指定核心scala类库scala-library.jar
即可.repack-af-config_2.12-0.1.jar
是scala实现的批处理程序的jar包.其源码为:
import java.io.{BufferedOutputStream, File, FileOutputStream}
import scala.sys.process._
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
// Rebuild apk using apktool with Appsflyer meta data in AndroidManifest.xml.
// Using multi-thread to speed up the process.
object MainPar extends App {
var pwd = "pwd".!!
if (pwd.endsWith("\n")) pwd = pwd.dropRight(1)
if (pwd.endsWith("\r")) pwd = pwd.dropRight(1)
println("pwd:" + pwd)
val (channelPrefix, start, stop, jar, delTmpDir) = parseArgs(args)
val apktool = pwd + "/" + jar.getOrElse("apktool_2.3.4.jar")
println("using apktool:" + apktool)
val files = getApkFiles(new File(pwd + "/" + "data"), List{"apk"})
assert(files.length >= 1)
// just using one build.
val apk = files(0)
println("build based on this apk:" + apk)
var namePrefix: Option[String] = None
val futures = for (i <- start.get to stop.get) yield Future {
println("index:" + i + " " + Thread.currentThread())
val channel = channelPrefix.getOrElse("") + i
val unzipDir = apk.getAbsolutePath.dropRight(4) + channel
val rebuildedApk = apk.getAbsolutePath.dropRight(4) + "-" + channel + ".apk"
// apktool decode.
var rc = s"java -jar ${apktool} d -s -o ${unzipDir} ${apk}".!
assert(rc == 0)
val manifestFileOriginal = unzipDir + "/" + "AndroidManifest.xml"
val manifestFile = manifestFileOriginal + ".u"
val data = scala.io.Source.fromFile(manifestFileOriginal).mkString
val index = data.indexOf("</application>")
assert(index != -1)
val prev = data.substring(0, index)
val last = data.substring(index)
val meta = "<meta-data android:name=" + "\"" + "AF_PRE_INSTALL_NAME" + "\"" + " android:value=" + "\"" + channel + "\"" + "/>"
val updated = prev + meta + "\n" + last
val outputStream = new BufferedOutputStream(new FileOutputStream(new File(manifestFile)))
outputStream.write(updated.getBytes)
outputStream.close
var bool = new File(manifestFileOriginal).delete
assert(bool == true)
bool = new File(manifestFile).renameTo(new File(manifestFileOriginal))
assert(bool == true)
// apktool build.
rc = s"java -jar ${apktool} b -o ${rebuildedApk} ${unzipDir}".!
assert(rc == 0)
println("rebuild apk:" + rebuildedApk)
}
for (f <- futures) Await.ready(f, Duration.Inf)
if (delTmpDir.get) {
thread {
val tmpDirs = getTmpDirs(new File(pwd + "/" + "data"))
tmpDirs.foreach {
file =>
val deleted = s"rm -r -f ${file}".!
assert(deleted == 0)
}
}
}
private def parseArgs(a: Array[String]) = {
val argc = a.length
println("args length:" + argc)
if (argc % 2 == 1) {
usage
System.exit(1)
}
if (argc < 4) {
usage
System.exit(2)
}
var p: Option[String] = None
var s: Option[Int] = None
var t: Option[Int] = None
var j: Option[String] = None
var d: Option[Boolean] = Some(false)
for (i <- 0 until a.length by 2) {
a(i) match {
case "-p" => p = Some(a(i+1))
case "-s" => s = Some(a(i+1).toInt)
case "-t" => t = Some(a(i+1).toInt)
case "-j" => j = Some(a(i+1))
case "-d" => {
if (a(i+1) == "0" || a(i+1) == "false") {
d = Some(false)
} else {
d = Some(true)
}
}
case _ => usage; System.exit(3)
}
}
(p, s, t, j, d)
}
private def getApkFiles(dir: File, extensions: List[String]): List[File] = {
dir.listFiles.filter(_.isFile).toList.filter {
file => extensions.exists(file.getName.endsWith(_))
}
}
private def getTmpDirs(dir: File): List[File] = {
dir.listFiles.filter(_.isDirectory).toList
}
private def thread(body: => Unit) = {
val t = new Thread {
override def run(): Unit = body
}
t.start
t
}
private def usage = {
println(
"""program parameters: -p channelPrefix -s start -t stop -j apktoolJar -d isDeleteTmpDir
|
| channelPrefix: optional, depends on the channel naming;
| start: mandatory, is a number;
| stop: mandatory, is a number;
| apktoolJar: optional, apktool jar.
| isDeleteTmpDir: optional, true or false.
|
| here is an example, if we need 200 packages and AppsFlyer channel is from ca00101 to ca00300. then,
| channelPrefix is ca00,
| start is 101,
| stop is 300.
""".stripMargin)
}
}
当运行run脚本时, 输入的参数请参考usage
函数的说明.因为apktool
工具的decode
和build
操作比较耗时, 当批量生成大量的包时, 单线程模式可能比较慢, 所以程序中使用了多线程的模式进行处理.