### 注意
- 由于从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”这个动态链接库**
- 前言
- JNI基础知识
- C语言知识点总结
- ①基本语法
- ②数据类型
- 枚举类型
- 自定义类型(类型定义)
- ③格式化输入输出
- printf函数
- scanf函数
- 编程规范
- ④变量和常量
- 局部变量和外部变量
- ⑤类型转换
- ⑥运算符
- ⑦结构语句
- 1、分支结构(选择语句)
- 2、循环结构
- 退出循环
- break语句
- continue语句
- goto语句
- ⑧函数
- 函数的定义和调用
- 参数
- 函数的返回值
- 递归函数
- 零起点学通C语言摘要
- 内部函数和外部函数
- 变量存储类别
- ⑨数组
- 指针
- 结构体
- 联合体(共用体)
- 预处理器
- 预处理器的工作原理
- 预处理指令
- 宏定义
- 简单的宏
- 带参数的宏
- 预定义宏
- 文件包含
- 条件编译
- 内存中的数据
- C语言读文件和写文件
- JNI知识点总结
- 前情回顾
- JNI规范
- jni开发
- jni开发中常见的错误
- JNI实战演练
- C++(CPP)在Android开发中的应用
- 掘金网友总结的音视频开发知识
- 音视频学习一、C 语言入门
- 1.程序结构
- 2. 基本语法
- 3. 数据类型
- 4. 变量
- 5. 常量
- 6. 存储类型关键字
- 7. 运算符
- 8. 判断
- 9. 循环
- 10. 函数
- 11. 作用域规则
- 12. 数组
- 13. 枚举
- 14. 指针
- 15. 函数指针与回调函数
- 16. 字符串
- 17. 结构体
- 18. 共用体
- 19. typedef
- 20. 输入 & 输出
- 21.文件读写
- 22. 预处理器
- 23.头文件
- 24. 强制类型转换
- 25. 错误处理
- 26. 递归
- 27. 可变参数
- 28. 内存管理
- 29. 命令行参数
- 总结
- 音视频学习二 、C++ 语言入门
- 1. 基本语法
- 2. C++ 关键字
- 3. 数据类型
- 4. 变量类型
- 5. 变量作用域
- 6. 常量
- 7. 修饰符类型
- 8. 存储类
- 9. 运算符
- 10. 循环
- 11. 判断
- 12. 函数
- 13. 数学运算
- 14. 数组
- 15. 字符串
- 16. 指针
- 17. 引用
- 18. 日期 & 时间
- 19. 输入输出
- 20. 数据结构
- 21. 类 & 对象
- 22. 继承
- 23. 重载运算符和重载函数
- 24. 多态
- 25. 数据封装
- 26. 接口(抽象类)
- 27. 文件和流
- 28. 异常处理
- 29. 动态内存
- 30. 命名空间
- 31. 预处理器
- 32. 多线程
- 总结
- 音视频学习 (三) JNI 从入门到掌握
- 音视频学习 (四) 交叉编译动态库、静态库的入门学习
- 音视频学习 (五) Shell 脚本入门
- 音视频学习 (六) 一键编译 32/64 位 FFmpeg 4.2.2
- 音视频学习 (七) 掌握音频基础知识并使用 AudioTrack、OpenSL ES 渲染 PCM 数据
- 音视频学习 (八) 掌握视频基础知识并使用 OpenGL ES 2.0 渲染 YUV 数据
- 音视频学习 (九) 从 0 ~ 1 开发一款 Android 端播放器(支持多协议网络拉流/本地文件)
- 音视频学习 (十) 基于 Nginx 搭建(rtmp、http)直播服务器
- 音视频学习 (十一) Android 端实现 rtmp 推流
- 音视频学习 (十二) 基于 FFmpeg + OpenSLES 实现音频万能播放器
- 音视频学习 (十三) Android 中通过 FFmpeg 命令对音视频编辑处理(已开源)