跳转到内容

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

创建项目

  1. 打开 Android Studio
  2. 点击 New Project
  3. 选择 Native C++ 模板,点击 Next
  4. 配置:
    • 项目名称:CloudJsToSo
    • 包名:com.cloud.so
  5. 语言选择 Java,勾选 Include C++ Support,完成创建。

添加 Java 文件

  1. 打开 app/src/main/java/com/cloud/so/ 目录。
  2. 在与 MainActivity.java 同一级目录下,右键点击 → NewJava Class
  3. 命名为 Test.java 并创建。
  4. Test.java 中编写你需要调用 C++ 的逻辑。
java
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 中点击:

bash
Build -> Make Project

完成后会生成以下关键文件

bash
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/ 目录:

bash
cd app/src/main/java/
javah -d ../jni -encoding UTF-8 com.cloud.so.Test

或者也可以在 jni 目录下执行如下命令 (需要修改路径):

bash
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 方法:

c++
#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 文件,内容如下:

makefile
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 文件:

makefile
APP_PLATFORM := android-15
APP_ALLOW_MISSING_DEPS=true

配置 Android NDK

确保你已经配置了 Android NDK 路径

路径配置方式
Android Studio:
File → Project Structure → SDK Location → Android NDK location

你的 NDK 路径可能类似于:

plaintext
E:\application\Android\Androidsdk\ndk\21.0.6113669

编译生成 SO 文件

然后打开终端,进入到 jni 目录,执行编译命令:

bash
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

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 中也能正确访问。

示例代码如下:

js


// 获取设备的 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)) {
    /* 继续剩下的逻辑 */

}