Android中NDK开发基础

时间:2021-07-03 20:07:22

简介
Android NDK 是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google称为“NDK”。
众所周知,Android程序运行在Dalvik虚拟机中,NDK允许用户使用类似C / C++之类的原生代码语言执行部分程序。
NDK包括了:
从C / C++生成原生代码库所需要的工具和build files。
将一致的原生库嵌入可以在Android设备上部署的应用程序包文件(application packages files ,即.apk文件)中。
支持所有未来Android平台的一系列原生系统头文件和库
为何要用到NDK?
概括来说主要分为以下几种情况:
1. 代码的保护,由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大。
2. 在NDK中调用第三方C/C++库,因为大部分的开源库都是用C/C++代码编写的。
3. 便于移植,用C/C++写的库可以方便在其他的嵌入式平台上再次使用。
本篇文章环境为Android studio2.2.3

流程
1,新建android工程,其中创建加载本地java类,这里命名为MyLocalUtil.class。

public class MyLocalUtil {
{
System.loadLibrary("china");
}

/**
* 让C代码做加法运算,把结果返回
* @param x
* @param y
* @return
*/

public native int add(int x, int y);

/**
* 从java传入字符串,C代码进程拼接
*
* @param s I am from java
* @return I am form java add I am from C
*/

public native String addString(String s);

/**
* 让C代码给每个元素都加上10
* @param intArray
* @return
*/

public native int[] increaseArrayEles(int[] intArray);
/*
* 应用: 检查密码是否正确, 如果正确返回200, 否则返回400
*/

public native int checkPwd(String pwd);
}

这里要注意的是,有些教程在写加载本地Library时,前面会加上static关键字,作用不言而喻,是希望在类加载前就调用本地文件。
这里,我们没有这种需求,就没有添加static关键字。
2.通过提示创建出.c文件
Android中NDK开发基础
如果没有在gradle(app)里面配置的话,是提示不出来的。所以这一步进行还需要在gradle文件defaultConfig里面添加

        ndk{
//对应MyLocalUtil中加载本地文件名称(必须)
moduleName "china"
// 指定要ndk需要兼容的架构(这样其他依赖包里mips,x86,armeabi,arm-v8之类的so会被过滤掉)
abiFilters "armeabi","armeabi-v7a","x86"
}

对应的gradle配置文件截图如下
Android中NDK开发基础
有了上面的操作,通过选择提示,我们就会在项目的app里面看到自动创建了cpp文件夹,里面有我们自动生成的c文件。
Android中NDK开发基础
同时c文件里面会自动生成jni方法。这里的命名是有规范的,这里Java_renk_addndk_MyLocalUtil_add。》Java_包名类名方法名。在JNI中变量类型也不在是java里面的数据类型,关于数据类型变化请看下表:
基本类型映射
Android中NDK开发基础
非基本类型映射
Android中NDK开发基础
这里还要注意的是自动生成的方法里面的参数(JNIEnv * env, jobject jobj,jint jx, jint jy) ,第一个是C指针,第二个,是java对象的引用。第三四个则是,调用本地方法传入的两个参数。当然,在本地方法中jint可以和int互用。故在jni方法中可以将add方法这样写

JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_add(JNIEnv * env, jobject jobj,jint jx, jint jy) {
int result = jx + jy;
return result;
};

最后,本地调用此方法就可以直接的到结果,不用再转换数据格式了。当然,现在还没完。因为我们还没有.h文件,其作用有点类似于java代码中的接口,提供规范标准。C也一样,我们直接在C文件里面添加方法名是不起作用的。还要在gradle.properties文件里面加上android.useDeprecatedNdk=true
Android中NDK开发基础
3.生成.h文件。
生成.h文件需要用到javah命令,具体用法,是打开,Android studio下面的 Terminal面板。进入到项目中的java文件路径中。输入命令cd app\src\main\java 进入java文件,然后再输入 javah -d ..\jni 包名+类名 回车。这样就会自动在main下面jni文件夹下生成.h文件。
注意的是,这里的包名+类名,是加载本地文件的类名,倘若,你不是专门用一个类来加载native方法的话,就应该使用 写native方法的那个类的全路径。..\jni代表同级jni引用路径。
Android中NDK开发基础
生成了.h文件,然后再C文件引用它。在C文件顶部#include

public native int[] increaseArrayEles(int[] intArray); 
public native String addString(String s);

然后再.h文件里面添加相应方法。

JNIEXPORT jstring
JNICALL Java_renk_testndk_MyLocalUtil_addString
(JNIEnv * , jobject, jstring);

JNIEXPORT jintArray
JNICALL Java_renk_testndk_MyLocalUtil_increaseArrayEles
(JNIEnv * , jobject, jintArray);

在.C文件里面实现具体方法


JNIEXPORT jintArray
JNICALL Java_renk_addndk_MyLocalUtil_increaseArrayEles(JNIEnv * env, jobject jobj,jintArray jarray) {

//1.得到数组的长度
//jsize (*GetArrayLength)(JNIEnv*, jarray);
jsize size = (*env)->GetArrayLength(env, jarray);
//2.得到数组元素
//jint* (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
jint *intArray = (*env)->GetIntArrayElements(env, jarray, JNI_FALSE);
//3.遍历数组,给每个元素加上10
int i;
for(i = 0;i<size;i++){
// *(intArray+i) = *(intArray+i) + 10;
*(intArray+i) += 10;
}
//4.返回结果
return jarray;
}
JNIEXPORT jstring JNICALL Java_renk_addndk_MyLocalUtil_sayHello
(JNIEnv * env, jobject jobj,jstring jstr) {
char *fromJava = mJString2CStr(env, jstr);//转换方法
//拼接函数strcat,C/C++自带
strcat(fromJava, fromC);//把拼接的结果放在第一参数里面
//jstring (*NewStringUTF)(JNIEnv*, const char*);
return (*env)->NewStringUTF(env, fromJava);
}

这里在字符串操作的时候有一个转换,因为java字符串与C中的字符串是不一样的,字符串在C中是char类型的指针数组,所以要单独写一个转换方法具体如下

/**
* 把一个jstring转换成一个c语言的char* 类型.
*/

char *mJString2CStr(JNIEnv * env, jstringjstr) {
char *rtn = "";
//得到java类
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
//新建一个java字符串(GB2312)
jstring strencode = (*env)->NewStringUTF(env, "GB2312");
//获得String类的转换Byte数组方法的方法Id
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
//通过方法Id将字符串转化为数组
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,strencode); // String.getByte("GB2312");
//得到数组长度
jsize alen = (*env)->GetArrayLength(env, barr);
//将数组元素分别装入开辟的内存
jbyte *ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
//C/C++自带,malloc,memcpy函数
rtn = (char *) malloc(alen + 1); //"\0"
memcpy(rtn, ba, alen);
rtn[alen]=0;
}
//释放内存
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}

好了,这样就可以调用了,在jni里面主要是用到C的知识比较多,而C 最主要的就是指针啦(数组就是特殊的指针)。对C/C++不熟悉的,就可以学习一下。
最后,我们在实现一个小功能,用JNI验证密码。在java层传入密码,在C层验证密码是否正确,这样可以用于本地验证登陆。
同样的步骤,先.h文件写出相应的方法名,然后再C文件中写具体实现。最后在需要用到的地方调用该类的方法即可。
.h文件方法

JNIEXPORT jint
JNICALL Java_renk_addndk_MyLocalUtil_checkPwd
(JNIEnv * , jobject, jstring);

.c文件方法

JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_checkPwd(JNIEnv * env, jobject jobj,jstring jstr) {
//服务器的密码是
char *origin = "renk";
char *fromUser = mJString2CStr(env, jstr);
//函数比较字符串是否相同,C/C++自带
int code = strcmp(origin, fromUser);
if(code==0){
return 200;
}else{
return 400;
}
}

运行后,把SO文件拿出来,就可以用了。而且反编译难度很大。相对很很安全的。
最后,今天要讲的东西,就这么多,以上就是基本类型与引用类型的基本用法,内容很简单,以后会加大点难度。谢谢。国际惯例,源码贴出啦。
C文件

#include <jni.h>
#include <renk_addndk_MyLocalUtil.h>
JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_add(JNIEnv * env, jobject jobj,jint jx, jint jy) {
int result = jx + jy;
return result;
};

JNIEXPORT jintArray
JNICALL Java_renk_addndk_MyLocalUtil_increaseArrayEles(JNIEnv * env, jobject jobj,jintArray jarray) {

//1.得到数组的长度
//jsize (*GetArrayLength)(JNIEnv*, jarray);
jsize size = (*env)->GetArrayLength(env, jarray);
//2.得到数组元素
//jint* (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
jint *intArray = (*env)->GetIntArrayElements(env, jarray, JNI_FALSE);
//3.遍历数组,给每个元素加上10
int i;
for(i = 0;i<size;i++){
// *(intArray+i) = *(intArray+i) + 10;
*(intArray+i) += 10;
}
//4.返回结果
return jarray;
}
JNIEXPORT jstring JNICALL Java_renk_addndk_MyLocalUtil_addString
(JNIEnv * env, jobject jobj,jstring jstr) {

char *fromJava = mJString2CStr(env, jstr);
//I am form java add I am from C
//c:
char *fromC = "add I am from C";
//拼接函数strcat
strcat(fromJava, fromC);//把拼接的结果放在第一参数里面
//jstring (*NewStringUTF)(JNIEnv*, const char*);
// LOGD("fromJava===%s\n",fromJava);
return (*env)->NewStringUTF(env, fromJava);
}
/**
* 把一个jstring转换成一个c语言的char* 类型.
*/

char *mJString2CStr(JNIEnv * env, jstringjstr) {
char *rtn = "";
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
jstring strencode = (*env)->NewStringUTF(env, "GB2312");
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,strencode); // String .getByte("GB2312");
jsize alen = (*env)->GetArrayLength(env, barr);
jbyte *ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
rtn = (char *) malloc(alen + 1); //"\0"
memcpy(rtn, ba, alen);
rtn[alen]=0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}
JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_checkPwd(JNIEnv * env, jobject jobj,jstring jstr) {
//存储的密码是
char *origin = "liguo";
//获得传入的密码。转换为C识别的类型
char *fromUser = mJString2CStr(env, jstr);

//函数比较字符串是否相同
int code = strcmp(origin, fromUser);
if(code==0){
return 200;
}else{
return 400;
}
}

H文件

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

#ifndef _Included_renk_addndk_MyLocalUtil
#define _Included_renk_addndk_MyLocalUtil
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: renk_addndk_MyLocalUtil
* Method: add
* Signature: (II)I
*/

JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_add
(JNIEnv *, jobject, jint, jint);

JNIEXPORT jstring
JNICALL Java_renk_addndk_MyLocalUtil_addString
(JNIEnv * , jobject, jstring);

JNIEXPORT jintArray
JNICALL Java_renk_addndk_MyLocalUtil_increaseArrayEles
(JNIEnv * , jobject, jintArray);
JNIEXPORT jint
JNICALL Java_renk_addndk_MyLocalUtil_checkPwd
(JNIEnv * , jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

本地加载类

public class MyLocalUtil {
static {
System.loadLibrary("china");
}
public native int add(int x, int y);

public native int[] increaseArrayEles(int[] intArray);

public native String addString(String s);
public native int checkPwd(String password);
}

build.gradle

apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
applicationId "renk.addndk"
minSdkVersion 17
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk{
moduleName "china"
abiFilters "armeabi","armeabi-v7a","x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
jniDebuggable true
}
}
}

dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.0.1'
testCompile 'junit:junit:4.12'
}

End