Android jni/ndk编程一:jni初级认识与实战体验

Android平台很多地方都可以看到jni的身影,比如之前接触到一个投屏的项目,主要的代码是c/c++写的,然后通过Jni供Java层调用;另外,就拿Android系统中的Service来说,很多的Service都有java层代码和native层代码组成,native层代码会在android启动的过程中完成向java层的注册。总之,由于无法甩开jni的身影,所以我打算花点时间系统的学习下Android下的jni开发。

一.开发工具

所谓工欲善其事,必先利其器,在学习android系统的jni编程之前,先了解下jni编程使用的工具。

1.1NDK(Native Development Kit)

NDK翻译过来就是本地代码开发工具集,本地代码主要指c/c++,因此,我们的c/c++代码可以使用NDK中提供的工具完成编译,我们可以把C/C++代码编译成动态库,然后在java层访问动态库,这样就是了java调用C/C++的功能。NDK众多的工具中,ndk-build主要用来编译native代码,它在windows和Linux平台下均有响应的版本可以使用。它的用法似乎和在android源码下编译一个模块使用mm命令很相似。之所以说他们相似是因为他们都需要一个Android.mk文件,而且文件的格式完全一样,比如说有如下Android.mk:

      LOCAL_PATH := $(call my-dir)
      include $(CLEAR_VARS)

      LOCAL_MODULE_TAGS := optional
      LOCAL_PRELINK_MODULE := false


      LOCAL_SRC_FILES := hello.c
      LOCAL_MODULE := hello
      include $(BUILD_EXECUTABLE)

我们在Android源码目录下使用mm命令编译该模块和在windows下使用ndk-build编译该模块都能产生libhello.so库,表面上还真看不出差别。 
使用ndk-build编译native代码时,除了需要Android.mk文件之外,可能有必要添加一个Application.mk,这个文件通常是由一行:

APP_ABI := x86

这里我们指明了需要编译的二进制库的格式。ABI(Application Binary Interface)与处理器相关,对于arm处理器,APP_ABI 可能要配置成 armeabi ,对于mips处理器,APP_ABI应该配置为mips,当然,我们还可以一次生成所有平台的库,此时只需要给APP_ABI赋值ALL就可以了。

二.jni的初步认识

2.1 JNI的作用

JNI(Java Native Interface)它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。以上是百度百科上copy的话,也算是交代了下JNI的作用吧。

2.2 JNI使用流程

我们使用JNI的起点一般都是System.loadLibrary(“xxx”);开始的,xxx代表了需要加载的库名。可以认为是它加载我们c/c++代码到虚拟机中,这样,我们的Java虚拟机就知道了c/c++中的函数了,之后,我们就可以调用它。 
因此,使用jni只需两步: 
1.首先,我们要有一个动态库,这个库我们可以使用ndk-build来编译生成。 
2.其次,我们需要使用System.loadLibrary(“xxx”)来加载这个库,加载完成后,就可以和本地代码交互了。 
2.3 查阅一个使用JNI的c文件 
为了认识JNI,找一个使用JNI的文件,比如:android-ndk\android-ndk-r10\samples\hello-gl2\jni\gl_code.cpp:

...
extern "C" {
    JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_init(JNIEnv * env, jobject obj,  jint width, jint height);
    JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_step(JNIEnv * env, jobject obj);
};

JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_init(JNIEnv * env, jobject obj,  jint width, jint height)
{
    setupGraphics(width, height);
}

JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_step(JNIEnv * env, jobject obj)
{
    r

这里节选了其中的一些,我们会发现其中有很多奇怪的字段,比如JNIEXPORT 、JNICALL等,所以,接下来,我们得先搞清楚它们的意义。

2.4 JNIEXPORT 和 JNICALL

这两个字段定义在jni.h中,定义如下:

#define JNIIMPORT
#define JNIEXPORT  __attribute__ ((visibility ("default")))
#define JNICALL __NDK_FPABI__

因为在在Windows中编译dll动态库时,如果动态库中的函数要被外部调用,需要在函数声明中添加 attribute ((visibility (“default”)))标识,表示将该函数导出在外部可以调用。在Linux/Unix系统中,这两个宏可以省略不加。因为在Linux/Unix平台下,这两个宏为空,所以加不加都没关系,当然还是建议加上哈,这样linux下的代码就可以直接拿到linux下编译了。

2.4 extern “C”

extern “C”从字面上看有两部分的内容:extern和“C” 
extern是编程语言中的一种属性,它表征了变量、函数等类型的作用域(可见性)属性,是编程语言中的关键字。当进行编译时,该关键字告诉编译器它所声明的函数和变量等可以在本模块或者文件以及其他模块或文件中使用。 
“C”表明了一种编译规约。 
因此,extern “C”表明了一种编译规约,其中extern是关键字属性,“C”表征了编译器链接规范。 
使用extern “C” 声明的函数将采用C语言的编译方式编译,也就是说只有在C++代码中extern “C”才有意义,之所以这样显示声明适应C语言的编译方式编译该代码块,是因为c和c++是有差异的,举例来说,有如下函数: 
void hello(int,int); 
这个函数在C编译器中,它的函数名师_hello,而在c++编译器中它的函数名是hello_int_int,之所以这样是因为c++支持函数重载,函数名可以相同,表征一个函数的除了函数名还有函数的参数列表。这因为有如此不同,因此我们可以想象如下情景: 
加入我要在c++中调用一个c函数 
1.首先,我要在hello.h中声明hello(int,int)函数,然后在对应的.c文件中实现它。 
2.c++文件需要包含hello.h文件,然后执行hello(1,1);完成调用。 
那么此时c编译器生成的函数名为_hello。而c++编译器会寻找_hello_int_int的函数名,这不就找不到了吗? 
因此,extern “C”主要用于c++代码调用c代码时,避免出现函数找不到的问题。

三.JVM查找native代码的简要过程

JVM查找native方法有两种方式: 
1.静态方式:按照JNI规范的命名规则 
2.动态方式:调用JNI提供的RegisterNatives函数,将本地函数注册到JVM中。

3.1静态方式

静态方式使用的是按照JNI的命名规范来查找native函数,JNI函数命名规则为: 
Java_类全路径_方法名 
比如我们打算向com.jinwei.hellotest包中的MainActivity类注册名为sayHello的方法,那么,我们的函数命名就应该为:Java_com_jinwei_jnitesthello_MainActivity_sayHello

3.2动态方式

了解动态注册就要涉及到System.loadLibrary函数的工作流程了,这个函数打开一个动态库后,会找到JNI_OnLoad这个函数的地址,然后调用这个函数,因此我们可以在这个函数中完成向JVM注册native方法的工作。 
比如,Android源码中有如下代码片段:

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        goto bail;
    }
    assert(env != NULL);

    if (register_android_media_ImageWriter(env) != JNI_OK) {
        ALOGE("ERROR: ImageWriter native registration failed");
        goto bail;
    }
    ...

JNI_OnLoad调用register_android_media_ImageWriter函数进一步注册native方法:

int register_android_media_ImageWriter(JNIEnv *env) {
...
    int ret2 = AndroidRuntime::registerNativeMethods(env,
                   "android/media/ImageWriter$WriterSurfaceImage", gImageMethods, NELEM(gImageMethods));

    return (ret1 || ret2);
}

该函数中使用AndroidRuntime::registerNativeMethods真正完成native方法的注册,这其中用到一个结构体:gImageMethods,其定义如下:

static JNINativeMethod gImageMethods[] = {
    {"nativeCreatePlanes",      "(II)[Landroid/media/ImageWriter$WriterSurfaceImage$SurfacePlane;",
                                                              (void*)Image_createSurfacePlanes },
    {"nativeGetWidth",         "()I",                         (void*)Image_getWidth },
    {"nativeGetHeight",        "()I",                         (void*)Image_getHeight },
    {"nativeGetFormat",        "()I",                         (void*)Image_getFormat },
};

它是一个函数映射表,前边是java层使用的函数名,后边是native层使用的函数名,中间是函数签名。 
签名是一种用参数个数和类型区分同名方法的手段,即解决方法重载问题。 
假如有下面Java方法: 
long f (int n, String s, int[] arr); 
签名后: “(ILjava/lang/String;[I)J” 
其中要特别注意的是: 
1. 类描述符开头的’L’与结尾的’;’必须要有 
2. 数组描述符,开头的’[‘必须有. 
3. 方法描述符规则: “(各参数描述符)返回值描述符”,其中参数描述符间没有任何分隔 
符号 
签名中使用的符号总结如下: 
这里写图片描述

四.实战

4.1静态方式

在随便一个jni目录下添加如下三个文件: 
hello.c,Android.mk,Application.mk 
hello.c

#include <stdio.h>
#include <jni.h>
#include <stdlib.h>
JNIEXPORT jstring JNICALL Java_com_jinwei_jnitesthello_MainActivity_sayHello(JNIEnv * env, jobject obj){
    return (*env)->NewStringUTF(env,"jni say hello to you");
}

Android.mk

      LOCAL_PATH := $(call my-dir)
      include $(CLEAR_VARS)

      LOCAL_MODULE_TAGS := optional
      LOCAL_PRELINK_MODULE := false
      LOCAL_MODULE_PATH := hellolib


      LOCAL_SRC_FILES := hello.c
      LOCAL_MODULE := hello
      include $(BUILD_SHARED_LIBRARY)

Application.mk

APP_ABI := armeabi

然后使用cmd进入到该目录下,执行ndk-build。能执行ndk-build是因为我已经把ndk-build工具所在的目录添加到环境变量path中了。编译成功后会在上级目录的libs目录的armeabi目录下生成libhello.so文件。 
在Android Studio工程的src/main下新建jniLibs目录,在jniLibs目录下新建armeabi目录,然后把libhello.so拷贝到armeabi目录下。这样,就可以在Android应用程序中访问libhello.so库了。关于jniLibs目录的名字,这是Android gradle默认的jni库目录,我们是可以自定义的,这里就不啰嗦了,可以参考下我的详细配置Android Studio中的Gradle 
之后运行app就可以看到jni say hello to you的字样了。 
一下是Android Studio中相关的文件: 
MainActivity.java

package com.jinwei.jnitesthello;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    TextView textView = null;
    static {
        System.loadLibrary("hello");
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.text);
        String hehe =  this.sayHello();
        textView.setText(hehe);
    }
    native public String sayHello();
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.jinwei.jnitesthello.MainActivity">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
</RelativeLayout>

4.2 动态方式

动态注册的使用流程之前已经分析过,它和静态的区别也只体现在hello.c文件上,这里只把hello.c文件贴出来:

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



jstring native_sayHello(JNIEnv * env, jobject obj){
    return (*env)->NewStringUTF(env,"jni say hello to you");
}

static JNINativeMethod gMethods[] = {  
{"sayHello", "()Ljava/lang/String;", (void *)native_sayHello},  
};  

JNIEXPORT jint  JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)  
{  
    JNIEnv* env = NULL; //注册时在JNIEnv中实现的,所以必须首先获取它
    jint result = -1;

    if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK) //从JavaVM获取JNIEnv,一般使用1.4的版本
      return -1;

    jclass clazz;
    static const char* const kClassName="com/jinwei/jnitesthello/MainActivity";

    clazz = (*env)->FindClass(env, kClassName); //这里可以找到要注册的类,前提是这个类已经加载到java虚拟机中。 这里说明,动态库和有native方法的类之间,没有任何对应关系。

    if(clazz == NULL)
    {
      printf("cannot get class:%s\n", kClassName);
      return -1;
    }

    if((*env)->RegisterNatives(env,clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) //这里就是关键了,把本地函数和一个java类方法关联起来。不管之前是否关联过,一律把之前的替换掉!
    {
      printf("register native method failed!\n");
      return -1;
    } 

    return JNI_VERSION_1_4;  
}

还是一样,使用ndk-build编译,把编译生成的库文件拷贝到android studio工程src/main/jniLibs/armeabi目录下,然后运行该项目即可。 
注意:如果你的android设备或者虚拟机使用的x86等其他格式的镜像,注意修改Application.mk文件,修改方法文章的第一小节已经介绍过了。

总结:通过以上基础知识的介绍和两个实战的案例,我们应该初步理解了Jni工作的过程,对静态方式和动态方式使用JNI有了直观的体验,但JNI毕竟非常复杂,我们还有很多的知识要学习,下一节主要介绍jni类型的转换,就是怎么把java层的String,int等转换到c/c++层对应的类型。

posted @ 2017-05-23 18:35  chenxibobo  阅读(867)  评论(0编辑  收藏  举报