如何生成Android批量包,配置Appsflyer Pre-Install Campaign

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可以設置爲從af001af1000. Appsflyer的這個配置是作爲AndroidManifest.xml中的meta-data項,配置在<application> tag下面的.

自動化程序的核心思想是調用apktool工具將一個基準APK解包,然後將Appsflyer配置數據寫入AndroidManifest.xml中, 再調用apktool工具生成APK包. 根據需求,自動化程序可以生成批量的包, 配置不同的Appsflyer source. 自動化處理程序以scala語言實現,編譯爲classjar包, 並且在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工具的decodebuild操作比較耗時, 當批量生成大量的包時, 單線程模式可以比較慢, 所以程序中使用了多線程的模式進行處理.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章