Android NDK 开发从一窍不通到入门

来源:互联网 发布:免费的经济数据库软件 编辑:程序博客网 时间:2024/04/30 19:19

一、前言

● NDK

Native Development Kit(NDK)是一系列工具的集合。它提供了一系列的工具,帮助开发者快速开发C/C++的动态库,并能自动将so和Java一起打包成apk。

● JNI

Java Native Interface(JNI)标准是java平台的一部分,JNI是Java语言提供的Java和C/C++相互沟通的机制,Java可以通过JNI调用C/C++代码,C/C++的代码也可以调用java代码。

● JNI与NDK的关系

NDK可以为我们生成了C/C++的动态链接库,JNI是java和C/C++沟通的接口,两者与Android没有半毛钱关系,只因为安卓是java程序语言开发,然后通过JNI又能与C/C++沟通,所以我们可以使用NDK+JNI来实现“Java+C”的开发方式。

● 为什么要NDK开发

NDK开发具有以下优点: 
1. 项目需要调用底层的一些C/C++的一些东西(java无法直接访问到操作系统底层(如系统硬件等)),或者已经在C/C++环境下实现了功能代码(大部分现存的开源库都是用C/C++代码编写的。),直接使用即可。NDK开发常用于驱动开发、无线热点共享、数学运算、实时渲染的游戏、音视频处理、文件压缩、人脸识别、图片处理等。 
2. 为了效率更加高效些。将要求高性能的应用逻辑使用C/C++开发,从而提高应用程序的执行效率。但是C/C++代码虽然是高效的,在java与C/C++相互调用时却增大了开销; 
3. 基于安全性的考虑。防止代码被反编译,为了安全起见,使用C/C++语言来编写重要的部分以增大系统的安全性,最后生成so库(用过第三方库的应该都不陌生)便于给人提供方便。(任何有效的代码混淆对于会smail语法反编译你apk是分分钟的事,即使你加壳也不能幸免高手的攻击) 
4. 便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。

二、安装与配置

1. 配置ndk环境: 

File->Settings->Appearance&Behavior->System Setings->Android SDK,选中SDK Tools标签页,选择CMakeLLDBNDK进行安装如下图: 
CMake:编译配置工具。 
LLDB:调试C代码。 
NDK:开发工具包。 

这里写图片描述

下载完成后查看 File->Project Structure,是否配置ndk,如果没有就配置一下:

这里写图片描述

2. 创建工程

接下来重启IDE,新建一个jni工程:

需要勾选include c++ support,项目就可以进行ndk开发 
这里写图片描述

此处有三个可选项目:

1. C++ Standard 

指定编译库的环境,其中Toolchain Default使用的是默认的CMake环境。建议选择C++ 11,表示支持C++ 11库。

2. Exceptions Support 

如果选中复选框,则表示当前项目支持C++异常处理,建议勾选。
同理,选中复选框,项目支持RTTI,建议勾选。

3. Runtime Type Information Support 

项目打开后我们查看目录结构,与常规项目不同的是多了.externalNativeBuild文件夹、cpp文件夹、CMakeLists.txt文件,如下图: 
这里写图片描述 
这三个东西都是NDK部分: 
1. .externalNativeBuild文件夹:cmake编译好的文件, 显示支持的各种硬件等信息。系统生成。 
2. cpp文件夹:存放C/C++代码文件,native-lib.cpp文件是该Demo中自带的,可更改。需要自己编写。 (cpp文件相当于原先的jni文件夹)
3. CMakeLists.txt文件:CMake脚本配置的文件。需要自己配置编写。(相当于原先的Android.mk文件)

Gradle中也有两处不同: 

这里写图片描述

CMakelist.txt的内容如下:

# Sets the minimum version of CMake required to build the native# library. You should either keep the default value or only pass a# value of 3.4.0 or lower.cmake_minimum_required(VERSION 3.4.1)# Creates and names a library, sets it as either STATIC# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds it for you.# Gradle automatically packages shared libraries with your APK.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).             # Associated headers in the same location as their source             # file are automatically included.             src/main/cpp/native-lib.cpp )# Searches for a specified prebuilt library and stores the path as a# variable. Because system libraries are included in the search path by# default, you only need to specify the name of the public NDK library# you want to add. CMake verifies that the library exists before# completing its build.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 )# Specifies libraries CMake should link to your target library. You# can link multiple libraries, such as libraries you define in the# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library.                       native-lib                       # Links the target library to the log library                       # included in the NDK.                       ${log-lib} )
含义如下:

  • cmake_minimum_required(VERSION 3.4.1)
    CMake最小版本使用的是3.4.1。

  • add_library()
    配置so库信息(为当前当前脚本文件添加库)

    • native-lib
      这个是声明引用so库的名称,在项目中,如果需要使用这个so文件,引用的名称就是这个。值得注意的是,实际上生成的so文件名称是libnative-lib。当Run项目或者build项目是,在Module级别的build文件下的intermediates\transforms\mergeJniLibs\debug\folders\2000\1f\main下会生成相应的so库文件。

    • SHARED
      这个参数表示共享so库文件,也就是在Run项目或者build项目时会在目录intermediates\transforms\mergeJniLibs\debug\folders\2000\1f\main下生成so库文。此外,so库文件都会在打包到.apk里面,可以通过选择菜单栏的*Build->Analyze Apk...**查看apk中是否存在so库文件,一般它会存放在lib目录下。

    • src/main/cpp/native-lib.cpp
      构建so库的源文件。

  • 链接:http://www.jianshu.com/p/4eefb16d83e3

    STATIC:静态库,是目标文件的归档文件,在链接其它目标的时候使用。

    SHARED:动态库,会被动态链接,在运行时被加载。

    MODULE:模块库,是不会被链接到其它目标中的插件,但是可能会在运行时使用dlopen-系列的函数动态链接。

    拓展:使用第三方库

    在一些情况下,我们没有能力开发so库,当别人抛一个库过来的时候我们直接使用就好了。

    首先,我们告诉脚本我们只需要导入so库,不需要构建操作。

    add_library( imported-lib             SHARED             IMPORTED )
    • IMPORTED:表示只需要导入,不需要构建so库。

    接着,我们要设置so库的路径了:

    set_target_properties(                      imported-lib // so库的名称                      PROPERTIES IMPORTED_LOCATION // import so库                      libs/libimported-lib.so // so库路径)
    当使用已经存在so库时,不应该配置target_link_libraries()方法,因为只有在build 库文件时才能进行link操作。


    3. 添加调用

    在Java代码中添加一个native修饰的函数调用,并添加对lib的调用:
    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");    }


    即可完成调用。

    4. 自动添加native方法

    首先在Java代码中添加native修饰的方法名:
        public native int add(int x, int y);//java传入int值给C,C返回int值给java    public static native String passString(String str);//传入String值,返回String值    public static native int[] passIntArray(int[] intArray);//传入int数组,返回int数组    public native void passOthers(char m_char, boolean m_boolean, byte m_byte, short m_short, long m_long, float m_float, double m_double);//传入其他基本数据类型,无返回值
    在声明接口后,用快捷键可在cpp生成相应的函数,如下:
    #include "native-lib.h"JNIEXPORT jint JNICALLJava_com_hecc_jnitest_JNIManager_add(JNIEnv *env, jobject instance, jint x, jint y) {    return x + y;//返回x,y的和}JNIEXPORT jstring JNICALLJava_com_hecc_jnitest_JNIManager_passString(JNIEnv *env, jclass type, jstring m_str) {    char *str = (char *) env->GetStringUTFChars(m_str, 0);//将java中的String类型的值转化成C识别的char*类型的值    // 对字符串进行(移位)处理    int length = strlen(str);    for (int i = 0; i < length; ++i) {        *(str + i) += 1;    }    env->ReleaseStringUTFChars(m_str, str);//释放内存    return env->NewStringUTF(str);//返回一个处理后的字符串}JNIEXPORT jintArray JNICALLJava_com_hecc_jnitest_JNIManager_passIntArray(JNIEnv *env, jclass type, jintArray m_intArray) {    jint *intArray = env->GetIntArrayElements(m_intArray, NULL);//获取数组的指针    jsize length = env->GetArrayLength(m_intArray);//获取数组长度    // 对数组进行(元素+10)处理    for (int i = 0; i < length; i++) {        *(intArray + i) += 10;    }    env->ReleaseIntArrayElements(m_intArray, intArray, 0);//释放内存    return m_intArray;//返回一个处理后的数组}JNIEXPORT void JNICALLJava_com_hecc_jnitest_JNIManager_passOthers(JNIEnv *env, jobject instance, jchar m_char,                                            jboolean m_boolean, jbyte m_byte, jshort m_short,                                            jlong m_long, jfloat m_float, jdouble m_double) {    LOGI("m_char=%c,m_boolean=%d,m_byte=%d,m_short=%hd,m_long=%ld,m_float=%f,m_double=%lf",         m_char, m_boolean, m_byte, m_short, m_long, m_float, m_double);    LOGE("打印:LOGE");}


    ndk环境下的书写格式都是固定的。 
    C本地函数命名规则: Java_包名_类名_本地方法名 
    JNIEXPORTJNICALL:是系统定义的宏,JNIEXPORT后面写函数的输出类型,JNICALL后面写函数名,实测这两个宏可写可不写。 
    JNIEnv*env: 是结构体JNINativeInterface的二级指针,重定义了大量的函数指针,这些函数指针在jni开发中很常用。可以理解为JNI的上下文环境,需要通过env调用各种接口。 
    jobject instance :调用本地函数的Java对象,在此例中就是JNIManager的实例。
    头文件定义:
    #ifndef JNITEST_NATIVE_LIB_H#define JNITEST_NATIVE_LIB_H#include <jni.h>#include <string>#include <android/log.h>#define  LOG_TAG    "System.out.fromC"#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)extern "C" {JNIEXPORT jint JNICALL        Java_com_hecc_jnitest_JNIManager_add(JNIEnv *env, jobject instance, jint x, jint y);JNIEXPORT jstring JNICALL        Java_com_hecc_jnitest_JNIManager_passString(JNIEnv *env, jclass type, jstring str_);JNIEXPORT jintArray JNICALL        Java_com_hecc_jnitest_JNIManager_passIntArray(JNIEnv *env, jclass type, jintArray intArray_);JNIEXPORT void JNICALL              Java_com_hecc_jnitest_JNIManager_passOthers(JNIEnv *env, jobject instance,               jchar m_char, jboolean m_boolean, jbyte m_byte, jshort m_short, jlong m_long,               jfloat m_float, jdouble m_double);}#endif
    然后在MainActivity写测试JNI的代码,如下:
    public class MainActivity extends AppCompatActivity {    JNIManager manager = JNIManager.getInstance();    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        testJNI();    }    private void testJNI() {        int sum = manager.add(3, 4);        Log.i("MainActivity", "3+4=" + sum);        String str = JNIManager.passString("abc");        Log.i("MainActivity", "abc from C:" + str);        int[] arr = {1, 2, 3};        int[] newArr = manager.passIntArray(arr);        printArr(arr);//打印原数组        printArr(newArr);//打印JNI处理后的新数组    }    void printArr(int[] arr) {        for (int i = 0; i < arr.length; i++) {            Log.i("MainActivity", "元素" + i + "=" + arr[i]);        }    }}
    打印结果如下:
    print
    查看结果正确,调用jni函数成功!值得注意的是,此处我打印了两次数组,发现原数组和新数组的结果一致。是不是很意外,其实不难理解,原数组传给C后(对照代码),获取原数组的指针和长度,然后是对其内存地址进行操作。然后返回值是操作后的原数组(所谓的新数组)。因此,两则结果一致就有理可据了,类似思想在JNI中会大量的用到。

    如何在NDK环境中打印LOG? 

    答:需要调用系统库,在CMakeList.txt中配置相关log库(模板demo已配置好),然后在头文件中定义相关宏,如下:

    log

    char m_char = 'a'; boolean m_boolean = true; byte m_byte = 1; short m_short = 2; long m_long = 30000000000L; float m_float = 1.2F; double m_double = 3.333D; manager.passOthers(m_char, m_boolean, m_byte, m_short, m_long, m_float, m_double)
    log

    5. Java->C++的调用和数据传输

    现在,我打算在java中写JNIBean类,存放各种数据类型;在C++中写一个CppBean.cpp和CppBean.h,用于和java数据进行交互。将java的实体类创建对象并赋值,传递给本地,再赋值给c++的映射的单例对象,大致的流程就是java–>ndk–>c/c++。 
    在cpp目录添加C/C++文件时,需要CMakeList.txt中进行配置,如下: 
    cmakelist.txt
    JNIBean的代码,如下:
    public class JNIBean {    public static int jb_int;//静态变量    public boolean   jb_boolean;    public byte      jb_byte;    public short     jb_short;    public long      jb_long;    public float     jb_float;    public double    jb_double;    public String    jb_str;    public int[]     jb_intArr;}
    CppBean.h的代码,如下:
    #ifndef JNITEST_CPPBEAN_H#define JNITEST_CPPBEAN_H#include <string>using namespace std;#define MCppBean  (CppBean::GetInstance())typedef signed char Bool;#define MPTrue          1#define MPFalse         0typedef unsigned char Byte;class CppBean {public:    int getcpp_int() const;    void setcpp_int(int jb_int);    Bool getcpp_Bool() const;    void setcpp_Bool(Bool jb_boolean);    Byte getcpp_byte() const;    void setcpp_byte(Byte jb_byte);    short getcpp_short() const;    void setcpp_short(short jb_short);    long getcpp_long() const;    void setcpp_long(long jb_long);    float getcpp_float() const;    void setcpp_float(float jb_float);    double getcpp_double() const;    void setcpp_double(double jb_double);    string getcpp_str() const;    void setcpp_str(char *str);    int *getcpp_intArr();    void setcpp_intArr(int *intArr, int length);public:    static CppBean &GetInstance() {        static CppBean instance;        return instance;    }    virtual ~CppBean();private:    CppBean();    CppBean(const CppBean &);    CppBean &operator=(const CppBean &);private:    int cpp_int;    Bool cpp_boolean;    Byte cpp_byte;    short cpp_short;    long cpp_long;    float cpp_float;    double cpp_double;    string cpp_str;    int cpp_intArr[];};#endif
    CppBean.cpp的代码,如下:
    #include "cppbean.h"CppBean::CppBean() {}CppBean::~CppBean() {}int CppBean::getcpp_int() const {    return cpp_int;}void CppBean::setcpp_int(int jb_int) {    cpp_int = jb_int;}Bool CppBean::getcpp_Bool() const {    return cpp_boolean;}void CppBean::setcpp_Bool(Bool jb_boolean) {    cpp_boolean = jb_boolean;}Byte CppBean::getcpp_byte() const {    return cpp_byte;}void CppBean::setcpp_byte(Byte jb_byte) {    cpp_byte = jb_byte;}short CppBean::getcpp_short() const {    return cpp_short;}void CppBean::setcpp_short(short jb_short) {    cpp_short = jb_short;}long CppBean::getcpp_long() const {    return cpp_long;}void CppBean::setcpp_long(long jb_long) {    cpp_long = jb_long;}float CppBean::getcpp_float() const {    return cpp_float;}void CppBean::setcpp_float(float jb_float) {    cpp_float = jb_float;}double CppBean::getcpp_double() const {    return cpp_double;}void CppBean::setcpp_double(double jb_double) {    cpp_double = jb_double;}string CppBean::getcpp_str() const {    return cpp_str;}void CppBean::setcpp_str(char *str) {    cpp_str.assign(str);}int *CppBean::getcpp_intArr() {    return cpp_intArr;}void CppBean::setcpp_intArr(int *intArr, int length) {    memcpy(cpp_intArr, intArr, length * 4);}
    然后我们需要在Java中添加接口:
    public native void passData(JNIBean bean);
    然后在native-lib.h中声明,并在native-lib.cpp中实现(可自动生成):
    extern "C" {        ...JNIEXPORT void JNICALL        Java_com_hecc_jnitest_JNIManager_passData(JNIEnv *env, jobject instance, jobject bean);}
    #include "native-lib.h"#include "cppbean.h"//包含cppbean.h头文件        ...JNIEXPORT void JNICALLJava_com_hecc_jnitest_JNIManager_passData(JNIEnv *env, jobject instance, jobject bean) {    jclass cls_bean = env->GetObjectClass(bean);//获取字节码对象    jfieldID ids_int = env->GetStaticFieldID(cls_bean, "jb_int", "I");//获取静态字段ID对象    jint jb_int = env->GetStaticIntField(cls_bean, ids_int);//获取相应静态字段的int值    MCppBean.setcpp_int(jb_int);//将int值赋给CppBean中的映射值    jfieldID id_boolean = env->GetFieldID(cls_bean, "jb_boolean", "Z");//获取非静态字段ID对象    jboolean jb_boolean = env->GetBooleanField(bean, id_boolean);//获取相应非静态字段的boolean值    MCppBean.setcpp_Bool(jb_boolean);//将boolean值赋给CppBean中的映射值    jbyte jb_byte = env->GetByteField(bean, env->GetFieldID(cls_bean, "jb_byte", "B"));    MCppBean.setcpp_byte(jb_byte);    MCppBean.setcpp_short(env->GetShortField(bean, env->GetFieldID(cls_bean, "jb_short", "S")));    MCppBean.setcpp_long(env->GetLongField(bean, env->GetFieldID(cls_bean, "jb_long", "J")));    MCppBean.setcpp_float(env->GetFloatField(bean, env->GetFieldID(cls_bean, "jb_float", "F")));    MCppBean.setcpp_double(env->GetDoubleField(bean, env->GetFieldID(cls_bean, "jb_double", "D")));    LOGI("MCppBean中:int字段值=%d,Bool字段值=%d,Byte字段值=%d,short字段值=%hd,long字段值=%ld,float字段值=%f,double字段值=%lf",         MCppBean.getcpp_int(), MCppBean.getcpp_Bool(), MCppBean.getcpp_byte(),         MCppBean.getcpp_short(), MCppBean.getcpp_long(),         MCppBean.getcpp_float(), MCppBean.getcpp_double());    //字符串    jfieldID ids_str = env->GetFieldID(cls_bean, "jb_str", "Ljava/lang/String;");    jstring jb_str = (jstring) env->GetObjectField(bean, ids_str);    char *str = (char *) env->GetStringUTFChars(jb_str, 0);    MCppBean.setcpp_str(str);    env->ReleaseStringUTFChars(jb_str, str);    LOGI("MCppBean中:string字段值=%s", MCppBean.getcpp_str().c_str());    //数组    jfieldID ids_intArr = env->GetFieldID(cls_bean, "jb_intArr", "[I");    jintArray jb_intArr = (jintArray) env->GetObjectField(bean, ids_intArr);    jint *intArray = env->GetIntArrayElements(jb_intArr, NULL);    jsize length = env->GetArrayLength(jb_intArr);    MCppBean.setcpp_intArr(intArray, length);    env->ReleaseIntArrayElements(jb_intArr, intArray, 0);    for (int i = 0; i < length; i++) {        LOGI("数组元素%d:%d", i, *(MCppBean.getcpp_intArr() + i));    }}

    在NDK中基本数据类型转换的写法是固定的: 
    1.获取javabean的字节码对象 
    除了通过env->GetObjectClass(object对象)方法,还可通过env->FindClass(“com/hecc/jnitest/JNIManager”)获取,形参填写javabean的全类名(注意要把.转换成/)

    2.获取字段ID对象 
    java中字段分为静态和非静态两种,对应的api也有两种: 
    jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig); 
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig); 
    形参1:正是步骤一的字节码对象。形参2:方法名。形参3:字段描述符,简单说每个基本数据类型都有一个固定的标记作识别(参考下表)。 


    (注:另外数组类型的简写,则用”[“加上如表所示的对应类型的简写形式进行表示就可以了, 
    比如:[I 表示 int []。[L全类名; 表示类类型数组。另外,引用类型(除基本类型的数组外)的标示最后都有个”;”)

    3.获取字段值 
    字段值
    获取字段值的api也分静态和非静态(本地类型和java类型查上表): 
    本地基本数据类型 GetStatic[java类型]Field(jclass clazz, jfieldID fieldID); 
    本地基本数据类型 Get[java类型]Field(jobject obj, jfieldID fieldID); 
    形参1:若是静态,为字节码对象;非静态,为实例对象。形参2:正是步骤二的字段ID对象。

    对于字符串和数组的获取稍微复杂点,所以单独列出。 
    字符串:jstring其实是_jobject的子类,所以用GetObjectField()获取jstring,而c/c++并不识别jstring类型,别担心,ndk提供了相关api—-GetStringUTFChars(jstring string, jboolean* isCopy)来转换成char *类型。 
    数组(int数组为例):jintArray也是_jobject的子类,同时ndk提供了相关api来获取数组首地—-GetIntArrayElements(jintArray array, jboolean* isCopy)和数组个数—-GetArrayLength(jarray array)。

    在MainActivity添加如下代码,进行测试:

    private void testJNI() {     ...        JNIBean bean = new JNIBean();        JNIBean.jb_int = 11;        bean.jb_boolean = false;        bean.jb_byte = 22;        bean.jb_short = 33;        bean.jb_long = 30000000000L;        bean.jb_float = 12.34f;        bean.jb_double = 56.789d;        bean.jb_str = "我是测试代码";        bean.jb_intArr = new int[]{1, 23, 456, 78, 9};        manager.passData(bean);    }

    运行,打印结果如图: 
    result

    如何将内部类,类类型数组的数据传递给本地?

    ndk中数据的转换方式是相当固定的,如果理解了上述的实例,对于类似的需要并不难实现。 
    首先,在JNIBean 中添加内部类和类类型数组。
    public class JNIBean {    ...    public JNIChildBean jb_ChildBean;    public JNIChildBean[] jb_arr_ChildBean;    public class JNIChildBean {        public int jcb_int;    }}
    在native-lib中添加如下代码:
    JNIEXPORT void JNICALLJava_com_hecc_jnitest_JNIManager_passData(JNIEnv *env, jobject instance, jobject bean) {    ...    //内部类    jfieldID ids_child = env->GetFieldID(cls_bean, "jb_ChildBean", "Lcom/hecc/jnitest/JNIBean$JNIChildBean;");//内部类用$符号,而不是/符号。    jobject jb_child = env->GetObjectField(bean, ids_child);    jclass cls_child = env->GetObjectClass(jb_child);    jint jcb_int = env->GetIntField(jb_child, env->GetFieldID(cls_child, "jcb_int", "I"));    LOGI("jcb_int=%d", jcb_int);    //类类型数组    jfieldID ids_arr_child = env->GetFieldID(cls_bean, "jb_arr_ChildBean",                                     "[Lcom/hecc/jnitest/JNIBean$JNIChildBean;");    jobjectArray jb_arr_child = (jobjectArray) env->GetObjectField(bean, ids_arr_child);    jsize size = env->GetArrayLength(jb_arr_child);    for (int i = 0; i < size; ++i) {        jobject jab_child = env->GetObjectArrayElement(jb_arr_child, i);//获取每个数组的元素        jint jacb_int = env->GetIntField(jab_child,                                 env->GetFieldID(env->GetObjectClass(jab_child), "jcb_int", "I"));        LOGI("jacb_int=%d",jacb_int);    }}
    即可获取内部类的数据。

    5. C++->Java的回调流程

    以上示例,都是以java–>c/c++的形式,实际需求中可能需要c/c++–>java或则java–>c/c++–>java的形式传递。那么现在就以java–>c/c++–>java,我打算在界面上写个按钮,点击按钮开启一个线程调用本地方法,ndk再调用c++的函数处理逻辑(这里让线程睡了3秒,模拟耗时操作),然后c++通过ndk回调java的函数,并打印日志。事件逻辑可能有点复杂,下面按照先后流程贴代码:

    在MainActivity在添加如下代码:

    public class MainActivity extends AppCompatActivity {    ...    //按钮点击事件    public void test(View v) {        new Thread(new Runnable() {            @Override            public void run() {                manager.test();            }        }).start();    }}
    在JNIManager在添加如下代码:
    public class JNIManager {   ...    public native void test();    //c++调用java的函数    public void printFromC(int sec) {        Log.i("JNIManager", "hello from c after " + sec + "s");    }}
    我打算先创建c++的类,并在CMakeLists.txt中配置 
    text.h文件:
    #ifndef JNITEST_TEST_H#define JNITEST_TEST_Hextern void runTest();#endif
    text.cpp文件:
    #include "test.h"#include <unistd.h>#include "native-lib.h"//包含native-lib.h头文件void runTest() {    int sec = 3;    ::sleep(sec); // 睡3秒    call_printFromC(sec);//需要在native-lib.h中声明}
    这里写图片描述

    配置完成后点击同步按钮。

    此时在native-lib.h中声明函数:

    ...extern "C" {    ...JNIEXPORT void JNICALL        Java_com_hecc_jnitest_JNIManager_test(JNIEnv *env, jobject instance);}extern void call_printFromC(int sec);//回调函数
    最后是native-lib.cpp的代码:
    #include "native-lib.h"#include "cppbean.h"#include "test.h"//包含test.h头文件static JavaVM *m_JavaVM;//java虚拟机static jclass m_jcls_JNI;JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *java_vm, void *reserved) {    m_JavaVM = java_vm;    JNIEnv *jni_env = 0;    //获取JavaVM的JNIEnv     if (m_JavaVM->GetEnv((void **) (&jni_env), JNI_VERSION_1_4) != JNI_OK) {        return -1;    }    jclass jcls_JNI = jni_env->FindClass("com/hecc/jnitest/JNIManager");    m_jcls_JNI = (jclass) jni_env->NewGlobalRef((jobject) jcls_JNI);//全局变量,方便调用    return JNI_VERSION_1_4;}...JNIEXPORT void JNICALLJava_com_hecc_jnitest_JNIManager_test(JNIEnv *env, jobject instance) {    runTest();//调用test.h的方法}void call_printFromC(int sec) {    JNIEnv *env;    m_JavaVM->AttachCurrentThread(&env, NULL);//获取当前线程的JNIEnv     jmethodID methodID = env->GetMethodID(m_jcls_JNI, "printFromC", "(I)V");    jobject obj_manager = env->AllocObject(m_jcls_JNI);//获取实例对象    env->CallVoidMethod(obj_manager,methodID,sec);}

    众所周知,ndk环境下调用api离不开JNIEnv指针,但是如何在c/c++函数中获取JNIEnv呢? 
    答:要获取JNIEnv之前,先了解JavaVM ,它代表java的虚拟机,所有的工作都是从获取虚拟机的接口开始的。在加载动态链接库的时候,JVM会调用JNI_OnLoad(JavaVM* jvm, void* reserved)(如果定义了该函数),第一个参数会传入JavaVM指针。然后,通过JVM的AttachCurrentThread(JNIEnv** p_env, void* thr_args)的函数获取JNIEnv。 
    获取到JNIEnv后,想要调用其他api就方便了。对于ndk回调java函数的格式也是相当固定的:

    1.获取函数ID对象 
    java中函数分为静态非静态,对应的api: 
    jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig); 
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig); 
    形参1:字节码对象。形参2:方法名。形参3:函数描述符,具体格式是(形参类型)返回值类型,另外形参可以写任意个数,没有返回值用V(表示void型)表示。

    2.执行回调函数 
    也分静态非静态: 
    本地基本数据类型 CallStatic(返回值类型)Method(jclass clazz, jmethodID methodID, …) 
    本地基本数据类型 Call(返回值类型)Method(jobject obj, jmethodID methodID, …) 
    形参1:若是静态,为字节码对象;非静态,为实例对象。形参2:正是步骤一的函数ID对象。其他形参:这里填写的就是java函数对应的参数值,可以写任意个,没有就不用写。

    回调java函数完成,点击test按钮,运行查看结果(等待3秒): 

    这里写图片描述

    6. C++->Java 赋值

    JeanBean里有一个静态的int型二维数组,通过JNI给其赋值。
    public class JNIBean {...    public static int[][] jb_intArr2;}
    来看看具体的实现过程:
    ... void call_printFromC(int sec) {    int pInt[2][3] = {{1, 2, 3}, {4, 5, 6}};//模拟数据    JNIEnv *env;    m_JavaVM->AttachCurrentThread(&env, NULL);    processJNIBean(env, pInt);//处理数据    jmethodID methodID = env->GetMethodID(m_jcls_JNI, "printFromC", "(I)V");    jobject newManager = env->AllocObject(m_jcls_JNI);    env->CallVoidMethod(newManager, methodID, sec);}void processJNIBean(JNIEnv *env, int pInt[2][3]) {    jclass cls_JB = env->FindClass("com/hecc/jnitest/JNIBean");    jobjectArray _infoArr2 = env->NewObjectArray(2, env->FindClass("[I"), NULL);//创建一个长度为2,元素为int[]的jobjectArray     for (int i = 0; i < 2; ++i) {        jintArray _infoArr = env->NewIntArray(3);//创建长度为3的jintArray 数组        env->SetIntArrayRegion(_infoArr, 0, 3, pInt[i]);//给jintArray赋值         env->SetObjectArrayElement(_infoArr2, i, _infoArr);//给jobjectArray赋值    }    jfieldID id_infoArr2 = env->GetStaticFieldID(cls_JB, "jb_intArr2", "[[I"); //静态二维数组ID对象    env->SetStaticObjectField(cls_JB, id_infoArr2, _infoArr2);//设置java端数组数据}
    千万不要忘了在native-lib.h声明函数:
    static void processJNIBean(JNIEnv *env,int pInt[2][3]);
    然后打印数组元素:
    public class JNIManager {   ...    //c++调用java的函数    public void printFromC(int sec) {        for (int i = 0; i < 2; i++) {            for (int j = 0; j < 3; j++) {                Log.i("JNIManager","元素["+i+"]["+j+"]="+JNIBean.jb_intArr2[i][j]);            }        }    }}
    这里写图片描述
    赋值成功,和模拟的数据一样。

    7. 把现有工程转为NDK工程

    对于普通Android项目,都可以通过鼠标右击选择Link C++ Project with Gradle转为NDK项目,

    通过选择CMake或者ndk-build使开发NDK采用CMake或者JNI方法,不过此种转化方法必须符合CMake或者JNI的目录结构,也就是说,在上图操作完成之前,必须先在项目中建好符合CMake或者JNI规则的目录,假如CMakeLists.txt文件中有对xx.cpp/xx.c的引用,那么必须在cpp目录下新建好对应的xx.cpp/xx.c,同理,android.mk文件要是引用cpp/c文件,那么就要在jni目录新建好对应得文件。

    android studio 2.2之后新建NDK项目只能得到CMake方式,所以要想JNI开发,只能普通项目转。

    转载:
    http://blog.csdn.net/xiaoyu_93/article/details/53082088
    http://blog.csdn.net/heccjoel/article/details/57085351
    http://www.cnblogs.com/feijian/p/6307649.html

    阅读全文
    0 0
    原创粉丝点击