Android NDK简单使用

我们知道,Java的最大优点就是跨平台性,Java编译后的.class文件只需要一次编译,就可以运行在不同的操作系统中了。不过有的时候,我们需要使用不同操作系统的特性,这个时候可能使用Java就达不到目的了。此时我们需要使用JNI,通过JNI来调用底层的C/c++库,从而达到使用不同操作系统特性的目的。

NDK是Android所提供的一个工具集合,通过NDK可以在Android中更加方便的使用JNI来访问本地代码,比如c或者c++。NDK还提供了交叉编译器,开发者只需要简单的修改mk文件就可以生成特定的CPU平台的动态库了。

在这里我们主要通过一个简单的例子来说明如何来使用NDK来进行c/c++开发。这里包括了如下两个方面:

  • Java如何调用C/C++代码
  • C/C++代码如何调用Java代码

在本文通过C++来进行底层代码的编写,C和C++类似,在这里就不多说明。

1.NDK下载配置

在我们使用NDK开发之前,我们需要做如下准备:

1)下载NDK,在这里直接通过SDK manager下载安装即可
这里写图片描述
2)将NDK的路径设置在PATH里,在我的机器配置如下D:\Android\sdk\ndk-bundle
3)运行ndk-build,如果可以找到这个命令,说明NDK环境配置成功

2.Java如何调用C/C++代码

1).创建一个Android项目,然后创建一个类文件JniTest.java,代码如下:

package com.example.alesjia.jniexample;
public class JniTest {
    public native String getString();
}

2).编译JniTest.java文件得到JniTest.class文件.

E:\project\JNIExample\app\src\main\java>javac com/example/alesjia/jniexample/JniTest.java

3).通过javah生成头文件, 文件名为“com_example_alesjia_jniexample_JniTest.h”

E:\project\JNIExample\app\src\main\java>javah com.example.alesjia.jniexample.JniTest

4).头文件内容如下

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_alesjia_jniexample_JniTest */

#ifndef _Included_com_example_alesjia_jniexample_JniTest
#define _Included_com_example_alesjia_jniexample_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_alesjia_jniexample_JniTest
 * Method:    getString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_alesjia_jniexample_JniTest_getString
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

我们看看这头文件,主要做了如下事情:

  • 通过预编译处理防止这个头文件被重复引用
  • 如果是c++编译器,那么函数名需要采用c语言的风格来编译,否则会导致JNI找不到定义的函数
  • JNIEXPORT和JNICALL是jni.h定义的宏
  • JNIEnv*是一个JNI环境指针,通过它你可以调用JNI已经定义的函数
  • jobject是Java中的this

5).在main目录下创建jni目录,将生成的头文件放入这个目录。然后在这个目录创建test.cpp文件,Android.mk文件和Application.mk文件。此时jni目录包括了头文件,test.cpp,Android.mk以及Application.mk文件,共4个文件。

6).在test.cpp文件中实现JniTest.java中定义的native方法,代码如下

#include "com_example_alesjia_jniexample_JniTest.h"
JNIEXPORT jstring JNICALL Java_com_example_alesjia_jniexample_JniTest_getString(JNIEnv *env, jobject thiz)
{
    return env->NewStringUTF("Hello from c++!");
}

7).编写makefile文件Android.mk

Android.mk文件指定了编译后的库文件名为jni-test,源文件为test.cpp

LOCAL_PATH := ${call my-dir}

include ${CLEAR_VARS}

LOCAL_MODULE := jni-test

LOCAL_SRC_FILES := test.cpp

include ${BUILD_SHARED_LIBRARY}

8).Application.mk文件
Application.mk文件则指定了CPU的类型

APP_ABI := x86_64

在这里,由于我的模拟器为x86_64的,那么我这里就指定为x86的64位的,这里你可以根据需要指定armeabi,mips等。或者直接指定为all,那么会生成所有支持所有CPU体系架构的.so库。

9).在activity中调用这个JniTest的native方法

package com.example.alesjia.jniexample;

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

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("jni-test");//导入这个库
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView mTvJniText = (TextView) findViewById(R.id.tv_jni_text);
        JniTest jniTest = new JniTest();
        mTvJniText.setText(jniTest.getString());//使用这个native方法

    }
}

10).通过gradlew installDebug编译生成app包,你会发现报错

Execution failed for task ':app:compileDebugNdk'.
> Error: Your project contains C++ files but it is not using a supported native build system.
Consider using CMake or ndk-build integration with the stable Android Gradle plugin:
 https://developer.android.com/studio/projects/add-native-code.html
or use the experimental plugin:
 http://tools.android.com/tech-docs/new-build-system/gradle-experimental.

这个时候你需要做的就是打开根目录的gradle.properties文件然后加入android.useDeprecatedNdk=true,然后再执行这个gradlew installDebug命令,将这个app安装到模拟器中

11).在模拟器中启动该应用,结果发现报错,这个错误的意思很简单,就是没有找到该库文件

  Process: com.example.alesjia.jniexample, PID: 5429
                                                                              java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.alesjia.jniexample-2/base.apk"],nativeLibraryDirectories=[/data/app/com.example.alesjia.jniexample-2/lib/x86_64, /data/app/com.example.alesjia.jniexample-2/base.apk!/lib/x86_64, /system/lib64, /vendor/lib64]]] couldn't find "libjni-test.so"
                                                                                  at java.lang.Runtime.loadLibrary0(Runtime.java:972)
                                                                                  at java.lang.System.loadLibrary(System.java:1530)
                                                                                  at com.example.alesjia.jniexample.MainActivity.<clinit>(MainActivity.java:10)
                                                                                  at 

12).我们通过android studio的build->Analyze APK…,然后点击打开,发现在这个应用包的libs目录下包含了“libapp.so”。在这里需要说明的是动态库的命名有个规则,我们在loadLibray的时候加载的包名不包括前面的lib和后面的.so。为什么是”libapp.so”这个包名呢?我猜想是默认的包名。所以在这里报错也是正常,因为不存在libjni-test.so文件。

13).此时我们需要在app的gradle文件的defaultConfig配置ndk的模块名,具体如下

 defaultConfig {
        applicationId "com.example.alesjia.jniexample"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        ndk{
            moduleName "jni-test"//指定包名为jni-test
        }
    }

14).重新编译打包,然后安装到模拟器,运行这个app,发现此时可以执行成功,界面如下:

这里写图片描述

3.C/C++代码如何调用Java代码

我们已经实现了Java通过JNI接口来调用C++代码。下面介绍一下如何通过C++通过JNI接口来调用Java代码。因为java方法分为静态和成员方法两种,这里我们分别看看,从c++代码该如何调用Java的静态和成员方法。

1).首先我们在JniTest.java文件中增加一个静态方法,这个方法供c++调用,代码如下:

package com.example.alesjia.jniexample;

import android.util.Log;

public class JniTest {
    public JniTest() {
    }

    public native String getString();

    public static void staticMethodCalledByJNI(String jniMsg) {
        Log.d("JniTest", "methodCalledByJNI, message from jni = " + jniMsg);
    }
}

2).然后通过修改test.cpp文件,代码如下

//
// Created by Administrator on 2017/04/04 0004.
//
#include "com_example_alesjia_jniexample_JniTest.h"
#include <stdio.h>

void callJavaStaticMethod(JNIEnv *env, jobject thiz){
    //通过类名查找到该class类变量
    jclass clazz = env->FindClass("com/example/alesjia/jniexample/JniTest");
    if(NULL == clazz){
        printf("find class Jnitest failed");
    }
    //通过这个方法名找到这个方法id
    jmethodID id = env->GetStaticMethodID(clazz, "staticMethodCalledByJNI", "(Ljava/lang/String;)V");//这里有个方法签名需要注意一下
    if(NULL == id)
    {
        printf("find class method methodCalledByJNI failed.");
    }
    jstring msg = env->NewStringUTF("message send by c++!");
    //通过类变量,方法id和字符串调用java静态方法
    env->CallStaticVoidMethod(clazz,id, msg);
}

//这个方法是给Java调用的,Java通过JNI调用c++代码
JNIEXPORT jstring JNICALL Java_com_example_alesjia_jniexample_JniTest_getString(JNIEnv *env, jobject thiz)
{
    callJavaStaticMethod(env, thiz);//在这里我们通过c++代码通过JNI调用java代码
    return env->NewStringUTF("Hello from c++!");
}

3).查看打印日志,说明此时C++已经成功调用了Java方法

com.example.alesjia.jniexample D/JniTest: methodCalledByJNI, message from jni = message send by c++!

4).上面是通过C++代码调用java中静态方法,那么如果是成员方法该如何调用呢?在JniTest.java文件添加一个方法memberMethodCalledByJNI方法,代码如下

package com.example.alesjia.jniexample;

import android.util.Log;

/**
 * Created by Administrator on 2017/04/04 0004.
 */

public class JniTest {
    public native String getString();
    public static void staticMethodCalledByJNI(String jniMsg) {
        Log.d("JniTest", "staticMethodCalledByJNI, message from jni = " + jniMsg);
    }
    //新增的成员方法
    public void memberMethodCalledByJNI(String jniMsg){
        Log.d("JniTest", "memberMethodCalledByJNI, message from jni = " + jniMsg);
    }
}

5).修改test.cpp文件,代码如下

//
// Created by Administrator on 2017/04/04 0004.
//
#include "com_example_alesjia_jniexample_JniTest.h"
#include <stdio.h>

void callJavaStaticMethod(JNIEnv *env, jobject thiz){
    jclass clazz = env->FindClass("com/example/alesjia/jniexample/JniTest");
    if(NULL == clazz){
        printf("find class Jnitest failed");
    }
    jmethodID id = env->GetStaticMethodID(clazz, "staticMethodCalledByJNI", "(Ljava/lang/String;)V");
    if(NULL == id)
    {
        printf("find class method methodCalledByJNI failed.");
    }
    jstring msg = env->NewStringUTF("message send by c++!");
    env->CallStaticVoidMethod(clazz,id, msg);
}

void callJavaMemberMethod(JNIEnv *env, jobject thiz){
    jclass clazz = env->FindClass("com/example/alesjia/jniexample/JniTest");
    if(NULL == clazz){
        printf("find class Jnitest failed");
    }
    jobject obj = env->AllocObject(clazz);//初始化一个对象
    if(NULL == obj){
        printf("alloc the object failed");
    }
    jmethodID id = env->GetMethodID(clazz, "memberMethodCalledByJNI", "(Ljava/lang/String;)V");
    if(NULL == id)
    {
        printf("find class method methodCalledByJNI failed.");
    }
    jstring msg = env->NewStringUTF("message send by c++!");
    //通过对象,方法id和参数调用成员函数
    env->CallVoidMethod(obj,id, msg);
}

JNIEXPORT jstring JNICALL Java_com_example_alesjia_jniexample_JniTest_getString(JNIEnv *env, jobject thiz)
{
    callJavaStaticMethod(env, thiz);
    callJavaMemberMethod(env, thiz);
    return env->NewStringUTF("Hello from c++!");
}

6).通过查看日志,说明调用成员方法成功。

com.example.alesjia.jniexample D/JniTest: staticMethodCalledByJNI, message from jni = message send by c++!
com.example.alesjia.jniexample D/JniTest: memberMethodCalledByJNI, message from jni = message send by c++!

4.注意事项

NDK命令会默认指定jni目录为本地源码的目录,具体目录在main/jni这个目录,如果你需要修改源码路径则需要在gradle文件中指定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值