android jni编程笔记

文章目录

  • android jni编程笔记
    • JNI
    • 情景应用
      • 性能
      • 代码安全性
    • 示例
    • jni规范
    • JNI类型映射
      • 基本类型
      • 引用类型映射
    • 获得方法签名
    • 引用类型
    • 函数注册的两种方式
      • 静态注册
      • 动态注册
    • JNIEnv
    • 异常奔溃处理
    • jni-回调到应用层
    • 返回多个参数到应用层
    • 多线程(POSIX线程)
      • java线程和native线程的对比
        • pthread_cond_signal的两种写法
    • 传递对象
    • 原生API(Android Native API)
    • 配置CMake
    • 手动配置Gradle
    • JNI_COMMIT,JNI_ABORT
    • 注意
    • 其他
      • 日志工具
      • 字符处理
      • 时间描述工具
      • CMakeLists.txt例子
      • build.gradle例子
    • 参考

android jni编程笔记

  • 本文章示例代码已经放在github上
  • https://github.com/huweijian5/NativeDemo

JNI

  • 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

引用类型映射

android jni编程笔记_第1张图片

获得方法签名

Java生成类方法签名,JNI回调应用层时特别有用

  • 首先Java类需编译生成class字节码
  • cd到字节码所在目录
  • 执行cmd: javap -s 类名(不需要加上.class)
  • 控制台即会打印签名信息,如下
Compiled from "JniParser.java"
public class com.gosuncn.libparser.jni.JniParser {
  public static com.gosuncn.libparser.jni.JniParser getInstance();
    descriptor: ()Lcom/gosuncn/libparser/jni/JniParser;

  public void responseDataCallBack(long, int, java.lang.String);
    descriptor: (JILjava/lang/String;)V

  public native long createParser();
    descriptor: ()J
    
    ...
}

  • android jni编程笔记_第2张图片

引用类型

  • 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函数
  • android jni编程笔记_第3张图片android jni编程笔记_第4张图片android jni编程笔记_第5张图片
  • 总的来说,如果对性能有极致要求,认为几毫秒天差地别的应用那么就使用动态注册,如果不是很看重性能,那么静态注册也够了,差别只是在首次调用的快慢而已

静态注册

  • 静态注册方式网上教程最完备,这里就不再重复了,只是有一点需要注意,由于静态注册是依靠下划线来分隔的,因此当方法名也出现下划线时,需要在下划线后加“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.
android jni编程笔记_第6张图片

  • 不论检查中多少个线程,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, "", "()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();
    
    //回调///

    /**
     * GxxApp异常回调
     * descriptor: (ILjava/lang/String;)V
     * @param status 0--下线 1--上线
     */
    public void gxxAppExceptionCallback(int status,String json){
        Log.e(TAG, "gxxAppExceptionCallback:status(0--下线 1--上线)= "+status);
    }
  • Step 2.初始化回调
// c或cpp文件中
#include 
JavaVM *jvmInstant= NULL;
jobject objInstant = NULL;
jmethodID gxxAppExceptionCallback=NULL;

extern "C"
JNIEXPORT jint JNICALL
Java_com_gosuncn_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);
    //此处的gxxAppExceptionCallback为应用层的回调方法名,回调方法必须在应用层的java类里,如本例是在JniInstant类里,关于方法对应的方法签名(如"(I)V")可使用javap获得,具体在文章中有说明
    gxxAppExceptionCallback = env->GetMethodID(objclass, "gxxAppExceptionCallback", "(ILjava/lang/String;)V");
    return 0;
}
 ...
  • Step 3.回调应用层
/**
* 判断是否已经绑定线程
* @param env   注意这里用取地址符
* @return  true--已经绑定成功,false--已经绑定,无需再绑定
*/
bool attachThread(JavaVM *mJavaVM,JNIEnv* &env) {
    bool attached = false;
    if (mJavaVM == NULL) {
        LOGE("JavaVM == NULL!!!,please check");
        return attached;
    }
    switch (mJavaVM->GetEnv((void **) &env, JNI_VERSION_1_6)) {
        case JNI_OK://已绑定
            LOGI("JNI_OK");
            break;
        case JNI_EDETACHED://已解绑
            LOGI("JNI_EDETACHED");
            if (mJavaVM->AttachCurrentThread(&env, NULL) != JNI_OK) {
                LOGE("Could not attach current thread. ");
            } else {
                attached = true;
            }

            break;
        case JNI_EVERSION:
            LOGE("Invalid java version. ");
    }
    return attached;
}
/**
 * GxxApp异常消息回调
 * @param lHandle  登陆句柄
 * @param eEvent 异常事件
 * @param pUserData 用户数据
 */
void funPtrExceptionCallback(long lHandle,
                             EnumExceptionEvent eEvent,char *jsonData
                             void *pUserData) {   
    JNIEnv *env;
    bool attached = attachThread(jvmInstant, env);
    //在此处将数据回调到应用层,,如本例中JniInstant类中的gxxAppExceptionCallback即可收到此回调
    if (env != NULL && objInstant != NULL && gxxAppExceptionCallback != NULL) {
    	jstring msg = env->NewStringUTF(jsonData);//返回到java层,由jvm进行释放
	    //CallVoidMethod后面为方法的参数,方法签名时有多少个,就得写多少个,本例中只有一个整型参数
        env->CallVoidMethod(objInstant, gxxAppExceptionCallback, (jint) eEvent,msg );
    }
    if (attached) {
        jvmInstant->DetachCurrentThread();
    }

}

返回多个参数到应用层

有时候经常需要返回多个参数到应用层,那么目前想到用数组作为返回参数,以下示例

 public native String[] test();
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_gosuncn_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 
#include 
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/gosuncn/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, "", "()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 


#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了

  • GSLog.h文件

    //
    // Created by HuWeiJian on 2019/12/20.
    //
    
    #ifndef GSLOG_H
    #define GSLOG_H
    
    #include 
    
    #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
    
    
  • GSLog.cpp文件

    //
    // Created by HuWeiJian on 2019/12/20.
    //
    
    #include "GSLog.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, "", "([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 

#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 
#include 
#include 

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块中的则是各种配置

参考

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

你可能感兴趣的:(android)