5、Android JNI 动态注册中的“签名“详解

Android JNI 动态注册中的"签名"详解

一、引言

在动态注册中,JNINativeMethod 结构体有三个字段:

cpp 复制代码
typedef struct {
    const char* name;        // Java 方法名
    const char* signature;   // 方法签名  ← 本文主角
    void*       fnPtr;       // C/C++ 函数指针
} JNINativeMethod;

其中 signature(签名)是最容易写错、也最容易导致注册失败的字段。本文系统讲清楚:签名是什么、规则如何、怎么获取、为什么必须精确、以及与"APK 签名"的区别。


二、签名是什么

签名 = JVM 的方法类型描述符(method descriptor) ,是一段描述方法参数类型和返回值类型的字符串。

格式:

复制代码
(参数类型1参数类型2... )返回值类型
  • 括号内:按顺序拼接每个参数的类型描述符;
  • 括号后:返回值类型描述符;
  • 无参数:括号内留空。

举例:Java 方法 int add(int a, int b) 的签名是 (II)I。

签名的作用是和 name 一起唯一确定一个 Java 方法。因为 Java 支持重载------同名方法可能多个------只有"名字 + 签名"才能精确定位。


三、签名规则

3.1 基本类型映射

Java 类型 描述符 记忆点
boolean Z 不是 B
byte B
char C
short S
int I
long J 不是 L,大写 J
float F
double D
void V 仅返回值可用

高频错误 :long 写成 L、boolean 写成 B(B 是 byte)。这两个错得最多。

3.2 引用类型

类 :L + 全限定名(. 换成 /)+ ;

Java 类型 描述符
String Ljava/lang/String;
Object Ljava/lang/Object;
com.example.Foo Lcom/example/Foo;
com.kl.minttisdk.ble.callback.ICppCallback Lcom/kl/minttisdk/ble/callback/ICppCallback;

高频错误 :漏结尾的 ;,或没写 L 前缀。

数组 :[ + 元素类型描述符

Java 类型 描述符
int[] [I
short[] [S
double[] [D
String[] [Ljava/lang/String;
byte[][] [[B
int[][] [[I

多维数组就是多个 [ 前缀。

3.3 组合示例

Java 方法 签名 拆解
int add(int a, int b) (II)I 两个 int 参数,返回 int
long mul(long a, long b) (JJ)J 两个 long,返回 long
void foo() ()V 无参无返回
String hello(String s) (Ljava/lang/String;)Ljava/lang/String; String → String
int getHeartRate(short[] data) ([S)I short\[\] → int
int ACHD_process(short[], int[], double[], String) ([S[I[DLjava/lang/String;)I 四参数 → int
long init_aec_aes() ()J 无参 → long(句柄)
void aec_aes_process(long, short[], short[], short[], short[]) (J[S[S[S[S)V 句柄 + 四个数组 → void
void release_aec_aes(long) (J)V 句柄 → void
int PCG_Segment(short[], String, int[]) ([SLjava/lang/String;[I)I 三参数 → int
void useFilter(short[], String, String, String, ICppCallback) ([SLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/kl/minttisdk/ble/callback/ICppCallback;)V 五参数 → void
int noise_Detection(short[], String, String) ([SLjava/lang/String;Ljava/lang/String;)I 三参数 → int

最后几行正是你 SmarthoAlgo 注册表里的真实签名,可对照理解。


四、为什么必须精确

RegisterNatives 依靠 name + signature 共同匹配 Java 方法。签名对不上,注册直接失败:

cpp 复制代码
if (env->RegisterNatives(clazz, kMethods, count) != JNI_OK) {
    // 返回 JNI_ERR
}

随后 Java 调用该 native 方法时抛:

复制代码
java.lang.UnsatisfiedLinkError: No implementation found for int
com.example.NativeBridge.add(int, int)

4.1 重载场景下签名是唯一区分手段

java 复制代码
public static native int calc(int a);        // (I)I
public static native int calc(int a, int b); // (II)I

注册表必须两条,靠签名区分:

cpp 复制代码
{"calc", "(I)I",  reinterpret_cast<void*>(calc1)},
{"calc", "(II)I", reinterpret_cast<void*>(calc2)},
  • 同名不同签名 → 正常共存;
  • 同名同签名重复注册 → 后者覆盖前者(且很难察觉,是隐蔽 bug)。

4.2 签名与 static 无关,但与"第二个参数"有关

签名只描述参数与返回值类型,不体现 static。但 C++ 实现的第二个参数类型不同:

cpp 复制代码
// Java: public static native int add(int, int);
static jint nativeAdd(JNIEnv* env, jclass clazz, jint a, jint b);
//                             ^^^^^^ static → jclass

// Java: public native int add(int, int);
static jint nativeAdd(JNIEnv* env, jobject thiz, jint a, jint b);
//                             ^^^^^^^ 非 static → jobject

jclass 和 jobject 底层都是指针,类型写错不会立即报错 ,但语义错,后续用 clazz 调实例方法会出问题。签名本身不区分 static,签名之外还要注意这个参数类型。


五、如何获取签名(不要手写)

5.1 javap(最可靠,首选)

对编译出的 .class 执行:

bash 复制代码
javap -s -p app/build/intermediates/javac/debug/classes/com/example/jnidemo/NativeBridge.class

输出:

复制代码
public static native int add(int, int);
  descriptor: (II)I

public static native long mul(long, long);
  descriptor: (JJ)J

public static native java.lang.String hello(java.lang.String);
  descriptor: (Ljava/lang/String;)Ljava/lang/String;

descriptor: 后面就是签名,直接复制进注册表。

  • -s:打印签名描述符;
  • -p:包含 private 成员;
  • 也可对 jar 用:javap -s -p -classpath xxx.jar com.example.NativeBridge;
  • 对 APK:dex2jar 转 jar 后同上(注意混淆后的名字)。

5.2 Android Studio 插件

  • JNI Helper 等插件:在 native 方法上右键生成签名;
  • 各类 JNI 生成器插件可直接产出 JNINativeMethod 骨架。

5.3 从注册表反推(校验用)

已有注册表里写的签名,可以对照 Java 声明反查是否一致:

bash 复制代码
javap -s -p ... | grep -A1 "native"

5.4 混淆注意事项

若 Java 层开了 R8/ProGuard:

  • native 方法所在类和 native 方法本身必须 keep ,否则类名/方法名被混淆,FindClass 和 RegisterNatives 都失败:
proguard 复制代码
-keep class com.kl.minttisdk.algo.SmarthoAlgo { native <methods>; }
-keepclasseswithmembernames class * { native <methods>; }
  • 若坚持混淆类名/方法名,则注册表里的 className、name 必须改成混淆后的值------这通常不划算,建议 keep。

六、常见错误对照

错误写法 正确写法 错因
(II)int (II)I 返回值必须用描述符,不能写 Java 类型名
(Ljava/lang/String)Ljava/lang/String (Ljava/lang/String;)Ljava/lang/String; 引用类型结尾漏 ;
(JJ)Long (JJ)J long 用 J
(BB)B(想表达 boolean) (ZZ)Z boolean 是 Z,B 是 byte
(Lcom/example/Foo)V (Lcom/example/Foo;)V 漏 ;
(com/example/Foo)V (Lcom/example/Foo;)V 漏 L 前缀
(I[I)Z ([I)Z 多写参数 / 数组要整体作为 [I
()String ()Ljava/lang/String; 引用类型必须 L...;
([s)I ([S)I 描述符大小写敏感
([S[I[D Ljava/lang/String;)I ([S[I[DLjava/lang/String;)I 不能有空格

签名对空格、大小写、分号都敏感 。一个字符错,RegisterNatives 就失败。


七、与"APK 签名"的区别(避免概念混淆)

"签名"在 Android 里有多重含义,JNI 动态注册的签名与这些无关:

术语 含义 使用场景
JNI 方法签名(本文) 类型描述符字符串,如 (II)I JNINativeMethod.signature
APK 签名 证书对 APK 的加密签名 发布、安装校验
代码签名 代码完整性校验 安全
函数签名(泛称) 函数名 + 参数类型 一般编程语境

在 JNI 语境下说"签名",特指方法描述符字符串,与证书、哈希无关。


八、实战:结合你的 SmarthoAlgo

你同事的注册表里,每条都是 {name, signature, fnPtr}。以其中一条为例:

cpp 复制代码
{const_cast<char *>("useFilter"),
 const_cast<char *>("([SLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/kl/minttisdk/ble/callback/ICppCallback;)V"),
 reinterpret_cast<void *>(Java_com_kl_minttisdk_algo_SmarthoAlgo_useFilter)},

对应 Java 声明应为:

java 复制代码
public static native void useFilter(
    short[] data,
    String a,
    String b,
    String c,
    com.kl.minttisdk.ble.callback.ICppCallback callback);

签名拆解:

复制代码
(                              参数开始
 [S                            short[]
 Ljava/lang/String;            String
 Ljava/lang/String;            String
 Ljava/lang/String;            String
 Lcom/kl/minttisdk/ble/callback/ICppCallback;   ICppCallback
)                              参数结束
V                              返回 void

你可以用 javap -s -p 对 SmarthoAlgo.class 逐条核对他的注册表 ,这是你作为接入方验证他注册正确性的有效手段:

bash 复制代码
javap -s -p app/build/.../SmarthoAlgo.class
# 把输出里的 descriptor 与 kSmarthoAlgoMethods[] 里的 signature 逐条比对

若发现不一致,说明注册表写错(大概率手写导致),需要反馈同事修正------这是你接入前就能发现的问题 ,避免上线后 UnsatisfiedLinkError。

关于 const_cast

cpp 复制代码
const_cast<char *>("([S)I")

NDK 的 JNINativeMethod 字段已经是 const char*,const_cast 是冗余的 。但既然源码是同事的、你"尽量不改",建议保持原样,或作为独立小 PR 和他商量统一去掉,不要混在别的改动里。


九、小结

要点 内容
定义 描述方法参数与返回值类型的 JVM 描述符字符串,格式 (参数)返回值
作用 与 name 一起唯一确定 Java 方法,重载靠它区分
规则 基本类型单字母(I J Z B C S F D V),引用类型 L...;,数组 [ 前缀
获取 必用 javap -s -p,不要手写
失败表现 RegisterNatives 返回 JNI_ERR;调用时抛 UnsatisfiedLinkError
高频错误 long→L、boolean→B、漏 ;、漏 L、大小写、空格
与 static 签名不体现 static,但 C++ 第二个参数 jclass vs jobject 要对应
与 APK 签名 完全无关,是纯类型描述信息
接入方用法 用 javap 逐条核对他注册表的签名,提前发现错误

一句话 :动态注册的"签名"就是方法类型描述符 ,格式 (参数类型)返回值类型,用 javap -s -p 生成、逐字复制、绝不手写;它是 RegisterNatives 能否成功的决定性因素,也是你作为接入方校验同事注册表正确性的最直接依据。

相关推荐
zhaoshuzhaoshu3 小时前
Android 系统启动与调试中的日志抓取方式全解析
android
雨声不在4 小时前
android 动画掉帧
android
在多学一点5 小时前
MYSQL第二次作业
android·数据库·mysql
骑着蜗牛撵大象3276 小时前
MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异
android·数据库·sql·mysql·优化器·执行计划
我叫张土豆6 小时前
Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密
android
booboosui10 小时前
Android 悬浮窗藏不住自己:一个标志位与 140 毫秒
android
传奇开心果10 小时前
【Flutter踩坑排雷】第5课:扩展方法冲突坑|同名扩展引发编译歧义,故障规避手段
android·flutter
.痞老板.12 小时前
mysql初阶--数据类型
android·数据库·mysql
StarsGreen12 小时前
Android 启动流程详解
android·linux·单片机·嵌入式硬件