jni基础

2023-11-18

JNI相关
静态注册
// java代码需要和C++代码相符通讯就需要通过JNI来进行注册
//java: public native String stringFromJNI(); //代表该函数的实现在so
//so: Java_com_first_firstndkdemo_MainActivity_stringFromJNI(...)
//静态注册时,so中的方法名编写规则:Java_+java中该函数所在的 包名_+类名_+方法名_
JNI_onLoad
//1.so中各种函数的执行时机:
//	init、init_array、JNI_OnLoad
	
//2.JNI_Onload的定义:
JNIEXPORT jint JNI_OnLoad(JavaVM *vm,void *reserved){  //JavaVM是在jni.h中定义好的一个结构体
    JNIEnv *env = nullptr;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK){  //GetEnv:获取jni env(得到jni的环境)
        LOGD("GetEnv failed");
        return -1;
    }
    return JNI_VERSION_1_6;
}

//3.注意事项
	//一个so中可以不定义JNI_OnLoad
	//一旦定义了JNI_OnLoad,在so首次被加载的时候就会被执行
	//必须返回JNI版本 JNI_VERSION_1_6

Java VM
//在jni.h中用C++定义的一个结构体,里面常用的几个方法,GetEnv(在主线程中获取jni的环境等)。。。。

//JavaVM的获取方式:
	//JNI_OnLoad的第一个参数
	//JNI_OnUnLoad的第一个参数
	//env->GetJavaVM
	//对比各种方式获取的JavaVM指针是否一致
//作用:获取JNI_Env,然后实现so与Java进行通信
JNIEnv
//1.它本身是一个在jni.h中定义的一个很大的结构体
//2.通过它里面的函数让C++代码与Java代码进行交互
so函数注册
'''
一般不是静态注册的函数不会出现在导出表里面
1.jin函数的静态注册
	必须遵循一定的命名规则,一般是Java_包名_类名_方法名
	系统会通过dlopen加载对应的so,通过dlsym来获取指定的函数地址,然后调用静态注册jni函数,必然在导出表里
	
2.jni函数的动态注册
	通过env->RegisterNatives注册函数,通常在JNI_OnLoad中注册
	JNINativeMethod
	函数签名
	可以给同一个Java函数注册多个native函数,以最后一次为准
	
	
	//翻译java层的代码,将这个函数作为注册的第三个函数
jstring encodeFromC(JNIEnv* env,jobject obj,jint a,jbyteArray b,jstring c){
    return env->NewStringUTF("encodeFromC:Hello from C++");
    //NewStringUTF:将C语言的char*转为jstring
}
	
	//注册操作是在 JNI_OnLoad中操作的,所以在加载so的时候就会进行注册操作
JNIEXPORT jint JNI_OnLoad(JavaVM *vm,void *reserved){
    JNIEnv *env = nullptr;
    if(vm->GetEnv((void **) &env,JNI_VERSION_1_6)!=JNI_OK){
        LOGD("GetEnv failed");
        return -1;
    }


    //=======================动态注册=============================
    // 动态注册是在加载so的时候就注册的,静态注册是在调用他的时候在注册的
    //首先去寻找类,因为是根据类来注册的(就是需要注册的函数所在的类)
    jclass MainActivityClass = env->FindClass("com/first/firstndk/MainActivity");
    //因为通常是注册多个函数,所以这个地方定义成一个数组
    //JNINativeMethod的原型就是一个结构体
//    typedef struct {
//        const char* name;
//        const char* signature;
//        void*       fnPtr;
//    } JNINativeMethod;
    JNINativeMethod methods[] = {
            //public native String encode(int i,String str,byte[] byt);
            //{"需要注册的函数名字","函数的签名(传入的参数和返回的类型)",(void *)encodeFromC}
            {"stringFromJNI2", "(I[BLjava/lang/String;)Ljava/lang/String;",(void *)encodeFromC}
            //(I[B)Ljava/lang/String;): I:int [B:字节数组,L;:对象,Ljava/lang/String;:代表java/lang/String对象
            //后面的哪个 Ljava/lang/String; 代表返回值
            //(void *)encodeFromC:就是将java成的那个函数翻译为C代码
    };
    //jint RegisterNatives(jclass clazz(类), const JNINativeMethod* methods(地址),jint nMethods(注册数量))
    env->RegisterNatives(MainActivityClass,methods,sizeof(methods)/sizeof(JNINativeMethod));


    JavaVM *vm2;
    env->GetJavaVM(&vm2);
    //因为在同一个线程中,所以这两个env打印出来的值肯定是一样的
    LOGD("JNI_OnLoad JavaVM1: %p",vm);
    LOGD("JNI_OnLoad JavaVM2: %p",vm2);
    LOGD("JNI_OnLoad GetEnv: %p",env);
    return JNI_VERSION_1_6;
}

//当函数为动态注册时,这是不会出现在导出表里面,因为我们在动态注册的时候是在JNI_OnLoad中注册的,这时候就在导出表中直接搜jni_onload,然后进入

//arm中:DCQ表示8个字节,就是当传入的参数是指针的时候用这个模式(字母d切换模式)
//可以给同一个java函数注册多个native函数,以最后一次为准
so中常见的Log输出
//原生
#include <android/log.h> //安卓中的log包
    //这里的数字3 是代编选择什么样的形式输出(info,debugger,error.....),在源码里面这个第一个参数就是一个枚举类型,
    //枚举中第一个参数是0,依次往后推,故这里的3就是选的第四个参数
    //参数:sheation是标签名
    //参数:"cstr: %s",hello.c_str()是要输出的内容
__android_log_print(3,"sheation","cstr: %s",hello.c_str());

//封装后
#define TAG "sheation"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
LOGD("cstr: %s",hello.c_str());
多个cpp编译成一个so
//1.步骤
//	编写多个cpp文件
	//当创建一个cpp文件的时候,这时候想要在其他文件是使用他的话,需要在当前cpp文件中加上一个 extern 函数名();
//	修改CMakeLists.txt 文件(需要该两个)
		add_library( # Sets the name of the library.
             native-lib  # 这个名字就是编译后的so文件的名字

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             file_1.cpp file_2.cpp ...)
            
        target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
    //如果要编译成多个so,那就把这个在复制一个 改一下so的名字 和下面的需要编译的cpp文件的名字
      add_library( # Sets the name of the library.
             native-lib_2  # 这个名字就是编译后的so文件的名字

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             file_3.cpp file_4.cpp ...)
            
       target_link_libraries( # Specifies the target library.
                       native-lib_2

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
//	Java静态代码块加载多个so
    //如果需要使用该so就必须在java层加载该函数
     static {
        System.loadLibrary("native-lib");
        System.loadLibrary("native-lib1");
    }
so路径的动态获取
//由于32位和64位存放的路径是不一样的,为了更加通用,可以用代码动态获取so的路径
//当手机安装好app后,存放路径在data/app目录下的
public class Utils {
    public String getPath(Context cxt){
        // 通过上下文context获取包管理器
        PackageManager pm = cxt.getPackageManager();
        //通过包管理器得到已经安装的所有app
        List<PackageInfo> pkgList = pm.getInstalledPackages(0);
        
        if(pkgList == null || pkgList.size() == 0) return null;
        for (PackageInfo pi : pkgList){
            if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app")
            && pi.packageName.startsWith("com.first.firstndk")){
                return pi.applicationInfo.nativeLibraryDir;
            }
        }
        return null;
    }
}
so之间的相互调用
//1.使用dlopen,dlsym,dlclose获取函数地址,然后调用,需要导入dlfcn.h(安卓7.0,7.1用不了 dlopen)
jstring encodeFromC(JNIEnv* env,jobject obj,jint a,jbyteArray b,jstring c){
    //将jstring转换为C中的char*
    const char *soPath = env->GetStringUTFChars(c, nullptr);
    //获取函数地址
    void *soinfo = dlopen(soPath,RTLD_NOW);  //RTLD_NOW:加载完后立马初始化
    //定义一个函数指针
    void (*def)() = nullptr;
    //dlsym(soinfo,"_Z7fromSoBPc"):soinfo:so文件的指针,"_Z7fromSoBPc"对应的cpp函数名(这里的函数名是符号修饰过后的名字,也即是该函数在汇编里面的样子)
    //返回该函数的地址,这里需要将其强转成函数指针后才能用
    //函数指针定义:void (*test)();  void (*)() 代表函数指针的类型,test代表函数指针的变量的名字
    def = reinterpret_cast<void (*)()>(dlsym(soinfo,"_Z4testv"));
    //通过函数指针调用函数
    def();
    return env->NewStringUTF("encodeFromC:Hello from C++");
    //NewStringUTF:将C语言的char*转为jstring
}

通过jni在so中创建Java对象
// 1.通过NewObject创建对象
	jclass clazz = env->FindClass("com/sheation/ndk/NDKDemo"); //寻找类
	jmethodID methodID = env->GetMethodID(clazz "<init>","()V"); //寻找方法
	jobject ReflecDemoObj = env->NewObject(clazz,methodID);//实例化对象
	LOGD("ReflectDemoObj %p",ReflectDemoObj);

//2.通过ALLocObject创建对象
	jclass clazz = env-FindClass("com/sheation/ndk/NDKDemo");  //寻找类
	jmethodID methodID2 = env-?GetMethodID(clazz,"<init>","(Ljava/lang/String;I)V");//寻找方法
	jobject ReflectDemoObj2 = env->AllocObject(clazz); //分配内存
	jstring jstr = env->NewStringUTF("from jni str");
	env->CallNovirtualVoidMethod(ReflectDemoObj2,clazz,methodID2,jstr,100);//初始化对象
通过jni访问Java属性
//1.获取静态字段
	//GetStaticFieldID这个方法是根据查找方法的类型而定的
	jfieldID privateStaticStringFieldID = env->GetStaticFieldID(NDKDemoClazz,"privateStaticStringField","Ljava/lang/String;");
    //获取属性结果,需要转换一下,因为GetStaticObjectField返回的jobject,而我们需要的是jstring
    jstring jstr = static_cast<jstring>(env->GetStaticObjectField(NDKDemoClazz,
                                                                  privateStaticStringFieldID));
    //如果需要在当前文件中打印的话还需要将jstring转为cstring
    const char* cstr = env->GetStringUTFChars(jstr, nullptr);
    LOGD("cstr %s",cstr);

//获取对象字段
	jfieldID privateStringFieldID = env->GetFieldID(NDKDemoClazz,"privateStringField","Ljava/lang/String;");
    //这里传的是对象的名字,不是类
    jstring jstr1 = static_cast<jstring>(env->GetObjectField(ndkobj,privateStringFieldID));
    const char* cstr1 = env->GetStringUTFChars(jstr1, nullptr);
    LOGD("cstr1 %s",cstr1);
env->ReleaseStringChars(jstr1, reinterpret_cast<const jchar *>(cstr1));//操作完释放一下

//3。设置值
//先获取fieldID
    jfieldID privateStringFieldID1 = env->GetFieldID(NDKDemoClazz,"privateStringField","Ljava/lang/String;");
    //然后设置值
    env->SetObjectField(ndkobj,privateStringFieldID1,env->NewStringUTF("sheation"));
    //接着获取值
    jstring setJstr = static_cast<jstring>(env->GetObjectField(ndkobj, privateStringFieldID1));
    //转换类型
    const char* setCstr = env->GetStringUTFChars(setJstr, nullptr);
    LOGD("setCstr %s",setCstr);
通过jni访问java数组
//访问数组
    //获取数组字段ID
    jfieldID byteArrayID = env->GetFieldID(NDKDemoClazz,"byteArray","[B");
    //获取数组
    jbyteArray byteArray = static_cast<jbyteArray>(env->GetObjectField(ndkobj, byteArrayID));
    //获取数组长度
    jsize _byteArrayLength = env->GetArrayLength(byteArray);
    LOGD("byteArrayLength",_byteArrayLength);
	

	//修改数组字段
    jbyte newArray[_byteArrayLength];
    for (int i = 0; i < _byteArrayLength; ++i) {
        newArray[i] = i*100;
    }
    env->SetByteArrayRegion(byteArray, 0, _byteArrayLength,reinterpret_cast<const jbyte *>(&newArray));


    //获取数组元素
    char* cbyteArray = reinterpret_cast<char *>(env->GetByteArrayElements(byteArray, nullptr));
    //打印出数组元素
    for (int i = 0; i < _byteArrayLength; ++i) {
        LOGD("byteArray[%d]=%d",i,cbyteArray[i]);
    }
    //释放数组 
    env->ReleaseByteArrayElements(byteArray, reinterpret_cast<jbyte *>(cbyteArray), 0);
通过jni访问java方法
//调用静态函数
    //获取函数ID
    jmethodID publicStaticFuncID = env->GetStaticMethodID(NDKDemoClazz,"publicStaticFunc","()V");
    //调用函数
    env->CallStaticVoidMethod(NDKDemoClazz,publicStaticFuncID);

//调用对象函数
    jmethodID publicFuncID = env->GetMethodID(NDKDemoClazz,"publicFunc","()V");
    env->CallVoidMethod(ndkobj,publicFuncID);
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

jni基础 的相关文章

  • 编译时运算符

    有人可以列出 C 中可用的所有编译时运算符吗 C 中有两个运算符 无论操作数如何 它们的结果始终可以在编译时确定 它们是sizeof 1 and 2 当然 其他运算符的许多特殊用途可以在编译时解决 例如标准中列出的那些整数常量表达式 1 与
  • EF Core Group By 翻译支持条件总和

    听说 EF Core 2 1 将支持翻译小组 我感到非常兴奋 我下载了预览版并开始测试它 但发现我在很多地方仍然没有得到翻译分组 在下面的代码片段中 对 TotalFlagCases 的查询将阻止翻译分组工作 无论如何 我可以重写这个以便我
  • 没有强命名的代码签名是否会让您的应用程序容易被滥用?

    尝试了解authenticode代码签名和强命名 我是否正确地认为 如果我对引用一些 dll 非强命名 的 exe 进行代码签名 恶意用户就可以替换我的 DLL 并以看似由我签名但正在运行的方式分发应用程序他们的代码 假设这是真的 那么您似
  • Web 客户端和 Expect100Continue

    使用 WebClient C NET 时设置 Expect100Continue 的最佳方法是什么 我有下面的代码 我仍然在标题中看到 100 continue 愚蠢的 apache 仍然抱怨 505 错误 string url http
  • 在哪里可以找到列出 SSE 内在函数操作的官方参考资料?

    是否有官方参考列出了 GCC 的 SSE 内部函数的操作 即 头文件中的函数 除了 Intel 的 vol 2 PDF 手册外 还有一个在线内在指南 https www intel com content www us en docs in
  • 查找c中结构元素的偏移量

    struct a struct b int i float j x struct c int k float l y z 谁能解释一下如何找到偏移量int k这样我们就可以找到地址int i Use offsetof 找到从开始处的偏移量z
  • 为什么当实例化新的游戏对象时,它没有向它们添加标签? [复制]

    这个问题在这里已经有答案了 using System Collections using System Collections Generic using UnityEngine public class Test MonoBehaviou
  • 如何从 appsettings.json 文件中的对象数组读取值

    我的 appsettings json 文件 StudentBirthdays Anne 01 11 2000 Peter 29 07 2001 Jane 15 10 2001 John Not Mentioned 我有一个单独的配置类 p
  • 将多个表映射到实体框架中的单个实体类

    我正在开发一个旧数据库 该数据库有 2 个具有 1 1 关系的表 目前 我为每个定义的表定义了一种类型 1Test 1Result 我想将这些特定的表合并到一个类中 当前的类型如下所示 public class Result public
  • 重载<<的返回值

    include
  • 显示UnityWebRequest的进度

    我正在尝试使用下载 assetbundle统一网络请求 https docs unity3d com ScriptReference Networking UnityWebRequest GetAssetBundle html并显示进度 根
  • 使用 Bearer Token 访问 IdentityServer4 上受保护的 API

    我试图寻找此问题的解决方案 但尚未找到正确的搜索文本 我的问题是 如何配置我的 IdentityServer 以便它也可以接受 授权带有 BearerTokens 的 Api 请求 我已经配置并运行了 IdentityServer4 我还在
  • 如何序列化/反序列化自定义数据集

    我有一个 winforms 应用程序 它使用强类型的自定义数据集来保存数据进行处理 它由数据库中的数据填充 我有一个用户控件 它接受任何自定义数据集并在数据网格中显示内容 这用于测试和调试 为了使控件可重用 我将自定义数据集视为普通的 Sy
  • 如何查看网络连接状态是否发生变化?

    我正在编写一个应用程序 用于检查计算机是否连接到某个特定网络 并为我们的用户带来一些魔力 该应用程序将在后台运行并执行检查是否用户请求 托盘中的菜单 我还希望应用程序能够自动检查用户是否从有线更改为无线 或者断开连接并连接到新网络 并执行魔
  • 链接器错误:已定义

    我尝试在 Microsoft Visual Studio 2012 中编译我的 Visual C 项目 使用 MFC 但出现以下错误 error LNK2005 void cdecl operator new unsigned int 2
  • WPF/C# 将自定义对象列表数据绑定到列表框?

    我在将自定义对象列表的数据绑定到ListBox in WPF 这是自定义对象 public class FileItem public string Name get set public string Path get set 这是列表
  • 如何从两个不同的项目中获取文件夹的相对路径

    我有两个项目和一个共享库 用于从此文件夹加载图像 C MainProject Project1 Images 项目1的文件夹 C MainProject Project1 Files Bin x86 Debug 其中有project1 ex
  • 基于 OpenCV 边缘的物体检测 C++

    我有一个应用程序 我必须检测场景中某些项目的存在 这些项目可以旋转并稍微缩放 更大或更小 我尝试过使用关键点检测器 但它们不够快且不够准确 因此 我决定首先使用 Canny 或更快的边缘检测算法 检测模板和搜索区域中的边缘 然后匹配边缘以查
  • 哪种 C 数据类型可以表示 40 位二进制数?

    我需要表示一个40位的二进制数 应该使用哪种 C 数据类型来处理这个问题 如果您使用的是 C99 或 C11 兼容编译器 则使用int least64 t以获得最大的兼容性 或者 如果您想要无符号类型 uint least64 t 这些都定
  • 使用.NET技术录制屏幕视频[关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 有没有一种方法可以使用 NET 技术来录制屏幕 无论是桌面还是窗口 我的目标是免费的 我喜欢小型 低

随机推荐

  • Jfugue编程概要

    转自 http www sudu cn info html edu java 20060912 304274 html JFugue是个用于音乐作曲的Java API 和其他的音乐API不同 他能够让你用数据字符串来指定音符 乐器 和弦 及
  • java sql 查询中的转义序列不对_在 JDBC 中使用 SQL 转义序列 - SQL Server

    使用 SQL 转义序列Using SQL escape sequences 08 12 2019 本文内容 按照 JDBC API 的定义 Microsoft JDBC Driver for SQL ServerMicrosoft JDBC
  • 20天零基础自学Python

    大家好 我是宁一 Python 数字数据类型是用来存储数值的 是我们从小学就开始接触的老朋友了 也是python中最基础的数据类型 1 Number 数据类型 python3的 Number 数据类型包括 int 整数 float 浮点数
  • B站化播放量为播放时长,是谁的狂欢?

    6月26日晚 B站举办了14周年庆典晚会 在晚会上 除了周深 美依礼芽同框献唱受到关注 B站董事长兼CEO陈睿的演讲内容同样值得深思 一来 陈睿提到 要将目前B站视频前台显示的播放量数据从次数改为分钟数 计划未来几周 将完成产品更新 二来
  • 堆栈内存和闭包

    思维导图 堆栈内存小科普 1 js中的内存分为 堆内存和栈内存 堆内存 只要用来存储引用数据类型的值 对象存的是键值对 函数存的是字符串 栈内存 供js运行的环境 函数执行 存基本数据类型的值 堆栈内存的释放问题 我们每次给变量存值或者执行
  • 程序员秋招最全Java面试题及答案整理(2023最新版)

    前言 大家好 最近一个月 花了不少时间 给大家整理了一套 2023 的技术面试资料 包括各大厂最新面试题以及面经解析涉及JVM Mysql 并发 Spring Mybatis Redis RocketMQ Kafka Zookeeper N
  • 【C刷题】day1

    一 选择题 1 正确的输出结果是 int x 5 y 7 void swap int z z x x y y z int main int x 3 y 8 swap printf d d n x y return 0 答案 3 8 解析 考
  • 怎样将excel文件导入mysql中

    1 整理好excel表中的字段 2 在Navicat中创建表 如果导入的是一个追加的表 则无需创建新表 CREATE TABLE orderinfo orderid VARCHAR 10 NULL 订单 id 主键 userid INT 1
  • 华为OD机试2023年最新题库(JAVA、Python、C++)

    我是一名软件开发培训机构老师 我的学生已经有上百人通过了华为OD机试 学生们每次考完试 会把题目拿出来一起交流分享 重要 5 11月份考的都是OD统一考试 B卷 2023年5月份题库已经更新为OD统一考试 B卷 题库由三部分组成 1 202
  • 【H5】 svg内text、image、path标签的使用

    H5 svg内text image path标签的使用 text标签 div style width 500px height 500px border 2px solid pink margin 50px auto 0 div
  • XML中约束文档的引用和书写

    在XML中定义了一套规则 来对文档内容进行约束 这叫做XML约束 常用的俩种约束语言 DTD约束 Schema约束 XML文档中可以引入多个约束文档 为了防止出现不同含义的同名名称冲突 所以 所以可以XML提供了名称空间 1 DTD语法 D
  • 【HTML】列表标签、表格标签、块级标签、表单标签

    文章目录 一 列表标签 1 无序列表 2 无序列表 3 定义列表 项目列表 二 表格标签 1 表格整体架构 2 表格的标签介绍 3 table标签的属性 4 tr标签的属性 5 th td标签的属性 6 跨行 跨列的表格 三 块级标签 1
  • vue猜数字游戏

    div p msg p div
  • node.js JSON对象和string的相互转化

    JSON stringify obj 将JSON转为字符串 var json aa sdddssd bb 892394829342394792399 23894723984729374932874 cc 11111111111111 gt
  • html5期末知识点归纳总结,web期末考试知识点

    题型及知识点 一 知识点 上课内容全覆盖 除补充的html5和css3的内容 常用的html标记及属性 弄清楚哪些是块元素 哪些是行内元素 特殊字符标记 p40 Css属性 i 字体 font size font family font w
  • 蓝桥杯JAVA B组 2020(1)第五题 排序

    一 题目描述 小蓝最近学习了一些排序算法 其中冒泡排序让他印象深刻 在冒泡排序中 每次只能交换相邻的两个元素 小蓝发现 如果对一个字符串中的字符排序 只允许交换相邻的两个字符 则在所有可能的排序方案中 冒泡排序的总交换次数是最少的 例如 对
  • SQLCipher核心思想

    加密原理 page data iv hmac iv是一段随机数 可以保证每一页的iv值都不一样 和page data一起作用 用于生成hmac值 sizeof page data p
  • kubeasz 二进制安装k8s高可用集群

    一 kubeasz介绍 项目致力于提供快速部署高可用k8s集群的工具 同时也努力成为k8s实践 使用的参考书 基于二进制方式部署和利用ansible playbook实现自动化 既提供一键安装脚本 也可以根据安装指南分步执行安装各个组件 二
  • Maven2部署构件到Nexus时出现的Failed to transfer file错误

    原文出处 http www javatang com archives 2010 01 23 4518375 html 作者 Jet Mah from Java堂 声明 可以非商业性任意转载 转载时请务必以超链接形式标明文章原始出处 作者信
  • jni基础

    JNI相关 静态注册 java代码需要和C 代码相符通讯就需要通过JNI来进行注册 java public native String stringFromJNI 代表该函数的实现在so so Java com first firstndk