• 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

  • 相关阅读:
    利用window.onerror收集js代码异常
    js 基础题复习
    正则12和\1的理解
    CodeReview 方法 规范
    前端的登陆 几种类型
    http协议相关知识
    javascript中apply、call和bind的区别 详细易懂
    http常见的状态码,400,401,403 前端看
    vue中使用file-saver 下载各类文件
    js下载文件到本地各种方法总结
  • 原文地址:https://www.cnblogs.com/AutumnRhyme/p/4321335.html
Copyright © 2020-2023  润新知