JNI入门
[TOC]
说明
关于jni api的使用,引用的深入学习,异常与使用优化,请参见我的另一篇文章:JNI代码实践
- Demo: cmake脚本+jni层socket通讯: TestCmakeLinuxSocket
- Demo: linux多线程+native 代码与托管代码互调+kotlin使用jni: TestKotlinLinuxThread
cmake方式编译jni
cmake脚本可以自动化生成makefile文件进行编译
环境准备
- sdk mgr里下载ndk cmake lldb
- 配置ndk环境变量
- as中新建项目,勾选支持c++
项目基础结构
默认Module结构
src
|-- main
|-- cpp
|-- native-lib.cpp
|-- java
build.gradle
CMakeLists.txt
c++代码放置在main下与java目录同级的cpp目录
构建脚本为CMakeLists.txt
,默认与Module的build.gradle
脚本同级
build.gradle增加cmake的相关声明
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions"
}
}
}
externalNativeBuild {
cmake {
// 可以修改cmake脚本路径
path "CMakeLists.txt"
}
}
}
增加STL的支持
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions"
// 将stl作为c++静态库编入
arguments "-DANDROID_STL=c++_static"
}
}
基本cmake脚本
包含四个基本指令
- cmake_minimum_required 设置构建native库的最低版本
- add_library 声明本库/导入第三方库
- find_library 查找一个预编译的(系统)库
- target_link_libraries 给本库添加依赖库
# 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)
# 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.
# 库的标识名
native-lib
# Sets the library as a shared library.
# 动态库(so)标识
SHARED
# Provides a relative path to your source file(s).
# 库要编译的源码文件列表
src/main/cpp/native-lib.cpp)
# 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.
# 给本库添加log库的依赖
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib})
Java层的native声明
class JniClient {
// 声明native方法
public native String stringFromJNI();
static {
// 载入so库
System.loadLibrary("native-lib");
}
}
cpp声明对应的方法
#include
#include
extern "C" JNIEXPORT jstring JNICALL
Java_cn_rexih_android_testjni2_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
- cpp的函数名为Java加上java方法名的全路径,使用
_
分隔 - cpp函数的入参比java方法增加JNIEnv和jobject(jobject表示java方法声明所在的类的实例对象this)
- 其他入参对应转换到c层的类型
javah生成的jni接口头文件说明
#ifndef CountJni_H
#define CountJni_H
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: cn_rexih_android_testkotlinlinuxthread_jni_CountJni
* Method: setCallback
* Signature: (Ljava/lang/Object;)V
*/
JNIEXPORT void JNICALL Java_cn_rexih_android_testkotlinlinuxthread_jni_CountJni_setCallback
(JNIEnv *, jobject, jobject);
#ifdef __cplusplus
}
#endif
#endif
- 通过ifndef - define - endif 防止多重引用时重复定义
- 通过ifdef __cplusplus - endif来混编c++/c代码
- c++文件中函数定义时,应该复制h文件中的函数完整签名,包含JNIEXPORT和JNICALL的宏定义字段
// in c++ file
JNIEXPORT void JNICALL Java_cn_rexih_android_testkotlinlinuxthread_jni_CountJni_setCallback
(JNIEnv * env, jobject thiz, jobject callback){
// statement
}
extern "C"
原因:混编c++/c代码需要:
- c代码编译时方法签名是函数名,c++因为有函数重载,所以方法签名会有其他信息(类似java的方法签名)
- 如果把一个c编译器编译的目标代码与c++的进行链接,就会出现连接失败
- 解决:ifdef __cplusplus判断是否是c++代码,如果是的话,使用 extern "C" 来用传统的C函数编译方法对该函数进行编译
JNIEXPORT和JNICALL
- JNIEXPORT保证函数在导出的库的函数表中可见,否则JNI找不到此方法
- 如果在JNI_onLoad调用registerNatives去动态注册方法,则不要加上JNIEXPORT标记
- JNICALL保证编译器使用相同的调用转换(从左往右、从右往左调用入参),与cpu架构有关,应该始终添加此宏定义
- 参见when to use jniexport and jnicall in android ndk
调用native层的安卓库
以logcat为例
# cmake中,target_link_libraries链接上log库
# find_library(log-lib log)
# target_link_libraries(native-lib ${log-lib})
// 声明头文件
#include
// 调用api
__android_log_print(ANDROID_LOG_FATAL, "rexih", "executeCount :test point1:%s:", cThreadName);
include头文件
-
自动导包提示导入的头文件,和网上博客中说明的可能不一致,但是可能存在包含关系,比如
netinet/in.h
中也include了linux/in.h
// netinet/in.h #include
#include #include #include #include #include #include 自动导包导入的头文件,应当是最原始的声明头文件。
有时候没有include相应的头文件也能使用,是因为其他头文件中有层层include
所有include的头文件所形成的依赖中如果没有所需的头文件,是无法使用对应的函数,对象,变量的
追溯timeval的头文件
发现没有include和timeval有关的头文件,但是可以直接使用timeval,通过注释头文件include的代码,发现是string声明了,再查看string的头文件include,进行排查,以此类推,最终追溯到linux/time.h
,与自动导包导入的头文件一致
//#include
// ------------------ for
//#include <__config>
//#include
//#include
//#include
//#include // For EOF.
//#include
//#include
//#include
//#include
//#include
//#include
//#include
//#include
//#include <__functional_base>
//#include
//#include <__debug>
//#include <__undef_macros>
// ------------------ for
//#include <__config>
//#include
//#include
//#include
// ------------------ for
//#include <__config>
//#include // for mbstate_t
// ------------------ for
//#include
//#include
//
//#include
//#include
//#include
//#include
//
//#include
//#include
//#include
// ------------------ for
//#include
//#include
//#include
// ------------------ for
//#include
//#include
#include
//#include
void test(){
struct timeval timeout = {1, 0};
}
#include_next
追溯time.h过程中,遇到wchar.h
中有一条#include_next
表示略过当前位置的头文件,包含指定的这个文件所在的路径的后面路径的那个文件
//ndk-bundle/sources/cxx-stl/llvm-libc++/include/wchar.h
#include_next
参见gcc:预处理语句--#include和#include_next
jniLibs/jni/cpp目录区别
- jni是ndk-build方式构建native代码方式存放c/c++代码的目录
- cpp是cmake方式构建native代码方式存放c/c++代码的目录
- jniLibs是默认的已编译的so库存放的位置
待编译与已编译so库路径冲突: More than one file was found with OS independent path 'lib/armeabi/libxxx.so'
默认so库存放路径
/src/main/jniLibs
修改默认so库存放路径
android {
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
实际上so库存放路径是一个可以修改的变量jniLibs.srcDirs
,这个变量下的so库在编译时都会添加到产出里(例如打入apk),所以可以修改这个值可以切换到任何路径
so文件编译后的产出路径
- 可以在Module的
build/intermediates/cmake
下找到编译好的so文件
指定so产出目录
cmake脚本中指定
# PROJECT_SOURCE_DIR是Module的主CMakeList.txt所在位置
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/artifacts/${ANDROID_ABI})
产出目录不能指定为jniLibs
根据小标题的报错信息,猜测
jni代码编译后,在Module编译时,会有一个"虚拟的路径" lib/armeabi/ 指向jni编译后产出的so库
编译过程:
- 先编译jni代码,生成so库
- 再编译Module,将项目代码和依赖库,包括所有jni目录下的so一起打包,生成aar或者apk
如果将产出目录指定为jniLibs,则在第一步就会将产出输出到jniLibs下;在第二步中就会将第一步输出到jniLibs下的so加入打包;
而jni代码编译所产生的"虚拟路径下"又会有同名的so文件要参与打包,则发生冲突。
所以产出目录不能指定为jniLibs
CMake脚本
系统预设变量/环境变量/全局变量/变量
- 预设变量可以查看cmake-variables(7)
- 预设属性可以查看[cmake-properties(7)
-
通过
set
指令可以设置自定义变量,或者修改系统预设变量# 修改当前项目产出库文件的路径CMAKE_LIBRARY_OUTPUT_DIRECTORY set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${ARTIFACT_PATH}/${ANDROID_ABI}) # 自定义产出路径ARTIFACT_PATH set(ARTIFACT_PATH ${PROJECT_SOURCE_DIR}/../artifacts)
-
可以使用系统的环境变量
参见ENV和Set Environment Variable
# set(ENV{
} ...) # Set the current process environment to the given value. # 通过$ENV{环境变量名}获取到环境变量的值 # Use the syntax $ENV{VAR} to read environment variable VAR. message($ENV{HOME}/tmp)
CMAKE_CURRENT_SOURCE_DIR
指向CMakeList.txt文件所在的目录
PROJECT_SOURCE_DIR
指向Project的根目录
- 调用
project
指令声明项目名称时所在的目录,如果是在入口脚本文件里声明,则目录为入口脚本文件所在的目录,不局限于cpp目录或者Module目录
PROJECT_BINARY_DIR
项目的Cmake构建目录(Full path to build directory for project)
message(PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR})
message(PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR})
message(CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR})
# 对应输出
Variant=debug ABI=x86_64 :PROJECT_BINARY_DIR:/Users/rexih/dev/project/AndroidStudioProjects/TestJni/jnilib/.externalNativeBuild/cmake/debug/x86_64
Variant=debug ABI=x86_64 :PROJECT_SOURCE_DIR:/Users/rexih/dev/project/AndroidStudioProjects/TestJni/jnilib/src/main/cpp
Variant=debug ABI=x86_64 :CMAKE_CURRENT_SOURCE_DIR:/Users/rexih/dev/project/AndroidStudioProjects/TestJni/jnilib/src/main/cpp
CMAKE_COMMAND
cmake执行文件所在的绝对路径
这个变量通常是在add_custom_command自定义指令中使用,通过这个变量去调用
cmake -E
来执行平台无关的操作指令,参见Command-Line Tool Mode-
将cmake路径添加到环境变量,然后在命令行中输入
cmake -E
可以看到支持的通用指令CMake Error: cmake version 3.6.0-rc2 Usage: cmake -E
[arguments...] Available commands: chdir dir cmd [args...] - run command in a given directory compare_files file1 file2 - check if file1 is same as file2 copy ... destination - copy files to destination (either file or directory) copy_directory ... destination - copy content of ... directories to 'destination' directory copy_if_different ... destination - copy files if it has changed echo [ ...] - displays arguments as text echo_append [ ...] - displays arguments as text but no new line env [--unset=NAME]... [NAME=VALUE]... COMMAND [ARG]... - run command in a modified environment environment - display the current environment make_directory ... - create parent and directories md5sum ... - create MD5 checksum of files remove [-f] ... - remove the file(s), use -f to force it remove_directory dir - remove a directory and its contents rename oldname newname - rename a file or directory (on one volume) tar [cxt][vf][zjJ] file.tar [file/dir1 file/dir2 ...] - create or extract a tar or zip archive sleep ... - sleep for given number of seconds time command [args...] - run command and return elapsed time touch file - touch a file. touch_nocreate file - touch a file but do not create it. Available on UNIX only: create_symlink old new - create a symbolic link new -> old
CMAKE_VERBOSE_MAKEFILE
默认false,手动设置后可以看到更多的make构建日志(Enable verbose output from Makefile builds)
CMAKE_LIBRARY_OUTPUT_DIRECTORY
给项目所有的library的target初始化LIBRARY_OUTPUT_DIRECTORY属性,但是如果target主动设置则被覆盖
系统预设属性
LIBRARY_OUTPUT_DIRECTORY
- LIBRARY_OUTPUT_DIRECTORY是系统预设的Targets的属性
- 定义了目标库构建后的产出路径,会被CMAKE_LIBRARY_OUTPUT_DIRECTORY初始化
- 参见LIBRARY_OUTPUT_DIRECTORY
- 设置target属性见下文
OUTPUT_NAME
修改target输出时的库的名字,是系统预设的Targets的属性,默认是target的名字
set_target_properties(${LIB_NAME} PROPERTIES OUTPUT_NAME "long")
IMPORTED_LOCATION
导入的第三方库,需要设置这个系统预设的Targets的属性,来指定库的位置
cmake指令说明
cmake_minimum_required
见上文
find_library
见上文
target_link_libraries
见上文
set
见上文
project
见上文
message
打印信息,可以用${}转义变量值
message(PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR})
add_subdirectory
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
- 引入其他目录,会去引入的目录里查询其他CMakelist.txt文件来加入构建过程
- 调用add_subdirectory前声明的变量,在子脚本中可以直接引用
- 单参数表示源码的路径,可以用绝对路径,或者基于当前CMakelist.txt文件所在路径的相对路径
- 参见add_subdirectory
add_library
add_library( [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])
-
声明库
# add_library指令声明库需要三部分参数 库名 STATIC(.a静态库)/SHARED(.so动态库) 源文件列表 add_library(${LIB_NAME} SHARED ${SRC_LIST})
-
导入第三方库
# 引入第三方库 IMPORTED表示是导入的库 add_library(third-party SHARED IMPORTED)
set_target_properties
set_target_properties(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)
设置库target的属性值
-
举例:设置该库的产出位置
set_target_properties(${LIB_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${ARTIFACT_PATH}/${LIB_NAME}/lib/${ANDROID_ABI}")
target_include_directories
target_include_directories( [SYSTEM] [BEFORE]
[items1...]
[ [items2...] ...])
给target设置include_directories,若使用这个指令导入第三方库的include头文件目录,在使用第三方的头文件时,不需要处理路径,与系统头文件的导入方式一致
//#include "../../myint/include/my-int.h"
#include
target_include_directories(${LIB_NAME} PRIVATE
${PROJECT_SOURCE_DIR}/myint/include
${PROJECT_SOURCE_DIR}/mylong/include)
add_custom_command
增加自定义的构建规则到生成的构建系统中,两种使用方式
- 增加一个自定义的文件输出
- 为某个目标(库/可执行文件)增加一个自定义指令,需要指定触发时机(PRE_BUILD | PRE_LINK| POST_BUILD)
举例:构建后导出头文件到指定目录
add_custom_command(
# 指定关联的TARGET
TARGET ${LIB_NAME}
# TARGET 构建后执行
POST_BUILD
# TARGET 构建后执行的指令
COMMAND "${CMAKE_COMMAND}" -E
copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/include"
"${ARTIFACT_PATH}/${LIB_NAME}/include"
# 该自定义指令的注释,构建前会打印
COMMENT "Copying ${LIB_NAME} header files to artifact directory")
Module下jni多库、主从库的结构
cpp
|-- main
|-- include
|-- CMakeLists.txt
|-- lib1
|-- include
|-- CMakeLists.txt
|-- lib2
|-- include
|-- CMakeLists.txt
CMakeLists.txt
- cpp目录下的CMakeLists.txt作为cmake脚本入口,在Module的build.gradle脚本中注册其路径
- cpp目录下的CMakeLists.txt可以配置项目环境,查找公共库并声明,声明全局变量等,再通过ADD_SUBDIRECTORY指令添加各个库自己的cmake脚本
- lib1/lib2作为主库main的从库
- 各个库有其自己的CMakeLists.txt来声明库和源文件,进行链接等操作
入口脚本
cmake_minimum_required(VERSION 3.4.1)
SET(CMAKE_VERBOSE_MAKEFILE on)
# 设置项目名称
project(REXIH)
# 打印信息
message(PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR})
message(PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR})
message(CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR})
# 设置产出目录的根目录
set(ARTIFACT_PATH ${PROJECT_SOURCE_DIR}/../artifacts)
# 查找log库便于各个库链接
find_library(log-lib log)
# 设置全局变量
set(LIB_MAIN my-jni)
set(LIB_LONG my-long)
set(LIB_INT my-int)
# 添加各个库的子目录
ADD_SUBDIRECTORY(main)
ADD_SUBDIRECTORY(myint)
ADD_SUBDIRECTORY(mylong)
主、从库脚本
set(LIB_NAME ${LIB_LONG})
# 设置 库的源文件列表
set(SRC_LIST my-long.cpp)
# add_library指令声明库需要三部分参数 库名 STATIC(.a静态库)/SHARED(.so动态库) 源文件列表
add_library(${LIB_NAME} SHARED ${SRC_LIST})
# 设置产生的so的文件名
set_target_properties(${LIB_NAME} PROPERTIES OUTPUT_NAME "long")
# 设置产出目录
set_target_properties(${LIB_NAME}
PROPERTIES LIBRARY_OUTPUT_DIRECTORY
"${ARTIFACT_PATH}/${LIB_NAME}/lib/${ANDROID_ABI}")
# 设置导出头文件
add_custom_command(TARGET ${LIB_NAME}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E
copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/include"
"${ARTIFACT_PATH}/${LIB_NAME}/include"
COMMENT "Copying ${LIB_NAME} header files to artifact directory")
# 没发现有什么用途
# include_directories(include/)
target_link_libraries(${LIB_NAME} ${log-lib})
# 主库脚本中可以链接从库
target_link_libraries(${LIB_NAME} ${log-lib} my-int my-long)
引入第三方so库
- 第三方的so库需要放到
jniLibs
目录下 -
${CMAKE_SOURCE_DIR}
表示的是CMakeLists.txt
所在的路径,我们指定第三方so
所在路径时,应当以这个常量为起点。
# 引入第三方库
add_library(third-party SHARED IMPORTED)
# 设置第三方库的路径
set_target_properties(third-party PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libthird-party.so )
# 链接第三方库
target_link_libraries(${LIB_NAME} third-party ${log-lib})
资料
cmake 学习笔记(一)
使用 CMake 进行 NDK 开发之如何编写 CMakeLists.txt 脚本
AndroidStudio之NDK开发CMake CMakeLists.txt编写入门
Android Studio NDK CMake 指定so输出路径以及生成多个so的案例与总结
native回调java层
此节仅做简单说明,详细内容见下篇文章。
FindClass查找类的jclass对象
env->FindClass("cn/rexih/android/testkotlinlinuxthread/jni/OnDataChangedListener"));
GetMethodID/GetFieldID查找类的成员方法/变量
env->GetMethodID(g_java_class.classOnDataChangedListener, "onChanged", "(Ljava/lang/String;II)V");
CallXXXXMethod/GetXXXField 执行某个方法/获取某个变量
env->CallVoidMethod(g_cache.callback, g_cache.methodOnChanged,
env->NewStringUTF(threadName), count, type);
JNI_OnLoad/JNI_OnUnload加载jobject全局缓存信息
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK)
return result;
g_cache.vm = vm;
g_java_class.classOnDataChangedListener = (jclass) env->NewGlobalRef(
env->FindClass("cn/rexih/android/testkotlinlinuxthread/jni/OnDataChangedListener"));
g_cache.methodOnChanged = env->GetMethodID(g_java_class.classOnDataChangedListener, "onChanged",
"(Ljava/lang/String;II)V");
return JNI_VERSION_1_6;
}
void JNICALL releaseGlobalCache(JNIEnv *env) {
env->DeleteGlobalRef(g_java_class.classOnDataChangedListener);
env->DeleteGlobalRef(g_cache.callback);
g_cache.callback = NULL;
g_cache.methodOnChanged = NULL;
g_cache.pNotifyCallback = NULL;
g_java_class.classOnDataChangedListener = NULL;
}
void JNI_OnUnload(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return;
}
std::map *map = g_cache.threadMap;
for (std::map::iterator itor = map->begin(); itor != map->end(); itor++) {
delete itor->second;
}
delete g_cache.threadMap;
g_cache.threadMap = NULL;
pthread_mutex_destroy(&g_cache.lock);
releaseGlobalCache(env);
}
全局引用与局部引用
jobject对象可以通过env->NewGlobalRef(jobj)
将一个对象转为全局引用的对象,直到DeleteGlobalRef
。因为在jni接口方法结束后,对象可能就会被回收,需要通过NewGlobalRef来持有引用,防止函数执行完对象被销毁
JNI字符串处理
- 一般使用带UTF的字符串相关方法
- 使用完后释放
- 中文乱码还待研究,若有需要可以通过FindClass调用String类的API进行编码转换
JNI_Onload动态注册jni接口api
声明一个JNINativeMethod
结构体的数组表明native和java方法的映射关系。并在JNI_OnLoad
方法中调用RegisterNatives
注册以使用。
java层的方法声明
public native void testMethods(DetailProduct entity);
public native void testFields(DetailProduct entity);
public static native Field testReflection(Product entity, Field idField,Method setQuantityMethod);
c++层的方法声明
extern "C" JNIEXPORT jobject JNICALL testReflection(JNIEnv *env, jclass clazz, jobject entity, jobject idField, jobject setQuantityMethod);
extern "C" JNIEXPORT void JNICALL testMethods(JNIEnv *env, jobject instance, jobject entity);
extern "C" JNIEXPORT void JNICALL testFields(JNIEnv *env, jobject instance, jobject entity);
注册方法的映射关系
static JNINativeMethod gMethods[] = {
{"testReflection", "(Lcn/rexih/android/testnativeinterface/entity/Product;Ljava/lang/reflect/Field;Ljava/lang/reflect/Method;)Ljava/lang/reflect/Field;", (void *) testReflection},
{"testFields","(Lcn/rexih/android/testnativeinterface/entity/DetailProduct;)V",(void*)testFields},
{"testMethods","(Lcn/rexih/android/testnativeinterface/entity/DetailProduct;)V",(void*)testMethods}
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
//...
jclass clazz = env->FindClass("cn/rexih/android/testnativeinterface/MainActivity");
// ...
// jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*, jint);
if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
return JNI_FALSE;
}
return JNI_VERSION_1_6;
}
JNINativeMethod是一个成员都是指针的struct
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
其实例对象长度固定,所以RegisterNatives的方法个数参数,可以用"struct数组总大小/一个元素的大小"得到。
参考资料
Android之JNI动态注册native方法和JNI数据简单使用
动态注册JNI、多JNI调用
JNI:使用RegisterNatives方法传递和使用Java自定义类
Linux c 多线程
在native层通过pthread库使用多线程
创建线程pthread_create
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
void pthread_exit(void* __return_value) __noreturn;
创建线程需要四个参数:
- 一个用于返回新建线程信息(包含tid)的指针
- 线程附加属性pthread_attr_t,可以是NULL
- 线程执行函数的函数指针(类似java的Runnable),入参是 void* , 返回值是void*
- 作为第三个参数所指向的函数的入参
线程执行函数的入参
此入参是一个void*指针,其应当指向一个由堆分配的内存区域。如果是栈分配的,在线程中可能获取不到此指针指向的数据。(因为堆是线程共享,而栈是线程各自持有的)
所以如果要向pthread_create的线程执行函数传递入参指针,可以考虑先new
出对象,再在合适的时候delete
掉创建的对象
设置线程属性
pthread_create的第二个参数,可以是一个pthread_attr_t
//实例化属性
pthread_attr_t threadAttr_;
//初始化属性
pthread_attr_init(&threadAttr_);
//设置属性值
pthread_attr_setdetachstate(&threadAttr_, PTHREAD_CREATE_DETACHED);
//创建线程
int result = pthread_create(&tid, &threadAttr_, executeCount, info);
//销毁属性
pthread_attr_destroy(&threadAttr_);
joinable/detached
线程的状态要么是joinable要么是detached
joinable: 线程结束后,需要在其他线程,手动调用pthread_join
阻塞等待指定线程,直到获取到线程的返回值,进行资源释放操作
detached:在其他线程调用pthread_detach
指定其他线程为分离线程,不会阻塞等待,指定线程结束后自动释放资源
pthread_create(&tid, NULL, (void*)thread1, NULL);
pthread_detach(tid); // 使线程处于分离状态
如果一个线程是分离线程,pthread_create后,执行函数速度很快可能已经结束,并把线程号交给其他线程使用,此时pthread_create返回的pthread_t信息可能是错误的,为此可以考虑在执行函数中,调用pthread_cond_timewait
函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回
退出线程
void pthread_exit(void* __return_value) __noreturn;
pthread_exit((void*)tmp);
可以在线程执行函数需要退出时调用,并传递返回值
创建和销毁线程的互斥锁
pthread_mutex_init(&g_cache.lock, NULL);
pthread_mutex_destroy(&g_cache.lock);
使用互斥锁和解锁
pthread_mutex_lock(&g_cache.lock);
int currentFlag = *pCancelFlag;
pthread_mutex_unlock(&g_cache.lock);
__android_log_print(ANDROID_LOG_FATAL, "rexih", "excuteCount :thread:%s :flag:%d :count:%d", cThreadName, currentFlag, i);
JNI下Linux多线程
native层创建的pthread线程没有关联虚拟机,是无法使用JavaVM和JNIEnv的,如果需要使用,必须先用全局的JavaVM去AttachCurrentThread
,再用JavaVM获取当前线程的JNIEnv对象。
JavaVM *javaVM = g_cache.vm;
JNIEnv *env;
jint res = javaVM->GetEnv((void **) &env, JNI_VERSION_1_6);
if (res != JNI_OK) {
res = javaVM->AttachCurrentThread(&env, NULL);
if (JNI_OK != res) {
LOGE("Failed to AttachCurrentThread, ErrorCode = %d", res);
return NULL;
}
}
在这个过程中,会在Java层的main Thread Group中创建一个新的Thread对象,这样这个native层的JNIENVISION就有了环境。
使用结束后调用DetachCurrentThread
释放此线程资源
资料
参见
POSIX thread (pthread) libraries
创建脱离线程 pthread_attr_setdetachstate
Linux c socket通讯
参见Linux的SOCKET编程详解
客户端连接服务端
- 创建服务端地址的struct
- 创建客户端的socket套接字,获取到socket描述符
- setsockopt设置此描述符对应的socket的属性
- connect连接服务端地址
- c/s发送和接收数据
- 关闭socket结束通讯
void connectSocket(const char* srvIp, int port)
{
// 1. 创建服务端地址的struct
// sockaddr_in 在
struct sockaddr_in srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
// inet_addr 在
srvAddr.sin_addr.s_addr = inet_addr(srvIp);
// htons 在
srvAddr.sin_port = htons(port);
// 2. 创建客户端的socket套接字,获取到socket描述符
// 0表示根据第二个参数自动设置第三个参数协议
int socketDescriptor = socket(PF_INET, SOCK_STREAM, 0);
if(0 > socketDescriptor)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return;
}
// 3. setsockopt设置此描述符对应的socket的属性
struct timeval timeout = {1, 0};
// 设置发送超时
setsockopt(socketDescriptor, SOL_SOCKET, SO_SNDTIMEO, (char *) &timeout,
sizeof(struct timeval));
// 设置接收超时
setsockopt(socketDescriptor, SOL_SOCKET, SO_RCVTIMEO, (char *) &timeout,
sizeof(struct timeval));
// 接收缓冲区设置为32K
int nRecvBuf = 32 * 1024;
setsockopt(socketDescriptor, SOL_SOCKET, SO_RCVBUF, (const char *) &nRecvBuf,
sizeof(int));
// 发送缓冲区设置为32K
int nSendBuf = 32 * 1024;
setsockopt(socketDescriptor, SOL_SOCKET, SO_SNDBUF, (const char *) &nSendBuf,
sizeof(int));
// 4. connect连接服务端地址
int ret = connect(socketDescriptor, (struct sockaddr *) &srvAddr,
sizeof(struct sockaddr));
if(0 > ret)
{
printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
return;
}
// 5. c/s发送和接收数据
char buf[BUFSIZ] = "from client~";
// send 数据
if(0 > send(socketDescriptor, buf, strlen(buf), 0))
{
perror("send msg error");
return;
}
// 6. 关闭socket结束通讯
shutdown(socketDescriptor, SHUT_RDWR);
}
代码说明
- app如果没有网络权限,
socket(PF_INET, SOCK_STREAM, 0)
将不能创建套接字,fd小于0 - 如果请求的服务端地址不对,
connect
将返回-1,注意服务端监听套接字时,不要使用127.0.0.1
或者localhost
,否则客户端即使连接的是正确的ip,例如192.168.1.6
,connect
也会失败
memset
头文件string.h
void * memset(void *b, int c, size_t len);
将b指针指向的对象,使用c的值,填充到对象的中,须要声明长度
memset(&srvAddr, 0, sizeof(srvAddr));
使用0,填充srvAddr结构,从指针指向位置开始,填充长度由len指定
timeval
timeval有两个值,一个是秒,一个是微秒(us)
struct timeval {
__kernel_time_t tv_sec;
__kernel_suseconds_t tv_usec;
};
errno & strerror & perror
The
header file defines the integer variable errno, which
is set by system calls and some library functions in the event of an
error to indicate what went wrong.
- errno是系统调用或者某些库函数的错误码,当相关的调用出错会将errno设置为对应的错误码
- 使用
strerror(errno)
可以获取到错误码的简单描述信息 -
perror("tag")
相当于打印strerror(errno)
并加上"tag"前缀标签
参见Linux Programmer's Manual ERRNO(3) ; Linux errno详解
setsockopt
extern int setsockopt(int __fd, int __level, int __option, const void *__value, socklen_t __value_length)
- 根据__option操作编号,对fd表示的套接字设置对应的属性值
- 最后两个参数表示要设置的属性值得指针地址及其长度
- __level表示所要设置的选项所位于的层中。同一个选项可能存在于多个层的协议中
- 返回值:成功0,失败-1,errno被设置为
- EBADF:__fd不是有效的文件描述词
- ENOTSOCK:__fd描述的不是套接字
- ENOPROTOOPT:__level指定的协议层不能识别选项
- EFAULT:__value指向的内存并非有效的进程空间
- EINVAL:在调用setsockopt()时,__value_length无效
setsockopt(socketFd, SOL_SOCKET, SO_SNDTIMEO, (char *) &timeout, sizeof(struct timeval));
参见:setsockopt()函数功能介绍
shutdown和close
extern int shutdown(int __fd, int __how)
- shutdown通过第二个参数控制关闭行为:
- SHUT_RD:值为0,关闭连接的读这一半。
- SHUT_WR:值为1,关闭连接的写这一半。
- SHUT_RDWR:值为2,连接的读和写都关闭。
- 多进程共享同一个套接字,每个进程中调用close,当前进程不可再使用此套接字,引用计数-1,直到0则断开连接,释放套接字
- shutdown后所有进程都按照关闭方式关闭使用。
参考资料
shutdown和close的区别
linux网络编程之shutdown() 与 close()函数详解
Android Socket通信--通过jni用c++实现客户端
Kotlin声明native方法
加载so库
// 在类的伴生类或者单例类中加载so库
object CountJni {
init {
System.loadLibrary("native-lib")
}
}
class TestJni {
companion object {
init {
System.loadLibrary("test-lib")
}
}
}
external前缀声明native方法
class TestJni {
companion object {
init {
System.loadLibrary("test-lib")
}
}
external fun setCallback(callback: OnDataChangedListener?)
external fun createTask(tag: String)
external fun destroy(): Int
}
kotlin在native层的对应关系
与java保持一致,java+包路径+方法名,以_
分隔
JNIEXPORT void JNICALL Java_cn_rexih_android_testkotlinlinuxthread_jni_CountJni_setCallback
(JNIEnv *, jobject, jobject);
在native代码中查找kotlin中的类和方法,与java一致
interface OnDataChangedListener {
companion object {
val TYPE_CREATE = 1001
val TYPE_UPDATE = 1002
val TYPE_DESTROY = 1003
}
fun onChanged(threadName: String,count: Int,type: Int)
}
g_java_class.classOnDataChangedListener = (jclass) env->NewGlobalRef(
env->FindClass
("cn/rexih/android/testkotlinlinuxthread/jni/OnDataChangedListener"));
g_cache.methodOnChanged = env->GetMethodID(g_java_class.classOnDataChangedListener, "onChanged", "(Ljava/lang/String;II)V");
参考资料
Linux&Android平台下Socket编程
下一步的学习目标
- native代码写加密解密
- 移植ffmpeg
- native-activity
- jni层使用sqlite
- jni层使用libevent进行网络请求
- Bitmap的native优化库
- 回炉重造c++
- opengl
参考资料
NDK开发 从入门到放弃(六:JAVA与C++灰化图片的效率对比)
Android NDK开发之旅 目录