Android初步学习NDK和JNI开发(命令行的方式)

大家都知道Java的有点就是跨平台,但是作为有点的同时,也出现了自身的短板,就是与本地交互的不足。

但是google为我们提供了坚决之策,这就是我们接下来要熟悉NDK和JNI。

先说说JNI(Java Native Interface)意为Java本地接口,Java提供了JNI专门用于和本地代码交互,增强了java语言的本地交互能力。

说说NDK,NDK是android所提供的一个工具集合,通过NDK就可以在Android中更加方便的通过JNI来访问本地代码而且NDK提供了交叉编译器,开发人员只需要简单的修改mk文件就可以生成特点cpu平台的动态库了。

最后说说为什么要使用NDK和JNI:

理由有如下几点: 1.提高代码的安全性,由于so库反编译比较困难。
                                 2.可以很方便的使用目前已经有的C/C++库。
                                 3.便于平台一致,生成的so库能够很方便的在其他平台使用。
                                 4.提高程序在某些特定情形下的执行效率。比如某些多时间要求特高的应用,android必须运行在linux的虚拟机上,反应速度肯定不及底层的C代码了

大部分开发人员都不需要深刻了解JNI,但是作为程序即便是略懂也比不懂的好,别人问到至少知道是怎么回事。ps:至少面试的时候问你会不会,你可以回答“会”!

说完了简单的基础理论,现在我们实战一下如何写出第一个NDK应用。

为了跟充分的了解具体原理,我们先手动的实现NDK开发的流程,后续会介绍一下Android Studio是如何自动生成so文件的。

先说说我的工作环境,本人使用的是Ubuntu,而Window的操作方式基本一致。

首先我们打开Android Studio新建一个应用,然后新建一个Java类,我们通过这个类来访问本地C代码,我们用简单的set和get方法简单的模拟要调用的本地方法

项目基本结构如图:


刚才说了我们要使用手动(命令行)的方式来学习。所以先抛开IDE。

接下来打开文件所在的目录(这里选中的是main文件夹下的java目录)。启动terminal,编译java打开为class字节码

javac com/example/sharpay/firstndk/JniTest.java 

拿到了class,后面就是编译为c代码

javah com.example.sharpay.firstndk.JniTest

这就是我们的c代码,学过c的都知道,这是c语言的头文件,但是还需要我们具体去实现。

先打开看看里面到底是什么东西

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

#ifndef _Included_com_example_sharpay_firstndk_JniTest
#define _Included_com_example_sharpay_firstndk_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_sharpay_firstndk_JniTest
 * Method:    get
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_sharpay_firstndk_JniTest_get
  (JNIEnv *, jobject);

/*
 * Class:     com_example_sharpay_firstndk_JniTest
 * Method:    set
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_example_sharpay_firstndk_JniTest_set
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif
如果是第一次看还真一头雾水,简单的介绍一下。

首先函数名的格式为Java_包名_类名_方法名

比如之前的set方法就是我们上面的Java_com_example_sharpay_firstndk_JniTest_set(JNIEnv *, jobject, jstring)

jString代表的是Java的set方法的String参数

JNIEnv *表示一个指向JNI环境的指针,可以通过他来访问JNI提供的接口方法;比如(*env)-> NewStringUTF("Hello world!");

jobject 表示一个Java中的this。

JNIEXPORT和JNICALL:他们是JNI中所定义的宏

#ifdef __cplusplus
extern "C" {
#endif
这个宏定义是必须的,他指定extern“C”内部的函数采用C语言的命名风格来编译。否则当JNI采用C++来实现的时候,C和C++因为命名风格不同,导致JNI链接的时候找不到具体的函数,因为查询方法的规则不同,所以需要统一规则都按C语言的方式来查找链接到具体的方法。

接下来我们来实现一个C++语言的JNI方法(C与它大致雷同,关键是调用jni提供的接口方法的时候有略微的区别)

首先我们在main目录下创建一个名为jni的目录(与java目录同级,文件名和目录不一定非要这样,但是为了与android studio自动生成的方式一致,也便于后续的理解)

然后在jni目录里面创建一个test.c的代码,再把刚才的生成的头文件移动到jni目录下,然后打开头文件复制到test.cpp里面去。接下来就是实现本地的set和get方法了!

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

#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_sharpay_firstndk_JniTest
 * Method:    get
 * Signature: ()Ljava/lang/String;
 */
jstring Java_com_example_sharpay_firstndk_JniTest_get(JNIEnv *env, jobject thiz){
    printf("invoke get from C++\n");
    return env->NewStringUTF("Hello form JNI in libjni-test.so");
}

/*
 * Class:     com_example_sharpay_firstndk_JniTest
 * Method:    set
 * Signature: (Ljava/lang/String;)V
 */
void Java_com_example_sharpay_firstndk_JniTest_set(JNIEnv *env, jobject thiz, jstring string){
    printf("invoke set form C++\n");
    char* str = (char*) env->GetStringUTFChars(string,NULL);
    printf("%s\n",str);
    env->ReleaseStringUTFChars(string,str);
}

#ifdef __cplusplus
}
#endif

方法实现特别简单,只是复制出头文件的方法,然后给方法添加参数名,最后简单的实现方法。

到了这一步我们就可以使用gcc命令生成so库了,

gcc -shared -I /disk/lib/jvm/java/include -fPIC test.cpp -o libjni-test.so

不过在这里我们并不需要这么做。因为我们是做ndk开发,需要编译出适应不同CPU设备的so文件。下面我们就进入NDK的步骤了

这在之前,你得确保直接的环境支持NDK,如果还没配置NDK环境,首先你要去网上下载NDK,下载解压后配置环境变量,

1.    vim ~/.bashrc 打开用户环境变量的配置文件

2.   在文件后面添加信息: export PATH=~/Android/android-ndk-r10d:$PATH 其中~/Android/android-ndk-r10d代表的就是你NDK的解压路径

3.   添加完毕执行 source ~/.bashrc来立刻启动环境变量,否则需要重启才能生效。

还是在当前目录(main/jni)下,我们在当前的cpp文件的同级目录新建两个文件,分别是Android.mk、Application.mk

两个文件的内容为:

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 mips x86 

简单的介绍一下:

LOCAL_MODULE代表模块的名称,也是生成之后的so文件名。

LOCAL_SRC_FILES需要编译的文件名,即实现的c++文件

APP_ABI:他表示CPU的架构类型,目前市面上流行的常见架构平台有armeabi、x86、mips。绝大部分都是的移动设备都是armeabi,所以大部分apk都只包含armeabi的so库。all表示编译所有CPU平台的so库

这个时候我们只需要通过ndk-bulid命令就能生成so文件了,文件在当前jni同级目录的libs目录下

终于拿到so文件了,细心的朋友发现我们LOCAL_MODULE := jni-test 为什么生成是确是libjni-test.so? 这个无需关系,是build的时候系统自动帮我们添加的。

接下来就是使用so文件的过程了。

我们复制其中armeabi下的so文件到Android应用程序中,

默认没有jniLibs文件夹,第一次创建即可,然后新建armeabi,最后拷贝进去。

最后的最后就是调用的问题了。但在这之前需要设置工程支持NDK运行

在项目工程的gradle.properties文件里添加android.useDeprecatedNdk=true即可添加对so文件的支持

Activity的代码很简单,一个textview直接显示来自底层c++的返回

最后上运行结果


刚刚说了android如何调用jni底层代码,那反过来,jni如何调用Java代码呢,如果底层需要一个来自上层的数据,如何做到呢?其实大致流程就是:先通过类名找到类,然后再根据方法名找到方法ID,最后就可以调用这个方法了。不管是静态类还是非静态,其实方法都一样,只是非静态类需要先构造出对象后才能调用它。

比如在有个静态的JAVA类(JniTest)

public static void showMessage(String msg){
   Log.d(TAG,"get Message is:" + msg);
}
然后在JNI中调用上面定义的静态方法:

void callJavaMethod(JNIEnv *env,jobject thiz){
        jclass class = env->FindClass("com/example/sharpay/firstndk/JNITest");
        if(clazz == NULL){
            printf("not find this Class");
            return;
        }
        jmethodID id = env->GetStaticMethodID(clazz,"showMessage","(LJava/lang/String;)V");
        if(id == null){
            printf("find method showMessage error!");
        }
        jstring msg = env->NewStringUTF("msg send by jni method in test.cpp");
        env->CallStaticVoidMethod(clazz,id,msg);
    }
这样就完成了一次jni向android的调用
这里(LJava/lang/String;)V可能会有人看不懂,这其实是数据签名的一种写法,java中的类、方法和数据在jni中都有对应的表达方式,我在另一封博客"JNI的数据类型和类型签名"有叙述。

这样就初步完成了通过NDK调用JNI来访问本地C++代码,但是还有另外一种方法,就是使用Android Studio来自动构建出so文件。后续将更新如何使用AS的方式来完成简单的调用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值