Android中Java代码与C的互相调用(JNI的简单使用)

引言

最近在做项目的时候,接触到JNI,想一想自己第一次接触这个东西的时候,还是好久之前,现在既然接触到了,那我就简单的跟大家讲一讲JNI的基本使用方法。

JNI(Java Native Interface):java本地开发接口,JNI是一个协议,这个协议用来沟通java代码和外部的本地代码(c/c++),外部的c/c++代码也可以调用java代码。

我们为什么要使用JNI呢,可以从效率和安全性两方面来说:

1. 安全性:java是版解释型语言,很容易比反编译拿到源代码,我们一些加密方面的问题,就可以用JNI来实现,

2. 效率:C/C++是本地语言,比java更高效。

做JNI,我们先的下载Android NDK(Native Development Kit )下载链接:(https://developer.android.google.cn/ndk/downloads/index.html),Android NDK是一套工具集合,允许你用像C/C++语言那样实现应用程序的一部分。

JNI和NDK的区别:

从工具上说,NDK其实多了一个把.so和.apk打包的工具,而JNI开发并没有打包,只是把.so文件放到文件系统的特定位置。

从编译库说,NDK开发C/C++只能使用NDK自带的有限的头文件,而使用JNI则可以使用文件系统中带的头文件。

从编写方式说,它们一样。

知识前瞻

Java类型和本地类型的对应关系:

Java类型 本地类型(JNI) 描述
boolean(布尔型) jboolean 无符号8个比特
byte(字节型) jbyte 有符号8个比特
char(字符型) jchar 无符号16个比特
short(短整型) jshort 有符号16个比特
int(整型) jint 有符号32个比特
long(长整型) jlong 有符号64个比特
float(浮点型) jfloat 32个比特
double(双精度浮点型) jdouble 64个比特
void(空型) void N/A
先就看这么多吧,网上也有很多大神写的博客,写得很好,有时间可以去看看,参考博客:https://blog.csdn.net/yuzhou_zang/article/details/78410632 ,这里我只是教大家如何使用Java与C/C++的互相调用。

实战

在这里我给大家做的例子是Java调用C语言和C语言调用Java来实现加法操作。

二话不说,开干,新建Android工程。XMl界面定义如下:

XML代码
















MainActivity代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText mEt1;
    private EditText mEt2;
    private TextView mResult;
    private int mNumber1;
    private int mNumber2;

    //加载本地C语言文件库。库名字为你写的C语言文件名
    static {
        System.loadLibrary("Test");
    }

    //调用本地C语言方法计算结果
    public native int getResult(int number1, int number2);

    //调用本地C语言方法
    public native int callCMethod(int number1, int number2);

    //本地C语言调用Java方法
    public static int calculateResult(int number1, int number2){
        return number1 + number2;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
    }

    private void initView(){
        mEt1 = findViewById(R.id.et1);
        mEt2 = findViewById(R.id.et2);
        mResult = findViewById(R.id.result);
        Button calCMethod = findViewById(R.id.calCMethod);
        calCMethod.setOnClickListener(this);
        Button calCToJavaMethod = findViewById(R.id.calCToJavaMethod);
        calCToJavaMethod.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.calCMethod) {
            getNumber();
            int result = getResult(mNumber1, mNumber2);
            if (mNumber1 + mNumber2 != result){
            Toast.makeText(this, "结果错误了!调用本地C语言失败!", Toast.LENGTH_LONG).show();
        }else {
            Toast.makeText(this, "结果正确!调用本地C语言成功!", Toast.LENGTH_LONG).show();
        }
        mResult.setText(String.valueOf(result));
    	}else if (id == R.id.calCToJavaMethod){
            getNumber();
            int result = callCMethod(mNumber1, mNumber2);
            if (mNumber1 + mNumber2 != result){
                Toast.makeText(this, "结果错误了!本地C语言调用Java方法失败!", Toast.LENGTH_LONG).show();
            }else {
                Toast.makeText(this, "结果正确!本地C语言调用Java方法成功!", Toast.LENGTH_LONG).show();
            }
	            mResult.setText(String.valueOf(result));
	    }

    }
    
    private void getNumber(){
        String text1 = mEt1.getText().toString().trim();
        String text2 = mEt2.getText().toString().trim();
        if (text1.equals("")) {
            text1 = mEt1.getHint().toString().trim();
        }
        if (text2.equals("")) {
            text2 = mEt2.getHint().toString().trim();
        }
        mNumber1 = Integer.parseInt(text1);
        mNumber2 = Integer.parseInt(text2);

    }
}

上述都是入门级别的代码了,除了ConstraintLayout(参考https://www.jianshu.com/p/17ec9bd6ca8a)使用较少之外,其他的so easy!不过ConstraintLayout没有使用过的小伙伴,建议大家学习和使用一下。

我们接下来将围绕这段代码展开分析

   //加载本地C语言文件库。
    static {
        System.loadLibrary("Test");
    }

    //调用本地C语言方法计算结果
    public native int getResult(int number1, int number2);

    //调用本地C语言方法
    public native int callCMethod(int number1, int number2);

    //本地C语言调用Java方法
    public static int calculateResult(int number1, int number2){
        return number1 + number2;
    }

Java调用本地C语言

Java文件中定义public native int getResult(int number1, int number2)这样的一个native方法,接下来我们需要调用javah命令来自动生成本地C语言头文件(javah -v -d F:\AndroidProject\JniLibsDemo\app\src\main\jni -jni com.wiky.jnilibsdemo.jni)。javah命令使用帮助


或者你也可以使用AS扩展工具, 自定义一些命令行工具。参考https://www.jianshu.com/p/9cb8514d1ba0
生成的头文件文件如下:

这个时候我们在jni目录下新建一个Test.c的文件。
Test.c的代码如下

//引用我们生成的头文件
#include

JNIEXPORT jint JNICALL Java_com_wiky_jnilibsdemo_MainActivity_getResult
    (JNIEnv *env, jobject obj, jint number1, jint number2) {
return number1 + number2;
}

这个时候要想java调用本地语言,我们还需要添加一个Android的.mk配置文件,例如Android.mk

LOCAL_PATH       :=  $(call my-dir)
include              $(CLEAR_VARS)
LOCAL_MODULE     :=  Test        //对应加载的文件库名字(System.loadLibrary("Test"))
LOCAL_SRC_FILES  :=  Test.c      //C语言文件名
include              $(BUILD_SHARED_LIBRARY)

有的人以为这样就可以了,有没有发现我们还咩有配置Android运行本地语言的环境,而且发现我们的Android.mk文件还没用到么,点击运行,报错如下:

Error: Your project contains C++ files but it is not using a supported native build system.
Consider using CMake or ndk-build integration. For more information, go to:
 https://d.android.com/r/studio-ui/add-native-code.html
Alternatively, you can use the experimental plugin:
 https://developer.android.com/r/tools/experimental-plugin.html

这个时候我们就需要配置一下

这样我们就关联起来了,或者在模块及build.gradle配置

externalNativeBuild {
    ndkBuild {
        path file('src/main/jni/Android.mk')
    }
}

点击同步,就可以了。我们运行一下

运行OK,大功告成!

本地C语言调用Java方法

同样的操作,我们利用我们输入的数字传入到C,在C中调用java方法计算得数,

定义这两个方法

  //调用本地C语言方法
    public native int callCMethod(int number1, int number2);

    //本地C语言调用Java方法
    public static int calculateResult(int number1, int number2){
        return number1 + number2;
    }

头文件中的方法定义这里就不再叙述,按照上述方法生成即可,我们接下来看Test.c文件中的实现

JNIEXPORT jint JNICALL Java_com_wiky_jnilibsdemo_MainActivity_callCMethod
    (JNIEnv *env, jobject obj, jint number1, jint number2) {
    char *classname = "com/wiky/jnilibsdemo/MainActivity";
    jclass jClazz = (*env)->FindClass(env, classname);
    //这里实现了互相调用,Java中调用了C的getStringFormc方法传递了x,y参数,这里C又调用了Java的add方法将x,y回传回去求和;
    jmethodID methodID = (*env)->GetStaticMethodID(env, jClazz, "calculateResult", "(II)I");
    jint result = (*env)->CallStaticIntMethod(env, jClazz, methodID, number1, number2);
    return result;
}

这段代码的就是利用反射获取到Java中的方法,然后去调用。在这里我要讲的是GetStaticMethodID方法中的第四个参数"(II)I",这个代表的是调用Java方法的的方法签名,关于签名,如下:

java类型 Signature 备注
boolean Z
byte B
char C
short S
int I
long L
float F
double D
void V object L用/分割的完整类名 例如: Ljava/lang/String表示String类型 Array [签名 例如: [I表示int数组, [Ljava/lang/String表示String数组 Method (参数签名)返回类型签名 例如: ([I)I表示参数类型为int数组, 返回int类型的方法

如本例中的函数声明:

JNIEXPORT jint JNICALL Java_J2C_write2proc(JNIEnv *, jobject, jint);

注释中的签名是 Signature: (I)I

通过上述的表格之后我们很自然的清楚上述代码的意思,点击运行

看到这里,比较欣喜,真正的打完收工。本人C++的已经忘记了,所以。。。。。。

扩展

如果你需要将本地语言移植到另一个项目,你可以考虑将本地语言编译成.so文件,首先添加.mk文件,例如Application.mk文件(参考https://blog.csdn.net/kwuwei/article/details/21718097)

APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := all
APP_PLATFORM := android-8

利用AS扩展工具, 自定义ndk-build命令,运行命令就可以看到.so文件了,或者cd到Application.mk的文件所在目录,输入ndk-build运行,这个命令在你的ndk文件夹中,记得配置环境变量。

总结

这些只是简单的JNI的开发使用,真正的实现,可以去参考大神们的博客。上述如有问题,可以给我留言,欢迎大家批评指正!谢谢大家!

你可能感兴趣的:(android)