由于工作上的需求需要使用java和c++互调实现功能,所以要对jni进行深入研究,故此入坑。对安卓也比较感兴趣,大学里还做过几个APP,现在已经很久没有写界面布局这方面的了...
JNI全程Java Native Interface,意为Java本地调用,它允许Java代码和其他语言写的代码进行交互,简单的说,一种在Java虚拟机控制下执行代码的标准机制。 可以用它实现java和c语言互调。对于初学者来讲,很容易吧jni和ndk的概念搞混淆(当然也可能只有博主一个人o(╯□╰)o),那jni和ndk的区别到底是什么?
Android NDK(Native Development Kit )是一套工具集合,允许你用像C/C++语言那样实现应用程序的一部分。 简单的说,NDK其实多了一个把.so和.apk打包的工具,而JNI开发并没有打包,只是把.so文件放到文件系统的特定位置。可以将NDK看做是Google提供的一个打包工具,方便开发者使用,有了这个工具,我们只需要关注代码的具体实现,而不需要关注如何编译动态链接库。
先看看jni中的数据类型:
函数操作(只列出了一些常用的):
函数 | Java数据类型 | 本地类型 | 函数说明 |
---|---|---|---|
GetBooleanArrayElements | boolean | jboolean | 需要调用ReleaseBooleanArrayElements 释放 |
GetByteArrayElements | byte | jbyte | 需要调用ReleaseByteArrayElements 释放 |
GetCharArrayElements | char | jchar | 需要调用ReleaseShortArrayElements 释 |
GetObjectArrayElement | 自定义对象 | jobject | |
SetObjectArrayElement | 自定义对象 | jobject | |
New<Type>Array | 创建一个指定长度的原始数据类型的数组 | ||
NewStringUTF | jstring类型的方法转换 | ||
DeleteLocalRef | 删除 localRef所指向的局部引用 | ||
DeleteGlobalRef | 删除 globalRef 所指向的全局引用 | ||
GetMethodID | 返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID() 可使未初始化的类初始化。要获得构造函数的方法 ID,应将<init> 作为方法名,同时将void (V) 作为返回类型。 | ||
GetStaticMethodID | 调用静态方法 | ||
CallVoidMethod | 调用实例方法 | ||
Call<type>Method |
首先得有ndk的环境,环境配置很简单,博主就不在这里演示了。直接新建一个工程,勾选上c++支持:
然后看看Android Studio给我们生成了什么:
#####初识cmake
makefile
文件编译,Windows 下会使用 project
文件编译。而 CMake
则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt
)生成 对应 makefile
或 project
文件,然后再调用底层的编译。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文件,都需要在这里申明一下,是不是有点像清单文件的感觉。
关于cmake的具体使用,网上有很多教程,博主就不多说了。
然后就是.cpp文件里的内容了:
#include <jni.h>
#include <string>
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());
}
一个一个分析。
cpp文件也讲完了,现在看看MainActivity里的代码:
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();
}
只需要将一下那个静态代码块,loadLibrary的时候,本来生成的.so文件为libnative-lib.so但是这里没有加是android studio会自动给我们加上去,如果这里再加上就会重复,所以只需要填写和CMakeLists.txt里的命名相同就行了。
首先在module级的build.gradle里加入:
defaultConfig {
ndk{
ldLibs "gomp"
}
}
然后在cpp中加入如下的宏定义:
#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
搞定,这个是固定写法,没什么好说的。
这个比较简单,这里就随便提一下,首先我新建了一个Hello类,写了两个方法,android studio会提示是否生成方法:
生成方法之后我只加了两句打印:
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);
}
然后在相应的地方调用一下,我是在MainActivity中调用的:
然后看一下后面的重点,c++中调用java层的方法和修改java层的属性。
在学习c++调用java方法时需要了解的是方法签名,关于方法签名,我觉得只要关注这两个地方就行了:
下面有请方法签名规则表开始表演:
Java类型 | 签名类型 |
---|---|
boolean | Z |
byte | B |
char | C |
long | J |
float | F |
double | D |
short | S |
int | I |
类 | L全限定类名 |
数组 | [全限定类名 |
上述中类的签名规则是:”L+全限定类名+;”三部分组成,其中全限定类名以”/”分隔,而不是用”.”或”_”分隔。
比如刚刚说的那两个方法:
有迷妹私信我了:这么复杂的吗?有没有简单快捷的方法,每次都这么麻烦,太浪费时间了吧!我的时间很宝贵的嘤嘤嘤,要是没有我砍死你
很大方的(迫不得已)交出偷懒方法:
javap -s 类的.class路径
可以说是很直观了(逃),博主用的as3.1,所以这个目录在工程,目录\module目录\build\intermediates\classes\debug下面。得到方法签名之后,就可以开始下面的操作了
在java中写了一个这样的方法:
public static void staticMethod(String data){
logMessage(data);
}
public static void logMessage(String data){
Log.d("hello", data);
}
我希望在cpp中调用staticMethod方法,该怎么做呢?先贴代码:
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层方法的调用。
最后不要忘记删除引用,不然会发生内存泄漏。
和静态方法的区别就两个地方,一个是GetStaticMethodID,一个是CallStaticVoidMethod:
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 ;
}
首先在java类中定义一个变量:
public String name = "im is java";
然后贴上jni代码,主要方法是GetFieldID,第一个参数传入变量所在类,第二个参数是变量名,第三个参数是签名类型:
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;
}
同理,静态变量也没啥好讲的了,这里就贴一下代码:
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的编译使用(
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有