JNI入门

JNI入门

[TOC]

说明

关于jni api的使用,引用的深入学习,异常与使用优化,请参见我的另一篇文章:JNI代码实践

  1. Demo: cmake脚本+jni层socket通讯: TestCmakeLinuxSocket
  2. Demo: linux多线程+native 代码与托管代码互调+kotlin使用jni: TestKotlinLinuxThread

cmake方式编译jni

cmake脚本可以自动化生成makefile文件进行编译

环境准备

  1. sdk mgr里下载ndk cmake lldb
  2. 配置ndk环境变量
  3. 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 <jni.h>
#include <string>

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 <android/log.h>

// 调用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 <endian.h>
    #include <netinet/in6.h>
    #include <sys/cdefs.h>
    #include <sys/socket.h>
    
    #include <linux/in.h>
    #include <linux/in6.h>
    #include <linux/ipv6.h>
    

    自动导包导入的头文件,应当是最原始的声明头文件。

  • 有时候没有include相应的头文件也能使用,是因为其他头文件中有层层include

  • 所有include的头文件所形成的依赖中如果没有所需的头文件,是无法使用对应的函数,对象,变量的

追溯timeval的头文件

发现没有include和timeval有关的头文件,但是可以直接使用timeval,通过注释头文件include的代码,发现是string声明了,再查看string的头文件include,进行排查,以此类推,最终追溯到linux/time.h,与自动导包导入的头文件一致

//#include <string>

// ------------------  for <string>
//#include <__config>
//#include <string_view>
//#include <iosfwd>
//#include <cstring>
//#include <cstdio>  // For EOF.
//#include <cwchar>
//#include <algorithm>
//#include <iterator>
//#include <utility>
//#include <memory>
//#include <stdexcept>
//#include <type_traits>
//#include <initializer_list>
//#include <__functional_base>
//#include <cstdint>
//#include <__debug>
//#include <__undef_macros>

// ------------------  for <stdexcept>
//#include <__config>
//#include <exception>
//#include <iosfwd>
//#include <cstdlib>

// ------------------  for <iosfwd>
//#include <__config>
//#include <wchar.h>  // for mbstate_t

// ------------------  for <wchar.h>
//#include <sys/cdefs.h>
//#include <stdio.h>
//
//#include <stdarg.h>
//#include <stddef.h>
//#include <time.h>
//#include <xlocale.h>
//
//#include <bits/mbstate_t.h>
//#include <bits/wchar_limits.h>
//#include <bits/wctype.h>

// ------------------  for <time.h>
//#include <sys/cdefs.h>
//#include <sys/time.h>
//#include <xlocale.h>

// ------------------  for <sys/time.h>
//#include <sys/cdefs.h>
//#include <sys/types.h>
#include <linux/time.h>
//#include <sys/select.h>


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 <wchar.h>

参见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脚本

系统预设变量/环境变量/全局变量/变量

  • 通过set指令可以设置自定义变量,或者修改系统预设变量

    # 修改当前项目产出库文件的路径CMAKE_LIBRARY_OUTPUT_DIRECTORY
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${ARTIFACT_PATH}/${ANDROID_ABI})
    # 自定义产出路径ARTIFACT_PATH
    set(ARTIFACT_PATH ${PROJECT_SOURCE_DIR}/../artifacts)
    
  • 可以使用系统的环境变量

    参见ENVSet Environment Variable

    # set(ENV{<variable>} <value>...)
    # Set the current process environment <variable> 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 <command> [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 <file>... destination  - copy files to destination (either file or directory)
      copy_directory <dir>... destination   - copy content of <dir>... directories to 'destination' directory
      copy_if_different <file>... destination  - copy files if it has changed
      echo [<string>...]        - displays arguments as text
      echo_append [<string>...] - 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 <dir>...   - create parent and <dir> directories
      md5sum <file>...          - create MD5 checksum of files
      remove [-f] <file>...     - 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 <number>...         - 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(<name> [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(<target> [SYSTEM] [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

给target设置include_directories,若使用这个指令导入第三方库的include头文件目录,在使用第三方的头文件时,不需要处理路径,与系统头文件的导入方式一致

//#include "../../myint/include/my-int.h"
#include <my-int.h>
target_include_directories(${LIB_NAME} PRIVATE
        ${PROJECT_SOURCE_DIR}/myint/include
        ${PROJECT_SOURCE_DIR}/mylong/include)

add_custom_command

增加自定义的构建规则到生成的构建系统中,两种使用方式

  1. 增加一个自定义的文件输出
  2. 为某个目标(库/可执行文件)增加一个自定义指令,需要指定触发时机(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<pthread_t, int *> *map = g_cache.threadMap;
    for (std::map<pthread_t, int *>::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;

创建线程需要四个参数:

  1. 一个用于返回新建线程信息(包含tid)的指针
  2. 线程附加属性pthread_attr_t,可以是NULL
  3. 线程执行函数的函数指针(类似java的Runnable),入参是 void* , 返回值是void*
  4. 作为第三个参数所指向的函数的入参

线程执行函数的入参

此入参是一个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编程详解

客户端连接服务端

  1. 创建服务端地址的struct
  2. 创建客户端的socket套接字,获取到socket描述符
  3. setsockopt设置此描述符对应的socket的属性
  4. connect连接服务端地址
  5. c/s发送和接收数据
  6. 关闭socket结束通讯
void connectSocket(const char* srvIp, int port)
{
    // 1. 创建服务端地址的struct
    // sockaddr_in 在 <linux/in.h>
    struct sockaddr_in srvAddr;
    
    memset(&srvAddr, 0, sizeof(srvAddr));
    srvAddr.sin_family = PF_INET;
    // inet_addr 在 <arpa/inet.h>
    srvAddr.sin_addr.s_addr = inet_addr(srvIp);
    // htons 在 <endian.h>
    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);
}

代码说明

  1. app如果没有网络权限,socket(PF_INET, SOCK_STREAM, 0)将不能创建套接字,fd小于0
  2. 如果请求的服务端地址不对,connect将返回-1,注意服务端监听套接字时,不要使用127.0.0.1或者localhost,否则客户端即使连接的是正确的ip,例如192.168.1.6connect也会失败

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 <errno.h> 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通过第二个参数控制关闭行为:
    1. SHUT_RD:值为0,关闭连接的读这一半。
    2. SHUT_WR:值为1,关闭连接的写这一半。
    3. 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编程

下一步的学习目标

  1. native代码写加密解密
  2. 移植ffmpeg
  3. native-activity
  4. jni层使用sqlite
  5. jni层使用libevent进行网络请求
  6. Bitmap的native优化库
  7. 回炉重造c++
  8. opengl

参考资料

NDK开发 从入门到放弃(六:JAVA与C++灰化图片的效率对比)

Android NDK开发之旅 目录

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容