zoukankan      html  css  js  c++  java
  • Android Native jni 编程入门

    在某些情况下,java编程已经不能满足我们的需要,比如一个复杂的算法处理,这时候就需要用到jni(java native interface)技术;

    • jni 其实就是java和c/cpp之间进行通信的一个接口规范,java可以调用c/cpp里面的函数,同样,c/cpp也可以调用java类的方法;

    jni开发工具ndk的安装:
    在最新的ndk版本中,安装ndk很简单,只需要装ndk的路径配置到系统环境变量中即可;
    在编译的时候,进入工程根目录;执行命令  ndk-build  即可完成编译;

    下面就通过一个例子一步一步的来初步学习jni

    一、HelloWorld

    新建一个工程,你甚至不需要其它额外的设置,然后在工程中添加一个jni目录,然后就可以开始了;

    1.新建一个java类HelloWorld.java

    复制代码
    package com.jni;
    
    public class HelloWorld {
        static {
            System.loadLibrary("helloworld");
        }
    
        public native String helloworld();
    }
    复制代码

    在HelloWorld中,定义了一个方法helloworld(),只不过这个方法被申明成了native的,并没有具体的实现,具体功能我们在接下来的cpp文件中实现;

    2.在jni目录下添加一个helloworld.cpp

    复制代码
    #include <jni.h>
    #include <android/log.h>
    #include <string.h>
    
    #ifndef _Included_com_jni_HelloWorld // 1
    #define _Included_com_jni_HelloWorld
    
    #ifdef __cplusplus // 2
    extern "C" {
    #endif // 2
    JNIEXPORT jstring JNICALL Java_com_jni_HelloWorld_helloworld(JNIEnv *, jobject);
    #ifdef __cplusplus // 3
    }
    #endif // 3
    #endif // 1
    
    JNIEXPORT jstring JNICALL Java_com_jni_HelloWorld_helloworld(JNIEnv * env,
            jobject obj) {
        return env->NewStringUTF("helloworld");
    }
    复制代码

    从上面这个cpp文件中可以很明白的看出,它有一个方法,具体包括方法申明和方法实现两个部分;但是相信大家也都看出来了,方法的命令很怪异,怎么这么长的方法名?

    我们在这里先思考一个问题,java类中的方法是如何调用c++中的方法的呢?要解决这个问题,就得先来看这个长长的方法名;

    其实这是jni的一个规范之一,用于映射java方法和c/c++中的方法对应;

    再来看在cpp中定义的函数名:Java_com_jni_HelloWorld_helloworld

    其实不难看出,java文件与cpp文件中函数名的配对定义方式为Java + 包名 + java类名 + 方法/函数名,中间用_分隔;其中两个参数分别是:

      • env:当前该线程的内容,包含线程里面全部内容;
      • obj:当前类的实例,指.java文件的内容(在该例子中即是HelloWorld类);

    这里的helloworld方法,其实就只是返回了一个单词"helloworld";

    3.在jni目录下添加一个Android.mk文件

    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE    := helloworld
    LOCAL_SRC_FILES := helloworld.cpp
    include $(BUILD_SHARED_LIBRARY)

    4.在命令行下进入工程目录执行 ndk-build 命令,然后运行程序,调用HelloWorld实例的helloworld方法就可以得到它的返回字符串了;

    二、jni调用Java类的方法(1)

    通过上面的helloworld练手之后,我们来看一下jni调用java类里面的方法的实现;

    1.新建设一个MethodCall.java文件如下

    复制代码
    public class MethodCall {
        final String tag = "MethodCall";
        static {
            System.loadLibrary("methodcall");
        }
    
        public native String jniCallMethod1();
    
        public native String jniCallMethod2();
    
        public native String jniCallStaticMethod();
    
        public void javaMethod1() {
            Log.e(tag, "javaMethod1");
        }
    
        public String javaMethod2() {
            Log.e(tag, "javaMethod2");
            return "javaMethod2";
        }
    
        public static void javaStaticMethod(String input) {
            Log.e("MethodCall", "" + input);
        }
    }
    复制代码

    该类有6个方法,其中有3个是java类的方法,另外3个是native方法,3个native方法分别去调用3个java方法;

    2.添加三个native方法具体实现 methodcall.cpp

    复制代码
    #include <jni.h>
    #include <android/log.h>
    #include <string.h>
    
    #ifndef _Included_com_jni_MethodCall
    #define _Included_com_jni_MethodCall
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallMethod1(JNIEnv *,
            jobject);
    JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallMethod2(JNIEnv *,
            jobject);
    JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallStaticMethod(JNIEnv *,
            jobject);
    #ifdef __cplusplus
    }
    #endif
    #endif
    
    JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallMethod1(JNIEnv * env,
            jobject obj) {
        jmethodID mid; // 方法标识id
        jclass cls = env->GetObjectClass(obj); // 类的对象实例
        mid = env->GetMethodID(cls, "javaMethod1", "()V");
        env->CallVoidMethod(obj, mid);
        return env->NewStringUTF("jniCallMethod1");
    }
    
    JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallMethod2(JNIEnv * env,
            jobject obj) {
        jmethodID mid; // 方法标识id
        jclass cls = env->GetObjectClass(obj); // 类的对象实例
        mid = env->GetMethodID(cls, "javaMethod2", "()Ljava/lang/String;");
        jstring js = (jstring) env->CallObjectMethod(obj, mid);
        return js;
    }
    
    JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallStaticMethod(
            JNIEnv * env, jobject obj) {
        jmethodID mid; // 方法标识id
        jclass cls = env->GetObjectClass(obj); // 类的对象实例
        mid = env->GetStaticMethodID(cls, "javaStaticMethod",
                "(Ljava/lang/String;)V");
        jstring input = env->NewStringUTF("jniCallStaticMethod->>javaStaticMethod");
        env->CallStaticVoidMethod(cls, mid, input);
        return env->NewStringUTF("jniCallStaticMethod");
    }
    复制代码

    该cpp文件中有3个方法(我这里把方法名都写得很明白直观,相信不需要注释都知道是调用的哪一个java方法)

    我们知道,在java编程中,对一个类的调用,其实是先创建一个类的对象实例,然后再调用它的方法(这里指的是非static方法) ,那么我们是如何在c/c++文件中调用java方法的呢?

    回到上面的HelloWorld,我们讲方法名的时候,下边有随便提到的方法的参数,其中,第二个参数obj其实就是我们在java中使用的类的实例,到这里,相信是如何调用java方法的大家都明白了吧;

    在java中,每一个方法其实都有一个id,我们在c/c++中不能直接通过obj来调用一个java方法,我们要先获取方法的id,通过GetMethodID()来获取,需要传入类的类型,方法名,方法的签名(方法签名在文章后面会讲到签名规则);然后再在线程里面调用java方法,通过env->Call****Method();需要传入对象实例,方法id,或者其它参数;(上面只展示了几个这种方法,其它的方法如果大家有需要用到可以自行查找资料解决);

    3.编写Android.mk文件,在Android.mk文件后面添加如下内容

    include $(CLEAR_VARS)
    LOCAL_MODULE    := methodcall
    LOCAL_SRC_FILES := methodcall.cpp
    include $(BUILD_SHARED_LIBRARY)

    4.执行ndk-build 命令,下面是分别执行3个jniCall****方法的结果

    三、jni调用Java类的方法(1)

    上面是c++调用java方法的例子,下面再帖一个c调用java方法的例子

    1.Java文件 MethodCall1.java

    复制代码
    package com.jni;
    
    public class MethodCall1 {
        static {
            System.loadLibrary("methodcall1");
        }
    
        public static int value = 0;
    
        public static void javaMethod() {
            value = 12;
        }
    
        public native int jniCalljavaMethod();
    }
    复制代码

    2.methodcall.c

    复制代码
    #include <string.h>
    #include <jni.h>
    
    jint Java_com_jni_MethodCall1_jniCalljavaMethod(JNIEnv* env, jobject thiz)
    //env:当前该线程的内容,包含线程全部的东西;thiz:当前类的实例,指.java文件的内容
    {
        jint si;
        jfieldID fid; // 一个字段,实际上对应java类里面的一个字段或属性;
        jclass cls = (*env)->GetObjectClass(env, thiz); // 类的对象实例
        jmethodID mid = (*env)->GetStaticMethodID(env, cls, "javaMethod", "()V"); // 一个方法的id
        //(I)V  (I)I
        if (mid == NULL) {
            return -1;
        }
        (*env)->CallStaticVoidMethod(env, cls, mid); //调用callback方法
        fid = (*env)->GetStaticFieldID(env, cls, "value", "I"); //取出value字段
        if (fid == NULL) {
            return -2;
        }
        si = (*env)->GetStaticIntField(env, cls, fid); //取出字段对应的值(fid字段对应的值)
        return si;
        //    return (*env)->NewStringUTF(env, "init success");
    }
    复制代码

    3.完善Android.mk文件,参照二里面第四步;

    4.运行代码

    MethodCall1 mc1 = new MethodCall1();
    Log.e(tag, MethodCall1.value + "->" + mc1.jniCalljavaMethod());

    四、方法签名规则 

    JNI类型签名规则
    Java类型 类型签名 Java类型 类型签名
    boolean Z long J
    byte B float F
    char C double D
    short S L全限定类名;
    int I 数组 [元素类型签名

    上面是各种数据类型对应的签名字符

    • 基本数据类型的签名很简单,只是一个选定的字母;
    • 类的签名规则是:"L" + 全限定类名+";"三部分组成,其中全限定类名以"/"分隔;

    方法的签名组成:"(参数签名)" + "返回值签名"

    例如Java方法long fun(int n, String str, int[] arr);
    根据上面的签名规则可以得到其签名为:(ILjava/lang/String;[I)J
    上面的签名分为两部分:括号里面为函数的参数,参数的内容分三部分"I","Ljava/lang/String;","[I",之间没有空格;括号外边是函数的返回类型签名。需要注意的是如果函数返回类型为void则其中返回类型签名为V;

    五、动态注册函数

    前面二和三都是c/c++里面方法的名称来映射函数,其实jni还为我们提供了动态注册函数的功能;

    1.添加java文件 DynamicRegisterMethod.java

    复制代码
    package com.jni;
    
    public class DynamicRegisterMethod {
        static {
            System.loadLibrary("dynamicregistermethod");
        }
    
        public native String dynamicRegisterMethod();
    }
    复制代码

    2.添加 c 文件 

    复制代码
    #include <string.h>
    #include <jni.h>
    
    #ifndef _Included_org_spring_SpringUtils
    #define _Included_org_spring_SpringUtils
    
    jstring JNICALL java_dynamicRegisterMethod(JNIEnv * env, jobject obj) {
        return (*env)->NewStringUTF(env, "dynamicRegisterMethod");
    }
    
    static JNINativeMethod gmethods[] = { { "dynamicRegisterMethod",
            "()Ljava/lang/String;", (void*) java_dynamicRegisterMethod } };
    
    static int registerNativeMethods(JNIEnv * env, const char* className,
            JNINativeMethod* gMethods, int numMethods) {
        jclass clazz;
        clazz = (*env)->FindClass(env, className);
        if (clazz == NULL)
            return JNI_FALSE;
        if (((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0)) {
            return JNI_FALSE;
        }
        return JNI_TRUE;
    }
    
    static int registerNatives(JNIEnv* env) {
        if (!registerNativeMethods(env, "com/jni/DynamicRegisterMethod", gmethods,
                sizeof(gmethods) / sizeof(gmethods[0]))) {
            return JNI_FALSE;
        }
        return JNI_TRUE;
    }
    
    jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        jint result = -1;
        JNIEnv* env = NULL;
        if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4)) {
            goto fail;
        }
        if (registerNatives(env) != JNI_TRUE) {
            goto fail;
        }
        result = JNI_VERSION_1_4;
        fail: return result;
    }
    
    #endif
    复制代码

    3.在Android.mk文件中进行编译的配置(省略,参考前面的例子)

    4.ndk-build编译项目 

    DynamicRegisterMethod drm = new DynamicRegisterMethod();
    Log.e(tag, drm.dynamicRegisterMethod());

    执行结果:

    可以看到通过动态注册方法的方式,也是成功的调用了 native 方法;

    六、加入链接库

    在程序开发过程中,会频繁的用到调试,方式有很多种,下面要讲的这一种是通过log打印信息来打印程序运行时的一些状态数值;

    修改Android.mk文件,添加一句代码

    include $(CLEAR_VARS)
    LOCAL_LDLIBS += -llog //LDLIBS:连接libs,后面跟的参数为需要链接的libs,-llog表示Android中的Log库;
    include $(BUILD_SHARED_LIBRARY)

    加入了log库之后,即可通过c/c++文件直接打印log信息;

    在c/c++中调用log打印输出信息:

    #include <android/log.h>
    __android_log_print(ANDROID_LOG_ERROR, "hello", "livingstone");    
    __android_log_print(ANDROID_LOG_DEBUG, "hello", "livingstone %d" ,23);

    例子拖管地址:

    https://github.com/a284628487/JniSample

  • 相关阅读:
    给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
    11
    实战 迁移学习 VGG19、ResNet50、InceptionV3 实践 猫狗大战 问题
    tx2系统备份与恢复
    如何在Ubuntu 18.04上安装和卸载TeamViewer
    bzoj 3732 Network (kruskal重构树)
    bzoj2152 聪聪可可 (树形dp)
    牛客 216D 消消乐 (二分图最小点覆盖)
    牛客 197E 01串
    Wannafly挑战赛23
  • 原文地址:https://www.cnblogs.com/AutumnRhyme/p/4321335.html
Copyright © 2011-2022 走看看