YCCardView

Introduction: 自定义阴影效果的控件,支持设置阴影偏移效果,支持设置扩散阴影,设置圆角大小,设置阴影颜色,还可以设置上下左右某个方法显示阴影效果,控件小巧但功能强大,方便维护和统一管理设置阴影操作。同时可以用于 RecyclerView 的 item 设置阴影,采用缓存可以有效避免设置阴影时创建大量 bitmap。
More: Author   ReportBugs   
Tags:

目录介绍

  • 01.学习 JNI 开发流程
    • 1.1 JNI 开发概念
    • 1.2 JNI 和 NDK 的关系
    • 1.3 JNI 实践步骤
    • 1.4 NDK 使用场景
    • 1.5 学习路线说明
  • 02.NDK 架构分层
    • 2.1 NDK 分层构建层
    • 2.2 NDK 分层 Java 层
    • 2.3 Native 层
  • 03.JNI 基础语法
    • 3.1 JNI 三种引用
    • 3.2 JNI 异常处理
    • 3.3 C 和 C++互相调用
    • 3.4 JNI 核心原理
    • 3.5 注册 Native 函数
    • 3.6 JNI 签名是什么
  • 04.一些必备操作
    • 4.1 so 库生成打包
    • 4.2 so 库查询操作
    • 4.3 so 库如何反编译
  • 05.实践几个案例
    • 5.1 Java 静态调用 C/C++
    • 5.2 C/C++调用 Java
    • 5.3 Java 调三方 so 中 API
    • 5.4 Java 动态调 C++
  • 06.一些技术原理
    • 6.1 JNIEnv 创建和释放
    • 6.2 动态注册的原理
    • 6.3 注册 JNI 流程图
  • 07.JNI 遇到的问题
    • 7.1 混淆的 bug
    • 7.2 注意字符串编译

01.学习 JNI 开发流程

1.1 JNI 开发概念

  • .SO 库是什么东西
    • NDK 为了方便使用,提供了一些脚本,使得更容易的编译 C/C++代码。在 Android 程序编译中会将 C/C++ 编译成动态库 so 文件,类似 java 库.jar 文件一样,它的生成需要使用 NDK 工具来打包。
    • so 是 shared object 的缩写,见名思义就是共享的对象,机器可以直接运行的二进制代码。实质 so 文件就是一堆 C、C++的头文件和实现文件打包成一个库。
  • JNI 是什么东西
    • JNI 的全称是 Java Native Interface,即本地 Java 接口。因为 Java 具备跨平台的特点,所以 Java 与 本地代码交互的能力非常弱。
    • 采用 JNI 特性可以增强 Java 与本地代码交互的能力,使 Java 和其他类型的语言如 C++/C 能够互相调用。

1.2 JNI 和 NDK 的关系

  • JNI 和 NDK 学习内容太难
    • 其实难的不是 JNI 和 NDK,而是 C/C++语言,JNI 和 NDK 只是个工具,很容易学习的。
  • JNI 和 NDK 有何联系
    • 学习 JNI 之前,首先得先知道 JNI、NDK、Java 和 C/C++之间的关系。
    • 在 Android 开发中,有时为了性能和安全性(反编译),需要使用 C/C++语言,但是 Android APP 层用的是 Java 语言,怎么才能让这两种语言进行交流呢,因为他们的编码方式是不一样的,这是就需要 JNI 了。
    • JNI 可以被看作是代理模式,JNI 是 java 接口,用于 Java 与 C/C++之间的交互,作为两者的桥梁,也就是 Java 让 JNI 代其与 C/C++沟通。
    • NDK 是 Android 工具开发包,帮助快速开发 C/C++动态库,相当于 JDK 开发 java 程序一样,同时能帮打包生成.so 库

1.3 JNI 实践步骤

  • 操作实践步骤
    • 第一步,编写 native 方法。
    • 第二步,根据此 native 方法编写 C 文件。
    • 第三步,使用 NDK 打包成.so 库。
    • 第四步,使用.so 库然后调用 api。
  • 如何使用 NDK 打包.so 库
    • 1,编写 Android.mk 文件,此文件用来告知 NDK 打包.so 库的规则
    • 2,使用 ndk-build 打包.so 库
  • 相关学习文档

1.4 NDK 使用场景

  • NDK 的使用场景一般在:
    • 1.为了提升这些模块的性能,对图形,视频,音频等计算密集型应用,将复杂模块计算封装在.so 或者.a 文件中处理。
    • 2.使用的是 C/C++进行编写的第三方库移植。如 ffmppeg,OpenGl 等。
    • 3.某些情况下为了提高数据安全性,也会封装 so 来实现。毕竟使用纯 Java 开发的 app 是有很多逆向工具可以破解的。

1.5 学习路线说明

  • JNI 学习路线介绍
    • 1.首先要有点 C/C++的基础,这个我是在 菜鸟教程 上学习的
    • 2.理解 NDK 和 JNI 的一些概念,以及 NDK 的一个大概的架构分层,JNI 的开发步骤是怎样的
    • 3.掌握案例练习,前期先写案例,比如 java 调用 c/c++,或者 c/c++调用 java。把这个案例写熟,跑通即可
    • 4.案例练习之后,然后在思考 NDK 是怎么编译的,如何打包 so 文件,loadLibrary 的流程,CMake 工作流程等一些基础的原理
    • 5.在实践过程中,先记录遇到的问题。这时候可能不一定懂,先放着,先实现案例或者简单的业务。然后边实践边琢磨问题和背后的原理
  • 注意事项介绍
    • 避免一开始就研究原理,或者把 C/C++整体学习一遍,那样会比较辛苦。焦点先放在 JNI 通信流程上,写案例学习
    • 把学习内容,分为几个不同类型:了解(能够扯淡),理解(大概知道什么意思),掌握(能够运用和实践),精通(能举一反三和分享讲清楚)

02.NDK 架构分层

  • 使用 NDK 开发最终目标是为了将 C/C++代码编译生成.so 动态库或者静态库文件,并将库文件提供给 Java 代码调用。
  • 所以按架构来分可以分为以下三层:
    • 1.构建层
    • 2.Java 层
    • 3.native 层

2.1 NDK 分层构建层

  • 要得到目标的 so 文件,需要有个构建环境以及过程,将这个过程和环境称为构建层。
    • 构建层需要将 C/C++代码编译为动态库 so,那么这个编译的过程就需要一个构建工具,构建工具按照开发者指定的规则方式来构建库文件,类似 apk 的 Gradle 构建过程。
  • 在讲解 NDK 构建工具之前,我们先来了解一些关于 CPU 架构的知识点:Android abi
    • ABI 即 Application Binary Interface,定义了二进制接口交互规则,以适应不同的 CPU,一个 ABI 对应一种类型的 CPU。
  • Android 目前支持以下 7 种 ABI:
    • 1.armeabi:第 5 代和 6 代的 ARM 处理器,早期手机用的比较多。
    • 2.armeabi-v7a:第 7 代及以上的 ARM 处理器。
    • 3.arm64-v8a:第 8 代,64 位 ARM 处理器。
    • 4.x86:一般用在平板,模拟器。
    • 5.x86_64:64 位平板。
  • 常规的 NDK 构建工具有两种:
    • 1.ndk-build:
    • 2.Cmake
  • ndk-build 其实就是一个脚本。早期的 NDK 开发一直都是使用这种模式
    • 运行 ndk-build 相当于运行一下命令:$GNUMAKE -f /build/core/build-local.mk
    • $GNUMAKE 指向 GNU Make 3.81 或更高版本, 则指向 NDK 安装目录
    • 使用 ndk-build 需要配合两个 mk 文件:Android.mk 和 Application.mk。
  • Cmake 是一个编译系统的生成器
    • 简单理解就是,他是用来生成 makefile 文件的,Android.mk 其实就是一个 makefile 类文件,cmake 使用一个 CmakeLists.txt 的配置文件来生成对应的 makefile 文件。
    • Cmake 构建 so 的过程其实包括两步:步骤 1:使用 Cmake 生成编译的 makefiles 文件;步骤 2:使用 Make 工具对步骤 1 中的 makefiles 文件进行编译为库或者可执行文件。
    • Cmake 优势在哪里呢?在生成 makefile 过程中会自动分析源代码,创建一个组件之间依赖的关系树,这样就可以大大缩减在 make 编译阶段的时间。
  • Cmake 构建项目配置
    • 使用 Cmake 进行构建需要在 build.gradle 配置文件中声明 externalNativeBuild

2.2 NDK 分层 Java 层

  • 如何选择正确的 so 库呢
    • 通常情况下,我们在编译 so 的时候就需要确定自己设备类型,根据设备类型选择对应 abiFilters。
    • 注意:使用 as 编译后的 so 会自动打包到 apk 中,如果需要提供给第三方使用,可以到 build/intermediates/cmake/debug or release 目录中 copy 出来。
  • Java 层如何调用 so 文件中的函数
    • 对于 Android 上层代码来说,在将包正确导入到项目中后,只需要一行代码就可以完成动态库的加载过程。有两种方式:
      System.load("/data/local/tmp/native_lib.so"); 
      System.loadLibrary("native_lib");
      
    • 1.加载路径不同:load 是加载 so 的完整路径,而 loadLibrary 是加载 so 的名称,然后加上前缀 lib 和后缀.so 去默认目录下查找。
    • 2.自动加载库的依赖库的不同:load 不会自动加载依赖库;而 loadLibrary 会自动加载依赖库。
  • 无论哪种方式,最终都会调用到 LoadNativeLibrary()方法,该方法主要操作:
    • 1.通过 dlopen 打开动态库文件
    • 2.通过 dlsym 找到 JNI_OnLoad 符号所对应的方法地址
    • 3.通过 JNI_OnLoad 去注册对应的 jni 方法

2.3 Native 层

  • 如何理解 JNI 的设计思想
    • JNI(全名 Java Native Interface)Java native 接口,其可以让一个运行在 Java 虚拟机中的 Java 代码被调用或者调用 native 层的用 C/C++编写的基于本机硬件和操作系统的程序。简单理解为就是一个连接 Java 层和 Native 层的桥梁。
    • 开发者可以在 native 层通过 JNI 调用到 Java 层的代码,也可以在 Java 层声明 native 方法的调用入口。
  • JNI 注册方式
    • 当 Java 代码中执行 Native 的代码的时候,首先是通过一定的方法来找到这些 native 方法。JNI 有静态注册和动态注册两种注册方式。
    • 静态注册先由 Java 得到本地方法的声明,然后再通过 JNI 实现该声明方法。动态注册先通过 JNI 重载 JNI_OnLoad()实现本地方法,然后直接在 Java 中调用本地方法。

03.JNI 基础语法

3.1 JNI 三种引用

  • 在 JNI 规范中定义了三种引用:
    • 局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
  • Local 引用
    • JNI 中使用 jobject, jclass, and jstring 等来标志一个 Java 对象,然而在 JNI 方法在使用的过程中会创建很多引用类型,如果使用过程中不注意就会导致内存泄露。
    • 直接使用:NewLocalRef 来创建。Local 引用其实就是 Java 中的局部引用,在声明这个局部变量的方法结束或者退出其作用域后就会被 GC 回收。
  • Global 引用全局引用
    • 全局引用可以跨方法、跨线程使用,直到被开发者显式释放。一个全局引用在被释放前保证引用对象不被 GC 回收。
    • 和局部应用不同的是,能创建全局引用的函数只有 NewGlobalRef,而释放它需要使用 ReleaseGlobalRef 函数。
  • Weak 引用
    • 弱引用可以使用全局声明的方式。弱引用在内存不足或者紧张的时候会自动回收掉,可能会出现短暂的内存泄露,但是不会出现内存溢出的情况。

3.2 JNI 异常处理

  • native 层异常
    • 处理方式 1:native 层自行处理
    • 处理方式 2:native 层抛出给 Java 层处理

3.4 JNI 核心原理

  • java 运行在 jvm,jvm 本身就是使用 C/C++编写的,因此 jni 只需要在 java 代码、jvm、C/C++代码之间做切换即可
    • image
  • JNIEnv 是什么?
    • JINEnv 是当前 Java 线程的执行环境,一个 JVM 对应一个 JavaVM 结构体,一个 JVM 中可能创建多个 Java 线程,每个线程对应一个 JNIEnv 结构,它们保存在线程本地存储 TLS 中。
    • 因此不同的线程 JNIEnv 不同,而不能相互共享使用。 JavaEnv 结构也是一个函数表,在本地代码通过 JNIEnv 函数表来操作 Java 数据或者调用 Java 方法。

3.5 注册 Native 函数

  • JNI 静态注册:
    • 步骤 1.在 Java 中声明 native 方法,比如:public native String stringFromJNI()
    • 步骤 2.在 native 层新建一个 C/C++文件,并创建对应的方法(建议使用 AS 快捷键自动生成函数名),比如:testjnilib.cpp: Line 8
  • JNI 动态注册
    • 通过 RegisterNatives 方法把 C/C++中的方法映射到 Java 中的 native 方法,而无需遵循特定的方法命名格式,这样书写起来会省事很多。
    • 动态注册其实就是使用到了前面分析的 so 加载原理:在最后一步的 JNI_OnLoad 中注册对应的 jni 方法。这样在类加载的过程中就可以自动注册 native 函数。比如:
    • 与 JNI_OnLoad()函数相对应的有 JNI_OnUnload()函数,当虚拟机释放该 C 库的时候,则会调用 JNI_OnUnload()函数来进行善后清除工作。
  • 那么如何选择使用静态注册 or 动态注册
    • 动态注册和静态注册最终都可以将 native 方法注册到虚拟机中,推荐使用动态注册,更不容易写错,静态注册每次增加一个新的方法都需要查看原函数类的包名。

3.6 JNI 签名是什么

  • 为什么 JNI 中突然多出了一个概念叫”签名”:
    • 因为 Java 是支持函数重载的,也就是说,可以定义相同方法名,但是不同参数的方法,然后 Java 根据其不同的参数,找到其对应的实现的方法。
    • 这样是很好,所以说 JNI 肯定要支持的,如果仅仅是根据函数名,没有办法找到重载的函数的,所以为了解决这个问题,JNI 就衍生了一个概念——”签名”,即将参数类型和返回值类型的组合。
    • 如果拥有一个该函数的签名信息和这个函数的函数名,就可以顺序的找到对应的 Java 层中的函数。
  • 如何查看签名呢:可以使用 javap 命令。
    • javap -s -p MainActivity.class

04.一些必备操作

4.1 so 库生成打包

  • 什么是 so 文件库
    • so 库,即将 C 或者 C++实现的功能进行打包,将其打包为共享库,让其他程序进行调用,这可以提高代码的复用性。
  • 关于.so 文件的生成有两种方式
    • 可以提供给大家参考,一种是 CMake 自动生成法,另一种是传统打包法。
  • so 文件在程序运行时就会加载
    • 所以想使用 Java 调用.so 文件,必有某个 Java 类运行时 load 了 native 库,并通过 JNI 调用了它的方法。
  • cmake 生成.so 方案
    • 第一步:创建 native C++ Project 项目,创建 native 函数并实现,先测试本地 JNI 函数调通
    • 第二步:获取.so 文件。将生成的.apk 文件改为.zip 文件,然后进行解压缩,就能看到.so 文件。如果想支持多种库架构,则可在 module 的 build.gradle 中配置 ndk 支持。
    • 第三步:so 文件测试。新建一个普通的 Android 程序,将 so 库放入程序,然后创建类(注意要相同的包名、文件名及方法名)去加载 so 库。
    • 总结一下:Android Studio 自动创建的 native C++项目默认支持 CMake 方式,它支持 JNI 函数调用的入口在 build.gradle 中。
  • 传统打包生成.so 方案【不推荐这种方式】
    • 第一步:在 Java 类中声明一个本地方法。
    • 第二步:执行指令 javah 获得 C 声明的.h 文件。
    • 第三步:获得.c 文件并实现本地方法。创建 Android.mk 和 Application.mk,并配置其参数,两个文件如不编写或编写正常会出现报错。
    • 第四步:打包.so 库。cd 到\app 目录下,执行命令 ndk-build 即可。生成 so 库后,最后测试 ok 即可。

4.2 so 库查询操作

  • so 库如何查找所对应的位置
    • 第一步:在 app 模块的 build.gradle 中,追加以下代码:
    • 第二步:执行命令行:./gradlew assembleDebug 【注意如果遇到 gradlew 找不到,则输入:chmod +x gradlew】
  • so 文件查询结果后。就可以查询到 so 文件属于那个 lib 库的!如下所示:libtestjnilib.so 文件属于 TestJniLib 库的
      find so file: /Users/yc/github/YCJniHelper/TestJniLib/build/intermediates/library_jni/debug/jni/armeabi-v7a/libtestjnilib.so
      find so file: /Users/yc/github/YCJniHelper/SafetyJniLib/build/intermediates/library_jni/debug/jni/armeabi-v7a/libsafetyjnilib.so
      find so file: /Users/yc/github/YCJniHelper/SignalHooker/build/intermediates/library_jni/debug/jni/armeabi-v7a/libsignal-hooker.so
    

05.实践几个案例

5.1 Java 静态调用 C/C++

  • Java 调用 C/C++函数调用流程
    • Java 层调用某个函数时,会从对应的 JNI 层中寻找该函数。根据 java 函数的包名、方法名、参数列表等多方面来确定函数是否存在。
    • 如果没有就会报错,如果存在就会就会建立一个关联关系,以后再调用时会直接使用这个函数,这部分的操作由虚拟机完成。
  • Java 层调用 C/C++方法操作步骤

    • 第一步:创建 java 类 NativeLib,然后定义 native 方法 stringFromJNI()
      public native String stringFromJNI();
      
    • 第二步:根据此 native 方法编写 C 文件,可以通过命令后或者 studio 提示生成 C++对应的方法函数

      //java 中 stringFromJNI
      //extern “C”    指定以"C"的方式来实现 native 函数
      extern "C"
      //JNIEXPORT     宏定义,用于指定该函数是 JNI 函数。表示此函数可以被外部调用,在 Android 开发中不可省略
      JNIEXPORT jstring
      //JNICALL       宏定义,用于指定该函数是 JNI 函数。,无实际意义,但是不可省略
      JNICALL
      //以注意到 jni 的取名规则,一般都是包名 + 类名,jni 方法只是在前面加上了 Java_,并把包名和类名之间的.换成了 _
      Java_com_yc_testjnilib_NativeLib_stringFromJNI(JNIEnv *env, jobject /* this */) {
        //JNIEnv 代表了 JNI 的环境,只要在本地代码中拿到了 JNIEnv 和 jobject
        //JNI 层实现的方法都是通过 JNIEnv 指针调用 JNI 层的方法访问 Java 虚拟机,进而操作 Java 对象,这样就能调用 Java 代码。
        //jobject thiz
        //在 AS 中自动为我们生成的 JNI 方法声明都会带一个这样的参数,这个 instance 就代表 Java 中 native 方法声明所在的
        std::string hello = "Hello from C++";
      
        //思考一下,为什么直接返回字符串会出现错误提示?
        //return "hello";
        return env->NewStringUTF(hello.c_str());
      }
      
  • 举一个例子
    • 例如在 NativeLib 类的 native stringFromJNI()方法,程序会自动在 JNI 层查找 Java_com_yc_testjnilib_NativeLib_stringFromJNI 函数接口,如未找到则报错。如找到,则会调用 native 库中的对应函数。

5.2 C/C++调用 Java

  • Native 层调用 Java 层的类的字段和方法的操作步骤
    • 第一步:创建一个 Native C++的 Android 项目,创建 Native Lib 项目
    • 第二步:在 cpp 文件夹下创建:calljnilib.cpp 文件,calljnilib.h 文件(用来声明 calljnilib.cpp 中的方法)。
    • 第三步:开始编写配置文件 CmkaeLists.txt 文件。使用 add_library 创建一个新的 so 库
    • 第四步:编写 calljnilib.cpp 文件。因为要实现 native 层调用 Java 层字段和方法,所以这里定义了两个方法:callJavaField 和 callJavaMethod
    • 第五步:编写 Java 层的调用代码此处要注意的是调用的类的类名以及包名都要和 c++文件中声明的一致,否则会报错。具体看:CallNativeLib
    • 第六步:调用代码进行测试。然后查看测试结果

5.3 Java 调三方 so 中 API

  • 直接拿前面案例的 calljnilib.so 来测试,但是为了实现三方调用还需要对文件进行改造
    • 第一步:要实现三方 so 库调用,在 calljnilib.h 中声明两个和 calljnilib.cpp 中对应的方法:callJavaField 和 callJavaMethod,一般情况下这个头文件是第三方库一起提供的给外部调用的。
    • 第二步:对 CMakeLists 配置文件改造。主要是做一些库的配置操作。
    • 第三步:编写 third_call.cpp 文件,在这内部调用第三方库。这里需要将第三方头文件导入进来,如果 CmakeLists 文件中没有声明头文件,就使用#include "include/calljnilib.h" 这种方式导入
    • 第四步:最后测试下:callThirdSoMethod("com/yc/testjnilib/HelloCallBack","updateName");

5.4 Java 动态调 C++

  • 先说一下静态调 C++的问题:
    • 在实现 stringFromJNI()时,可以看到 c++里面的方法名很长 Java_com_yc_testjnilib_NativeLib_stringFromJNI。
    • 这是 jni 静态注册的方式,按照 jni 规范的命名规则进行查找,格式为 Java类路径方法名。Studio 默认这种方式名字太长了,能否设置短一点。
    • 程序运行效率低,因为初次调用 native 函数时需要根据根据函数名在 JNI 层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时。
  • 动态注册方法解决上面问题
    • 当程序在 Java 层运行 System.loadLibrary("testjnilib");这行代码后,程序会去载入 testjnilib.so 文件。
    • 于此同时,产生一个 Load 事件,这个事件触发后,程序默认会在载入的.so 文件的函数列表中查找 JNI_OnLoad 函数并执行。与 Load 事件相对,在载入的.so 文件被卸载时,Unload 事件被触发。
    • 此时,程序默认会去载入的.so 文件的函数列表中查找 JNI_OnLoad 函数并执行,然后卸载.so 文件。
    • 因此开发者经常会在 JNI_OnLoad 中做一些初始化操作,动态注册就是在这里进行的,使用 env->RegisterNatives(clazz, gMethods, numMethods)。
  • 动态注册操作步骤:
    • 第一步:因为 System.loadLibrary()执行时会调用此方法,实现 JNI_OnLoad 方法。
    • 第二步:调用 FindClass 找到需要动态注册的 java 类【定义要关联的对应 Java 类】,注意这个是 native 方法那个类的路径字符串
    • 第三步:定义一个静态数据(JNINativeMethod 类型),里面存放需要动态注册的 native 方法,以及参数名称
    • 第四步:通过调用 jni 中的 RegisterNatives 函数将注册函数的 Java 类,以及注册函数的数组,以及个数注册在一起,这样就实现了绑定。
  • 动态注册优势分析
    • 相比静态注册,动态注册的灵活性更高,如果修改了 native 函数所在类的包名或类名,仅调整 native 函数的签名信息即可。
    • 还有一个优势:动态注册,java 代码不需要更改,只需要更改 native 代码。
    • 效率更高:通过在.so 文件载入初始化时,即 JNI_OnLoad 函数中,先行将 native 函数注册到 VM 的 native 函数链表中去,后续每次 java 调用 native 函数时都会在 VM 中的 native 函数链表中找到对应的函数,从而加快速度。

06.一些技术原理

6.1 JNIEnv 创建和释放

  • JNIEnv 的创建方式
    • C 中——JNIInvokeInterface:JNIInvokeInterface 是 C 语言环境中的 JavaVM 结构体,调用 (AttachCurrentThread)(JavaVM, JNIEnv*, void) 方法,能够获得 JNIEnv 结构体;
    • C++中 ——_JavaVM:_JavaVM 是 C++中 JavaVM 结构体,调用 jint AttachCurrentThread(JNIEnv* p_env, void thr_args) 方法,能够获取 JNIEnv 结构体;
  • JNIEnv 的释放:
    • C 中释放:调用 JavaVM 结构体 JNIInvokeInterface 中的(DetachCurrentThread)(JavaVM)方法,能够释放本线程的 JNIEnv
    • C++ 中释放:调用 JavaVM 结构体 _JavaVM 中的 jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); } 方法,就可以释放 本线程的 JNIEnv
  • JNIEnv 和线程的关系
    • JNIEnv 只在当前线程有效:JNIEnv 仅仅在当前线程有效,JNIEnv 不能在线程之间进行传递,在同一个线程中,多次调用 JNI 层方便,传入的 JNIEnv 是同样的
    • 本地方法匹配多个 JNIEnv:在 Java 层定义的本地方法,能够在不同的线程调用,因此能够接受不同的 JNIEnv

6.2 动态注册的原理

  • 在 Android 源码开发环境下,大多采用动态注册 native 方法。
    • 利用结构体 JNINativeMethod 保存 Java Native 函数和 JNI 函数的对应关系;
    • 在一个 JNINativeMethod 数组中保存所有 native 函数和 JNI 函数的对应关系;
    • 在 Java 中通过 System.loadLibrary 加载完 JNI 动态库之后,调用 JNI_OnLoad 函数,开始动态注册;
    • JNI_OnLoad 中会调用 AndroidRuntime::registerNativeMethods 函数进行函数注册;
    • AndroidRuntime::registerNativeMethods 中最终调用 jni RegisterNativeMethods 完成注册。
  • 动态注册原理分析
    • RegisterNatives 方式的本质是直接通过结构体指定映射关系,而不是等到调用 native 方法时搜索 JNI 函数指针,因此动态注册的 native 方法调用效率更高。
    • 此外,还能减少生成 so 库文件中导出符号的数量,则能够优化 so 库文件的体积。

6.3 注册 JNI 流程图

  • 提到了注册 JNI 函数(建立 Java native 方法和 JNI 函数的映射关系)有两种方式:静态注册和动态注册。
    • image
  • 分析下静态注册匹配 JNI 函数的执行过程
    • 第一步:以 loadLibrary() 加载 so 库的执行流程为线索进行分析的,最终定位到 FindNativeMethod() 这个方法。
    • 第二步:查看java_vm_ext.cc中 FindNativeMethod 方法,然后看到 jni_short_name 和 jni_long_name,获取 native 方法对应的短名称和长名称。
    • 第三步:在java_vm_ext.cc,通过 FindNativeMethodInternal 查找已经加载的 so 库中搜索,先搜索短名称,然后再搜索长名称
    • 第四步:建立内部数据结构,建立 Java native 方法与 JNI 函数的函数指针的映射关系,调用 native 方法,则直接调用已记录的函数指针。

07.JNI 遇到的问题

7.1 混淆的 bug

  • 在 Android 工程中要排除对 native 方法以及所在类的混淆(java 工程不需要),否则要注册的 java 类和 java 函数会找不到。proguard-rules.pro 中添加。
      # 设置所有 native 方法不被混淆
      -keepclasseswithmembernames class * {
          native <methods>;
      }
      # 不混淆类
      -keep class com.yc.testjnilib.** { *; }
    

7.2 注意字符串编译

  • 比如:对于 JNI 方法来说,使用如下方法返回或者调用直接崩溃了,有点搞不懂原理?
      env->CallMethod(objCallBack,_methodName,"123");
    
  • 这段代码编译没问题,但是在运行的时候就报错了:
      JNI DETECTED ERROR IN APPLICATION: use of deleted global reference
    
  • 最终定位到是最后一个参数需要使用 jstring 而不能直接使用字符串表示。如下所示:
      //思考一下,为什么直接返回字符串会出现错误提示?为何这样设计……
      //return "hello";
      return env->NewStringUTF(hello.c_str());
    

代码案例:https://github.com/yangchong211/YCJniHelper

其他案例:https://github.com/yangchong211/YCAppTool

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools