《Android开发艺术探索》之JNI和NDK编程(十六)

278 阅读5分钟

                                                                           第14章   JNI和NDK编程
JNI(Java Native Interface:java本地接口)是为了方便Java与C,C++等本地代码之间进行交互,Java的跨平台特性导致了本地交互的能力不够强大,故而提供JNI用于和本地交互。 
通过NDK可以在android中更加方便的通过jni来访问本地代码。比如c/c++,ndk还提供了交叉编译器,简单修改mk文件即可生成特定于CPU的动态库。优势如下:1.提高了代码的安全性;2.很方便使用目前已有的C/C++开源库;3.便于平台间的移植;4.提高程序在某些特定情形下的执行效率,但不能明显提升。JNI和NDK开发所用到的动态库是以.so为后缀的文件。
(1)JNI的开发流程
步骤一:Java中声明Native方法;

public class JniTest {
    static {
        System.loadLibrary("jni-test");
    }
    public static void main(String args[]){
        JniTest jniTest = new JniTest();
        System.out.println(jniTest.get());
        jniTest.set("hello world");
    }
    public native String get();
    public native void set(String str);
}

          步骤二:编译Java源文件得到class文件,通过javah导出JNI头文件

   Javac JniTest.java
   //JDK8以下
   Javah JniTest.java
   //JDK10
   Java -h . JniTest.java

        此时会产生头文件:

#include <jni.h>
/* Header for class com_nwu_hzk_myapplication_JniTest */

#ifndef _Included_com_nwu_hzk_myapplication_JniTest
#define _Included_com_nwu_hzk_myapplication_JniTest
#ifdef __cplusplus
//内部采用C语言的命名风格来编译
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_nwu_hzk_myapplication_JniTest_get
  (JNIEnv *, jobject);

 //函数名的格式为Java_包名_类名_方法名,比如jniTest中的set方法,
  /*这里只需要知道Java的String对应的JNI的jstring,
  JNIEXPORT,JNICALL,JNIEnv和jobject都是JNI标准定义的类型或者宏,它们的含义:
    1.JNIEnv*:表示一个指向JNI环境的指针,可以通过他来访问JNI提供的接口方法
    2.jobjct:表示Java对象的this
    3.JNIEXPORT,JNICALL:他们是JNI所定义的宏,可以在jni.h这个头文件中查看
*/
JNIEXPORT void JNICALL Java_com_nwu_hzk_myapplication_JniTest_set
  (JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif

         步骤三:实现JNI方法:

#include "com_nwu_hzk_myapplication_JniTest.h"
#include "stdio.h"
JNIEXPORT jstring JNICALL Java_com_nwu_hzk_myapplication_JniTest_get
  (JNIEnv *env, jobject thiz){
      printf("invoke get from C\n");
      return (*env)->NewStringUTF(env,"Hello from jni!");
  }

  JNIEXPORT void JNICALL Java_com_nwu_hzk_myapplication_JniTest_set
    (JNIEnv *env, jobject thiz, jstring string){
    printf("invoke set from C\n");
     char *str = (char*)(*env)->GetStringUTFChars(env,string,NULL);
    printf("%s\n",str);
    (*env)->ReleaseStringUTFChars(env,string,str);
    }

         步骤四:编译So库并在Java中调用,So库编译需要gcc,其指令如下所示:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
Java程序中调用so库,通过Java指令来执行Java程序,指明so库的路径:Java-Djava.library.path = JniTest。
(2)NDK的开发流程
NDK开发是基于JNI的,包括以下步骤:
步骤一:下载并配置NDK,注意下载r20可能会报确保错误,尽量选择低版本的譬如:NDK Version 16,NDK Version 17等。配置进环境变量。
步骤二:声明所需要的native方法,声明调用的so文件。

public class MainActivity extends AppCompatActivity {
    @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());
    }
    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }
}

       步骤三:编写CPP文件,实现项目中声明的Native类型的stringFronJNI方法。

#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_hzk_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

       步骤四:编译CMakeLists文件,里面声明如何编译。

cmake_minimum_required(VERSION 3.4.1)
add_library( # Sets the name of the library.
             native-lib
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

find_library( # Sets the name of the path variable.
              log-lib
              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

target_link_libraries( # Specifies the target library.
                       native-lib
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

       步骤五:build.gradle中包含CMakeList.txt这一文件,确保使用cmake对其进行编译。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "29.0.0"
    ....
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

dependencies {
   .....
}

          以上步骤都是由ndk自动生成的,自己按照自己的需要可以进行更改。
(3)JNI的数据类型和类型签名
JNI的数据类型主要有两种:基本类型和引用类型。基本类型为jboolean、jchar、jint等,它们和java中的类型一一对应。JNI中的引用类型包括类、对象和数组。

              

              
JNI的签名(或者称为Dalviik字节码)表示了一个特定的Java类型,可以类和方法,也可以是数据类型。类的签名比较简单,它使用了L+类名+包名+;的方式,将其中的.替换为/即可。譬如java.lang.String,应当改为Ljava/lang/String;
基本数据类型的签名采用一些列大写字母来表示。

            
举例来说:多维数组Int[][],可以写成[[I;boolean fun1(int a,String b,int []c);它的签名是(ILjava/lang/String;[I)Z;void fun1(int i);它的签名是(I)V。
(4)JNI调用Java方法的流程
JNI调用Java方法的流程是:先通过类名找到类,在通过方法名找到方法id,最后可以调用这个方法。如果是非静态对象方法,则需要构造出类的对象指后才能调用它,下面例子演示了如何在JNI中调用Java的静态方法。
步骤一:Java中定义静态方法供JNI调用。

public class MainActivity extends AppCompatActivity {
    private static String TAG = "MainActivity";
    @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 native String stringFromJNI();
    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }
    public static void methodCalledByJNI(String msgFromJni) {
        Log.d(TAG, "methodCalledByJNI: "+msgFromJni);
    }
}

        步骤二:在cpp中首先根据类名com/example/hzk/myapplication/MainActivity找到类,再根据类名、方法名、方法签名找到方法id,其中(Ljava/lang/String;)V为方法签名,最后通过env->CallStaticVoidMethod完成最终的调用过程。

#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_hzk_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    jclass clazz = env->FindClass("com/example/hzk/myapplication/MainActivity");
    if(clazz == NULL){
        printf("find the mainActivity error!");
        return NULL;
    }
    jmethodID id = env->GetStaticMethodID(clazz,"methodCalledByJNI","(Ljava/lang/String;)V");
    if(id == NULL){
        printf("find the methodCalledByJni error!");
    }
    jstring msg = env->NewStringUTF("msg from native stringFromJni.cpp");
    env->CallStaticVoidMethod(clazz,id,msg);
    return env->NewStringUTF(hello.c_str());
}

        最终的结果如下: