Android Jni的介绍和简单Demo实现

Android Jni的介绍和简单Demo实现

文章目录

一、JNI的简单介绍

JNI

JNI 全程:JNI(Java Native Interface),通俗翻译:Java本地方法

官方说法:提供一种Java字节码调用C/C++的解决方案,JNI描述的是一种技术。

所以这里的Nativie的本地的意思就是C/C++,所以JNI通俗理解就是Java调用C/C++的方案技术。

NDK

NDK(Native Development Kit),通俗翻译:本地发展(扩展)工具

Android NDK 是一组允许您将 C 或 C++("原生代码")嵌入到 Android 应用中的工具,NDK描述的是工具集。

同样,把这里的Native理解成C/C++,那么NDK的简单理解就是能把C/C++编译成Java识别的工具模块。

Android Studio中已经集成了NDK,所以才可以在Java代码中很方便就可以调用到C/C++的代码。

网上有些示例是用NDK命令来编译cpp生成so,并调用测试,这里不做介绍。

Jni的开发背景:

需要调用Java语言不支持的依赖于操作系统平台特性的一些功能,比如

复制代码
● 需要调用当前UNIX系统的某个功能,而Java不支持这个功能的时候,就要用到JNI
● 在程序对时间敏感或对性能要求特别高时,有必要用到更底层的语言来提高运行效率
● 音视频开发涉及到的音视频编解码需要更快的处理速度,这就需要用到JNI
● 为了整合一些以前的非Java语言开发的系统
● 需要用到早期实现的C/C++语言开发的一些功能或者系统,将这些功能整合到当前的系统或者新的版本中

其实就是为了调用C/C++代码

JNI是完善Java的一个重要功能,它让Java更加全面、封装了各个平台的差异性

JNI在 Android 开发里的主要应用场景:

复制代码
● 音视频开发
● 热修复
● 插件化
● 逆向开发
● 等等...

这些都是比较复杂的模块,很多实现Java代码无法实现或者使用Java代码实现会很低效的情况。

所以这些模块开发的就要提前掌握Jni相关技术才能实现复杂功能。

其实这些概念,看一百遍也是很容易忘记的,只要记住一点就行了:

Jni就是Java为了调用C语言的技术,其中过程用到了NDK相关工具。

java-jni-c/c++ 关系图也是比较简单明了的:

本文将介绍Jni相关的基础知识,对jni有了记录了解后,后续还会再写一些Jni 进阶的文章。

Jni基础很简单,比如:Java 代码中加载so库,定义native方法,jni代码中执行简单的实现,相信很多人都是会的;

Jni的进阶知识:jni添加日志,复制对象的调用,C++调用Java方法,Jni方法的动态注册和静态注册,Jni报错分析等等,这些都是有一定的难度的,经过一定的学习了解就可以掌握了。

这些Jni相关知识的学习,不需要系统源码环境,只需要电脑安装Android Studio,安装模拟器或者有安卓真机调试验证就可以了。

Jni在系统源码环境中也是有很多相关的代码和使用场景,

如果是入门学习,优先使用Android Stduio 创建的项目会好入手很多,

本文以及后续的分析学习文章都是基于非源码环境中,大部分人都可以进行学习和了解。

本文讲解jni基础部分还是比较全面和清晰的,有兴趣的可以先收藏。

二、JNI的简单Demo

这里以Android Studio创建Jni项目,并且提前安装了NDK相关工具。

1、Demo主要界面和效果展示

在Android Studio 中新建一个项目

如下图所示:

完成后,即可创建一个最简单的JNI项目。

可以直接点击运行在模拟器上或者连接的Android设备,即调用了CPP文件返回字符串并在Android界面显示Hello World,项目代码就包含了一个最基本的jni架构代码。

本文Jni 示例Demo实现加减乘除运算,以及字符串拼接,如下图所示:

里面具体实现都是在cpp代码中实现的。

这个JNI项目涉及的主要代码入下:

2、CMake编译加载文件

CMakeLists.txt 大致内容如下:

复制代码
cmake_minimum_required(VERSION 3.18.1) //版本
project("jnidemo") //加载的项目

//至少三个参数,参数直接没有标点符号哦,用换行隔开
第一个参数表示加载的库文件(动态.so/静态.a)
第二个参数表示库加载的方式,有动态SHARED,有静态STATIC(极少用)
后面的参数可以N个,表示要加载的CPP文件,也可以用集合指向
add_library(
        jnidemo
        SHARED
        native-lib.cpp
        TestCPlus.cpp
)

//下面两个是关联的,简单示例中不写也是可以正常运行的
find_library(
        log-libxx
        log)

target_link_libraries(
        jnidemo
        ${log-libxx})
add_library 指令的加载库说明:

SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;

STATIC: 表示静态库,集成到代码中会在编译时调用;

也就是说动态加载,只有进入那个类并且执行System.loadLibrary,才把库文件加载进来;

而静态加载会在应用编译的时候就把CPP的代码编译成虚拟机能识别的语句放到apk。

所以动态加载的库文件可以在应用运行中使用一定的手段进行替换,但是静态加载的库却不行。

find_library 指令

语法:find_library( name1 path1 path2 ...)

VAR 变量表示找到的库全路径,包含库文件名 。例如:

复制代码
find_library(libX X11 /usr/lib) 
find_library(log-lib log) #路径为空,应该是查找系统环境变量路径

find_library可以多个,并且给下面的 target_link_libraries 连接使用。

语法:target_link_libraries(target library <debug | optimized> library2...)

这个指令可以用来为 target 添加需要的链接的共享库(可以多个),同样也可以用于为自己编写的共享库添加

共享库链接。

比如下面示例:

复制代码
#指定 compress 工程需要用到 libjpeg 库和 log 库 
target_link_libraries(compress libjpeg ${log-lib})

3、MainActivity.java

复制代码
public class MainActivity extends AppCompatActivity {
	
	//加载so库名称的代码
   // Used to load the 'jnidemo' library on application startup.
    static {
        System.loadLibrary("jnidemo");
    }


    //界面显示代码省略


    //jni方法,加减乘除,最后一个参数是操作类型的字节数据类型
    public native float operationNumberFromJNI(float parameter1, float parameter2, char type);


    //jni方法,字符串拼接
    public native String addStringFromCPlusJNI(String parameter1, String parameter2);

4、native-lib.cpp

复制代码
#include <jni.h>
#include <string>


//创建Jni工程最简单的方法,返回一个字符串
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_jnidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "简易计算器";

    return env->NewStringUTF(hello.c_str());
}



//Jni增删改查的实现,因为float是基本数据类型,所以不需要转换就可以直接操作
extern "C"
JNIEXPORT jfloat JNICALL
Java_com_example_jnidemo_MainActivity_operationNumberFromJNI(JNIEnv *env, jobject thiz, jfloat parameter1,
                                                                  jfloat parameter2,jchar type) {
    switch (type) {
        case '+':
            return parameter1 + parameter2;
        case '-':
            return parameter1 - parameter2;
        case '*':
            return parameter1 * parameter2;
        case '/':
            return parameter1 / parameter2;
    }
    return parameter1 + parameter2;
}


//字符串的拼接,方法有N种,String不是基本数据类型,需要进行类型转换后才能进行操作
extern "C" JNIEXPORT _jstring * JNICALL
Java_com_example_jnidemo_MainActivity_addStringFromCPlusJNI(JNIEnv *env, jobject thiz,
                                                            jstring parameter1,
                                                            jstring parameter2) {
    char *c1 = (char *) (env->GetStringUTFChars(parameter1, JNI_FALSE));
    char *c2 = (char *) (env->GetStringUTFChars(parameter2, JNI_FALSE));
    char *res = strcat(c1, c2); //拼接两个字符串
    //释放c1和c2
    delete(c1);
    delete(c2);

    return env->NewStringUTF(res);


}

上面Jni的方法名也是有一定规则的,网上很多使用NDK工具生成的,

没啥必要了使用命令工具那些,手写就行了,现在新的Studio也是会自动生成的。

Java_com_example_jnidemo_MainActivity_addStringFromCPlusJNI 方法名的大致规则:

复制代码
Java_包名(中间的点.替换成下划线_)_类名_方法名

cpp文件加上CMakeList.txt就会编译出so文件,目录在:

app\build\intermediates\cmake\debug\obj\XXX[libjnidemo.so

xxx表示arm64-v8a,armeabi-v7a,x86,x86_64

5、Jni引用其他cpp文件代码

之前没怎么写过cpp文件,还不知道怎么引用其他文件的类的使用,后面发现也不难。

比如,native-lib.cpp需要调用TestCPlus.cpp的add方法

1、定义TestCPlus.h 文件
复制代码
class TestCPlus {

//定义变量和方法
private:
    int number;
public:
    int add(int parameter1,int parameter2); //定义方法

};
2、TestCPlus.cpp
复制代码
#include "TestCPlus.h" //添加头文件


//实现需要的方法
int TestCPlus::add(int parameter1, int parameter2) {
    int result = parameter1 + parameter2;
    return result;
}
3、native-lib.cpp文件中调用

#include "TestCPlus.h" //添加头文件声明

复制代码
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_jnidemo_MainActivity_addIntFromCPlusJNI(JNIEnv *env, jobject thiz, jint parameter1,
                                                         jint parameter2) {
    TestCPlus *testCPlus;
    return testCPlus->add(parameter1, parameter2);
//    return parameter1 + parameter2;
}

cpp文件的调用还有很多种方法,这里就不一一说明了。

这样就能在Java文件中调用native-lib.cpp再调用到别的cpp文件了。

同时,要加载的cpp文件也别忘了在CMakeList.txt中进行声明

根据上面的基础知识的学习,就知道如下修改就可以:

复制代码
add_library(
        # Sets the name of the library.
        jnidemo
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        native-lib.cpp
        TestCPlus.cpp
)

这里添加编译 TestCPlus.cpp就可以,因为自身的.h头文件,系统会自动识别导入,如果这里没添加声明直接调用会编译不通过。

一个简单的JniDemo就介绍到这里,Demo源码:
https://download.csdn.net/download/wenzhi20102321/86239831

三、其他

1、Jni基础的几点内容

复制代码
1、记住编写关键流程
    类的静态方法中loadLibrary
    类中定义native方法
    类的使用中调用jni方法
    cpp文件中编写jni实现

2、CMakeList.txt 的基本规则

3、jni.cpp文件中的方法命名

4、Java到jni.cpp文件中类型转换

5、有一定的c/c++语言基础

类型转换是需要记忆一下的,

比如Java的int 在jni的cpp文件中是jint,

Java中的String 在jni的cpp文件是jstring

jni.cpp文件中的数据类型是特殊定义的,定义了不同与Java和c的类型标识。

基本类型是可以直接透传的,比如int类型数据,Java(int)-->Jni(jint)-->cpp(int)

可以直接从Java文件传入到jni.cpp文件,在传入到别的c/cpp文件进行使用

但是非基本类型的数据是要转换后才能使用的,比如字符串String,数组int[]等数据

Java(String)-->Jni(jstring)-->c/c++(指针或char数组)

大致数据类型映射关系如下,看几遍基本能记住,复杂的再进行细查即可。

2、java数据类型与jni类型映射表

Java 类型 JNI本地类型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++带符号的8位整型
char jchar C/C++无符号的16位整型
short jshort C/C++带符号的16位整型
int jint C/C++带符号的32位整型
long jlong C/C++带符号的64位整型e
float jfloat C/C++32位浮点型
double jdouble C/C++64位浮点型
Object jobject 任何Java对象,或者没有对应java类型的对象
Class jclass Class对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组

这个表格是有有啥用?很多人初学就会很懵逼。Jni使用过程会两三个表格的转换关系和要清楚具体使用场景。

上面这个表格就是让Java的数据类型转换成Jni的数据类型,比如示例代码中的:

复制代码
    //XXX.java 定义的 jni方法
    public native float operationNumberFromJNI(float parameter1, float parameter2, char type);
    
    //Jni.cpp文件中的对应方法
extern "C"
JNIEXPORT jfloat JNICALL
Java_com_example_jnidemo_MainActivity_operationNumberFromJNI(JNIEnv *env, jobject thiz, jfloat parameter1,jfloat parameter2,jchar type) {//看后面三个参数
 。。。
    return XXX;
}

Java代码和Jni代码同一个方法是有不同的显示形式,就需要上面那个表格对照转换,有些不清楚的类型就可以对照表格进行转换。

java数据类型与jni类型 转换总结:

复制代码
 1、java中的返回值void与jni中的void是完全对应的。
 2、java中的基本数据类型(byte,short,int,long,float,double,boolean,char)
 在jni中对应的数据类型在前面加上j
 (jbyte,jshort,jint,jlong,jfloat,jdouble,jboolean,jchar)。
 3、java中的对象,包括类库中定义的类、接口,都对应jni中的jobject。
 4、java中基本数据类型的数组对应与jni中的jXXXArray类型(type就是上面说的8中基本数据类型)。
 5、java中对象的数组对应于jni中jobjectArray类型。

3、Java签名类型 常用的数据类型及对应字符:

Java 类型 Jni中表示的符号 备注
boolean Z 不是类型首字母大写
byte B
char C
short S
int I
long L
float F
double D
void V
objects对象 Lfully-qualified-class-name;L全类名; 记得最后是有分号的
Arrays数组 [array-type [数组类型
methods方法 (argument-types)return-type(参数类型)返回类型

这个表格是有有啥用?就更多人懵逼了。

其实这些类型符号表示的是Java方法或者属性的一个签名,唯一性,目前就是为了让Jni.cpp调用到Java代码。

举个例子就很容易清楚了:

复制代码
//XXX.Java
  int age;
  String name;
  public  int add(int number1,int number2){
        System.out.println("c/C++居然调用了我");
        return number1+number2;
    }

//jni.cpp 修改Java属性值和调用Java方法示例

//获取类对象
jclass mainActivityCls=env->FindClass("com/zmw/jnitest/MainActivity");

//获取属性的fieldId,--》这里就用到了签名类型
jfieldID ageFid = env->GetFieldID(mainActivityCls,"age","I");
jfieldID nameFid=env->GetFieldID(mainActivityCls, "name", "Ljava/lang/String;");
//获取属性值
jint  age = env->GetIntField(mainActivityThis,ageFid);
jstring name = (jstring)env->GetObjectField(thiz,nameFid);//此处有编码转换问题未解决

//修改属性值,C++中修改变量值后,Java重新获取打印发现是修改过的
env->SetIntField(mainActivityThis, ageFid , 11);
env->SetObjectField(thiz, nameFid,Stringvalue);

//获取方法的methodId,--》这里就用到了签名类型
jmethodID addMid=env->GetMethodID(mainActivityCls, "add", "(II)I");
int result=env->CallIntMethod(mainActivityThis, addMid, 1, 1); //这里就能获取到2的值。

仔细看一下上面的代码,就大致能理解这个签名表格的具体作用:为了找到Java方法的参数和返回值的形式。

Java签名类型小结:

复制代码
(1)基础类型签名那些转换都是很容易记住的,基础类型中,特别留意一下boolean类型 是 Z 就行
(2)对象Object类型的转换是:L+全包名(包名直接用 /间隔)+类名+分号
(3)数组类型签名转换:[数组类型,比如[I,表示Java的 int[]

(4)方法签名的转换:(参数类型)返回类型,中间多个参数类型依此填写就行,
比如:Jni中的代码:env->GetMethodID("add", "(IILjava/lang/String;Ljava/lang/String;)Ljava/lang/String;")
如果不清楚上面的表格转换,看起来就头大,特别是那些有三四个以上参数的情况,但是学习过后就不难了,
查看表格对应关系可以知道,Java中的对应方法是:public String add(int a,int b,String c,String d)
其实就是先看括号后面的返回值,然后再一个个确定括号内的形参变量

还有一些比较复杂的,比如如何使用cpp调用到Java的类具体过程那些,

本文就不详细介绍先,可以看下面这篇文章:

https://blog.csdn.net/pfgmylove/article/details/7052839

上面已经介绍了两个表格,其实还有个Jni里面的api接口需要记忆一下:

4、JNI类型api调用表格

方法、变量修饰类型表格
函数描述 描述
GetFieldID 得到一个实例的域的ID
GetStaticFieldID 得到一个静态的域的ID
GetMethodID 得到一个实例的方法的ID
GetStaticMethodID 得到一个静态方法的ID

上面Jni.cpp调用Java代码已经用到部分api方法,并且从字面含义也是比较容易里面这个表格的api的具体作用。

这个表格的用于就是为了获取到方法的修饰类型,比如方法,静态方法,变量,静态变量。

毕竟不同的修饰类型,在编译过程是有差异的。所以要区分。

还有一个Jni api的表格虽然不是很常用,但是有些数组对象场景要是要用到的,表格内容如下:

数组对应变量类型api表格
函数描述 nativie数据对象 nativie类型 Java数据类型
GetBooleanArrayElements jbooleanArray jboolean boolean[]
GetByteArrayElements jbyteArray jbyte byte[]
GetCharArrayElements jcharArray jchar char[]
GetShortArrayElements jshortArray jshort short[]
GetIntArrayElements jintArray jint int[]
GetLongArrayElements jlongArray jlong long[]
GetFloatArrayElements jfloatArray jfloat float[]
GetDoubleArrayElements jdoubleArray jdouble double[]

以下面一段代码理解一下上面api的用法,

复制代码
Java代码:
public byte[] getBytes(String s){...}

Jni.cpp 关键代码:
	jclass     jstrObj   = env->FindClass("com/zmw/jnitest/MainActivity");
    jstring    encode    = env->NewStringUTF("utf-8");//设置编码格式
    jmethodID  methodId  = env->GetMethodID(jstrObj, "getBytes", "(Ljava/lang/String;)[B");

	//调用了java的getBytes 方法,获取jbyteArray对象
    jbyteArray byteArray = (jbyteArray)env->CallObjectMethod(jstr, methodId, encode);
	//获取数据长度
    jsize      strLen    = env->GetArrayLength(byteArray);
	//获取数据内容,jBuf指针对象,---》GetByteArrayElements api
    jbyte      *jBuf     = env->GetByteArrayElements(byteArray, JNI_FALSE);
    。。。中间处理过程
    //最后要释放指针
     env->ReleaseByteArrayElements(byteArray, jBuf, 0);

理解上面这段代码应该对第二个表格的含义有理解了。

Jni中还有很多相关的api方法在后续的分析中慢慢讲解。

上面四个表格,第一个主要是Java --> Jni的转换关系,后面三个表格都是Jni --> Java 的转换关系。

多看几次就理解了。

共勉:不用辜负每一天的时光。

相关推荐
stevenzqzq2 小时前
android中dp和px的关系
android
一一Null4 小时前
Token安全存储的几种方式
android·java·安全·android studio
JarvanMo5 小时前
flutter工程化之动态配置
android·flutter·ios
时光少年7 小时前
Android 副屏录制方案
android·前端
时光少年7 小时前
Android 局域网NIO案例实践
android·前端
alexhilton8 小时前
Jetpack Compose的性能优化建议
android·kotlin·android jetpack
流浪汉kylin8 小时前
Android TextView SpannableString 如何插入自定义View
android
火柴就是我9 小时前
git rebase -i,执行 squash 操作 进行提交合并
android
你说你说你来说10 小时前
安卓广播接收器(Broadcast Receiver)的介绍与使用
android·笔记
你说你说你来说10 小时前
安卓Content Provider介绍及使用
android·笔记