C++→SO 编译与调用(JNI)
更新: 2025/7/20 字数: 0 字 时长: 0 分钟
本节将介绍如何使用 Android Studio 创建一个包含 C++ 支持的项目,完成 .cpp
文件的编写与 .so
动态库的生成。
同时,我们还将演示如何通过 Bot.js 脚本调用该 .so
文件中的 JNI 接口方法,实现与原生 C++ 的通信与协作。
内容包括:
- 使用 Android Studio 创建带有 JNI 支持的 Android 项目
- 在
cpp/
目录下添加 C++ 源文件并配置CMakeLists.txt
- 构建
.so
动态链接库 - 使用 Java 代码加载
.so
并暴露接口 - 通过 Bot.js 使用 Java 接口间接调用
.so
方法
💡 最终目标是让 Bot.js 脚本具备调用 C/C++ 原生方法的能力,从而实现性能要求较高的任务(如图像处理、加密运算等)。
加密与安全场景中的应用价值
在实际开发中,将核心逻辑(如算法验证、签名生成、加密运算等)封装进 .so
动态库,具有以下优势:
- 代码保护:C/C++ 编译生成的
.so
更难被逆向分析,相较于 Java 层更具安全性; - 性能优化:复杂计算如哈希、AES 加解密等,在原生层运行效率更高;
- 逻辑隔离:安全逻辑与 UI 层完全解耦,便于模块化维护;
- 绕过脚本限制:通过 Bot.js 脚本动态调用
.so
实现对Bot.js的灵活扩展。
你可以将这些能力与 Bot.js 的自动化控制结合,实现在受限环境下执行关键加密流程的脚本化控制。
Android Studio 开发 SO
创建项目
- 打开 Android Studio。
- 点击
New Project
。 - 选择
Native C++
模板,点击Next
。 - 配置:
- 项目名称:
CloudJsToSo
- 包名:
com.cloud.so
- 项目名称:
- 语言选择
Java
,勾选Include C++ Support
,完成创建。
添加 Java 文件
- 打开
app/src/main/java/com/cloud/so/
目录。 - 在与
MainActivity.java
同一级目录下,右键点击 →New
→Java Class
。 - 命名为
Test.java
并创建。 - 在
Test.java
中编写你需要调用 C++ 的逻辑。
package com.cloud.so;
public class Test {
static{
System.loadLibrary("jni");
}
private String name;
private int age;
// 构造函数
public Test() {
this.name = "Bruce"; // 默认名字为 "1"
this.age = 18; // 默认年龄为 2
}
// 构造函数:接受字符串参数
public Test(String name) {
this.name = name;
this.age = 18;
}
// 获取姓名
public String getName() {
return name;
}
// 设置姓名
public void setName(String name) {
this.name = name;
}
// 获取年龄
public int getAge() {
return age;
}
// 设置年龄
public void setAge(int age) {
this.age = age;
}
// 通过C++ 修改字段
public native void modifyFields(String name, int age);
// 通过C++ 获取name
public native String getNameFromCpp();
}
生成 APK 与 JNI 接口文件
在 Android Studio 中点击:
Build -> Make Project
完成后会生成以下关键文件
app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/cloud/so/Test.class
app/build/outputs/apk/debug/app-debug.apk
创建 jni 目录并生成 .h 头文件
在 app/src/main/ 目录下新建 jni/ 文件夹。
打开 Android Studio 的终端,进入 app/src/main/java/ 目录:
cd app/src/main/java/
javah -d ../jni -encoding UTF-8 com.cloud.so.Test
或者也可以在 jni 目录下执行如下命令 (需要修改路径):
cd app/src/main/jni/
javah -jni -classpath G:\website\Android\JIN\CloudJsToSo\app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes com.cloud.so.Test
💡 此命令将在 jni 目录下生成 com_cloud_so_Test.h 文件。
实现 C++ 源文件(test.cpp)
在 jni 目录中新建一个 C/C++ Source File,命名为 test.cpp,实现 JNI 方法:
#include <jni.h>
#include "com_cloud_so_Test.h"
extern "C" {
// 修改字段的本地方法
JNIEXPORT void JNICALL Java_com_cloud_so_Test_modifyFields(JNIEnv *env, jobject obj, jstring newName, jint newAge) {
// 获取 Test 类的 jclass
jclass cls = env->GetObjectClass(obj);
// 获取 name 字段的 fieldID
jfieldID nameFieldID = env->GetFieldID(cls, "name", "Ljava/lang/String;");
// 获取 age 字段的 fieldID
jfieldID ageFieldID = env->GetFieldID(cls, "age", "I");
// 将传入的参数转换为 C 字符串和整数
const char *newNameStr = env->GetStringUTFChars(newName, JNI_FALSE);
// 修改字段值
env->SetObjectField(obj, nameFieldID, env->NewStringUTF(newNameStr));
env->SetIntField(obj, ageFieldID, newAge);
// 释放字符串资源
env->ReleaseStringUTFChars(newName, newNameStr);
}
// 获取 name 字段的本地方法
JNIEXPORT jstring JNICALL Java_com_cloud_so_Test_getNameFromCpp(JNIEnv *env, jobject obj) {
// 获取 Test 类的 jclass
jclass cls = env->GetObjectClass(obj);
// 获取 name 字段的 fieldID
jfieldID nameFieldID = env->GetFieldID(cls, "name", "Ljava/lang/String;");
// 获取字段值
jstring name = (jstring) env->GetObjectField(obj, nameFieldID);
return name;
}
}
编写 Android.mk 文件
在 app/src/main/jni/
目录下新建 Android.mk
文件,内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jni
LOCAL_SRC_FILES := test.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
编写 Application.mk 文件
同样在 jni/ 目录下新建 Application.mk 文件:
APP_PLATFORM := android-15
APP_ALLOW_MISSING_DEPS=true
配置 Android NDK
确保你已经配置了 Android NDK 路径:
路径配置方式
Android Studio:File → Project Structure → SDK Location → Android NDK location
你的 NDK 路径可能类似于:
E:\application\Android\Androidsdk\ndk\21.0.6113669
编译生成 SO 文件
然后打开终端,进入到 jni 目录,执行编译命令:
ndk-build
运行成功后,会在 app/src/main/libs/ 目录下生成各个架构对应的 libjni.so 动态链接库。
从 APK 中提取 DEX 文件
解压 app-debug.apk
,提取 classes3.dex
(具体要看 Test
类在哪个 classes.dex
里,推荐使用 MT管理器 进行反编译查看;如果有多个 dex 包含需要的类,可以用 MT管理器合并为一个 dex)
将 libs
目录和 classes3.dex
复制到手机上的 /sdcard/cloud/so/
路径下。
Bot.js 脚本调用 DEX SO 文件
在手机的
/sdcard/cloud/so/
目录下,新建用于调用 SO 和 DEX 的脚本文件main.js
。
/*注意:由于so库释放需要时间,脚本结束后需要再间隔 20秒后再次运行 */
// 获取设备的 CPU 架构
var cpuAbi = android.os.Build.CPU_ABI;
// 打印设备的 CPU 架构
console.log("当前设备得CPU 架构: " + cpuAbi);
var soFileName = "libjni.so";
var dexPath = "/sdcard/cloud/so/classes3.dex";
var soPath = "/sdcard/cloud/so/" + cpuAbi + "/" + soFileName;
console.log("dexPath = " + dexPath);
console.log("soPath = " + soPath);
var jniPath = context.getDir("libs", android.app.Activity.MODE_PRIVATE);
console.log("jniPath = " + jniPath);
var dirPath = context.getDir("dex", android.app.Activity.MODE_PRIVATE).getAbsolutePath();
console.log("dirPath = " + dirPath);
var soFile = new java.io.File(jniPath, "libjni.so");
// 复制 so 文件到私有目录
copyFile(soPath, soFile.getAbsolutePath());
// 创建 DexClassLoader 实例
var dexClassLoader = new Packages.dalvik.system.DexClassLoader(
dexPath,
dirPath,
jniPath,
java.lang.ClassLoader.getSystemClassLoader()
);
try {
// 加载类
var textClass = dexClassLoader.loadClass("com.cloud.so.Test");
/* **************创建实例方式1 带参数************** */
// 获取构造函数
var constructor = textClass.getConstructor(java.lang.String);
// 使用构造函数创建实例
var instance = constructor.newInstance("张三");
/* **************创建实例方式2 不带参数************** */
// var instance = textClass.newInstance();
console.log("修改前age: " + instance.age);
console.log("修改前name: " + instance.name);
instance.setAge(25);
instance.setName("王麻子");
console.log("修改后age: " + instance.getAge());
console.log("修改后name: " + instance.getName());
// 通过c++方式修改
instance.modifyFields("李四", 30);
// 输出修改后的实例属性
console.log("通过c++修改后age: " + instance.getAge());
console.log("通过c++修改后name: " + instance.getName());
console.log("通过c++获取修改后name" + instance.getNameFromCpp());
} catch (e) {
console.error("Error: " + e);
} finally {
console.log("删除已加载的类以释放库");
// 删除已加载的类以释放库
delete textClass;
// 释放 DexClassLoader 实例
dexClassLoader = null;
java.lang.System.gc(); // 手动触发垃圾回收
// 删除共享库文件;
var file = new java.io.File(jniPath, soFileName);
if (file.exists()) {
console.log(file.path);
file.delete();
}
}
/* 拷贝 */
function copyFile(sourcePath, destPath) {
var fis = new java.io.FileInputStream(sourcePath);
var fos = new java.io.FileOutputStream(destPath);
var buffer = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 1024);
var bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fis.close();
fos.close();
}
打包注意事项
在打包 APK 应用时,项目根目录下或其任意子目录中的所以文件(包含.dex
、.jar
、.so
……等 )会被自动打包进最终的 APK 中。因此:
- 建议将 DEX 和 SO 文件放置在项目内的特定目录中(例如
./so/
),以便管理。 - 在脚本中使用相对路径加载 DEX 和 SO 文件,确保在 APK 中也能正确访问。
示例代码如下:
// 获取设备的 CPU 架构
var cpuAbi = android.os.Build.CPU_ABI;
// 打印设备的 CPU 架构
console.log("当前设备得CPU 架构: " + cpuAbi);
var soFileName = "libjni.so";
// 指定项目内相对路径,指向 dex 文件所在位置
var dexPath = "./so/classes3.dex";
// 指定项目内相对路径,指向 so 文件所在位置
var soPath = "./so/" + cpuAbi + "/" + soFileName;
console.log("dexPath = " + dexPath);
console.log("soPath = " + soPath);
// 检查 dex 和 so 文件是否存在(支持 APK 内路径)
if (files.exists(dexPath)&&files.exists(soPath)) {
/* 继续剩下的逻辑 */
}