Android开发艺术探索——第十四章:JNI和NDK编程

JNI的意思是Java Native Interface(java本地接口),它是为了方便java调用C,C++等本地代码所封装的一层接口,我们都知道,JAVA的优点是跨平台,但是作为有蒂娜的同时,其在本地交互的时候出现了短板,java的跨平台性导致了本地交互的能力不够强大,一些和操作系统相关的特性无法满足,这才出现了java JNI

NDK是android所提供的一个工具借,通过NDK可以在android中更加方便的通过jni来访问本地代码。比如c/c++,ndk还提供了交叉编译器,开发人员只需要简单的修改mk文件就可以生成特定的CPU动态库:

  • 1.提高了代码的安全性,由于so库反编译比较困难,因此NDK提高了Android程序的安全性
  • 2.可以很方便的使用目前已有的C/C++开源库
  • 3.由于平台间的移植,通过C/C++实现的动态库可以很方便的在其他平台使用
  • 4.提高程序在某些特定情形下的执行效率,但是并不能明显提升Android程序的性能

由于jni和ndk比较适合在linux环境下开发,这里也同样用ubuntu来说明

一.JNI的开发流程

JNI的开发流程有如下几个步骤,首先需要在JAVA中声明native方法,接着用C或者C++实现native方法,然后编译运行

1.在java中声明native方法

public class JniTest {

    static {
        System.loadLibrary("jni-test");
    }

    public static void main(String [] args){
        JniTest jniTest = new JniTest();
        System.out.print(jniTest.get());
        jniTest.set("Hello Jni");
    }

    public native String get();
    public native void set(String str);
}

可以看到代码,声明了两个native方法,get和set(string),这两个需要在JNI中实现,jniTest的头部有一个加载动态库的过程,其中jni-test是so库的标识,so库完整的名称为libjni-test.so,这是加载so库的规范。

2.编译Java源文件得到class文件,然后通过javah命令到处JNI的头文件

具体的命令

javac jniTest.java
javah jniTest

这样就会自动生成一个头文件,这里注意下,函数名的规则是:Java_包名类名方法名,比如jniTest中的set方法,到这里就变成 JNIEXPORT void JNICALL Java_com.liuguilin_jniTest_set(JNIEnv*,jobject,jstring),关于Java和Jni的数据类型之间的关系会在后面介绍,这里只需要知道Java的String对应的JNI的jstring,JNIEXPORT,JNICALL,JNIEnv和jobject都是JNI标准定义的类型或者宏,他们的含义:

  • JNIEnv*:表示一个指向JNI环境的指针,可以通过他来访问JNI提供的接口方法
  • jobjct:表示Java对象的this
  • JNIEXPORT,JNICALL:他们是JNI所定义的宏,可以在jni.h这个头文件中查看

下面的宏定义是必须的,他指定extern “C”内部的函数采用C语言的命名规范来编译,否则当JNI采用C++来实现时,由于命名风格不同,这将导致JNI在链接时无法根据函数名查找到具体的函数,那么JNI调用就无法完成,更多的细节实际上有关C和C++编译时的一些问题,这里就不展开了

#ifdef _cplusplus
extern "C"{
#endif

3.实现JNI方法

JNI方法是指Java中声明的native方法,这里可以选择C或者C++来实现,他们的实现过程都类似,只有少量的区别,下面分别使用C和C++来实现JNI的方法,首先,在工程里创建一个子目录,这里叫做jni,然后将之前通过javah生成的头文件复制尽力啊,接着创建test.cpp和test.c文件

test.cpp和test.c很类似,但是他们对env的操作方法有所不同,

C++:env->NewStringUTF("Hello from JNI!");
C:(*env)->NewStringUTF("Hello from JNI!");

4.编译so库并在Java中调用

so库的编译这里采用gcc,切换到jni目录中,对于test.cpp和test.c来说,他们的编译指令如下

C++:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.cpp -o libjni-test.so
C:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so

上面的语句中,/usr/lib/jvm/java-7-openjdk-amd64是本地的JDK安装路径,在其他环境编译时也可以将其指向本机的jdk路径即可,而libjni-test.so则是生成so库的名字,在JAVA中可以通过如下方式加载System.loadLibaray(“jni-test”),其中so库名字中的“lib”和“.so”是不需要明确指明的,so编译后,就可以调用了

首先采用C++产生so库的日志

invoke get in C++
Hello from JNI!
invoke set from C++
hello world

然后是C

invoke get from C
Hello from JNI!
invoke set from C
hello world

通过上面的例子可以方便的调用C、C++代码

二.NDK的开发流程

NDK的开发是基于JNI的

1.下载和配置NDK

首先要从Android官网下载NDK,然后配置环境变量

2.创建一个Android项目,声明native方法

public class JniActivity extends AppCompatActivity{

    static {
        System.loadLibrary("jni-test");
    }

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

        TextView textView = findViewById(R.id.mTextView);
        textView.setText(get());
        set("Hello JNI");
    }

    public native String get();
    public native void set(String str);
}

3.实现Android项目中所声明的native方法

在的外部创建jni目录,然后再jni目录下创建三个文件,test.cpp,Android.mk,Application.mk

//test-cpp

#include <jni.h>
#include <stdio.h>

#ifdef _cplusplus
extern "C"{
#endif

    jstring Java_com_liuguilin_androidsample_JniTestApp_JniActivity_get(JNIEnv*env,jobject thiz){
        printf("invoke get in c++ \n");
        return env->NewStringUTF("Hello JNI");
    }

    void Java_com_liuguilin_androidsample_JniActivity_set(JNIEnv*env,jobject thiz,jstring string){
       printf("invoke get in c++ \n");
       char* str = env -> GetStringUTFChars(string,NULL);
       printf("%s\n",str);
       env->ReleaseStringUTFChars(string,str);
    }

#ifdef _cplusplus
}
#endif


#Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := jni-test
LOCAL_SRC_FILES := test.cpp

include $(BUILD_SHARED_LIBRARY)


#Application.mk
APP_ABI := armeabi

这里对Android.mk和Application.mk做了简单的介绍,在Android.mk中,LOCAL_MODULE表示模块的名称,LOCAL_SRC_FILES表示需要参与编译的源文件,Application.mk中常用的配置项是APP_ABI,他表示CPU的加载平台的类型,目前市面上常见的架构平台是armeabi,x86,mips,其中在移动设备中有主要地位的是armeabi,这也是大部分apk中只包含armeabi平台的原因

4.切换到jni目录的父目录,然后通过ndk-build命令编译产生的so

这个时候NDK回切换一个和jni目录平级的目录,libs,在这面放置so,需要主要的是,ndk-build命令会默认jni目录为本地源码的目录,如果庅存放的目录名字不是jni则编译不通过

然后再创建一个jniLibs放进去即可

当然,我们需要配置一下build.gradle

    sourceSets.main{
        jniLibs.srcDir 'src/main/jniLibs'
    }

除了手动使用ndk-build命令创建so库,还可以通过AS来自动编译,不过就比较复杂了,为了让AS自动编译JNI,我们需要配置NDK的选项

  ndk{
            modelName "jni-test"
     }

这样就可以自动编译JNI代码了,但是这个时候AS会把所有平台的so都打包到apk中,我们一般要配置一下的

    productFlavors{
        arm{
            ndk{
                abiFilter "armeabi"
            }
        }
        x86{
            ndk{
                abiFilter "x86"
            }
        }

三.JNI的数据类型和类型签名

JNI的数据类型包含两种,基本类型和引用类型,基本类型主要有jboolean,jchar,jint等,他们和Java中的数据类型对应关系

这里写图片描述

JNI中的引用类型主要有类,对象和数组,他们在java中的引用类型的对应关系如图:

这里写图片描述

JNI的类型签名标识了一个特定的JAVA类型,这个类型即可是方法,也可以是数据类型

类的签名比较简单,采用了L+包名+类名+.的形式,只需要将其中的.替换为/即可,比如java.lang.String,他的签名是Ljava/lang/String,注意末尾也是签名的一部分

基本数据类型采用的是一系列大写字母表示:

这里写图片描述

从表可以看出,基本数据类型的签名是有规律的,一般是首字母的大写,但是boolean除外,因为B已经被byte占用,而long的签名之所以不是L,那是因为L表示的是类的签名

对象和数组的签名稍微复杂一些,对于对象来说,他的签名就是对象所属的类的签名,比如String对象,他的签名是Ljava/lang/String,对于数组来说,他的签名为【+类型签名,比如int数组,其类型为int,而int的签名为I,所以int数组的签名为【I,同理就是可以得出如下的签名对应关系

char[] [C
float[] [F
double[] [D
long[] [J
String[] [Ljava/lang/String
Object[] [Ljava/lang/Object

对于多维数组来说,他的签名为n+[+类型签名,其中n表示数组的维度,比兔int[][]的签名[[I

方法的签名为(参数类型签名)+返回值类型签名,这有点不好理解,举个例子,如下方法:boolean fun1(int a,double b,int[]c),根据签名的规定可以知道,他的参数类型的签名连在一起读ID[I,返回值类型的签名为Z,所以整个办法的签名就是(ID[I)Z。再举例子:

int fun1()  签名为()I
void fun1(int i) 签名为(I)V

四.JNI调用JAVA方法的流程

JNI调用Java方法的流程是闲通过类名找到类,然后再根据方法名找到方法的id,最后就可以调用这个方法了,如果调用JAVA中的费静态方法,那么需要构造出类的对象后才能调用
,下面的例子演示了如何在JNI中调用java的静态方法,至于非静态的只是多了异步构造队形的过程

首先需要在java中定义一个静态方法供JNI调用

    public static void methodCalledByJni(String msg) {
        Log.i(TAG, "methodCalledByJni:" + msg);
    }
    void callJavaMethod(JNIEnv * ev,jobject thiz){
        jclass clszz = env->FindClass("com/liuguilin/androidsample/JniActivity");
        if(clazz == NULL){
            return;
        }
        jmethodID id = env->GetStaticMethodID(clszz,"methodCalledByJni","(LJava/lang/String;)V");
        if(id == NULL){
            printf("error");
        }
        jstring msg = env->NewStringUTF("msg send by callJavaMethod in test.cpp");
        env->CallStaticVoidMethod(clazz,id,msg);
    }

从callJavaMethod的实现可以看出,程序会根据类名找到类,然后再去找这个方法,接着完成最终的调用,最后在get中的使用:

    jstring Java_com_liuguilin_androidsample_JniTestApp_JniActivity_get(JNIEnv*env,jobject thiz){
        printf("invoke get in c++ \n");
        callJavaMethod(env,thiz);
        return env->NewStringUTF("Hello JNI");
    }

由于MainActivity会调用JNI中的get方法,set方法优惠调用callJavaMethod方法,而callJavaMethod方法又会反过来调用,这样就完成了一次从Java调用JNI然后从JNI中调用Java的方法,安装运行程序,可以看出,已经调用成功了

我们可以发现,JNI调用java过程和java方法的定义有很大的关联,针对不同类型的java方法,JNIEnv提供了不同的接口和调用,这只是一个初步的介绍,更多的需要读者自己去查阅

猜你喜欢

转载自blog.csdn.net/qq_26787115/article/details/81031662