服务设计Android 反编译注入
MeteorCat如果接触过 游戏联运 和 广告变现 基本上都对 渠道分包 有一定了解, 主要核心就是应用反编译
本质上就是需要给游戏应用提供或者上传一份应用证书签名, 然后方便后台界面触发解包反编译注入对应参数之后再打包成新的渠道包
这里有以下概念需要了解
-
母包: 开发商输出无渠道参数的原始未签名/测试签名游戏安装包, 也就是等待被解包并重新打包
-
签名文件
Android: 渠道方提供正式签名 keystore(.jks/.keystore)/别名/密钥密码; 部分自研渠道使用自有统一证书
iOS: 渠道开发者证书/描述文件/推送证书
- 之所以需要证书, 是因为应用上架/渠道校验/支付唤起/广告 SDK 校验都强制校验签名, 母包分包后必须用新的渠道证书重新签名
-
反编译/解包: 利用 apktool 或者解压方式修改内部核心配置文件, 将所需参数注入进去
这部分其实更适合有经验客户端来讲解, 但是有时候部分服务端可以简单处理下自定义集成 SDK 库, 从而实现类似效果
环境搭建
推荐编辑器为 IDEA 或者 Android Studio
之后就是构建自己的打包 SDK 类库, 这部分我将其命名为 nova-lib, 需要按以下步骤
-
创建空目录, 并且用 IDEA/Android Studio 打开
-
依次点击 File > New > New Module
-
在随即显示的 Create New Module 信息框中填写, 依次点击 Android Library 并填写相关库信息
-
这里采用 Android API 28 即可(目前兼容性最广), 最后点击 Create 创建项目
-
Build Configuration 这项是打包脚本, 官方虽然主推 kotlin 脚本打包方式(但是我不太推荐)
具体的配置参数如下所示

之后需要修改配置, 首先是项目根目录下创建 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 26 27 28 29 30
| buildscript { repositories { maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/jcenter' } maven { url 'https://maven.aliyun.com/repository/public' } google() mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:7.4.2" } }
allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/jcenter' } maven { url 'https://maven.aliyun.com/repository/public' } google() mavenCentral() } }
tasks.register('clean', Delete) { delete rootProject.buildDir }
|
之后就是采用 GradleWrapper 来做版本锁定, 否则依赖全部乱飞
找到项目根目录下 gradle/wrapper/gradle-wrapper.properties(没有就创建) 将内容替换成以下方式
1 2 3 4 5 6 7
| distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://mirrors.aliyun.com/gradle/distributions/v7.5.0/gradle-7.5-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists
|
在编辑器当中点击 Build > Build Project 确认输出编译成功即可, 之后就是配置第三方镜像库和引入 AGP
在 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 26 27 28
| plugins { id 'com.android.library' }
android { namespace 'me.meteorcat.nova' compileSdk 33
defaultConfig { minSdk 28
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" }
buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } }
dependencies {
}
|
如果 build.gradle 编译打包文件会出现以下错误
这是因为目前的没有指定 JDK 来编译打包, 默认选择系统 SDK, 这里最好下载 Android SDK 来打包
-
依次点击 File > Settings... > Languages & Frameworks > Android SDK Updater
-
在 SDK Platform 勾选之前创建的 Android API SDK 版本(之前选择 Android API 28, 对应的是 Android 9)
-
在 SDK Tools 勾选 Android Build Tools 和 Android SDK Platform Tools, 继续 Tools 的版本(目前工具最新是 36)
-
选中之后点击 OK 就会弹出提示安装 Android 打包编译工具
-
之后就是选择编译的 JDK, 依次点击 File > Project Structure > Project
-
选中当前打包的 JDK, 我这边系统采用 Java11
-
依次点击 File > Settings... > Build,Execution,Deployment > Build Tools > Gradle
-
在 Gradle JVM 选择和打包一样的 JDK(一般采用默认 Project SDK 即可)
如果 JDK 版本不符会出现异常错误 Your build is currently configured to use incompatible {Jdk版本} and {Gradle版本}
重新构建下项目一般不会出现其他问题, 之后就是需要细化修改 nova-lib/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 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 86 87 88 89 90 91 92 93 94 95
| plugins { id 'com.android.library' }
android { namespace 'me.meteorcat.nova' compileSdk 33
defaultConfig { minSdk 28
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" }
buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 coreLibraryDesugaringEnabled true }
packagingOptions { exclude 'META-INF/DEPENDENCIES.txt' exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE.txt' exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/notice.txt' exclude 'META-INF/license.txt' exclude 'META-INF/dependencies.txt' exclude 'META-INF/LGPL2.1' exclude 'META-INF/versions/9/module-info.class' }
buildTypes {
debug { debuggable true minifyEnabled false versionNameSuffix "-debug" }
release { debuggable false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' versionNameSuffix "-release" } } }
dependencies {
api fileTree(include: ['*.jar', '*.aar'], dir: 'extra')
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.10"
implementation "androidx.core:core:1.10.1" implementation "androidx.appcompat:appcompat:1.6.1"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' }
|
这里会发现打包文件都是黑色的不可用的, 需要在项目根目录建立 settings.gradle 文件, 添加子目录模块
如果编译的时候出现以下错误内容
-
Task 'prepareKotlinBuildScriptModel' not found in project ':nova-lib'.
- 默认编辑器启用
KotlinDSL 支持而找不到脚本所以错误, 关闭窗口删除 .idea 和 build 目录重新打开自动加载
-
SDK location not found. Define a valid SDK location with an ANDROID_HOME environment variable
- 这是因为 Gradle 找不到安卓 SDK 路径, 可能是没有配置
ANDROID_HOME 环境变量, 按照以下方法处理
- 配置
ANDROID_HOME 环境变量指向 Android/Sdk 路径
- 在根目录创建
local.properties 文件, 内部设置 sdk.dir=D\\Android\\Sdk SDK目录(路径分隔符用双反斜杠\\)
- 本地开发我一般习惯配置
ANDROID_HOME 环境变量, 具体按照个人需求来选择处理
-
Set android.useAndroidX=true in the gradle.properties file and retry.
- 目前Google强制需要添加
AndroidX 相关支持
- 需要在项目根目录的
gradle.properties(没有该文件就创建) 文件添加如下配置
android.useAndroidX=true: 开启AndroidX
android.enableJetifier=true: 关闭旧支持库自动迁移警告
后续重新编译项目确认没问题, 接下来就是自定义可被注入的配置文件
定义渠道配置
一般母包注入以下以下参数
-
app_ident: 后台系统的母包唯一标识, 指向数据库当中 app_base.app_ident 应用
-
app_key: 后台系统分配给接入商的客户端密钥, 用于做请求参数签名处理
-
channel_ident: 分包的渠道唯一标识, 指向数据库渠道表 app_channel.channel_ident 应用
目前有两种分包处理方式, 这部分可以了解下
一般建议 assets 创建独有配置文件即可, AndroidManifest 主要问题是怕配置名可能会出现冲突
这里创建独有的配置文件 nova_config.json, 文件放入 nova-lib/src/main/assets/nova_config.json 中, 内容如下
1 2 3 4 5
| { "app_ident": "", "app_key": "", "channel_ident": "" }
|
这里 app_ident/app_key 留空是提供给应用接入方去手动填写, 反编译分包的时候会重新覆盖写入
现在就需要在 nova-lib 之中编写功能类, 暴露给接入方来调用和加载数据:
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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| package me.meteorcat.nova;
import android.content.Context; import org.json.JSONException; import org.json.JSONObject;
import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets;
public class NovaConfig {
private static final String CONFIG_FILE = "nova_config.json";
private static NovaConfig instance;
private NovaConfig() { }
public static NovaConfig getInstance() { if (instance == null) { instance = new NovaConfig(); } return instance; }
private String appIdentifier;
public String getAppIdentifier() { return appIdentifier; }
private String appKey;
public String getAppKey() { return appKey; }
private String channelIdentifier;
public String getChannelIdentifier() { return channelIdentifier; }
public void init(Context ctx) throws IOException, JSONException {
InputStream is = ctx.getAssets().open(CONFIG_FILE); byte[] buffer = new byte[is.available()]; int ignore = is.read(buffer); is.close();
String json = new String(buffer, StandardCharsets.UTF_8); JSONObject data = new JSONObject(json);
this.appIdentifier = data.optString("app_ident", ""); this.appKey = data.optString("app_key", ""); this.channelIdentifier = data.optString("channel_ident", ""); } }
|
配置加载类一般比较简单, 这里仅仅只是做母包注入处理方法, 执行打包之后就会生成对应 arr 库
假设我们现在已经要开始做某个渠道了, 这里打包出我们最新合作的 arr 第三方库:

这里打包出 nova-lib-debug.aar(测试包) 和 nova-lib-release.aar(正式包), 一般推荐只需要给 release.aar 即可
接下来就是接入商来引入该 aar 对接到自己的安卓应用当中
创建应用
这里首先在根目录创建新的模块, 也就是模拟我们应用接入的步骤来实现接入
-
依次点击 File > New > New Module
-
在随即显示的 Create New Module 信息框中填写, 依次点击 Phone & Tablet 并填写应用信息
-
需要注意下一步, 如果采用 Java 语言就不需要选择 Empty Activity, 而是要选择 Empty Views Activity
-
最后设置 Activity 名称, 这里直接保持默认交 MainActivity/activity_main 即可



这里没有配置调试虚拟机, 所以需要创建下测试虚拟机来测试运行我们创建的 NovaGame 应用
这里建议选择我们之前的 Android API 28 版本, 部分游戏厂商SDK是偏老的

这里直接运行可能会出现报错, 因为默认生成的项目工程内部的 androidx.activity 版本比较新(android特有的破坏性更新)
为了保持兼容 compileSdk 33 就必须要降级指定的 androidx.activity, 在 game 目录的 build.gradle 修改以下包
1 2 3 4 5 6 7 8
| configurations.all { resolutionStrategy.force "androidx.activity:activity:1.7.2" }
dependencies { }
|
这里处理编译之后会报相关 Android resource linking failed 的错误, 实际上就是对应的图标文件丢失
-
打开 game/src/main/AndroidManifest.xml 文件
-
找到 <application> 标签内容
-
将 android:icon 和 android:roundIcon 相关修改存在图标或者注释掉
之后可能会报相关的 EdgeToEdge 错误, 该组件是 androidx.activity 1.8.0 初始化自带(没什么用), 可以直接删除引入的相关类
完成之后就可以看到具体 Hello.World 界面

现在就是准备把 aar 包引入并且在窗口当中打印相关参数
引入SDK
这里直接在应用目录创建 libs 目录, 也就是创建 game/libs 目录, 目录结构如下
1 2 3 4 5 6 7 8 9 10
| game/ ├── src/ │ └── main/ │ ├── java/ │ ├── res/ │ └── assets/ ├── libs/ │ └── nova-lib-release.aar ├── build.gradle └── AndroidManifest.xml
|
nova-lib-release.aar 就是之前我们建议生成的渠道SDK库, 但是目前需要给应用追加应用初始化类方便给 nova-lib 初始化
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
| package me.meteorcat.game;
import android.app.Application; import android.util.Log; import me.meteorcat.nova.NovaConfig;
import java.util.Objects;
public class IApplication extends Application {
private static final String TAG = "NovaGame";
@Override public void onCreate() { super.onCreate(); try { NovaConfig.getInstance().init(this); } catch (Exception e) { Log.e(TAG, Objects.requireNonNull(e.getMessage())); throw new RuntimeException(e); }
NovaConfig cfg = NovaConfig.getInstance(); Log.d(TAG, "app_ident=" + cfg.getAppIdentifier()); Log.d(TAG, "app_key=" + cfg.getAppKey()); Log.d(TAG, "channel_ident=" + cfg.getChannelIdentifier()); }
}
|
编写完成之后在 AndroidManifest.xml 注册该初始化类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <application android:name=".IApplication" android:allowBackup="true" android:icon="@android:drawable/sym_def_app_icon" android:roundIcon="@android:drawable/sym_def_app_icon" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.GameHost">
<activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application>
|
之后重新编译启动应用查看打印日志就可以看到具体内容信息

在游戏项目的目录创建新的 nova_config.json 文件, 确认是否可以加载指定配置文件的参数
配置文件地址 game/src/main/assets/nova_config.json, 文件内容如下
1 2 3 4 5
| { "app_ident": "s34x123x", "app_key": "7d2f9c4e8b1a3056zxcvbnmqwertyu", "channel_ident": "" }
|
再次启动应用之后确认日志打印是否一致

这里为了方便看到后续渠道分包效果, 建议在页面上将配置文件打印出来, 所以需要在 MainActivity.java 文件修改处理下
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
| package me.meteorcat.game;
import android.os.Bundle;
import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import me.meteorcat.nova.NovaConfig;
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
NovaConfig novaConfig = NovaConfig.getInstance();
String infoBuilder = "===== 渠道分包配置信息 =====\n" + "母包唯一标识 app_ident:" + novaConfig.getAppIdentifier() + "\n\n" + "客户端密钥 app_key:" + novaConfig.getAppKey() + "\n\n" + "渠道唯一标识 channel_ident:" + novaConfig.getChannelIdentifier() + "\n\n";
TextView tvChannelInfo = findViewById(R.id.nova_info); tvChannelInfo.setText(infoBuilder);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return insets; }); } }
|
这里需要在 res/layout/activity_main.xml 其中添加文本节点用于展示信息
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
| <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<TextView android:id="@+id/nova_info" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="15sp" android:lineSpacingExtra="8sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
|
再次启动也能够在页面上面看到具体内容信息

确认没问题后接下来就是出包流程, 出包的话就需要关键的应用包签名证书, 这就是后面需要说明的
出包流程
应用接入方(也就是对接的研发方)一般出包的情况如下
那么作为SDK提供方, 我们一般会要求对方编译生成原始母包, 也就是 {应用包名}-release-unsigned.apk
安卓生成母包比较简单, 在 IDEA 之中直接点击出包即可

如图片所见这里 game-release-unsigned.apk 就是生成母包, 也就是上传到对应管理后台来等待渠道分包
这里提供该 apk 方便自己了解测试: game-release-unsigned.apk
现在就是核心的分包脚本命令, 后续会采用 Python 做全自动打包, 目前为了加深理解所以会以命令方式处理
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 86 87 88 89 90
| # 注意: 这里采用 linux 的 bash 环境命令 mkdir -p channel/xiaomi # 创建小米渠道目录, 编译打包的产物在这里处理 cd channel/xiaomi # 进入小米渠道打包目录
# 这里一般是用脚本把母包复制过来, 这部分需要调取数据的 app_base 母包应用表文件路径 # 我为了简约直接手动复制, 后续做自动化打包需要执行处理包的下载处理流程
# Linux 命令, 获取文件的大小, 单位: byte stat -c "%s" game-release-unsigned.apk
# 这里需要生成替换的注入的配置文件 nova_config.json # 这个文件也是需要从数据库当中取出渠道信息并且以 JSON 格式保存的文件 # 这里我手动生成该文件, 内容如下 # {"app_ident":"s34x123x","app_key":"7d2f9c4e8b1a3056zxcvbnmqwertyu","channel_ident":"xiaomi"} # 在原基础追加 channel_ident 信息 stat -c "%s" nova_config.json # 查看配置文件大小, 只是看看是否存在文件
# 没问题就准备反编译解包 # 这里需要用到 apktool 工具: https://apktool.org/ # 具体看官网配置即可, 按照下面命令输入打印成功就代表安装成功 # 这里不采用全局环境安装, 而是在上级目录配置打包环境内容 java -jar ../apktool.jar v # 打包工具放置在上一级通用
# -------------------------- # 开始核心步骤解包注入 # --------------------------
# 1. 解包原始母包, 输出至临时文件夹 unpack java -jar ../apktool.jar d -f game-release-unsigned.apk -o unpack
# 2. 防止如果解包后无assets文件夹, 需要先创建再复制 mkdir -p unpack/assets
# 3. 覆盖assets目录下的渠道配置 cp nova_config.json unpack/assets/nova_config.json
# 4. 重新打包资源并生成未签名中间包 java -jar ../apktool.jar b unpack -o unsigned.apk
# -------------------------- # 签名证书使用 # --------------------------
# 这里的证书一般是数据库提取出来本地文件或者CDN文件下载 # 一般不会在打包的时候才去生成证书, 而是在管理后台为对应渠道生成渠道签名证书
# 首先生成 keystore JKS 证书, 直接配置静默生成 # -storepass 和 -keypass 就是核心证书密码, 这部分其实应该数据库获取, 这里直接演示 123456 即可 ../keytool.exe -genkey -v \ -keystore xiaomi.jks \ -alias xiaomi_key \ -keyalg RSA \ -keysize 2048 \ -validity 36500 \ -storepass 123456 \ -keypass 123456 \ -dname "CN=NovaGame, OU=Dev, O=Company, L=Guangzhou, ST=Guangdong, C=CN"
# 之后替换证书路径/库密码/证书别名 # 目前大部分平台都要求 APK 上架商店强制要求4K对齐, 这一步最好加上 # 一般使用 apksigner 签名之后默认4K对齐, 该工具在 `Android/Sdk/build-tools/{API版本}/lib/apksigner.jar` 之中 # --ks-pass 和 --key-pass 就是之前配置的 JKS 证书密码 java -jar ../apksigner.jar sign \ --ks xiaomi.jks \ --ks-key-alias xiaomi_key \ --ks-pass pass:123456 \ --key-pass pass:123456 \ --min-sdk-version 28 \ --rotation-min-sdk-version 28 \ --v1-signing-enabled true \ --v2-signing-enabled true \ --v3-signing-enabled true \ --out xiaomi_align.apk \ unsigned.apk
# 签名合法性验证 java -jar ../apksigner.jar verify --verbose xiaomi_align.apk
# 至此完成渠道分包功能, 查看最终渠道成品APK字节大小 # xiaomi_align.apk 就是小米指定渠道包 stat -c "%s" xiaomi_align.apk
# 至此完成整体的渠道分包流程
|
最终就可以看到分包注入的参数

简单注入渠道信息的包: xiaomi_align.apk
这就是渠道简单分包原理, 这部分其实需要结合管理后台业务来看, 具体证书这部分其实应该有管理后台控制
不过涉及到的知识点就比较广, 并且一般是有客户端编写 Python 跨平台脚本来运行