凌霄的博客
jni开发探索之旅
jni开发探索之旅

由于工作上的需求需要使用java和c++互调实现功能,所以要对jni进行深入研究,故此入坑。已经很久没有写界面布局这方面的了,对于android开发来讲,更加偏向于前端,不知道是好是坏…

JNI是什么

JNI全程Java Native Interface,意为Java本地调用,它允许Java代码和其他语言写的代码进行交互,简单的说,一种在Java虚拟机控制下执行代码的标准机制。 可以用它实现java和c语言互调。对于初学者来讲,很容易吧jni和ndk的概念搞混淆(当然也可能只有博主一个人o(╯□╰)o),那jni和ndk的区别到底是什么?

NDK是什么

Android NDK(Native Development Kit )是一套工具集合,允许你用像C/C++语言那样实现应用程序的一部分。 简单的说,NDK其实多了一个把.so和.apk打包的工具,而JNI开发并没有打包,只是把.so文件放到文件系统的特定位置。可以将NDK看做是Google提供的一个打包工具,方便开发者使用,有了这个工具,我们只需要关注代码的具体实现,而不需要关注如何编译动态链接库。

上手之前

先看看jni中的数据类型:

http://oqz3bypff.bkt.clouddn.com/jni.png

函数操作(只列出了一些常用的):

函数 Java数据类型 本地类型 函数说明
GetBooleanArrayElements boolean jboolean 需要调用ReleaseBooleanArrayElements 释放
GetByteArrayElements byte jbyte 需要调用ReleaseByteArrayElements 释放
GetCharArrayElements char jchar 需要调用ReleaseShortArrayElements 释
GetObjectArrayElement 自定义对象 jobject
SetObjectArrayElement 自定义对象 jobject
NewArray 创建一个指定长度的原始数据类型的数组
NewStringUTF jstring类型的方法转换
DeleteLocalRef 删除 localRef所指向的局部引用
DeleteGlobalRef 删除 globalRef 所指向的全局引用
GetMethodID 返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID() 可使未初始化的类初始化。要获得构造函数的方法 ID,应将 作为方法名,同时将void (V) 作为返回类型。
GetStaticMethodID 调用静态方法
CallVoidMethod 调用实例方法
CallMethod

天才第一步,Hello World来一个

首先得有ndk的环境,环境配置很简单,博主就不在这里演示了。直接新建一个工程,勾选上c++支持:

http://oqz3bypff.bkt.clouddn.com/hellondk.png

然后看看Android Studio给我们生成了什么:

http://oqz3bypff.bkt.clouddn.com/hello_ndk_1.png

#####初识cmake

  1. cmake是什么:脱离 Android 开发来看,c/c++ 的编译文件在不同平台是不一样的。Unix 下会使用 makefile 文件编译,Windows 下会使用 project 文件编译。而 CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefileproject 文件,然后再调用底层的编译。
  2. 和ndk的区别:在 Android Studio 2.2 之后你有2种选择来编译你写的 c/c++ 代码。一个是 ndk-build + Android.mk + Application.mk 组合,另一个是 CMake + CMakeLists.txt 组合。这2个组合与Android代码和c/c++代码无关,只是不同的构建脚本和构建命令。说白了,cmake就是ndk的替代者。

本文使用的是后者即cmake构建,这也是google官方主推的。

cmake工程和普通的工程相比就多了这三个地方,一个是CMakeLists.txt文件,文件内容如下:

cmake_minimum_required(VERSION 3.4.1)
add_library( # 生成的so库名称,此处生成的so文件名称是libnative-lib.so
             native-lib
             # SHARED是动态库,会被动态链接,在运行时被加载
             # STATIC:静态库,是目标文件的归档文件,在链接其它目标的时候使用
             # MODULE:模块库,是不会被链接到其它目标中的插件
             SHARED
             # 资源路径是相对路径,相对于本CMakeLists.txt所在目录
             src/main/cpp/native-lib.cpp )
# 从系统查找依赖库
find_library( # android系统每个类型的库会存放一个特定的位置,而log库存放在log-lib中
              log-lib
              # android系统在c环境下打log到logcat的库
              log )
# 配置库的链接(依赖关系)
target_link_libraries( # 目标库
                       native-lib
                       # 依赖于
                       ${log-lib} )

注释写的很明确了,对于初学者,只需要注意的两个地方是,第一处和第三处的名字必须是相同的,第二处只要你在cpp文件夹下新建了.cpp文件,都需要在这里申明一下,是不是有点像清单文件的感觉。

http://oqz3bypff.bkt.clouddn.com/hello_ndk_3.png

关于cmake的具体使用,网上有很多教程,博主就不多说了。

cpp文件分析

然后就是.cpp文件里的内容了:

“`c++
#include <jni.h>
#include
extern “C” JNIEXPORT jstring
JNICALL
Java_com_ndk_lingxiao_ndkproject_MainActivity_stringFromJNI(
JNIEnv env,
jobject /
this */) {
std::string hello = “Hello from C++”;
return env->NewStringUTF(hello.c_str());
}


一个一个分析。 1. 首先前两句是头文件,没什么好说的。 2. extern "C"主要作用就是为了能够正确实现C++代码调用其他C语言代码 ,也就是兼容c语言。 3. JNIEXPORT 在Jni编程中所有本地语言实现Jni接口的方法前面都有一个"JNIEXPORT",这个可以看做是Jni的一个标志,表示此函数是被jni调用的 4. jstring 返回值类型是string类型的 5. JNICALL 这个可以理解为Jni 和Call两个部分,和起来的意思就是 Jni调用XXX(后面的XXX就是JAVA的方法名) 6. Java_com_ndk_lingxiao_ndkproject_MainActivity_stringFromJNI,别看这玩意儿这么长,他就是吓唬你的,我相信人有所长,你一定比他长,不要被吓到[]~( ̄▽ ̄)~*。固定写法Java_+类名全路径+_方法名,只是把类名的“.”替换为了下划线"_"。很简单的有木有。 7. JNIEnv * env:这个env可以看做是Jni接口本身的一个对象,jni.h头文件中存在着大量被封装好的函数,这些函数也是Jni编程中经常被使用到的,要想调用这些函数就需要使用JNIEnv这个对象。例如:env->GetObjectClass()。 8. jobject obj 有两种情况,一种是可以看做Java类的一个实例化对象 ,如Hello hello = new Hello(),hello.method(),这时候的obj 就是hello。哎,一不小心又new了一个对象出来。一种是可以看做是java类的本身 ,如果method是静态方法,它不是属于一个对象的,而是属于一个类的 ,这时候就代表Hello.class。 9. std::string hello = "Hello from C++" 相当于stirng str = "Hello from C++",但是c++的字符串和java的字符串不一样,所以需要转换一下再返回,所以通过env对象调用方法转换为java能识别的env->NewStringUTF(hello.c_str()) cpp文件也讲完了,现在看看MainActivity里的代码: ```java public class MainActivity extends AppCompatActivity { static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Example of a call to a native method TextView tv = (TextView) findViewById(R.id.sample_text); tv.setText(stringFromJNI()); } public static native String stringFromJNI(); } </code></pre> 只需要将一下那个静态代码块,loadLibrary的时候,本来生成的.so文件为libnative-lib.so但是这里没有加是android studio会自动给我们加上去,如果这里再加上就会重复,所以只需要填写和CMakeLists.txt里的命名相同就行了。 <h5>c语言里打印Log</h5> 首先在module级的build.gradle里加入: <pre><code class="language-java ">defaultConfig { ndk{ ldLibs "gomp" } } </code></pre> 然后在cpp中加入如下的宏定义: ```c++ #include <android/log.h> #define LOG_TAG "NATIVE_LIB" #define DEBUG #define ANDROID_PLATFORM #ifdef DEBUG #ifdef ANDROID_PLATFORM #define LOGD(...) ((void)android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS)) #define LOGI(...) ((void)android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS)) #define LOGW(...) ((void)android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS)) #define LOGE(...) ((void)android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS)) #else #define LOGD(fmt, ...) printf(fmt"\n", ##VA_ARGS) #define LOGI(fmt, ...) printf(fmt"\n", ##VA_ARGS) #define LOGW(fmt, ...) printf(fmt"\n", ##VA_ARGS) #define LOGE(fmt, ...) printf(fmt"\n", ##VA_ARGS) #endif #else #define LOGD(...) #define LOGI(...) #define LOGW(...) #define LOGE(...) #endif

搞定,这个是固定写法,没什么好说的。 ##### java调用C++方法 这个比较简单,这里就随便提一下,首先我新建了一个Hello类,写了两个方法,android studio会提示是否生成方法: ![](http://oqz3bypff.bkt.clouddn.com/ndk_method.png) 生成方法之后我只加了两句打印: ```c++ extern "C" JNIEXPORT void JNICALL Java_com_ndk_lingxiao_ndkproject_Hello_callStaticMethod(JNIEnv *env, jclass type, jint i) { LOGD("im from static moethod C++ , value is : %d",i); } extern "C" JNIEXPORT void JNICALL Java_com_ndk_lingxiao_ndkproject_Hello_callInstanceMethod (JNIEnv *, jobject, jint i){ LOGD("im from instance moethod C++ , value is : %d",i); } </code></pre> 然后在相应的地方调用一下,我是在MainActivity中调用的: <img src="http://oqz3bypff.bkt.clouddn.com/ndk_method2.png" alt="" /> 然后看一下后面的重点,c++中调用java层的方法和修改java层的属性。 <h5>方法签名</h5> 在学习c++调用java方法时需要了解的是方法签名,关于方法签名,我觉得只要关注这两个地方就行了: <ol> <li>什么是方法签名:方法签名由方法名称和一个参数列表(方法的参数的顺序和类型)组成。</li> <li>为什么要用方法签名:c语言中没有方法重载这个概念,如果java中有两个方法:long test(int n, String str, int[] arr) ,long test(String str) 。那么没有方法签名来标注一下,编译器不就懵逼了嘛(ノ`Д)ノ。</li> </ol> 下面有请方法签名规则表开始表演: <table> <thead> <tr> <th>Java类型</th> <th>签名类型</th> </tr> </thead> <tbody> <tr> <td>boolean</td> <td>Z</td> </tr> <tr> <td>byte</td> <td>B</td> </tr> <tr> <td>char</td> <td>C</td> </tr> <tr> <td>long</td> <td>J</td> </tr> <tr> <td>float</td> <td>F</td> </tr> <tr> <td>double</td> <td>D</td> </tr> <tr> <td>short</td> <td>S</td> </tr> <tr> <td>int</td> <td>I</td> </tr> <tr> <td>类</td> <td>L全限定类名</td> </tr> <tr> <td>数组</td> <td>[全限定类名</td> </tr> </tbody> </table> 上述中类的签名规则是:”L+全限定类名+;”三部分组成,其中全限定类名以”/”分隔,而不是用”.”或”_”分隔。 比如刚刚说的那两个方法: <ol> <li>long test(String str) :方法签名为(Ljava/lang/String;)J ,括号里的内容代表string括号后面是返回值类型签名,J代表long型。</li> <li>long test(int n, String str, int[] arr) :其方法签名为(ILjava/lang/String;[I)J括号里的内容分成三部分,之间没有空格,即”I”,”Ljava/lang/String;”和”[I”,分别代表int,String,int[]</li> </ol> 有迷妹私信我了:这么复杂的吗?有没有简单快捷的方法,每次都这么麻烦,太浪费时间了吧!我的时间很宝贵的嘤嘤嘤,要是没有我砍死你 <img src="http://omsaa4hdo.bkt.clouddn.com/face/dadao30.jpg" alt="30米的大刀" /> 很大方的(迫不得已)交出偷懒方法: <pre><code class="">javap -s 类的.class路径 </code></pre> 可以说是很直观了(逃),博主用的as3.1,所以这个目录在工程,目录\module目录\build\intermediates\classes\debug下面。得到方法签名之后,就可以开始下面的操作了 <h5>C++调用Java静态方法</h5> 在java中写了一个这样的方法: <pre><code class="language-java ">public static void staticMethod(String data){ logMessage(data); } public static void logMessage(String data){ Log.d("hello", data); } </code></pre> 我希望在cpp中调用staticMethod方法,该怎么做呢?先贴代码: ```c++ extern "C" JNIEXPORT void JNICALL Java_com_ndk_lingxiao_ndkproject_Hello_callJavaStaticMethod(JNIEnv *env, jclass type) { jclass clazz = NULL; jmethodID method_id = NULL; jstring str_log = NULL;
clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello");
if (clazz == NULL){
    LOGD("没有发现该类");
    return;
}
method_id = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;)V");
if (method_id == NULL){
    LOGD("没有发现该方法名");
    return;
}
str_log = env->NewStringUTF("c++ 调用java的静态方法");
env->CallStaticVoidMethod(clazz,method_id,str_log);

env->DeleteLocalRef(clazz);
env->DeleteLocalRef(str_log);
return ;

}


这里如果对jvm虚拟机比较了解的同学可能会更容易理解,博主正在了解中,所以假装解释一波,只是按照我自己的理解,来解释,可能后面会改动(~ ̄▽ ̄)~ 。 首先定义了三个变量,然后使用env调用封装好的方法FindClass,传入类名全路径,在jvm中如果有加载这个类,那么就会返回我们的这个类。 接着是获取方法的id,使用env调用GetStaticMethodID,第一个参数是方法所在的类,第二个是方法名,第三个是方法签名。 然后使用env调用CallStaticVoidMethod,传入类和方法和参数,完成对java层方法的调用。 最后不要忘记删除引用,不然会发生内存泄漏。 ##### C++调用Java实例方法 和静态方法的区别就两个地方,一个是GetStaticMethodID,一个是CallStaticVoidMethod: ```c++ extern "C" JNIEXPORT void JNICALL Java_com_ndk_lingxiao_ndkproject_Hello_callJavaInstanceMethod(JNIEnv *env, jobject instance) { jclass clazz = NULL; jmethodID method_id = NULL; jstring str_log = NULL; clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello"); if (clazz == NULL){ LOGD("没有发现该类"); return; } method_id = env->GetMethodID(clazz,"instanceMethod","(Ljava/lang/String;)V"); if (method_id == NULL){ LOGD("没有发现该方法名"); return; } str_log = env->NewStringUTF("c++ 调用java的实例方法"); env->CallVoidMethod(instance,method_id,str_log); //clazz 改为instance env->DeleteLocalRef(clazz); env->DeleteLocalRef(str_log); return ; } </code></pre> <h5>C++调用Java变量</h5> 首先在java类中定义一个变量: <pre><code class="language-java ">public String name = "im is java"; </code></pre> 然后贴上jni代码,主要方法是GetFieldID,第一个参数传入变量所在类,第二个参数是变量名,第三个参数是签名类型: ```c++ extern "C" JNIEXPORT void JNICALL Java_com_ndk_lingxiao_ndkproject_Hello_changeField(JNIEnv env, jobject instance) { jclass clazz = env->GetObjectClass(instance); if (clazz == NULL){ return; } jfieldID jfieldID = env->GetFieldID(clazz,"name","Ljava/lang/String;"); if (jfieldID == NULL){ return; } jstring obj_str = (jstring) env->GetObjectField(instance,jfieldID); if (obj_str == NULL){ return; } char c_str = (char*) env->GetStringUTFChars(obj_str,JNI_FALSE);
const char new_char[40] = "changed from c";
//复制new_char的内容到c_str
strcpy(c_str,new_char);

jstring new_str = env->NewStringUTF(c_str);
LOGD("%s",new_char);
env->SetObjectField(instance,jfieldID,new_str);

env->DeleteLocalRef(clazz);
env->DeleteLocalRef(obj_str);
env->DeleteLocalRef(new_str);
return;

}




##### C++调用Java静态变量 同理,静态变量也没啥好讲的了,这里就贴一下代码: ```c++ extern "C" JNIEXPORT void JNICALL Java_com_ndk_lingxiao_ndkproject_Hello_changeStaticField(JNIEnv *env, jclass type) { jclass clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello"); if (clazz == NULL){ return; } jfieldID jfieldID = env->GetStaticFieldID(clazz,"age","I"); if (jfieldID == NULL){ return; } int age = env->GetStaticIntField(clazz,jfieldID); LOGD("%d",age); jint change_int = 12; env->SetStaticIntField(clazz,jfieldID,change_int); env->DeleteLocalRef(clazz); }

学习JNI,个人建议是在平常的工作中能用到的才去深入学习,因为这个东西只有实践才有意义。关于如何在native中排查错误,可以使用ndk-stack工具,使用方法贼简单,一个命令行的事儿,这里就不说了。

本文demo的github地址:NdkDemo

参考链接:

JNI实战全面解析

Android NDK开发扫盲及最新CMake的编译使用

发表评论

textsms
account_circle
email

jni开发探索之旅
由于工作上的需求需要使用java和c++互调实现功能,所以要对jni进行深入研究,故此入坑。已经很久没有写界面布局这方面的了,对于android开发来讲,更加偏向于前端,不知道是好是坏... JN…
扫描二维码继续阅读
2018-08-19