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 能否成功的决定性因素,也是你作为接入方校验同事注册表正确性的最直接依据。