Android Android CPS 出包流程 MeteorCat 2025-12-11 2025-12-11 这里主要针对的是引入 AAR 之后需要区分启动之后的渠道包, 涉及到对引入 AAR 配置的重新打包, 也就是 CPS 包.
CPS(Cost Per Sale): 按销售付费追踪包, 用于订单归因、数据上报、返佣计算、链接生成加载统计有效买量成本
比如你作为 发行商, 第三方 研发商 在平台上架游戏, 我们就可能会帮助生成指定渠道包给线下推广或者直播主来付费推广.
这时候的付费推广安装包必须在启动的时候就获得属于自己的 标识, 从而方便把后续登陆/支付(买量和付费成本)纳入分成统计.
这样处理可以方便统计以下数据:
指定渠道的获取用户绩效: 某些渠道拉取新用户多, 方便其渠道下面用户维护其注册用户
指定渠道的付费转化率: 某些渠道付费转化率优秀, 这部分可以转化成推广佣金分成
需要区分 CPS 和 渠道包 的差别, 这两者虽然都是有特殊标识, 但是作用是完全不一样的:
渠道包: 指定平台(比如抖音/快手/谷歌)会单独出个包额外做标识, 独有的渠道标识区分平台来源
CPS: 针对自己推广下的来源标识, 用于自身做统一的推广渠道佣金分成
这里有几种 ARR 注入配置内嵌方法:
静态打包: 在 app/build.gradle 的时候写死渠道
外部配置: 在 src/main/assets 生成对应 config.properties 读取加载
服务端拉取: 配置特定带 ?channel=1000 或者 ?platform?=GOOGLE 链接拉取配置
静态配置加载
静态配置一般用于出渠道包(Google)的时候, 在应用的 app/build.gradle 声明以下定义配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 android { buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt' ), 'proguard-rules.pro' buildConfigField "String" , "PLATFORM_ID" , "\"GOOGLE\"" } debug { buildConfigField "String" , "PLATFORM_ID" , "\"GOOGLE\"" } } }
同步 Gradle 配置之后在 Application/Activity 的 onCreate 就可以加载对应属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MainActivity extends PinoActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); String platformId = BuildConfig.PLATFORM_ID; Log.d("PinoGame" , String.format("platform id = %s" , platformId)); } }
这里就会在 logcat 打印出指定的渠道常量:
1 2025-12-11 02:20:02.703 10369-10369 PinoGame io.meteorcat.pino.app D channel id = GOOGLE
这种一般用于外部平台打包注入, 也就是由 研发端(CP) 必须要传入的配置, 不需要我们作为发行方在 AAR 包处理.
外部配置文件
需要 app 和 lib 端的 src/main/assets 目录定义自己的配置文件, 一般都是 {项目名}-config.properties:
之后就是编写配置加载工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 import android.content.Context;import android.content.res.AssetManager;import android.util.Log;import java.io.InputStream;import java.util.Properties;public class ConfigManager { private static final String TAG = "PinoConfigManager" ; private static final String PINO_CONFIG_FILE = "pino-config.properties" ; private Properties properties; private static volatile ConfigManager instance; public static ConfigManager getInstance () { if (instance == null ) { synchronized (ConfigManager.class) { if (instance == null ) { instance = new ConfigManager (); } } } return instance; } public void init (Context context) { properties = new Properties (); boolean isAppConfigLoaded = loadConfigFromAssets(context.getAssets(), false ); if (!isAppConfigLoaded) { Log.w(TAG, "未找到App端CPS配置, 加载AAR内置默认配置" ); loadConfigFromAssets(context.getResources().getAssets(), true ); } } private boolean loadConfigFromAssets (AssetManager assetManager, boolean isAarDefault) { try (InputStream is = assetManager.open(PINO_CONFIG_FILE)) { properties.load(is); Log.d(TAG, (isAarDefault ? "AAR默认" : "App自定义" ) + "CPS配置加载成功:" + properties.toString()); return true ; } catch (Exception e) { Log.e(TAG, (isAarDefault ? "AAR默认" : "App自定义" ) + "CPS配置加载失败" , e); return false ; } } public String getChannelId () { return properties.getProperty("pino_channel_id" , "" ); } }
重新生成 AAR 的 SDK 包, 之后引入 app 包之中确认是否可用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import android.os.Bundle;import android.util.Log;import io.meteorcat.pino.PinoActivity;import io.meteorcat.pino.utils.ConfigManager;public class MainActivity extends PinoActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); String platformId = BuildConfig.PLATFORM_ID; Log.d("PinoGame" , String.format("platform id = %s" , platformId)); ConfigManager configManager = ConfigManager.getInstance(); configManager.init(getApplicationContext()); Log.d("PinoGame" , String.format("channel id = %s" , configManager.getChannelId())); } }
后续就是在 src/main/assets 创建 pino-config.properties 文件附加 pino_channel_id=10001 内容, 最后打印内容:
1 2 3 4 5 6 7 # 首次加载 --------------------------------- 2025-12-11 02:59:30.894 12858-12858 PinoGame io.meteorcat.pino.app D platform id = GOOGLE 2025-12-11 02:59:30.894 12858-12858 PinoGame io.meteorcat.pino.app D channel id = # 写入配置 --------------------------------- 2025-12-11 03:01:21.034 13005-13005 PinoGame io.meteorcat.pino.app D platform id = GOOGLE 2025-12-11 03:01:21.034 13005-13005 PinoGame io.meteorcat.pino.app D channel id = 10001
这里就是动态加载配置包信息, 但是这里涉及到另外的问题: 后台渠道ID是动态生成, 不可能让研发方一个个渠道出包, 怎么才能支持动态出包?
动态 properties 注入打包
这里就需要在后台通过脚本手动做安卓重新打包, 也就是重新对 apk 解包之后将值钱的 pino-config.properties 文件写入进去.
需要以下工具来做重新打包并重新对 apk 签名, 默认研发方生成的首个安装包为 母包, 后续重新签名的生成都为 子包:
可以将 apk 看作文件签名过的压缩包, 这里需要做的就是解压这个压缩包添加我们自己的文件重新压缩, 对这个新的压缩包重新签名.
注意: 母包和子包的签名相关信息都要一致, 否则会打包过程之中母包无法生成子包
这里采用 Linux 平台来处理, 一般动态分出 CPS 渠道都是由后台服务端创建并生成特定ID, 之后服务端调用脚本生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 # 需要保证上面的安装工具已经安装配置完成 # 假设默认创建的打包目录为 /tmp/android mkdir /tmp/android # 1. apk解包 # 这里假设游戏研发商提交了 apk 母包, 并且 CPS 渠道ID 为 10001 # 将母包资源解包到目录之中, 约等于压缩包解压 apktool d -f pino-relase.apk -o /tmp/android/10001 # 2. 写入内部 CPS 渠道配置文件 # 覆盖或者创建 assets 下的 pino-config.properties 文件 echo "pino_channel_id=10001" > /tmp/android/10001/assets/pino-config.properties # 3. 重新生成打包(这里的子包目前还未签名) apktool b -f /tmp/android/10001 -o /tmp/android/10001/unsigned.apk # 4. 对齐 APK 包优化 zipalign -f 4 /tmp/android/10001/unsigned.apk /tmp/android/10001/aligned.apk # 5. 最后开始针对对齐的 apk 包签名 # 注意这里需要指定的签名文件和信息: # {自定义的签名文件名}.jks, 一般可以在发行后台创建游戏应用的时候生成的 jks 签名文件 # 签名的别名, 这部分也是可以在平台平台创建应用的时候生成 # 签名的密码, 同上, 也是创建那一刻直接生成 # 密钥库密码, 同上, 也是创建那一刻直接生成 # # - /keys/1002/key.jks: 签名文件 # - /keys/1002/key.alias: 签名别名文件, 执行命令行脚本可以直接读取放置变量, 这里假设为 "pino_10001" ( {项目名}_{渠道ID} ) # - /keys/1002/key.store: 密钥库密码文件, 同上读取数据(也可以读取数据库加载), 随机生成 MD5 假设为 "xm4kp887d0hi809h225fk4ezwe50uc57" # - /keys/1002/key.pwd: 签名密码文件, 同上读取, 随机生成 MD5 假设为 "38de6w6sn20but6i22d017i5z6e4m8cz" # jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore /keys/1002/key.jks \ -storepass xm4kp887d0hi809h225fk4ezwe50uc57 -keypass 38de6w6sn20but6i22d017i5z6e4m8cz \ /tmp/android/10001/aligned.apk pino_10001 \ -signedjar /tmp/android/10001/signed.apk # 最后的 /tmp/android/10001/signed.apk 就是签名完成的文件, 可以将其复制到对应目录之下然后清理掉现有打包过程产生的目录
这样生成的 signed.apk 就是最终单独指向 channel=10001 来源的 apk, 后续就是对应渠道的推广员获取自己下载 apk 的链接去给用户下载.
签名文件和密码建议通过环境变量或加密存储/数据库/Redis之类读取, 而非硬编码在脚本中(容易服务器泄漏), 打包服务器最好单独部署不要涉及其他服务
不止 channel_id, 内部如果需要可以添加更多动态设置, 比如 SERVER_URL(放置域名失效和变动) / APPID(写死默认的应用ID) 等.
甚至有时候需要针对对应包来做 icon 和 应用名称 的变动修改, 这部分需要比较扎实的服务端处理经验
只是这里涉及到有的云端业务打包, 后面可能需要用 Git + Jenkins 做提交源码并启动远程动态打包处理.
另外需要提供自定义 Application 和 Activity 提供实现, 用于直接在 AAR 底层就处理自己内部初始化.
有时候需要特殊渠道买量包, 如 抖音导流 这种花费资金买流量的包, 比起 BuildConfig 编译, 动态注入 properties 扩展参数标识更灵活.
本地生成签名
上面说过签名需要用到的 *.jks 等相关内容和设置都可以直接通过命令行生成, 具体方式可以采用 Linux 命令处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 依赖 JDK 内置工具 keytool, 确保服务器已经安装 JDK # 1. 创建签名文件存储目录(按应用ID分类,示例应用ID=1002) mkdir -p /keys/1002 # 2. 生成JKS签名文件, 每一行配置说明如下: # -keystore /keys/1002/key.jks # -alias pino_1002 # -keyalg RSA # -keysize 2048 # -validity 3650 # -storepass xm4kp887d0hi809h225fk4ezwe50uc57 # -keypass 38de6w6sn20but6i22d017i5z6e4m8cz # 其他的 -dname 地区按照自己需要处理 # keytool -genkey -v \ -keystore /keys/1002/key.jks \ -alias pino_1002 \ -keyalg RSA \ -keysize 2048 \ -validity 3650 \ -storepass xm4kp887d0hi809h225fk4ezwe50uc57 \ -dname "CN=PinoGame, OU=MeteorCat, O=MeteorCat, L=Shanghai, ST=Shanghai, C=CN"
这里可以通过上面我们的测试项目的 debug 包来手动测试自己签名打包, 需要注意因为安卓项目变动很大, 有些指令可能因为版本不同而不可用.
一般如果作为 应用发行方 都是有自己的 发行后台, 需要按照以下流程处理:
游戏研发方就需要在发行后台创建应用
发行后台创建应用的同时生成母包所需的签名相关文件
游戏研发方获取这些签名文件和相关密码引入项目当中生成打包
游戏研发方将文件通过后台提交到发行方, 发行后台开始针对渠道生成子包
读取数据库对应的证书和密码, 调用服务端脚本对母包进行子包重新打包生成
完成子包的分包流程之后, 保存下载到数据提供给渠道用户下载指定的 apk
也有通过游戏研发方自己来提交 jks 和密码参数, 然后分包的时候引用指定参数的做法, 具体区分这些证书相关是要发行负责生成还是研发负责生成
流程目标拆解出来如下表所展示:
阶段
操作方
核心动作
关键产出/数据
1. 应用创建
发行方后台
运营人员创建应用(填写包名、应用名称、渠道规则等),自动生成签名套件
应用ID、JKS文件、别名、storepass、keypass
2. 签名交付
发行方→CP
后台生成签名套件的下载链接(带时效/权限),CP下载后集成到项目
CP本地集成签名的工程配置
3. 母包提报
CP→发行后台
CP用发行方签名打包母包,通过后台上传接口提交母包(校验包名/签名一致性)
母包文件、母包MD5、提报记录
4. 子包生成
发行方后台
读取渠道配置(channel_id)+ 签名信息,调用脚本批量生成子包
各渠道子包APK、子包下载链接、渠道映射表
5. 分发下载
渠道/用户
后台按渠道生成专属下载链接,用户下载对应子包
子包下载日志、渠道数据统计
而签名信息由 研发方 或者 发行方 生成的主要差别如下:
维度
模式:发行方生成签名
模式:研发方提供签名
核心主体
发行方掌控签名全生命周期
研发方自持签名,仅向发行方提交签名信息
适用场景
1. 发行方主导全渠道分发; 2. 研发方无成熟签名体系; 3. 需统一所有渠道包签名
1. 研发方已有应用市场上架签名; 2. 研发方对签名安全要求高; 3. 多发行方合作同一研发方
流程核心差异
发行后台创建应用时自动生成签名→研发方下载集成
研发方在后台上传JKS+填写密码→发行方校验后复用
优势
1. 签名统一,无适配风险; 2. 发行方可控分包流程; 3. 避免研发方泄露签名
1. 兼容研发方现有签名体系; 2. 无需研发方重新适配签名
风险
发行方需承担签名保管责任
1. 研发方提交错误签名导致分包失败; 2. 签名泄露风险由研发方承担
其实主要差别在是否已经将签名提交到应用市场, 如果不想修改原有签名则是要手动提交, 但是本质上这两种方式最好在发行平台都提供.
这章节开头虽然说是 安卓加载外部配置 相关, 但是基本都是围绕 CPS 出包例子说明, 主要是应用最广泛的就是这部分出包逻辑.
注意: 最好初始化的时候就加载到应用ID(APP_ID), 因为面向全球化的应用需要加载默认语言, 这部分需要加载到服务器应用配置的语言项.
初始化的时候, 内部应用其实还有很多事情需要处理, 比如加载默认 i18n 语言模板/获取远程的应用更新版本数据/判断应用是否禁止等