【Android面试速学】JNI 了解一下?

标题图

android面试中老是会问jni,但是我在小厂搬砖多年,可还没咋用过啊
哭~~~~
没用过那就了解一下吧。

编写:guuguo  校对:guuguo

名词解释

  • c++头文件: 头文件用来放置对应c++方法的声明,其实它的内容跟 .cpp 文件中的内容是一样的,都是 C++ 的源代码。但头文件不用被编译。头文件可以通过#include被包含到.cpp文件中。include仅仅是复制头文件的定义代码到.cpp文件中。所以头文件用来放置声明,而不是定义。因为多个源文件直接包含定义的话会有定义冲突,而声明就不会。(头文件也可以包含定义,但是尽量不要,如果 需要,通过#ifndef...#endif让编译器判断个名字是否被定义,再决定要不要继续编译后续的内容)
  • JNI (Java Native Interface,Java本地接口)是一种编程框架,使得Java虚拟机中的Java程序可以调用本地应用/或库,也可以被其他程序调用。
  • CMake 是一个跨平台构建工具,支持C/C++/Java等语言的工程构建。本文中用来编译c++代码。

这篇文章讲什么?

Android 系统中有大量的实现都是native实现的,中间通过JNI进行java层调用。学会JNI的使用,不光是能为我们开发和面试提供助力,还能为我们理解android 系统源码的基础多加两块砖。
说明一下这篇文章的内容和目的:

  1. 了解JNI 在开发中的基础使用
  2. Java 代码和 c++ 的native 方法链接原理
  3. JNI 框架是啥,都有哪些东西
  4. Ndk 是什么东西?

弄明白这四个小点,对于JNI也就有了初步的理解,在要利用其进行开发的时候也能信手拈来。

JNI 使用的小栗子(静态注册)

jni注册方式分静态注册和动态注册,

  • 静态注册:根据函数名找到对应的JNI函数,样式为Java_包名_类名_方法名
  • 动态注册:当我们使用System#loadLibarary方法加载so库的时候,Java虚拟机会找到JNI_OnLoad函数并主动调用。所以我们可以在JNI_OnLoad 调用 jniRegisterNativeMethods进行方法的动态注册。(先不学习该方式,欲了解可google)

下面我们就讲一下静态注册先:

  1. 创建demo jni sdk模块

我们创建一个sdk模块,承载native和jni代码,目录结构如下:

img

图中展示的主要目录如下:

  • src/main/java java源码
  • src/main/jni native源码
  • src/main/jni/CMakeLists.txt cmake的配置文件

并且在build.gradle 中配置好jni源码路径:

sourceSets {
    main {
        jni.srcDirs = ['src/main/jni']
    }
}
  1. 定义native java 方法

在kotlin 中,使用关键字external标识该方法是JNI方法。在调用该方法的时候,Java_包名_类名_方法名的c++函数。
我们先来创建JNI入口java类 JNI.java,定义好java的native方法。方法如下:

package top.guuguo.myapplication
class JNI {
    /**返回签名后的字符串*/
    external fun signString(str: String): String
    companion object {
        ///实例的创建一定要在native代码加载之后,如本例的 
        ///System.loadLibrary("jni-test")
        val instance by lazy { JNI() }
    }
}

我们定义了一个简单的native方法signString,模拟对字符串进行签名的方法。

  1. 生成对对应的头文件

java中提供了javah 工具。通过他可以自动生成native方法对应c++的头文件。通过javah -h 看看该工具的使用说明:

javah -h
用法: 
  javah [options] 
其中, [options] 包括:
  -o                 输出文件 (只能使用 -d 或 -o 之一)
  -d                  输出目录
  -v  -verbose             启用详细输出
  -h  --help  -?           输出此消息
  -version                 输出版本信息
  -jni                     生成 JNI 样式的标头文件 (默认值)
  -force                   始终写入输出文件
  -classpath         从中加载类的路径
  -cp                从中加载类的路径
  -bootclasspath     从中加载引导类的路径
 是使用其全限定名称指定的
(例如, java.lang.Object)。

使用方式如下: -cp 等同于-classpath,用来指定要生成头文件的class文件路径

javah -d app/src/main/cpp/header -cp "./app/build/tmp/kotlin-classes/debug/"  top.guuguo.myapplication.JNI

可以看到命令执行过后,.h文件被成功生成了

img

有了.h jni 声明文件后,我们在 jni.cpp中完成对应方法的实现,代码如下:

#include 
#include 
#include 
#include "header/top_guuguo_myapplication_JNI.h"

JNIEXPORT jstring JNICALL Java_top_guuguo_myapplication_JNI_signString(JNIEnv *env, jobject obj, jstring jStr) {
    const char *cstr = env->GetStringUTFChars(jStr, NULL);
    std::string str = std::string(cstr);
    env->ReleaseStringUTFChars(jStr, cstr);
    std::string cres = "signed:" + str;
    jstring jres = env->NewStringUTF(cres.c_str());
    return jres;
}

方法的定义实现很简单,只是对传入的字符串前面拼接了signed:字符串。

  1. 完善CmakeList.txt 和 build.gradle 编译.so产物

对于native源码的编译,当前有两种方案:cmake 和 ndk-build。CMake会更加流行一些,现在介绍一下CMake。
CMake 是一个跨平台构建工具,支持C/C++/Java等语言的工程构建。通过配置CMake 构建脚本CMakeLists.txt,我们可以利用CMake命令做好自定义的编译工作。
这是cmake使用的主要指令

  • set(all_src "./src"):该指令可以定义名为all_src的变量值
  • add_library:该指令的主要作用就是将指定的源文件生成链接文件,然后添加到工程中去

CMakeLists.txt

我们编辑一下该配置文件,使用如下内容

# Copyright (c) 2019 - 2020 The Alibaba DingTalk Authors. All rights reserved.

PROJECT(jni-test)
cmake_minimum_required(VERSION 3.4.1)

# 对一些c++编译期标识 赋值
#set(CMAKE_CXX_COMPILER      "clang++" )         # 显示指定使用的C++编译器
#set(CMAKE_CXX_FLAGS   "-std=c++11 -O2")             # c++11
#set(CMAKE_CXX_FLAGS   "-g")                     # 调试信息
#set(CMAKE_CXX_FLAGS   "-Wall")                  # 开启所有警告
#set(CMAKE_CXX_FLAGS_DEBUG   "-O0" )             # 调试包不优化
#set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG " )   # release包优化
set(CMAKE_CXX_FLAGS_RELEASE "-std=c++11 -O2 ")
set(CMAKE_CXX_FLAGS_DEBUG "-std=c++11 -O2 ")

# 对变量 SRC_ROOT 赋值
set(SRC_ROOT "./")

# 遍历目录下直属的所有.cpp文件保存到变量中
file(GLOB all_src
        "${SRC_ROOT}/*.hpp"
        "${SRC_ROOT}/*.cpp"
        "${SRC_ROOT}/src/*.h"
        "${SRC_ROOT}/src/*.hpp"
        "${SRC_ROOT}/header/*.h"
        "${SRC_ROOT}/header/*.hpp"
        )
# 将源码文件添加到编译动态库中
add_library(jni-test SHARED ${all_src})

build.gradle 添加native配置:

defaultConfig {
    /**...*/
    externalNativeBuild {
        cmake {
            ///编译目标名
            targets 'jni-test'
            //预编译行为配置 :-fexceptions 启用异常处理
            cppFlags "-std=c++11 -fexceptions -frtti"
            arguments "-DANDROID_STL=c++_shared"
        }
    }
}
externalNativeBuild {
    cmake {
        version '3.6.0'
        path 'src/main/jni/CMakeLists.txt'
    }
}

在以上代码中指定好一些必要参数,以及cmake版本和配置文件路径

编译:

接下来的编译中会自动 编译出相关类库,也可以通过以下的gradle命令直接打包出对应的so库和aar包

./gradlew :sdk:aR

也就是使用aR(assembleRelease)命令编译release包,在build/intermediates/cmake/release中能找到对应产物。

  1. 简单c++方法调用

完成了定义,我们简单实现一下调用:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        System.loadLibrary("jni-test")
        findViewById

我们在点击按钮之后,直接弹出吐司展示签名后的字符串。

这一块有一点需要注意!!

获取JNI实例的步骤,需要在System.loadLibrary之后。
这样才能正确调用到对应的native方法。

小结:

至此,最小化实现的一个jni样例就完成了,实现了native方法定义以及java对其的调用。
以此为基础,我们在未来能深入很多

  • 我们能够慢慢了解跨平台native sdk 如何在安卓中使用。
  • 能够为阅读aosp源码增加自己的基础功

Java 代码和 c++ 的native 方法如何连接起来

java调用native方法的时候,由art虚拟机对应做特殊处理。
参考Android ART执行类方法的过程,虚拟机在执行方法的时候判断是否native方法,执行。
客户端的实现很简单,就是上面提到的静态注册和动态注册方式。

JNI 框架是啥,都有哪些东西?

JNIEnv 表示 Java 调用 native 语言的环境,是一个封装了几乎全部 JNI 方法的指针。
我们查看 jni.h的源码(aosp源码路径source/libnativehelper/include_jni/jni.h)。
找到JNIEnv的定义:typedef _JNIEnv JNIEnv;
可以看到其实是_JNIEnv类型的别名。看看_JNIEnv结构的源码:

truct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;
    #if defined(__cplusplus)
    jint GetVersion()
    { return functions->GetVersion(this); }
    jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
        jsize bufLen)
    { return functions->DefineClass(this, name, loader, buf, bufLen); }
   // ...
    }

可以看出所有的JNIEnv方法都是间接调用的JNINativeInterface的方法,只是对JNINativeInterface结构体的一层封装。
我们JNI的大多数操作都是通过其进行。

NDK是啥,和jni什么关系?

ndk:Native Development Kit

Android NDK 支持使用 CMake 编译应用的 C 和 C++ 代码。
NDK是一系列工具的集合。

  • NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的。
  • NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。
  • NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。

NDK提供了一份稳定、功能有限的API头文件声明。包含有:C11标准库(libc)、标准数学库(libm)、c++17库、Log库(liblog)、压缩库(libz)、Vulkan渲染库(libvulkan)、openGl库(libGLESv3)等。
NDK可以为我们生成C/C++动态链接库。 我们对于native的开发是基于ndk的开发。

ndk和jni没什么关系,只是基于ndk开发的动态库,需要通过jni和java进行沟通。

最后

经过这一节的学习,接下来面试中碰到jni问题的话,总算可以说个123了:

  1. jni的native代码怎么关联?通过静态注册和动态注册方式。
  2. 加载so库需要注意什么?System.loadLibrary之后再获取实例调用native方法才能调用到对应实现。
  3. 怎么构建so库?ndk支持通过cmake实现代码编译构建。
  4. ndk和jdk的区别?

只有学习才能是我成长,只有学习才能是我进步,我要好好学习,为建设祖国贡献一份力量~~~

参考文章:

  • Android JNI介绍(八)- CMakeLists的使用
  • JNI方法注册及加载原理分析
  • JNI实现源码分析【四 函数调用】
  • Android ART执行类方法的过程

你可能感兴趣的:(【Android面试速学】JNI 了解一下?)