如何生成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项,配置在 tag下面的.

自动化程序的核心思想是调用apktool工具将一个基准APK解包,然后将Appsflyer配置数据写入AndroidManifest.xml中, 再调用apktool工具生成APK包. 根据需求,自动化程序可以生成批量的包, 配置不同的Appsflyer source. 自动化处理程序以scala语言实现,编译为classjar包, 并且在shell文件中调用执行.该执行环境的目录结构如下:
如何生成Android批量包,配置Appsflyer Pre-Install Campaign_第1张图片
首先将基准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("")
        assert(index != -1)
        val prev = data.substring(0, index)
        val last = data.substring(index)
        val meta = ""
        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操作比较耗时, 当批量生成大量的包时, 单线程模式可能比较慢, 所以程序中使用了多线程的模式进行处理.

你可能感兴趣的:(Android,Scala)