很久没更博了,这次来记录一下如何在Android Studio2.2中进行NDK开发吧,NDK开发嘛,就是将C/C++的代码编译成so类库,供java调用(当然c调用java也是可以的),还记得以前没有IDE的时候,需要在linux环境编译,非常麻烦,光是看完教程就不想弄了,但不得不说Android Studio是Android开发的神器,它将一切都弄的如此简单。废话不多说,马上进入主题吧。
开始之前,我们需要在SDK Manager中安装NDK开发组件,即LLDB和NDK,如下图
第二步,配置环境变量,在用户变量中添加NDK_ROOT = SDK所在目录/ndk-bundle
然后再在path变量中添加%NDK_ROOT%
第三步,选择工程文件的Project视图,在src/main下创建jni目录,在这个目录里就放mk文件和c/c++头文件、源代码文件,我们以hello-jni.c文件为例,说一下开发流程。
首先,配置Android.mk文件,如下所示
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)
另外,我们还可以配置Application.mk文件,这里面一般都用来配置全局设置,如有些编译头设置,可以参考如下:
APP_ABI := all
APP_PLATFORM := android-8
APP_CFLAGS += -DSTDC_HEADERS
下一步,需要在app的build.gradle中说明ndk-build的mk文件在哪里,在android范围内添加
externalNativeBuild {
ndkBuild {
path file("src\\main\\jni\\Android.mk")
}
}
整个build.gradle文件如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "com.example.dave.hellojni"
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
ndkBuild {
path file("src\\main\\jni\\Android.mk")
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
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:23.3.0'
testCompile 'junit:junit:4.12'
}
接下来,Android Studio为了我们开发方便,提供了小trick,就是我们可以在需要调用hello-jni的地方,先loadLibrary
static {
System.loadLibrary("hello-jni");
}
然后,声明需要调用的native方法,参数,返回值都写好后,如:
public static native String getHelloString();
接着,Android Studio可以帮助我们构建hello-jni.c文件,在错误的地方,alt+enter后,就会发现多了一个hello-jni.c文件,里面需要引用的文件和声明的函数头,as都帮我们做好了,我们只需要写好函数体就好,这功能真的非常贴心,我们开发只用关心业务逻辑即可。下面是as生成的hello-jni.c,我已经写好了返回值。
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_example_dave_hellojni_MainActivity_getHelloString(JNIEnv *env, jobject type) {
// TODO
return (*env)->NewStringUTF(env, "Hello, JNI");
}
好了,所有准备工作都已经完事了,需要注意的是,c代码中的函数名相信很多人都已经发现了,和我们在java代码中声明的native不同,长了一大串,它的格式其实是
JNIEXPORT 返回值类型 JNICALL
Java_java类包名_类名_函数名(JNIEnv *env, jobject type, 自定义参数...) {}
OK,既然前面提及过JNI也可以调用java方法,那么这里也一并说了吧。
首先,我们需要获取java中函数的methodID,例如我这里是获取设置进度条进度的方法,先获取方法所在jclass,需要注意的是,这里FindClass中第二个参数classname需要完整的类名,因此需要包名,而且用/连接,然后调用GetMethodID即可获取jmethodID,如下:
jmethodID getProgressMethod(JNIEnv *env, char* classname) {
// 1.找到java的MainActivity的class
jclass clazz = (*env)->FindClass(env, classname);
if (clazz == 0) {
LOGI("can't find clazz");
return 0;
}
LOGI("find clazz");
//2 找到class 里面的方法定义
jmethodID methodid = (*env)->GetMethodID(env, clazz, "setConvertProgress", "(I)V");
if (methodid == 0) {
LOGI("can't find methodid");
}
LOGI("find methodid");
(*env)->DeleteLocalRef(env,clazz);
return methodid;
}
顺便,这里给出jstring转换char*的c方法吧,挺常用的。
char *Jstring2CStr(JNIEnv *env, jstring jstr) {
char *rtn = NULL;
jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //String
jstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一个java字符串 "GB2312"
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
"(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");
jbyteArray barr = (jbyteArray) (*env)->CallObjectMethod(env, jstr, mid,
strencode); // String .getByte("GB2312");
jsize alen = (*env)->GetArrayLength(env, barr); // byte数组的长度
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;
}
最后,我们调用的方法是(*env)->CallXXXMethod,如下
(*env)->CallVoidMethod(env, instance, progressMethod, total);
还有,在JNI的c代码中需要注意,可以运行时会报如下错误
JNI ERROR (app bug): local reference table overflow (max=512)
这是JNI对java对象引用的限制,防止内存使用过多,这时候我们需要查看代码中是不是有些地方忘记释放java对象的引用,这时我们可以用DeleteLocalRef方法释放
参考http://blog.csdn.net/xpz445094213/article/details/46633889的解释,如下:
产生Local Reference的操作有: 1.FindClass 2.NewString/ NewStringUTF/NewObject/NewByteArray 3.GetObjectField/GetObjectClass/GetObjectArrayElement 4.GetByteArrayElements和GetStringUTFChars
解决方法: 在native method中引用完java对象后及时调用env->DeleteLocalRef方法手动释放本地引用 如果native method返回java对象就不需要手动release,因为java会自动回收
好了,NDK开发的基础就讲到这里吧,还是更博太少了,以后得保证一周一更,注意知识的积累才行~