在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
操作比較耗時, 當批量生成大量的包時, 單線程模式可以比較慢, 所以程序中使用了多線程的模式進行處理.