Android NDK 技术实践
本篇主要讲解ndk相关的内容,主要对开发实际用到的例子分析和相关技术点的总结,NDK开发环境的配置忽略了
定义:NDK是一套工具,通过这个技术可以将C/C++一些优秀的库或者函数在Android应用程序中使用。
NDK主要组件:
- 动态库:c/c++源文件构建或者第三方的.so文件。
- 静态库:c/c++源文件可以构建成.a文件,可以将静态库关联到其他的库中使用。
- java原生接口:JNI是Java和C++组件用于相互通信的接口
- 应用的而二进制接口(ABI):ABI 对应不同的架构ARM V7、ARM V8等
ndk {
abiFilters 'armeabi-v7a', "arm64-v8a"
}
在build.gradle 配置文件中常这样指定,表示Android应用程序兼容v7和v8两种架构。
配置Cmake
在Android应用程序中,使用CMakeLists.txt配饰编译规则,这个文件需要在build.gradle中进行配置,如下
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
NDK版本的配置
一般情况下不需要对NDK版本进行修改直接使用默认的就可以,如果有特殊需要,也可以在File->Project Structure中进行配置,或者直接在local.properties里直接将需要使用ndk的版本的绝对路径配置上
ndk.dir=D\:\\Program Files\\SDK\\ndk\\21.3.6528147
java 数据类型签名描述符
java | C/C++ | 签名符号 |
---|
void | void | V | boolean | jboolean | Z | int | jint | I | long | jlong | J | double | jdouble | D | float | jfloat | F | byte | jbyte | B | char | jchar | C | short | jshort | S | boolean[] | jbooleanArray | [Z | int[] | jintArray | [I | long[] | jlongArray | [J | double[ ] | jdoubleArray | [D | float[] | jfloatArray | [F | byte[] | jbyteArray | [B | char[] | jcharArray | [C | short[] | jshortArray | [S | class 对象 | jobject | L 加包名加类名 |
如果通过这个规则拼不出方法签名,可以先将java类编译成class文件,然后跳转到class所在文件夹下,执行:
D:\android-studio\jre\bin\javap -s *.class
JNI实际例子
首先,需要在java类中声明native方法
private native List<LinePointInfo> getLinePoint(byte[] var0, int var1, int var2, int var3);
private native LineBoxInfo getLineBox(byte[] var0, int var1, int var2);
private native String getVectorData(byte[] var0, int var1, int var2, float var3);
static {
System.loadLibrary("testImage");
}
上边是列出的是三个有代表性的三个native方法,返回值包含了String、Object对象、List集合。
其次就是在c++中添加native方法的具体实现
这个,方式比较简单,现在Android studio的功能很强大,在c/c++写本地方法的前边几个字母就给提示了。
方法的命名规则是: extern “C” JNIEXPORT 返回值 Java_包名_类名_方法名(JNIENV* env, jclass/* this *,…)。
extern "C" JNIEXPORT jobject
JNICALL
Java_com_test_testimage_ImageContext_getLinePoint(JNIEnv *env, jclass /* this */,
jbyteArray bytes, jint width,
jint height, jint lineNum) {
//对输入参数进行转化处理
jbyte *data = env->GetByteArrayElements(bytes, 0);
uint8_t *pSrcData = (uint8_t *) data;
//声明返回结果的对象
std::vector <std::pair<int, int>> resultPoints;
//声明c++函数对象
CTestEdgePoint cTestEdgePoint;
//执行c++函数
cTestEdgePoint.run(pSrcData, width * 4, width, height, lineNum, resultPoints);
//截止到这里通过c++已经拿到了需要需要的结果,后边就是需要构造返回的结果
//因为返回值是List<LinePointInfo> 所以需要构造 LinePointInfo对象 这个类里边有一个Point的属性
//通过反射得到 LinePointInfo class声明
jclass objectLinePointInfo_jcls = env->FindClass(AI_DATA_OBJECT_LINE_POINT_INFO_CLASS);
//得到构造函数的方法ID
jmethodID objectLinePointInfo_init = env->GetMethodID(objectLinePointInfo_jcls, "<init>",
"(Landroid/graphics/Point;)V");
//反射得到java层对象 ArrayList 对象
jclass list_jclas = env->FindClass("java/util/ArrayList");
jmethodID list_init = env->GetMethodID(list_jclas, "<init>", "()V");
jobject list_obj = env->NewObject(list_jclas, list_init);
//得到ArrayList中的add(Obejct ojecgt) 的方法ID
jmethodID list_add = env->GetMethodID(list_jclas, "add", "(ILjava/lang/Object;)V");
//反射得到 Point class声明
jclass point_jcls = env->FindClass("android/graphics/Point");
//得到Point中的两个属性X、Y
jfieldID x = env->GetFieldID(point_jcls, "x", "I");
jfieldID y = env->GetFieldID(point_jcls, "y", "I");
//遍历从c++中拿到的结果,赋值给point,并将point添加到ArrayList
for (int i = 0; i < resultPoints.size(); ++i) {
//得到Point对象
jobject point_jobj = env->AllocObject(point_jcls);
//给point的x、y 点赋值
env->SetIntField(point_jobj, x, resultPoints[i].first);
env->SetIntField(point_jobj, y, resultPoints[i].second);
//得到LinePointInfo对象,在构造方法中将Point传进去
jobject linePointObject = env->NewObject(objectLinePointInfo_jcls, objectLinePointInfo_init,point_jobj);
//将LinePointInfo对象调用ArrayList的add方法添加到数组中
env->CallVoidMethod(list_obj, list_add, i, linePointObject); //添加到数组
//释放point对象
env->DeleteLocalRef(point_jobj); //释放对象
}
//释放bytes数组
env->ReleaseByteArrayElements(bytes, data, 0);
return list_obj;
}
上面是JNI层的代码实现,这个里边包含几个部分
1.参数的改造解析,这里的输入参数一个图片,c++使用的话参数类型需要进行转化
2.c++方法的调用
3.返回结果的构造,这个过程中包含了如何构造一个java对象,如何得到一个函数方法Id以及在jni中调用java对象的一个方法。
其中AI_DATA_OBJECT_LINE_POINT_INFO_CLASS 是声明的一个常量 static const char *const AI_DATA_OBJECT_LINE_POINT_INFO_CLASS = “com/test/testimage/LinePointInfo”;
//对jstring类型参数的转换
const char * authorizeContent = env->GetStringUTFChars(content,0);
//相应的释放函数
env->ReleaseStringUTFChars(content,authorizeContent);
上边三个native方法的第一个参数是byte[],这个参数表示的是一个图片的数据,在Android中图片是Bitmap的形式,传到JNI层是通过byte[]的形式传递过来的。为了方便以后使用,转换的方法总结在下面:
public static byte[] bitmap2RGBA(Bitmap bitmap) {
int bytes = bitmap.getByteCount();
ByteBuffer buffer = ByteBuffer.allocate(bytes);
bitmap.copyPixelsToBuffer(buffer);
byte[] rgba = buffer.array();
return rgba;
}
NDK中日志的打印
#include <android/log.h>
#define LOG_TAG "native-dev" //定义常量
//... 传进来的数据会直接给VA_ARGS 这个变量 __android_log_print 这个是方法名 ANDROID_LOG_INFO 这个是头文件定义的常量
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
需要注意的点
-
好多时候将包含native的部分提供给三方时候,这个时候可以打成aar包的形式,这里就需要注意aar和app模块使用一样的ABI -
主要release版本混淆的问题 -keep class com.test.**{*;} -
避免库重名的问题,如果发生"More than one file was found with OS independent path …’’ 可以使用 packagingOptions配置修复: pickFirst:当有多个匹配项目的时候使用第一个就可以了 exclude:在打包的时候移除项目中的相关文件,不打入apk -
如果使用new的方式床架了c++对象,说明是创建的指针,需要使用delete关键字删除对象 -
如果使用malloc空间之后一定别忘了使用free释放了空间
NDK相关的内容就先总结这些,如果有错误的地方或者不足的地方,请雅正!!!
|