AndroidAndroid AAR SDK 构建
MeteorCat
官方文档(需魔法工具): android-library
AAR(Android Archive) 是 Android 专属的库归档格式, 可以理解为和后段的 Jar 包类似的格式:
| 特性 |
AAR(Android专属) |
Jar(通用Java) |
| 包含内容 |
编译后的class文件 + 资源(布局/图片/字符串) + AndroidManifest.xml + 原生库(so) |
仅编译后的class文件 + 清单(可选) |
| 适用场景 |
Android组件封装(自定义View、Activity、带资源的工具库) |
纯Java逻辑封装(无Android资源依赖) |
| 依赖Android框架 |
强依赖(需Android SDK编译) |
无依赖(可跨平台) |
| 资源处理 |
自带资源打包,主工程可直接引用库内资源 |
无资源打包能力 |
本篇以 海外游戏上架 的 发行方 来说明 AAR 在其中起到什么作用, 其他相关扩展知识可以去网上获取
在现代化当中开发之中, 手机游戏上架涉及到以下方面:
游戏发行方两者的资源是拆分开的, 也就是 研发方不可能获得底层发行方SDK代码, 发行方不可能获取研发的游戏开发资源.
在双方都不能获取对方源码之下, 需要发行方生成 aar 包让研发方引入并且调用内部关键 SDK 的接口功能, 从而唤醒发行方所有需要对应的接口功能.
在开发 aar 包之前, 首先必须说明的是发行 SDK 除非官方已经强制要求升级, 否则尽量采用 Java1.8 支持.
目前国内游戏 Android 端普遍版本不会更新太快(有的还在用 Unity2007 版本), 内置的打包机制可能没办法做高版本兼容;
所以开发通用的 aar 包尽量基于 Java 1.8, 甚至最好连 Kotlin 都不要采用而是继续保持 Java 包适配.
最好 Gradle 打包脚本还是用 Groovy DSL 默认, 不要用 Kotlin DSL 做打包脚本, 很多网站资料都不好找
还有 Gradle + AGP 的适配也是一团乱码, AGP(Android Gradle Plugin) 是将 Android 项目(Apk/AAR) 和 Gradle 核心编译工具
AGP 两者必须要强关联版本对应, 因为 Gradle 的每次都带有破坏性更新, 所以不像 Java 后端大部分情况下可能无痛升级.
这里提供常用的 Gradle + AGP 版本对应:
-
Gradle 7.0 → AGP 7.0.0+
-
Gradle 7.1 → AGP 7.1.0+
-
Gradle 7.2 → AGP 7.2.0+
-
Gradle 7.3 → AGP 7.3.0+
-
Gradle 7.4 → AGP 7.4.0+
-
Gradle 7.5 → AGP 7.4.2+
注意: Gradle 7.x 以上版本推荐构建环境为 JDK11, 而编译目标为 JDK1.8
项目构建
首先最好用 IDEA/AndroidStudio 打开个 空目录 以此为开发目录, 执行以下操作:
-
依次点击 File > New > New Module
-
在随即显示的 Create New Module 信息框中填写, 依次点击 Android Library 并填写相关库信息
-
这里采用 Android API 28 即可(目前适用面最广), 最后点击 Create 创建项目

我这里其实默认 Module Name 为 pino-lib, 图片上面没展示出来
创建完成就会在当前目录下面生成了 pino-lib, 这就是我们自己定义的 sdk 库.
修改配置
接下来就是创建项目根打包配置:
1 2
| # 注意: 这里目录其实就是之前所说的 `空目录`, 后续我们都是以此路径为根目录 touch build.gradle # 手动创建编译说明文件
|
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(没有就创建)
-
文件的 distributionUrl 设置为国内 gradle-wrapper 地址
-
设置地址 distributionUrl=https\://mirrors.aliyun.com/gradle/distributions/v7.5.0/gradle-7.5-all.zip
-
地址上面的版本(v7.5.0/7.5)按照自身需要调整, 代码版本提交需要将 gradle-wrapper.properties 一起提交上去锁定
最后的 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
|
这样就能明显加速 gradle-wrapper 下载速度, 之后就是配置第三方镜像库和引入 AGP.
现在就是在之前的 pino-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
| plugins { id 'com.android.library' }
android { namespace 'io.meteorcat.pino' 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 编译打包文件会出现 Your build is currently configured to use incompatible Java 21.0.9 and Gradle 7.5
这是因为目前的没有指定 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, 我这边系统采用 Java17
-
依次点击 File > Settings... > Build,Execution,Deployment > Build Tools > Gradle
-
在 Gradle JVM 选择和打包一样的 JDK, 目前是 Java17
之后刷新最新配置然后等待下载编译完成, 通过之后就是准备把自己创建 pino-lib 引入到全局.
编译出包
现在就需要测试下打出需要 AAR 包, 这里修改 pino-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
| plugins { id 'com.android.library' }
android { namespace 'io.meteorcat.pino' 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 "androidx.core:core:1.17.0" implementation "androidx.appcompat:appcompat:1.7.1"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' }
|
之后在根目录创建以下文件:
-
settings.gradle: 全局项目配置
-
local.properties: 编写本地的设置, 比如本地 AndroidSDK 地址(不要提交版本库)
-
gradle.properties: 编译全局属性, 这里需要追加 AndroidX 的支持
settings.gradle 内容如下:
1 2 3 4 5
| // 自定义根目录的项目名称 rootProject.name = "PinoGame"
// 引入自定义的 Pino 库目录 include(":pino-lib")
|
local.properties 内容如下:
1 2
| // 这里是我本地的 AndroidSDK 路径 sdk.dir=/data/Android/Sdk
|
gradle.properties 内容如下:
1 2
| android.useAndroidX=true
|
重新刷新 Gradle 之后就可以看到自定义的库项目, 这样就可以准备具体的业务代码开发, 最后编译出包如下图所示:

有些其他配置和第三方的包可以额外引入, 比如 okhttp 这种网络库等, 不过基本框架就是这样处理.
测试引入
现在编写自定义的 Android Application 用于被游戏研发商引用实现:
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
| package io.meteorcat.pino;
import android.app.Activity; import android.util.Log;
public class PinoActivity extends Activity {
@Override protected void onStart() { super.onStart(); Log.i("PinoSDKActivity", "onStart"); }
@Override protected void onRestart() { super.onRestart(); Log.i("PinoSDKActivity", "onStart"); }
@Override protected void onResume() { super.onResume(); Log.i("PinoSDKActivity", "onResume"); }
@Override protected void onStop() { super.onStop(); Log.i("PinoSDKActivity", "onStop"); }
@Override protected void onDestroy() { super.onDestroy(); Log.i("PinoSDKActivity", "onDestroy"); } }
|
重写编译出包, 将输出的 pino-lib-debug.aar 包复制出来备用.
一般这个包在 pino-lib/build/outputs/aar 目录下
后面创建 app 模块来测试引用实现是否成功, 和创建 pino-lib 一样 New Moudle, 不过信息方面不一样:

这里选择创建 Empty Activity, 之后创建 pino-app/libs 目录将之前复制的 pino-lib-debug.aar 放置进去.
最后就是在 pino-app/build.gradle 编译文件引入下面的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| dependencies {
api fileTree(include: ['*.jar', '*.aar'], dir: "${projectDir}/libs")
implementation 'androidx.appcompat:appcompat:1.6.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.5.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' }
|
最后就是设置 app 的 Activity, 继承我们的 pino-lib 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package io.meteorcat.pino.app;
import android.os.Bundle; import io.meteorcat.pino.PinoActivity;
public class MainActivity extends PinoActivity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } }
|
最后设置我们的 MainActivity 来打包启动, 打开 pino-app/src/main/AndroidManifest.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
| <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.PinoGame"> <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>
</manifest>
|
执行 Android 编译打包之后启动, 查看 Logcat 检索 PinoSDKActivity 的 Tag 就能查看到回调到自定义 SDK 的内容:

网络库依赖
大部分库都是依赖于网络, 一般常用的都是简单 JSON 交换数据, 最多加上需要 MD5 哈希数据的需求, 所以只需要依赖以下组件:
-
OkHttp: 基础网络库
-
gson: JSON 处理库
-
commons-codec: MD5 处理库
所以在对应封装的 pino-lib 发行 aar 追加依赖:
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
| android {
buildTypes {
debug { debuggable true minifyEnabled false versionNameSuffix "-debug"
buildConfigField "boolean", "DEBUG", "true" }
release { debuggable false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' versionNameSuffix "-release"
buildConfigField "boolean", "DEBUG", "false" } } }
dependencies {
implementation 'com.squareup.okhttp3:okhttp:5.3.2' implementation 'com.squareup.okhttp3:logging-interceptor:5.3.2'
implementation 'commons-codec:commons-codec:1.20.0'
implementation 'com.google.code.gson:gson:2.13.2'
}
|
那么这里就可以创建 MD5Utils 工具类来专门做哈希 MD5 处理:
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
| package io.meteorcat.pino.utils;
import org.apache.commons.codec.digest.DigestUtils;
import java.nio.charset.StandardCharsets;
public class MD5Utils {
public static String encrypt(String content) { return DigestUtils.md5Hex(content.getBytes(StandardCharsets.UTF_8)); }
public static String encrypt(byte[] bytes) { return DigestUtils.md5Hex(bytes); }
public static String encryptWithSalt(String content, String salt) { return encrypt(content + salt); } }
|
同时封装简单的 Web 请求库 SimpleHttpUtils:
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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
| package io.meteorcat.pino.utils;
import android.util.Log; import com.google.gson.Gson; import io.meteorcat.pino.BuildConfig; import okhttp3.*; import okhttp3.logging.HttpLoggingInterceptor;
import java.util.*; import java.util.concurrent.TimeUnit;
public class SimpleHttpUtils {
public static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
private static final Gson GSON = new Gson();
private static final OkHttpClient okHttpClient;
private static final String TAG = "SimpleHttpUtils";
static { HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(message -> Log.d(TAG, "OkHttp: " + message)); loggingInterceptor.setLevel(BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.NONE);
okHttpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .addInterceptor(loggingInterceptor) .retryOnConnectionFailure(true) .build(); }
public static <T> T fetch(String url, Map<String, Object> params, String salt, Class<T> clazz) { if (url == null || url.isEmpty() || clazz == null) { Log.e(TAG, "fetch: 参数异常(url/clazz不能为空)"); return null; } try { String paramStr = buildParamStr(params); String sign = MD5Utils.encryptWithSalt(paramStr, salt); Map<String, Object> finalParams = params == null ? new HashMap<>() : new HashMap<>(params); finalParams.put("sign", sign);
String jsonParams = GSON.toJson(finalParams); RequestBody requestBody = RequestBody.create(jsonParams, JSON_MEDIA_TYPE); Request request = new Request.Builder() .url(url) .post(requestBody) .build();
Response response = okHttpClient.newCall(request).execute(); if (response.isSuccessful()) { ResponseBody body = response.body(); if (body.contentLength() > 0) { String responseBody = body.string(); Log.d(TAG, "fetch success: " + responseBody); return GSON.fromJson(responseBody, clazz); } else { Log.e(TAG, "fetch: 响应体为空"); } } else { Log.e(TAG, "fetch: 接口返回非200状态码 " + response.code()); } } catch (Exception e) { Log.e(TAG, "fetch error", e); } return null; }
public static <T> T query(String url, Map<String, Object> params, String salt, Class<T> clazz) { if (url == null || url.isEmpty() || clazz == null) { Log.e(TAG, "query: 参数异常(url/clazz不能为空)"); return null; } try { String paramStr = buildParamStr(params); String sign = MD5Utils.encryptWithSalt(paramStr, salt); String urlWithParams = buildUrlWithParams(url, params, sign);
Request request = new Request.Builder() .url(urlWithParams) .get() .build();
Response response = okHttpClient.newCall(request).execute(); if (response.isSuccessful()) { ResponseBody body = response.body(); if (body.contentLength() > 0) { String responseBody = body.string(); Log.d(TAG, "query success: " + responseBody); return GSON.fromJson(responseBody, clazz); } else { Log.e(TAG, "query: 响应体为空"); } } else { Log.e(TAG, "query: 接口返回非200状态码 " + response.code()); } } catch (Exception e) { Log.e(TAG, "query error", e); } return null; }
private static String buildParamStr(Map<String, Object> params) { if (params == null || params.isEmpty()) { return ""; } List<Map.Entry<String, Object>> sortedEntries = new ArrayList<>(params.entrySet()); sortedEntries.sort(Map.Entry.comparingByKey());
StringBuilder sb = new StringBuilder(); for (int i = 0; i < sortedEntries.size(); i++) { Map.Entry<String, Object> entry = sortedEntries.get(i); if (entry.getValue() != null) { sb.append(entry.getKey()).append("=").append(entry.getValue()); if (i != sortedEntries.size() - 1) { sb.append("&"); } } } return sb.toString(); }
private static String buildUrlWithParams(String url, Map<String, Object> params, String sign) { return (url.contains("?") ? url : url + "?") + buildParamStr(params) + "&sign=" + sign; } }
|
编译生成之后将 aar 包重新放入 app 项目, 注意如果需要用到网络权限需要在 AndroidManifest.xml 追加配置:
1 2 3 4 5 6
| <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" > <uses-permission android:name="android.permission.INTERNET"/> </manifest>
|
之后就能够测试调用网络封装的网络请求方法了, 至此就可以准备编写内部所需的 SDK 功能业务.