android之一篇史上最适合最全面的JNI入门教程

Posted 小钟视野

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android之一篇史上最适合最全面的JNI入门教程相关的知识,希望对你有一定的参考价值。

前言:

   一定要下载demo,动手动脑,结合本篇博客来跑demo,否则看了也还是不会;写代码还是要勤动手才能掌握,否则里边的坑也只是想当然 demo

NDK的基础知识,强烈推荐小楠总的NDK系列博客,先拜读一遍,照着学习还是很厉害的


一.基础知识

      JNI:是java和c/c++交互的桥梁;有必要去弄明白整个开发流程;jni的效率比java要快,所以一些好性能的都会通过走底层来调用java     用途:用的比较多的是视频、美颜、相机、地图等涉及底层以及效率问题     NDK:是一针对jni中的工具包,包括底层c库、编译工具等               1. 编译
   xxx.c ——> windows .obj ; Linux .o –》 语法检查
 2. 链接:将函数之间的关系链接起来,生成一个静态或动态库文件(可执行文件)
                  .o —–> log.so .dll .exe
  静态库:静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分,已经整合进去了
  动态库:装入过程中将所有动态链接库载入内存。应用程序在运行时,将所有可能要运行到的模块都全部装入内存(共享内存),
  动态链接过程只是把需要调用的函数的路径做个标志(类似于头文件声明),直到运行用到函数时才会从内存载入
     jvm 是虚拟机内存,C/C++ 是 native内存,并且这个 so库 是放在 apk 的 lib 下面的
  当我们调用 java中native声明的javaDiff()方法 的时候会到 Java虚拟机 的内存当中来处理找这个方法,
  而加了 native 关键字的时候他就会去到 C++/c 的堆栈空间找这个 C++/c 的实现。

二.JNI开发注意事项

 jni开发中如果不是作为外部库或.c文件的函数不对外,则可以不生成.h头文件,头文件的声明只是想把当前.c/.cpp文件下的功能提供给外部文件使用,如:demo只有一个.c文件,函数定义  都是可以直接调用的,不需要声明;但是要是作为第三方库且库中的函数对外开放,则必须要有对应.c的头文件.h,否则无法调用相应的函数; 即:不管动态还是静态注册,只要不是对外(.c文件之间以及库和库之间)提供的函数功能,则不需要生成头文件; demo将分开来说,只要按照以下说的步骤,就能清楚知道静态注册、动态注册以及so库对外的函数会在当前JNI函数中被调用的情况。 期间会涉及到修改cmakeList.txt脚本,所以每次修改后,都要同步gradle和重新build--->makeProject(先删除build)
这里也会分两种方式说明: 一、不验证so库,直接gradle,build之后,运行:主要是验证jni的方法是否正确,算是调试吧 二、验证so库       验证so库的整个过程:
 想要只测试so库,需要将验证的so库放到jniLibs指定的目录,然后将build.gradle中相应的所有的cmake都注释掉如下
 externalNativeBuild
          cmake
          
            然后删除build以及.externalNative目录,再者gradle同步,其次build-->makeproject,最后运行,能正确调用相应的jni  的函数
 在c、c++中叫函数 java中叫方法,实际上两个是一样的玩意,这里为了区分jni和native所以有两个叫法

三.JNI开发分类

     1、静态注册

          1) . 静态注册的流程
按照链接给的步骤,就可以创建一个JNIdemo,静态注册,不需要导入so库,直接build->make project之后,点击as的运行,安装app 就可以正常使用native了
2).验证so库
  第一步build之后,在\\app\\build\\intermediates\\cmake目录下就拿到.so库
  1、将module下的build.gradle中的externalNativeBuild都注释掉(防止执行cmakeLists.txt脚本),这样就不会执行脚本生成so库
  2、将\\app\\.externalNativeBuild和app\\build目录删除,排除一切干扰
  3、将so库放到app\\src\\main\\jniLibs目录下,如果没有jniLibs目录就自己创建一个
  4、gradle同步一下,运行成功就验证so库是可以的

      2、动态注册

             大概就是当java中通System.loadLibrary(name);时会调用JNI_OnLoad()函数,所以在这函数中将java中native方法         声明和jni函数实现进行注册,也就是绑定(声明-实现)             生成动态so库的链接: 动态注册             代码如下:             
JNIEXPORT jstring JNICALL
string_from_JNI(
        JNIEnv *env,
        jobject instance/* this */) 
    /* 默认是以c++的方式
     * std::string hello = "Hello from C++";
      return env->NewStringUTF(hello.c_str());
     */
    const char *hello = "Hello from C";
    jstring content = (*env)->NewStringUTF(env, hello);
    (*env)->ReleaseStringChars(env, content, hello);
    return content;


/****
 *通过jni的方法访问java中方法
 */
JNIEXPORT void JNICALL
access_method(JNIEnv *env, jobject instance,jstring methodName) 
    jclass mainClass = (*env)->GetObjectClass(env, instance);
    const char* method_n = (*env)->GetStringUTFChars(env,methodName,NULL);
    jmethodID rid = (*env)->GetMethodID(env, mainClass, method_n,
                                        "(I)I");//最后一个参数是方法签名:即前一个I表示java方法的参数,最后一个I表示返回值
    jint rNum = (*env)->CallIntMethod(env, instance, rid, 20);
    printf("output from C : %d", rNum);


//

/***
参数1:name是Java中方法名。
参数2:signature签名,用字符串是描述了Java中函数的参数和返回值
参数3:fnPtr是函数指针,指向native函数。前面都要接 (void *) C/C++中对应函数的函数名(地址):即指针函数变量名必须和c/c++实现的函数名一样

*/
const JNINativeMethod gMethods[] = 
    "stringFromJNI1","()Ljava/lang/String;",(void*)string_from_JNI,
    "accessMethod1","(Ljava/lang/String;)V",(void*)access_method
;

int registerNatives(JNIEnv* engv) 
    LOGI("registerNatives begin");
    jclass  clazz;
    //这个是具体的类名,不能写错,写错就无法注册成功,也就无法调用jni函数了
    clazz = (*engv) -> FindClass(engv, "com/jni/www/jnidemo/dif/JNIDynamicUtil");

    if (clazz == NULL) 
        LOGI("clazz is null");
        return JNI_FALSE;
    

    if ((*engv) ->RegisterNatives(engv, clazz, gMethods, NELEM(gMethods)) < 0) 
        LOGI("RegisterNatives error");
        return JNI_FALSE;
    

    return JNI_TRUE;


/***
 当java中通System.loadLibrary(name);时会调用此方法
*/
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)

    LOGI("jni_OnLoad begin");

    JNIEnv* env = NULL;
    jint result = -1;

    if ((*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4) != JNI_OK) 
        LOGI("ERROR: GetEnv failed\\n");
        return -1;
    
    assert(env != NULL);

    registerNatives(env);
    return JNI_VERSION_1_4;

      注意gMethod数组和registerNatives(env)函数,重点是在注释;看懂流程才是重点;       这里流程是System.loadLibrary()-->JNI_onLoad()-->registerNatives()--->RegisterNatives(...)       一定要看注释,这个流程中registerNatives(env)中,注意这个参数值,是java中的具体类的路径名,只是将"."换成“/”
//这个是具体的类名,不能写错,写错就无法注册成功,也就无法调用jni函数了
clazz = (*engv) -> FindClass(engv, "com/jni/www/jnidemo/dif/JNIDynamicUtil");
    生成并验证so库     
   1.完成.c或.cpp文件编写(参考链接)
   2.需要在cmakeLists.txt中增加和静态库一样的三个方法,将静态注册中的.c/.cpp文件修改成当前编写的动态注册的.c/.cpp文件
   3.将\\app\\.externalNativeBuild和app\\build目录删除,排除一切干扰
   4.gradle同步后,(要先删除build目录和extenalNativeBuild目录)再build->make project;
   5.build之后,在\\app\\build\\intermediates\\cmake目录下就拿到.so库(如果还保留着静态注册的代码,则会生成两个so库,参考文末demo)
   6.将so库放到app\\src\\main\\jniLibs目录下,如果没有jniLibs目录就自己创建一个(注:确保so库的路径正确,如果存放在其它目录下,需要配置gradle文件的jniLibs.dir具体可以百度)
   7.java的代码中声明native方法,通过staticSystem.loadLibrary(name);加载so库,就可以直接通过调动native声明进而调用到jni函数的实现了
     注:虽然直接复制过来的so库name会带有前缀,直接将name中不要前缀lib 遵循linux生成so库的标准,会加上前缀,但是在调用此方法时要将前缀lib去掉) 
   8.将module下的build.gradle中的externalNativeBuild都注释掉(防止执行cmakeLists.txt脚本),这样就不会执行脚本生成so库
   9.点击gralde同步
   10.如果步骤正确,直接运行应该就成功了 
动态注册是需要JNIDynamicUtil类的具体路径才能进行动态注册的,所以如果要封装成第三发so库供给第三方使用,需要写一个工具类统一管理native的注册,然后将这个工具类做成jar包,将jar包和so供给第三方使用;(这只是个人看法,没有具体实施,希望有知道怎么做的,能不吝赐教)

注:不管是静态注册还是动态注册:都必须遵循 java的类名包名方法名和jni的函数一一对应
如:        静态注册:不管so库是不是在不同的项目中使用,都必须保证java中使用声明native的方法的类必须和JNI函数命名规则            保持一致:即java_包名_类名_方法名
       动态注册:由于动态注册也是要通过java类中的绝对路径来找到类中的class,才能进行映射;如代码中

   3.so库链接

零碎笔记建议:一定要看,不然坑死你

若是要引用第三方so库的话需要将第三方的头文件和当前的so库建立关系

as中写下native方法后,ctrl+1或者alt+enter会在当前目录下生成jni目录,会自动生成.c文件

快速获取头文件的方式: http://blog.csdn.net/wang_zhi_hao/article/details/49126955
归根结底都是通过命令 javah -d jni -jni -classpath class的路径生成
如:

javah -d jni -jni -classpath  

C:\\Users\\Administrator\\Desktop\\JNIDemo\\app\\build\\intermediates\\classes\\debug   com.jni.www.jnidemo.JNIUtil

会在当前目录下创建jni目录(没有的话),并将自动生成.h头文件在jni目录下,
注意此时的头文件是整个是:包名_类名.h 一定要改成和.c文件一样的文件名;此外还要.h头文件的函数声明也要和.c对应上,尤其是动态注册的函数一定一定要一一对应上,否则很容易报找不到方法

如果想要jni中调用第三方的so库,那么需要通过书写cmakeLists.txt脚本关联jni与第三方的so库,编写好之后build就可以是掉第三方so库的方法了;

如:在cmakeList.txt中加入,其中jni是存放头文件的目录

set(distribution_DIR $CMAKE_SOURCE_DIR/jni)
#加入头文件:第三方的so库的头文件加入编译到native-lib.so中,native-lib.so中才能使用它里边的函数
#参数是头文件所在的目录
target_include_directories(native-lib PRIVATE $distribution_DIR)

具体用法可以参考:
文档地址: https://developer.android.com/ndk/guides/cmake.html

build.gradle脚本中配置externalNativeBuild中的信息可以查看: 

app\\.externalNativeBuild\\cmake\\debug\\arm64-v8a\\cmake_build_command.txt,这里有build之后的具体信息

比如:
查询文档可以知道 arguments 中 -DANDROID_PLATFORM 代表编译的 android 平台,
文档建议直接设置 minSdkVersion 就行了,所以这个参数可忽略。
另一个参数 -DANDROID_TOOLCHAIN=clang,
CMake 一共有2种编译工具链 - clang 和 gcc,gcc 已经废弃,clang 是默认的。
    流程:      
第三方的so库和当前的so库建立连接,还是要通过System.loadLibrary("native-dy-lib");加载各个so库
1.获取第三方的so库提供给其它库使用的函数的头文件(也就是so库对外的函数对应的头文件)
2.将头文件加到本地so库以及和第三方so库链接
3.gradle后,build(要先删除build目录和extenalNativeBuild目录)-->make project获取到本地so库
4.将本地so库和第三方的so库导入项目。
5.需要使用的地方调用System.loadLibrary();加载so库
一定要保证加载的第三方的so库和放在jniLibs指定目录中的第三方so库是一个库,否则很可能会报找不到对应的so的函数
   可能频繁出现的异常    
java.lang.UnsatisfiedLinkError: dlopen failed: could not load library "libnative-dy-lib.so" needed by "libnative-lib.so"; caused by library "libnative-dy-lib.so" not found
libnative-dy-lib这个库也要放到jniLibs指定的目录否则native-lib.so找不到
报错:Fatal signal 11 (SIGSGV) at 0x00002820 (code=1),thread 23696 (xvdy.oa:vitamio) 这个问题都是调用的jni函数有问题,解决办法就是一边注释代码一边运行看看是哪行代码报错,或者通过调试来定位,调试有机会在讲讲

cmakeLists.txt: 生成静态库和动态库:区分于静态注册和动态注册:库是将函数打包成动态库,注册是函数注册,两者概念没关系   
 #静态注册的动态库---so库的cmake
add_library( # Sets the name of the library.也是.so库的名,生成的so库是在\\app\\build\\intermediates\\cmake
             native-lib


             # Sets the library as a shared library.SHARED:动态库 STATIC:静态库
             SHARED


             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.c )
find_library( log-lib
              log )
target_link_libraries( native-lib
                       $log-lib )
#-----静态注册的动态库结束-------


#----开始---动态注册的库---生成动态so库的cmake
add_library( # Sets the name of the library.也是.so库的名(可以自己修改,修改后一定要删除build和externalNativeBuild目录,重新build),生成的so库是在\\app\\build\\intermediates\\cmake
           native-dy-lib1
           SHARED
            src/main/cpp/native-dynamic-lib.c )
find_library( log-lib
          log )
target_link_libraries( native-dy-lib1
                       $log-lib )
#------动态库结束--------
#在 .externalNativeBuild/cmake/debug/abi/cmake_build_output.txt 中查看 log。
message(STATUS "execute CMakeLists")
set(CMAKE_VERBOSE_MAKEFILE on)

#当前cmakeList.txt的文件路径
set(lib_src_DIR $CMAKE_CURRENT_SOURCE_DIR)

set(lib_build_DIR $lib_src_DIR/tmp)
#创建目录。父目录不存在也会创建
file(MAKE_DIRECTORY $lib_build_DIR)

#外层的 CMakeLists 里面核心就是 add_subdirectory,查询CMake 官方文档可以知道这条命令的作用是为构建添加一个子路径。
#子路径中的 CMakeLists.txt 也会被执行。即会去分别执行 gmath 、gperf、mtxxJni 中的 CMakeLists.txt
#参数指定了源码路径,参数2指定了当前cmakeList执行结果的输出路径
#add_subdirectory($lib_src_DIR/gmath $lib_build_DIR/gmath)
#add_subdirectory($lib_src_DIR/gperf $lib_build_DIR/gperf)
add_subdirectory($lib_src_DIR/mtxxJni)#不指定参数2,默认输出路径

#更改库的输出路径为$distribution_DIR/gperf/lib/$ANDROID_ABI
set_target_properties(gperf
                      PROPERTIES
                      LIBRARY_OUTPUT_DIRECTORY
                      "$distribution_DIR/gperf/lib/$ANDROID_ABI")
set(CMAKE_VERBOSE_MAKEFILE on)
#创建目录
file(MAKE_DIRECTORY $distribution_DIR/gperf/include)
总结:      
验证so库的整个过程:
想要只测试so库,需要将验证的so库放到jniLibs指定的目录,然后将build.gradle中相应的所有的cmake都注释掉如下
externalNativeBuild 
         cmake 
         
    
然后删除build以及.externalNative目录,再者gradle同步,其次build-->makeproject,最后运行,能正确调用相应的jni的函数

demo

以下以下强烈推荐一篇博客:cmake的手册文档、ndk的手册文档在这篇博客都有讲解,会更新cmake的讲解
http://mp.weixin.qq.com/s/QTxEQg4s5ummtFNe8vRIvA

 CMake 的官方文档使用。

https://cmake.org/documentation/

同时在这推荐一个中文翻译的简易的 CMake手册

https://www.zybuluo.com/khan-lau/note/254724

以上是关于android之一篇史上最适合最全面的JNI入门教程的主要内容,如果未能解决你的问题,请参考以下文章

史上最详细最全面的Hadoop环境搭建

史上最简单&最全&最基础&入门到精通的opencv图像处理 第十九课:图像近似

史上最简单&最全&最基础&入门到精通的opencv图像处理 第一课:图像读入与灰度处理

从入门到精通手把手教你使用git(史上最详细,图文并茂)

史上最全单例模式的写法和破坏单例方式

史上最无私的MTK入门晋级资料