文章目录
android jni编程笔记
- 本文章示例代码已经放在github上
- https://github.com/huweijian5/NativeDemo
JNI
- Contents
- JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。
- 从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。
- 使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。
情景应用
性能
- 众所周知,Android开发一般采用Java语言,虽Google推出了Kotlin语言的开发方案,但其实Kotlin的本质亦是基于Java虚拟机,那么在Android系统上,亦是基于Dalvik虚拟机的,所以性能上,与跟采用Java开发是没有任何区别的。
- 由于Java是虚拟机语言(指需要被编译成虚拟机代码,由虚拟机执行的语言),所以无论是JVM(Java虚拟机)还是Dalvik(Android定制版JVM),其程序性能在性能需求较高的情况下,就显得有些不足了。
- 那么这个时候就需要编译型语言出马了,编译型语言将源代码编译为机器码,直接由CPU执行代码,使性能大幅提升。
代码安全性
- Java代码的安全性很弱,通过反编译即能清晰看到源码
- 而C\C++的代码,其编译之后就只有机器码,机器码可以反编译成汇编,但汇编比高级语言更加的晦涩难懂,没有一定技术功底的人无法直观的理解汇编代码。虽可通过一些神器(如:IDA F5)来获取伪码,但这些伪码相比Java的伪码,简直不堪入目。
- 因此编写原生代码,不但可以拥有更高的性能,还可获得一定的代码安全性保障
示例
- jni的示例demo请看 NativeDemo
jni规范
- https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html
JNI类型映射
基本类型
Java Language | Type | Native Type Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
引用类型映射
获得方法签名
Java生成类方法签名,JNI回调应用层时特别有用
- 首先Java类需编译生成class字节码
- cd到字节码所在目录
- 执行cmd: javap -s 类名(不需要加上.class)
- 控制台即会打印签名信息,如下
Compiled from "JniParser.java"
public class com.junmeng.libparser.jni.JniParser {
public static com.junmeng.libparser.jni.JniParser getInstance();
descriptor: ()Lcom/junmeng/libparser/jni/JniParser;
public void responseDataCallBack(long, int, java.lang.String);
descriptor: (JILjava/lang/String;)V
public native long createParser();
descriptor: ()J
...
}
引用类型
-
jni主要有三种引用类型
- 局部引用( local references)
- 全局引用(global references)
- 弱全局引用(weak global references)
-
上面三种JNI引用的作用不同,作用域也不尽一样,也有着不同的生命周期
-
对于局部引用,在本地方法被调用时创建,在方法返回时(return),该引用将会自动被释放,因此在方法返回之后再使用该引用是不合法的。并不是任意引用都可以使用在所有上下文环境。但在其有效时,将一直阻止所引用的对象被GC回收。
-
对于全局引用和弱全局引用,它们可以在多个方法中使用,在手动释放之前一直有效。但是弱全局引用不会阻止GC回收它所引用的对象,因此使用此引用前判空是必要的
函数注册的两种方式
- 如果我们观察MediaRecorder的源码,就会发现其实它内部使用的就是libmedia_jni.so这个so,通过libmedia_jni.so( /frameworks/base/media/jni/Android.bp),真正native层用的其实是libmedia.so(frameworks/av/media/libmedia)
- 如果我们观察native层的源码(/frameworks/base/media/jni/android_media_MediaRecorder.cpp),我们就会发现jni使用的都是动态注册的方式,这也从侧面反映了动态注册的高效,而动态注册的实际位置就在 /frameworks/base/media/jni/android_media_MediaPlayer.cpp中的JNI_OnLoad函数
- 总的来说,如果对性能有极致要求,认为几毫秒天差地别的应用那么就使用动态注册,如果不是很看重性能,那么静态注册也够了,差别只是在首次调用的快慢而已
静态注册
- 静态注册方式网上教程最完备,这里就不再重复了,只是有一点需要注意,由于静态注册是依靠下划线来分隔的,因此当方法名也出现下划线时,需要在下划线后加“1”,如native_init要变成native_1init
- 优点: 理解和使用方式简单, 使用相关工具按流程操作就行, 出错率低
- 缺点: 当更改类名,包名时, 几乎每个方法都要去修改函数名, 灵活性不高;函数名规则又臭又长,不过由于androidstudio工具的支持(会自动帮我们生成),这部分的缺点显然不是很明显了;运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时,初次调用建立关系以后再次调用就会比较快速了
动态注册
- 优点: 灵活性高, 更改类名,包名或方法时, 只需对更改模块进行少量修改, 效率高
- 缺点: 对新手来说稍微有点难理解, 同时会由于搞错签名, 方法, 导致注册失败
JNIEnv
A JNIEnv pointer is only valid in the thread associated with it. You must not
pass this pointer from one thread to another, or cache and use it in multiple
threads.
- 不论检查中多少个线程,JavaVM独此一份,在任意地方都可以使用它
- JNIEnv是当前线程有效,因此不能通过缓存在其他地方使用,如果需要JNIEnv,则只能通过JavaVM获得
- JavaVM的AttachCurrentThread函数可以得到这个线程的JNIEnv结构体,这样后台就可以回调Java函数
- 后台线程退出前,需要调用JvavVM的DetachCurrentThread函数来释放对应的资源
异常奔溃处理
- 我们知道JNI是可以回调Java层的,但是如果回调过程中java层出现异常了怎么办,那就奔溃了,这种情况虽然是java层的锅,但JNI还是可以处理一下这个锅的,这就涉及到JNIEnv.ExceptionOccurred这个东西了,使用它我们就可以在JNI层捕获到,然后直接清除异常,这样就不会直接奔溃这么直接了(但是说到底还是java层的锅,应该去好好反省下java层的代码)
JNIEXPORT void JNICALL
Java_com_junmeng_libnative_JniException_invokeJavaException(
JNIEnv *env,
jclass clazz//注意静态方法与普通方法的区别就是jobject变成了jclass
) {
//jclass jcls = env->FindClass("com/junmeng/libnative/JniException");//由于是静态方法,已经有了jclass,因此就不需要再FindClass了
jmethodID op = env->GetMethodID(clazz, "exception", "()I");//exception就是java层定的回调函数
jmethodID mid2 = env->GetMethodID(clazz, "<init>", "()V");
jobject job = env->NewObject(clazz, mid2);
env->CallIntMethod(job, op);
//一般是用在回调java层时,即使java层产生了异常,也不会直接奔溃掉
jthrowable exc = env->ExceptionOccurred();//检测java层是否有异常产生
if (exc) {
env->ExceptionDescribe();//输出异常信息
env->ExceptionClear();//清除异常
}
}
- 上面只是一个简单演示,关于crash的解决方案,网上这篇文章写的挺好的:JNI Crash:异常定位与捕获处理 - 简书
jni-回调到应用层
在jni中经常需要将数据回调给应用层,通常的做法是将要回调给应用层的对象序列化为json字符串(cJSON此库非常好用,下载后也有例子说明),传递到应用层再反序列化为对象使用。
下面举例说明
- Step 1.声明回调
public class JniInstant {
private static final String TAG = "JniInstant";
static {
System.loadLibrary("instant");
}
private static JniInstant instance;
private JniInstant() {
}
public static JniInstant getInstance() {
if (instance == null) {
instance = new JniInstant();
}
return instance;
}
/**
* 初始化
* 此接口必须首先调用,且成功后才可使用其他接口
*
* @return 0表示成功
*/
public native int init();
//回调///
/**
* 异常回调
* descriptor: (ILjava/lang/String;)V
*/
public void appExceptionCallback(int status,String json){
}
- Step 2.初始化回调
// c或cpp文件中
#include <jni.h>
JavaVM *jvmInstant= NULL;
jobject objInstant = NULL;
jmethodID appExceptionCallback=NULL;
extern "C"
JNIEXPORT jint JNICALL
Java_com_junmeng_instant_jni_JniInstant_init(JNIEnv *env, jobject instance) {
env->GetJavaVM(&jvmInstant);
//函数参数中 jobject 或者它的子类,其参数都是 local reference。Local reference 只在这个 JNI函数中有效,JNI函数返回后,引用的对象就被释放,它的生命周期就结束了。
// 若要留着日后使用,则需根据这个 local reference 创建 global reference。Global reference 不会被系统自动释放,它仅当被程序明确调用 DeleteGlobalReference 时才被回收。(JNI多线程机制)
objInstant = env->NewGlobalRef(instance);
//在子线程中不能这样用
// jclass objclass = env->FindClass( "com/xxx/xxx/JniRGBPlayer");
//这种写法可以用在子线程中,但限制了回调方法的位置必须在当前类中,如本例必须在JniInstant类里
jclass objclass = env->GetObjectClass(instance);
//此处的appExceptionCallback为应用层的回调方法名,回调方法必须在应用层的java类里,如本例是在JniInstant类里,关于方法对应的方法签名(如"(I)V")可使用javap获得,具体在文章中有说明
appExceptionCallback = env->GetMethodID(objclass, "appExceptionCallback", "(ILjava/lang/String;)V");
return 0;
}
...
- Step 3.回调应用层
/**
* 异常消息回调
* @param lHandle 登陆句柄
* @param eEvent 异常事件
* @param pUserData 用户数据
*/
void funPtrExceptionCallback(long lHandle,
EnumExceptionEvent eEvent,char *jsonData
void *pUserData) {
JNIEnv *env;
bool detached = jvmInstant->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_EDETACHED;
if (detached) {
jvmInstant->AttachCurrentThread(&env, nullptr);
}
//在此处将数据回调到应用层,,如本例中JniInstant类中的appExceptionCallback即可收到此回调
if (env != NULL && objInstant != NULL && appExceptionCallback != NULL) {
jstring msg = env->NewStringUTF(jsonData);//返回到java层,由jvm进行释放
//CallVoidMethod后面为方法的参数,方法签名时有多少个,就得写多少个,本例中只有一个整型参数
env->CallVoidMethod(objInstant, appExceptionCallback, (jint) eEvent,msg );
}
if (detached) {
jvmInstant->DetachCurrentThread();
}
}
返回多个参数到应用层
有时候经常需要返回多个参数到应用层,那么目前想到用数组作为返回参数,以下示例
public native String[] test();
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_junmeng_vs_JniAlarm_test(JNIEnv *env, jobject instance) {
// 获取类对象
jclass cls = env->FindClass("java/lang/String");
jobjectArray strArray = env->NewObjectArray(2, cls, NULL);
jobject s0 = env->NewStringUTF("1");
jobject s1 = env->NewStringUTF("中文ing");
env->SetObjectArrayElement(strArray, 0, s0);
env->SetObjectArrayElement(strArray, 1, s1);
return strArray;
}
多线程(POSIX线程)
- POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。
#include <unistd.h>
#include <sys/syscall.h>
int tid = (int)syscall(SYS_gettid); //获得线程id
int pid = (int)syscall(SYS_getpid);//获得进程id
java线程和native线程的对比
- native线程如posix线程,由操作系统调度; java线程由虚拟机调度。
- 如果你写jni程序,你一定要注意线程模型。选择java线程,可以跨平台。选择native线程,可能有些操作系统并不很好的支持。
- 两种线程模型混用可能会遇到麻烦,因为你不确定虚拟机层的线程具体实现是什么,可能是和你的native线程用的同一个模型,也可能不是
锁
- pthread_cond_wait()函数一进入wait状态就会自动release mutex
- pthread_cond_wait() 一旦wait成功获得cond 条件的时候会自动 lock mutex
pthread_cond_signal的两种写法
-
第一种写法
lock(&mutex); //一些操作 pthread_cond_signal(&cond); //一些操作 unlock(&mutex);
-
缺点:在某些线程的实现中,会造成等待线程从内核中唤醒(由于cond_signal)回到用户空间,然后pthread_cond_wait返回前需要加锁,但是发现锁没有被释放,又回到内核空间所以一来一回会有性能的问题。
-
但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。所以Linux中这样用没问题。
-
第二种写法
lock(&mutex); //一些操作 unlock(&mutex); pthread_cond_signal(&cond);
-
优点:不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了
-
缺点:如果unlock之后signal之前,发生进程交换,另一个进程(不是等待条件的进程)拿到这把梦寐以求的锁后加锁操作,那么等最终切换到等待条件的线程时锁被别人拿去还没归还,只能继续等待
传递对象
- 当我们有多个参数要传递时,我们会希望将参数封装为bean类,进而传递bean类对象,这样的思路是极好的,但不适用于jni
- 假如将多个参数封装为bean类进行传递,native层接收到的是一个jobject,这意味着要进行多次jni调用才能从jobject解析得到我们需要的字段,而我们应该尽量减少jni调用
- 因此不推荐在native和java之间传递对象,如果参数真的过多,那么可以在java层对参数进行bean封装,这样对外接口仍是友好的,性能也不会损耗
- 虽然不推荐传递对象,但以下还是演示了如何传递对象
//note:use "/" not "." to splite
char beanPackage[] = "com/junmeng/libvgis/bean";
/**
* 将java对象转为native层结构体
* @param[in] env
* @param[in] xy
* @param[out] xyValue
* @return 是否成功
*/
bool toXYValue(JNIEnv *env, jobject xy,xy_value * xyValue) {
jclass xy_cls = env->GetObjectClass(xy);
if (xy_cls == NULL) {
LOGE("toXYValue failed.");
return false;
}
jfieldID xFieldID = env->GetFieldID(xy_cls, "x", "I");
jfieldID yFieldID = env->GetFieldID(xy_cls, "y", "I");
jint x = env->GetIntField(xy, xFieldID);
jint y = env->GetIntField(xy, yFieldID);
xyValue->x = x;
xyValue->y = y;
LOGI("toXYValue: x=%d,y=%d", x, y);
return true;
}
jobject xy2jobject(JNIEnv *env, xy_value xyValue) {
char cls_path[64];
strcpy(cls_path, beanPackage);
strcat(cls_path, "/VgisXY");
jclass clas = env->FindClass(cls_path);
if (clas == NULL) {
LOGE("%s/VgisXY failed.", beanPackage);
return NULL;
}
jmethodID construct = env->GetMethodID(clas, "<init>", "()V");
jobject obj = env->NewObject(clas, construct);
jfieldID xFieldID = env->GetFieldID(clas, "x", "I");
jfieldID yFieldID = env->GetFieldID(clas, "y", "I");
env->SetIntField(obj, xFieldID, xyValue.x);
env->SetIntField(obj, yFieldID, xyValue.y);
return obj;
}
原生API(Android Native API)
- 利用native api,我们可以在native层完成许多功能,比如在native层就能执行拍照和相关媒体的处理,实际上java sdk也只是为我们封装了一层而已,如果有能力,完全可以在native层就实现
- 但android并非一开始就开放所有native api,因此每个功能点都有相应的最低版本要求,比如相机api就从24开始,音频 API(AAudio)就从26才开始提供,音频 API(OpenSL ES)从9就开始提供了
- 更多参加官方文档:https://developer.android.google.cn/ndk/guides/stable_apis.html?hl=zh_cn
配置CMake
- CMakeLists.txt文件的作用主要就是为你要编译的库声明各种配置关系,比如你要编译的库的源文件是什么,引用的预编译的库是什么,头文件的位置是什么等
- find_library命令可以找到NDK库并将其路径存储为一个变量,你可以使用此变量在编译脚本的其他部分引用 NDK 库
- target_link_libraries命令可以关联相关的库
- 更多参加官方文档:https://developer.android.google.cn/studio/projects/configure-cmake?hl=zh_cn
手动配置Gradle
- 除了在CMakeLists.txt中声明相关配置外,在build.gradle中也支持一些配置,比如使用的C++特性,对abi的过滤,CMakeLists.txt文件路径的配置等
- 更多参加官方文档:https://developer.android.google.cn/studio/projects/gradle-external-native-builds.html?hl=zh_cn#configure-gradle
JNI_COMMIT,JNI_ABORT
- GetByteArrayElements最后一个参数可以告诉你是否对原始数组进行了拷贝,经过测试,10KB(粗略估算)以内的数据Native层会进行复制一份副本,超过10KB(粗略估算)的数据Native直接引用原数据地址,没有复制
- ReleaseByteArrayElements最后一个参数有JNI_COMMIT,JNI_ABORT,0三种选择,这三种选择只对数据被复制了才生效,如果数据没有被复制,直接填0即可
- 使用JNI_COMMIT,native对数组进行更改,java层会同步更改,备份空间不会被释放
- 使用JNI_ABORT,native对字节数组进行更改,java层保持原来,备份空间将会被释放
- 使用0,native对数组进行更改,java层会同步更改,备份空间将会被释放
- 综上描述,在存在数据副本的情况下,假如你希望native层的修改不会影响到java层的字节数据,那么就使用JNI_ABORT;假设你希望native层的修改会影响到java层的字节数据,那么就使用0;之所以慎用JNI_COMMIT是因为其不会释放备份的空间,如果忘记释放则可能会引发内存泄漏
- 假如真的需要拷贝一份副本,那么最好使用GetByteArrayRegion,一是他可以减少jni调用次数(少了ReleaseByteArrayElements这一步),也能防止忘了调用ReleaseByteArrayElements这一步带来的内存泄漏的可能(GetByteArrayRegion不需要调用ReleaseByteArrayElements)
注意
- 如果是把cpp当成一个库进行依赖,那么如果库中过滤了abi类型,则主模块也需要添加abi过滤,即如果在库中添加了ndk{abiFilters “armeabi-v7a”},那么在主模块也需要添加这一条件,否则so会打不进apk里
- CMakeLists.txt暂不支持中文,因此最好不要在里面出现中文
- 为了提升性能,我们应尽量缓存各种方法id,字段id等,比如native回调java方法,java方法基本就是不变的,这时native应该就对java方法进行缓存,而不是每次重新查找
- 为了使程序更健壮,应该在发起可能会导致异常的jni调用后进行异常检测(ExceptionOccurred)
其他
- 当我们在创建对象时,如果由于内存不足或其他原因导致创建失败,但我们又不想因此抛出异常,那么我么可以使用std::nothrow,如
// We use std::nothrow so `new` returns a nullptr if the engine creation fails HelloOboeEngine *engine = new(std::nothrow) HelloOboeEngine();
日志工具
- 无论何时,在开发阶段日志的输出都是非常重要的,android就为我们提供了一个日志库log(要使用此库必须在CMakeLists.txt文件中声明),可以方便地把日志打印到logcat
- 一般网上都会教你这样使用,新建log.h文件如下
#ifndef MEDIAAPP_LOG_H
#define MEDIAAPP_LOG_H
#include <android/log.h>
#ifndef DEFAULT_LOG_TAG
#define DEFAULT_LOG_TAG "GS28181AgentSDK"
#endif
#define isDebug true
#define LOGV(...) if(isDebug){__android_log_print(ANDROID_LOG_VERBOSE, DEFAULT_LOG_TAG, __VA_ARGS__);}
#define LOGD(...) if(isDebug){__android_log_print(ANDROID_LOG_DEBUG, DEFAULT_LOG_TAG, __VA_ARGS__);}
#define LOGI(...) if(isDebug){__android_log_print(ANDROID_LOG_INFO, DEFAULT_LOG_TAG, __VA_ARGS__);}
#define LOGITAG(TAG,...) if(isDebug){__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);}
#define LOGW(...) if(isDebug){__android_log_print(ANDROID_LOG_WARN, DEFAULT_LOG_TAG, __VA_ARGS__);}
#define LOGE(...) if(isDebug){__android_log_print(ANDROID_LOG_ERROR, DEFAULT_LOG_TAG, __VA_ARGS__);}
#define LOGETAG(TAG,...) if(isDebug){__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);}
#endif //MEDIAAPP_LOG_H
-
以上
__VA_ARGS__
是一个可变参数的宏,上面的文件简单够用 -
但是你会不会需要动态地改变日志是否输出呢,也即日志是否打印是根据用户自己设置的,而不是硬编码的
-
那不是很容易吗,封装一下就可以了,但是__android_log_print里有可变参数,这下就不好封装了,不过我们可以借助stdarg.h中的va_list来帮助我们实现,注意使用的是__android_log_vprint而不是__android_log_print了
-
Log.h文件
#ifndef LOG_H #define LOG_H #include <android/log.h> #define DEFAULT_LOG_TAG "yourTagName" /** * 设置是否打印日志 * @param bSwitch */ void setLogSwitch(bool bSwitch); void LOGV(const char *fmt, ...); void LOGTV(const char* tag,const char *fmt, ...); void LOGD(const char *fmt, ...) ; void LOGTD(const char* tag,const char *fmt, ...); void LOGI(const char *fmt, ...) ; void LOGTI(const char* tag,const char *fmt, ...); void LOGW(const char *fmt, ...) ; void LOGTW(const char* tag,const char *fmt, ...) ; void LOGE(const char *fmt, ...); void LOGTE(const char* tag,const char *fmt, ...) ; void LOGF(const char *fmt, ...); void LOGTF(const char* tag,const char *fmt, ...) ; #endif //GSLOG_H
-
Log.cpp文件
#include "Log.h" bool m_log_switch=true; void setLogSwitch(bool bSwitch){ m_log_switch=bSwitch; } void LOGV(const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_VERBOSE, DEFAULT_LOG_TAG, fmt, arg); va_end(arg); } void LOGTV(const char* tag,const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_VERBOSE, tag, fmt, arg); va_end(arg); } void LOGD(const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_DEBUG, DEFAULT_LOG_TAG, fmt, arg); va_end(arg); } void LOGTD(const char* tag,const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_DEBUG, tag, fmt, arg); va_end(arg); } void LOGI(const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_INFO, DEFAULT_LOG_TAG, fmt, arg); va_end(arg); } void LOGTI(const char* tag,const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_INFO, tag, fmt, arg); va_end(arg); } void LOGW(const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_WARN, DEFAULT_LOG_TAG, fmt, arg); va_end(arg); } void LOGTW(const char* tag,const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_WARN, tag, fmt, arg); va_end(arg); } void LOGE(const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_ERROR, DEFAULT_LOG_TAG, fmt, arg); va_end(arg); } void LOGTE(const char* tag,const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_ERROR, tag, fmt, arg); va_end(arg); } void LOGF(const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_FATAL, DEFAULT_LOG_TAG, fmt, arg); va_end(arg); } void LOGTF(const char* tag,const char *fmt, ...) { va_list arg; va_start(arg, fmt); if (m_log_switch) __android_log_vprint(ANDROID_LOG_FATAL, tag, fmt, arg); va_end(arg); }
-
以上的va_start意思就是获取可变参数的首地址指针,第二个参数填的是可变参数(这里是指…)的前一个参数(这里是指fmt),va_end与va_start成对使用。
字符处理
/**
* 将jstring转化为GBK编码char数组
* @param [in]env
* @param [in]jstr
* @param [out]returnChar 用户自己管理内存,自己开辟自己销毁
* @param [in]returnCharLen 用户开辟的returnChar的长度,如果长度不够会导致失败
* @return 字符串长度,为-1则表示失败
*/
int jstringToPCharGBK(JNIEnv *env, jstring jstr, char *returnChar, int returnCharLen) {
if (returnChar == NULL) {
return 0;
}
jclass tmpClass = env->FindClass("java/lang/String");
jstring strencode = env->NewStringUTF("gb2312");
jmethodID mid = env->GetMethodID(tmpClass, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray) env->CallObjectMethod(jstr, mid, strencode);
jsize alen = env->GetArrayLength(barr);
if(alen<=0){
return 0;
}
if (returnCharLen <= alen) {
return -1;
}
jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
strcpy(returnChar, reinterpret_cast<const char *>(ba));
returnChar[alen]=0;
env->ReleaseByteArrayElements(barr, ba, 0);
return alen;
}
/**
* 将GBK编码的字符串char*转化为jstring
* @param env
* @param pchar
* @return
*/
jstring charToJstringGBK(JNIEnv *env, const char *pchar ) {
// 定义java String类 strClass
jclass strClass = env->FindClass("java/lang/String");
// 获取java String类方法String(byte[],String)的构造器,用于将本地byte[]数组转换为一个新String
jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
// 建立byte数组
jbyteArray bytes = env->NewByteArray(strlen(pat));
// 将char* 转换为byte数组
env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte *) pat);
//设置String, 保存语言类型,用于byte数组转换至String时的参数
jstring encoding = env->NewStringUTF(
"gb-2312");
//将byte数组转换为java String,并输出
jstring result = (jstring) env->NewObject(strClass, ctorID, bytes, encoding);
env->DeleteLocalRef(bytes);
env->DeleteLocalRef(encoding);
return result;
}
时间描述工具
//
// Created by HuWeiJian on 2018/6/20.
//
#ifndef INSTANTSDK_COMMONUTIL_H
#define INSTANTSDK_COMMONUTIL_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* 获取当前系统时间戳,精确到毫秒
* @return
*/
int64_t getCurrentTime();
/**
* 获得当前时间描述
* 格式yyyyMMddHHmmss
* @param [out]nowTimeString 输出时间描述
*/
void getNowTimeString(char * nowTimeString);
/**
* 获得当前时间描述
* 格式yyyy-MM-ddTHH:mm:ss
* @param [out]nowTimeString 输出时间描述
*/
void getNowTimeStandString(char *nowTimeString);
#ifdef __cplusplus
}
#endif
#endif //INSTANTSDK_COMMONUTIL_H
//
// Created by HuWeiJian on 2018/6/20.
//
#include "CommonUtil.h"
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
int64_t getCurrentTime() {
struct timeval tv;
gettimeofday(&tv, NULL);
int64_t ts = (int64_t)tv.tv_sec*1000 + tv.tv_usec/1000;
return ts;
}
/**
* 获得当前时间描述
* 格式yyyyMMddHHmmss
* @param nowTime
*/
void getNowTimeString(char *nowTime)
{
char acYear[5] = {0};
char acMonth[5] = {0};
char acDay[5] = {0};
char acHour[5] = {0};
char acMin[5] = {0};
char acSec[5] = {0};
time_t now;
struct tm* timenow;
time(&now);
timenow = localtime(&now);
strftime(acYear,sizeof(acYear),"%Y",timenow);
strftime(acMonth,sizeof(acMonth),"%m",timenow);
strftime(acDay,sizeof(acDay),"%d",timenow);
strftime(acHour,sizeof(acHour),"%H",timenow);
strftime(acMin,sizeof(acMin),"%M",timenow);
strftime(acSec,sizeof(acSec),"%S",timenow);
strncat(nowTime, acYear, 4);
strncat(nowTime, acMonth, 2);
strncat(nowTime, acDay, 2);
strncat(nowTime, acHour, 2);
strncat(nowTime, acMin, 2);
strncat(nowTime, acSec, 2);
}
/**
* 获得当前时间描述
* 格式yyyy-MM-ddTHH:mm:ss
* @param nowTime
*/
void getNowTimeStandString(char *nowTime)
{
char acYear[5] = {0};
char acMonth[5] = {0};
char acDay[5] = {0};
char acHour[5] = {0};
char acMin[5] = {0};
char acSec[5] = {0};
time_t now;
struct tm* timenow;
time(&now);
timenow = localtime(&now);
strftime(acYear,sizeof(acYear),"%Y",timenow);
strftime(acMonth,sizeof(acMonth),"%m",timenow);
strftime(acDay,sizeof(acDay),"%d",timenow);
strftime(acHour,sizeof(acHour),"%H",timenow);
strftime(acMin,sizeof(acMin),"%M",timenow);
strftime(acSec,sizeof(acSec),"%S",timenow);
strncat(nowTime, acYear, 4);
strncat(nowTime, "-", 1);
strncat(nowTime, acMonth, 2);
strncat(nowTime, "-", 1);
strncat(nowTime, acDay, 2);
strncat(nowTime, "T", 1);
strncat(nowTime, acHour, 2);
strncat(nowTime, ":", 1);
strncat(nowTime, acMin, 2);
strncat(nowTime, ":", 1);
strncat(nowTime, acSec, 2);
}
CMakeLists.txt例子
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
set(JNILIBS_SO_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs)
# 定义源文件目录
get_filename_component(CPP_SRC_DIR ${CMAKE_SOURCE_DIR}/src/main/cpp ABSOLUTE)
# 定义源文件目录下的源文件,虽然这样可以避免写一大堆文件列表,但由于ide支持不够好,因此有新文件增加时需要手动删除生成的.cxx文件夹后再编译
file(GLOB_RECURSE cpp_sources *.c *.cpp)
if (ANDROID_ABI MATCHES "^armeabi-v7a$")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=softfp -mfpu=neon")
elseif(ANDROID_ABI MATCHES "^arm64-v8a")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -ftree-vectorize")
endif()
# Specifies a path to native header files.
include_directories(src/main/cpp/encoder/)
include_directories(src/main/cpp/camera/)
include_directories(src/main/cpp/common/)
include_directories(src/main/cpp/audio/)
include_directories(src/main/cpp/media/)
include_directories(src/main/cpp/libyuv/include/libyuv/)
include_directories(src/main/cpp/libyuv/include/)
//添加动态库或静态库,注意动静态库都必须放置在${ANDROID_ABI}文件夹下
add_library(libcamera SHARED IMPORTED )
set_target_properties(libcamera PROPERTIES IMPORTED_LOCATION
${JNILIBS_SO_PATH}/${ANDROID_ABI}/libcamera.so )
add_library(libyuv SHARED IMPORTED )
set_target_properties(libyuv PROPERTIES IMPORTED_LOCATION
${JNILIBS_SO_PATH}/${ANDROID_ABI}/libyuv.so )
add_library(uv STATIC IMPORTED)
set_target_properties(uv
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libuv.a)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
instant
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${cpp_sources}
)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
# 如果有依赖关系,被依赖的要放置在后面,假如instant依赖libyuv,则libyuv要放置在后面
instant
libyuv
# Links the target library to the log library
# included in the NDK.
${log-lib} )
#升级到gradle4之后会报错误:More than one file was found with OS independent path 'lib/armeabi/libstreamhandler.so'
#解决办法有两个:一是删除jniLibs/armeabi/libstreamhandler.so,同时注释掉下面生成so输出路径的语句即可
#二是在当前build.gradle中添加 android{ packagingOptions { pickFirst 'lib/armeabi/libstreamhandler.so' }}
#在指定目录生成so文件,注意目录区分大小写,如jniLibs_DIR的“jniLibs”必须和后面build.gradle指定的sourceSet目录中指定的“jniLibs”完全一致
set(jniLibs_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs)
set_target_properties( instant
PROPERTIES
LIBRARY_OUTPUT_DIRECTORY
"${jniLibs_DIR}/${ANDROID_ABI}")
build.gradle例子
apply plugin: 'com.android.library'
android {
compileSdkVersion 27
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions -D_LINUX -Wno-error=format-security"
}
}
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your APK.
abiFilters 'armeabi-v7a'// ,'arm64-v8a','x86', 'x86_64', 'armeabi'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
packagingOptions {
pickFirst 'lib/armeabi-v7a/libinstant.so'
//假如so冲突,可在此进行排除
exclude 'lib/armeabi-v7a/libavcodec-57.so'
exclude 'lib/armeabi-v7a/libAvDecodePlugin.so'
exclude 'lib/armeabi-v7a/libavdevice-57.so'
exclude 'lib/armeabi-v7a/libavfilter-6.so'
exclude 'lib/armeabi-v7a/libavformat-57.so'
exclude 'lib/armeabi-v7a/libavutil-55.so'
exclude 'lib/armeabi-v7a/libGMFLib.so'
exclude 'lib/armeabi-v7a/libGSFoundation.so'
exclude 'lib/armeabi-v7a/libGSLog.so'
exclude 'lib/armeabi-v7a/libGSUtil.so'
exclude 'lib/armeabi-v7a/libpicture.so'
exclude 'lib/armeabi-v7a/libpostproc-54.so'
exclude 'lib/armeabi-v7a/libswresample-2.so'
exclude 'lib/armeabi-v7a/libswscale-4.so'
}
}
...
- 上面我们可以看到有两个externalNativeBuild ,一个在defaultConfig块中,一个在android块中,android块中的主要作用是链接gradle到cmake构建脚本,因而只需要声明CMakeLists.txt文件的路径即可,而defaultConfig块中的则是各种配置
参考
Contents
POSIX线程详解 - 我的梦
https://blog.csdn.net/u013457167/article/details/89290250
Android NDK——必知必会之Native线程操作及线程同步全面详解(六)最美不过,心中有梦,身旁有你!-CSDN博客
https://blog.csdn.net/CrazyMo/article/details/82498581
Unicode(UTF-8, UTF-16)令人混淆的概念 - Boblim - 博客园
https://www.cnblogs.com/fnlingnzb-learner/p/6163205.html
JNI十大缺陷,拉低性能 - 简书
https://www.jianshu.com/p/c2354a75f50c
IntelliJ IDEA平台下JNI编程(二)—类型映射 - 简书
https://www.jianshu.com/p/3d483597d641
Android深入理解JNI(一)JNI原理与静态、动态注册 - 简书
https://www.jianshu.com/p/b9a595022462
[原创]Android逆向新手答疑解惑篇——JNI与动态注册-『Android安全』-看雪安全论坛
https://bbs.pediy.com/thread-224672.htm
JNI笔记 : 在JNI中使用引用
https://blog.csdn.net/qq_28261343/article/details/80946828
Linux条件变量pthread_condition细节(为何先加锁,pthread_cond_wait为何先解锁,返回时又加锁)_Lupin-CSDN博客
https://blog.csdn.net/shichao1470/article/details/89856443
(74条消息) NDK学习笔记:线程JNIEnv,JavaVM,JNI_OnLoad(GetEnv返回NULL?FindClass返回NULL?)_志的csdn博客-CSDN博客
(85条消息) 【NDK】【020】JNI线程切换、AttachCurrentThread、DetachCurrentThread_命运之手-CSDN博客