🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
### 注意 - 由于从Android studio2.2版本开始,AS集成了CMake插件,使用AS开发支持C++的项目,更加方便。一切相关的配置如Android.mk,Application.mk这些配置都交给了CMake来处理,所以后面的jni开发的某些操作,在新的CMake操作下,就不需要了。 ### 交叉编译 - 交叉编译:在一个平台上去编译另一个平台上可以执行的本地代码 - CPU平台 arm、 X86 - 操作系统平台 windows linux mac os - 原理 模拟不同平台的特性去编译代码 ### JNI开发工具 ### - ndk native develop kit android提供的开发工具包 - ndk 目录(基于r9d版本),随着NDK版本的升级,功能的不断完善,该目录的各个子目录也不相同 - docs---> 帮助文档 - platforms---> 好多平台版本文件夹,选择时选择项目支持的最小版本号对应的文件夹 - include 文件夹---> jni开发中常用的.h头文件 - lib文件夹---> google打包好的提供给开发者使用的.so文件 - samples---> google官方提供的样例工程,可以参考进行开发 - android-ndk-r9d\build\tools---> linux系统下的批处理文件,在交叉编译时会自动调用 - ndk-build---> 交叉编译的命令 - ndk-r15c的目录结构如下图 ![NDKr15c目录](https://box.kancloud.cn/95b2c4a78d384d0c0f4b5127314df196_624x442.png =624x442) - cdt eclipse开发的插件用于高亮C的代码(只是适用于eclipse,不过现在主流的是AS,eclipse几乎快要淘汰) ### jni hello world #### jni开发步骤(以eclipse为开发工具) 1. 写java代码 声明本地方法,用到native关键字,java中本地方法不用实现 2. 在项目根目录下创建jni文件夹 3. 在jni文件夹下创建.c文件 - 本地函数的命名规则:Java_包名_类名_本地方法名 - JNIENV* env JNIEnv是结构体JNINativeInterface(接口函数指针表)的一级指针 - 结构体JNINativeInterface定义了大量的函数指针,这些函数指针在jni开发中很常用。(C语言中结构体中不能定义函数,可以定义函数指针) - env是结构体JNINativeInterface的二级指针 - (*env)-> 中“->“是间接引用函数符 指向结构体成员运算符,类似于结构成员运算符".";都是用来访问结构体成员的, *env是一级指针 - (*env)-> 调用结构体中的函数指针 - 第二个参数jobject 在C中表示的java的对象即thiz就是调用这个本地函数的java对象,在本列子中就是MainActivity的实例 4. 导入<jni.h> 5. 创建Android.mk makefile告诉编译器.c的源文件在什么地方,要生成的编译对象的名字是什么? **Android.mk** ``` LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= hello//编译完后生成的名字,指定了生成的动态链接库的名字 LOCAL_SRC_FILES := hello.c//C的源文件,有多个.c文件用空格隔开,指定了C的源文件在哪 include $(BUILD_SHARED_LIBRARY) ``` 6. 进入到项目所在的根目录,在DOS窗口,调用ndk-build编译C代码生成动态链接库.so文件,文件的位置在lib-->armeabi-->.so 7. 在java代码中加载动态链接库System.loadlibrary("动态链接库的名字");Android.mk中LOCAL_MODULE所指定的名字 8. 运行部署到模拟器或者真机上 以上是java调用C的步骤 **MainActivity**代码如下 package com.wsc.jnihelloworld; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void click(View view) { System.loadLibrary("hello"); String result =helloFromC(); Toast.makeText(this, result, Toast.LENGTH_SHORT).show(); } //声明 本地方法 使用native关键字本地方法不用实现 public native String helloFromC(); } **hello.c**代码如下 #include <stdlib.h> #include <stdio.h> #include <jni.h> /*jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env, jobject thiz )*/ //JNIEnv* env是结构体JNINativeInterface的二级指针 //JNIEnv是结构体JNINativeInterface(接口函数指针表)的一级指针 //接口函数指针表,该表定义了大量的函数,可以把C的数据类型转换成java可以理解的数据类型,也可以把java的数据类型转换成C可以理解的数据类型 //结构体JNINativeInterface定义了大量的函数指针,这些函数指针在jni开发中很常用 // (*env)-> 中“->“是间接引用函数符 指向结构体成员运算符,类似于结构成员运算符".";都是用来访问结构体成员的, *env是一级指针 //jobject 在C中表示的java的对象即thiz就是调用这个本地函数的java对象,在本列子中就是MainActivity的实例 //上面的这2个参数env和thiz是规定死的,这两个参数是JNI规则规定好的,如果java中还有其他参数,那么就在这2个参数后面延续,本列中没有参数,所以后面就没有 //c本地函数名命规则 Java_包名_类名_本地方法名 //jstring (*NewStringUTF)(JNIEnv*, const char*);//可以将char转换成jstring类型 jstring Java_com_wsc_jnihelloworld_MainActivity_helloFromC(JNIEnv* env,jobject thiz){ char* cstr="hello from c!"; return (*env)->NewStringUTF(env,cstr); } Android.mk文件如下 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= hello LOCAL_SRC_FILES := hello.c include $(BUILD_SHARED_LIBRARY) ![](http://i.imgur.com/5A31JMD.png) ![](http://i.imgur.com/zHPulRp.png) ### JNI简便开发流程 (以eclipse 为开发工具)### * ①写java代码,native声明本地方法 * ②添加本地支持,右键单击项目-->android tools-->add native support * 如果发现finish不能点击需要给工作空间配置ndk目录的位置 * window-->preference-->左侧选择android-->ndk 把ndk解压的目录指定出来 * ③如果写的是.c文件,先修改一下生成的.cpp文件的扩展名,不要忘了相应修改Android.mk文件中LOCAL_SRC_FILES := hello.c * ④javah生成头文件,在生成的头文件中拷贝c的函数名.c的文件 * ⑤CDT解决CDT插件报错 * 右键单击项目选择properties--->c/c++ general-->paths and symbolc--->include选项卡下,点击ADD-->file system,选择ndk目录下,platforms文件夹对应平台下(项目支持的最小版本)usr目录下arch-arm-->include确定后会解决代码提示和报错的问题 * ⑥编写C函数,如果需要单独编译一下C代码,就在C/C++视图工具栏处找到小锤子,锤一下,如果想直接运行到模拟器上,就不用小锤子了,控制台会自动锤一下 * ⑦java代码中不要忘了System.loadlibrary(),加载.so文件 **注意:Application.mk这个文件还是应该手动复制粘贴到jni目录中,当APP运行到X86模拟器上时,在Application.mk中指定** APP_ABI := armeabi x86 APP_PLATFORM := android-14 编译时会产生一个obj目录如图所示 ![](http://i.imgur.com/gpX3QYH.png) 运行时jni目录和obj目录是不起作用,只是编译时起作用,打包时不会打到APK中 ![](http://i.imgur.com/VFDLssV.png) ### java传递int类型数据给C ### 实现:点击按钮,调用java的代码,让java传递一些参数给C,C处理后返还给java。 #### java 与 c之间的数据传递 #### public native int add(int x, int y); public native String sayHelloInC(String s); public native int[] arrElementsIncrease(int[] intArray); #### 在c代码中使用logcat#### * Android.mk文件增加以下内容 LOCAL_LDLIBS += -llog//加载动态链接库,第一个l是load的缩写,log是so文件中文件名中除lib外的名字如liblog.so中的log * C代码中增加以下内容 #include <android/log.h> #define LOG_TAG "System.out" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) * C代码中使用logcat, 例: LOGI("info\n"); LOGD("debug\n"); * define C的宏定义,起别名,#define LOG_TAG "System.out",给"System.out"起别名为LOG_TAG * `#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)` * 给__android_log_print函数起别名,写死了前两个参数,第一个参数优先级,第二个TAG * `__VA_ARGS__`可变参数的固定写法 * LOGI(...)在调用的时候,用法和printf()一样。 > **打印LOG日志比较消耗性能,实际开发中应该注释掉** #### C中调用java #### C中回调java中的方法,需要用到反射的知识,C的代码中反射,拿到java类的字节码,拿到字节码就可以找到这里面的函数,创建对象,然后通过反射的方式调用方法 获取方法签名(java的方法可以重载,在一个类里面可以重载同一个方法,如何确定方法的唯一性,可以通过方法签名,可以通过javap -s) * ①找到字节码对象 * jclass (*FindClass)(JNIEnv*, const char*); * 第二个参数是要回调的java方法所在的(反射调用的)类的路径即com.wsc.callbackjava.JNI * ②通过字节码对象找到方法对象 * jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); * 第二个参数字节码对象;第三个参数 要反射调用的java方法名;第三个参数是方法名,要回调的方法名,第四个参数是方法签名(通过javap -s 包名+类名) * ③ 通过字节码对象创建java对象(可选),如果本地方法和要回调的java方法在同一个类里面可以直接用jni传过来的java对象,调用创建的method * jobject obj = (*env)->AllocObject(env, claz); * 当回调的方法和本地的方法不在一个类,需要通过刚创建的字节码对象手动创建一个java对象 * 再通过这个对象来回调java方法 * 注意:如果创建的是一个activity对象,回调的方法还包含上下文,这个方法行不通,会报空指针异常 * ④通过对象调用方法 * void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); * 第二个参数调用java方法的对象;第三个参数要调用的jmethodID对象; 第四个参数是可选的参数 调用方法时接收的参数 ### C Java JNI三者相互之间数据类型的转换 - Java中任何一种类型,在Jni中都有一种对应的类型,比如:String--->jstring,但是Java中String是双字节的,在C++中不是双字节的,这就涉及到一个字符串转换,编码的转换 #### Java和C++字符串转换 - string转换为jstring ~~~ jstring c2j(JNIEnv* env, string cstr) { return env->NewStringUTF(cstr.c_str()); } ~~~ - jstring转换为string ~~~ string j2c(JNIEnv* env, jstring jstr) { string ret; jclass stringClass = env->FindClass("java/lang/String"); jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B"); // 把参数用到的字符串转化成java的字符 jstring arg = c2j(env, "utf-8"); jbyteArray jbytes = (jbyteArray)env->CallObjectMethod(jstr, getBytes, arg); // 从jbytes中,提取UTF8格式的内容 jsize byteLen = env->GetArrayLength(jbytes); jbyte* JBuffer = env->GetByteArrayElements(jbytes, JNI_FALSE); // 将内容拷贝到C++内存中 if(byteLen > 0) { char* buf = (char*)JBuffer; std::copy(buf, buf+byteLen, back_inserter(ret)); } // 释放 env->ReleaseByteArrayElements(jbytes, JBuffer, 0); return ret; } ~~~ - jstring转换成c语言的char* 类型 ~~~ /** * 把一个jstring转换成一个c语言的char* 类型. */ char* _JString2CStr(JNIEnv* env, jstring jstr) { char* rtn = NULL; 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; } ~~~ ### JNI开发 ### **JNI开发,应用运行时和“.c”的源码没有任何关系,而且打包时“.c”源文件也不会打进去,“.c”源文件只是在编译阶段起作用,真正运行时调用时,起作用的是“.so”这个动态链接库**