【Android面試速學】JNI 了解一下?

標題圖

android面試中老是會問jni,但是我在小廠搬磚多年,可還沒咋用過啊
哭~~~~
沒用過那就了解一下吧。

編寫:guuguo  校對:guuguo

名詞解釋

  • c++頭文件: 頭文件用來放置對應c++方法的聲明,其實它的內(nèi)容跟 .cpp 文件中的內(nèi)容是一樣的,都是 C++ 的源代碼。但頭文件不用被編譯。頭文件可以通過#include被包含到.cpp文件中。include僅僅是復制頭文件的定義代碼到.cpp文件中。所以頭文件用來放置聲明,而不是定義。因為多個源文件直接包含定義的話會有定義沖突,而聲明就不會。(頭文件也可以包含定義,但是盡量不要,如果 需要,通過#ifndef...#endif讓編譯器判斷個名字是否被定義,再決定要不要繼續(xù)編譯后續(xù)的內(nèi)容)
  • JNI (Java Native Interface,Java本地接口)是一種編程框架,使得Java虛擬機中的Java程序可以調(diào)用本地應用/或庫,也可以被其他程序調(diào)用。
  • CMake 是一個跨平臺構建工具,支持C/C++/Java等語言的工程構建。本文中用來編譯c++代碼。

這篇文章講什么?

Android 系統(tǒng)中有大量的實現(xiàn)都是native實現(xiàn)的,中間通過JNI進行java層調(diào)用。學會JNI的使用,不光是能為我們開發(fā)和面試提供助力,還能為我們理解android 系統(tǒng)源碼的基礎多加兩塊磚。
說明一下這篇文章的內(nèi)容和目的:

  1. 了解JNI 在開發(fā)中的基礎使用
  2. Java 代碼和 c++ 的native 方法鏈接原理
  3. JNI 框架是啥,都有哪些東西
  4. Ndk 是什么東西?

弄明白這四個小點,對于JNI也就有了初步的理解,在要利用其進行開發(fā)的時候也能信手拈來。

JNI 使用的小栗子(靜態(tài)注冊)

jni注冊方式分靜態(tài)注冊和動態(tài)注冊,

  • 靜態(tài)注冊:根據(jù)函數(shù)名找到對應的JNI函數(shù),樣式為Java_包名_類名_方法名
  • 動態(tài)注冊:當我們使用System#loadLibarary方法加載so庫的時候,Java虛擬機會找到JNI_OnLoad函數(shù)并主動調(diào)用。所以我們可以在JNI_OnLoad 調(diào)用 jniRegisterNativeMethods進行方法的動態(tài)注冊。(先不學習該方式,欲了解可google)

下面我們就講一下靜態(tài)注冊先:

  1. 創(chuàng)建demo jni sdk模塊

我們創(chuàng)建一個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方法。在調(diào)用該方法的時候,Java_包名_類名_方法名的c++函數(shù)。
我們先來創(chuàng)建JNI入口java類 JNI.java,定義好java的native方法。方法如下:

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

我們定義了一個簡單的native方法signString,模擬對字符串進行簽名的方法。

  1. 生成對對應的頭文件

java中提供了javah 工具。通過他可以自動生成native方法對應c++的頭文件。通過javah -h 看看該工具的使用說明:

javah -h
用法: 
  javah [options] <classes>
其中, [options] 包括:
  -o <file>                輸出文件 (只能使用 -d 或 -o 之一)
  -d <dir>                 輸出目錄
  -v  -verbose             啟用詳細輸出
  -h  --help  -?           輸出此消息
  -version                 輸出版本信息
  -jni                     生成 JNI 樣式的標頭文件 (默認值)
  -force                   始終寫入輸出文件
  -classpath <path>        從中加載類的路徑
  -cp <path>               從中加載類的路徑
  -bootclasspath <path>    從中加載引導類的路徑
<classes> 是使用其全限定名稱指定的
(例如, java.lang.Object)。

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

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

可以看到命令執(zhí)行過后,.h文件被成功生成了

img

有了.h jni 聲明文件后,我們在 jni.cpp中完成對應方法的實現(xiàn),代碼如下:

#include <stdio.h>
#include <stdlib.h>
#include <string>
#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;
}

方法的定義實現(xiàn)很簡單,只是對傳入的字符串前面拼接了signed:字符串。

  1. 完善CmakeList.txt 和 build.gradle 編譯.so產(chǎn)物

對于native源碼的編譯,當前有兩種方案:cmake 和 ndk-build。CMake會更加流行一些,現(xiàn)在介紹一下CMake。
CMake 是一個跨平臺構建工具,支持C/C++/Java等語言的工程構建。通過配置CMake 構建腳本CMakeLists.txt,我們可以利用CMake命令做好自定義的編譯工作。
這是cmake使用的主要指令

  • set(all_src "./src"):該指令可以定義名為all_src的變量值
  • add_library:該指令的主要作用就是將指定的源文件生成鏈接文件,然后添加到工程中去

CMakeLists.txt

我們編輯一下該配置文件,使用如下內(nèi)容

# 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")                     # 調(diào)試信息
#set(CMAKE_CXX_FLAGS   "-Wall")                  # 開啟所有警告
#set(CMAKE_CXX_FLAGS_DEBUG   "-O0" )             # 調(diào)試包不優(yōu)化
#set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG " )   # release包優(yōu)化
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"
        )
# 將源碼文件添加到編譯動態(tài)庫中
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'
    }
}

在以上代碼中指定好一些必要參數(shù),以及cmake版本和配置文件路徑

編譯:

接下來的編譯中會自動 編譯出相關類庫,也可以通過以下的gradle命令直接打包出對應的so庫和aar包

./gradlew :sdk:aR

也就是使用aR(assembleRelease)命令編譯release包,在build/intermediates/cmake/release中能找到對應產(chǎn)物。

  1. 簡單c++方法調(diào)用

完成了定義,我們簡單實現(xiàn)一下調(diào)用:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        System.loadLibrary("jni-test")
        findViewById<Button>(R.id.button).setOnClickListener {
           Toast.makeText(this,JNI.instance.signString("hello world"),Toast.LENGTH_LONG).show()
        }
    }
}

我們在點擊按鈕之后,直接彈出吐司展示簽名后的字符串。

這一塊有一點需要注意!!

獲取JNI實例的步驟,需要在System.loadLibrary之后。
這樣才能正確調(diào)用到對應的native方法。

小結:

至此,最小化實現(xiàn)的一個jni樣例就完成了,實現(xiàn)了native方法定義以及java對其的調(diào)用。
以此為基礎,我們在未來能深入很多

  • 我們能夠慢慢了解跨平臺native sdk 如何在安卓中使用。
  • 能夠為閱讀aosp源碼增加自己的基礎功

Java 代碼和 c++ 的native 方法如何連接起來

java調(diào)用native方法的時候,由art虛擬機對應做特殊處理。
參考Android ART執(zhí)行類方法的過程,虛擬機在執(zhí)行方法的時候判斷是否native方法,執(zhí)行。
客戶端的實現(xiàn)很簡單,就是上面提到的靜態(tài)注冊和動態(tài)注冊方式。

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

JNIEnv 表示 Java 調(diào)用 native 語言的環(huán)境,是一個封裝了幾乎全部 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方法都是間接調(diào)用的JNINativeInterface的方法,只是對JNINativeInterface結構體的一層封裝。
我們JNI的大多數(shù)操作都是通過其進行。

NDK是啥,和jni什么關系?

ndk:Native Development Kit

Android NDK 支持使用 CMake 編譯應用的 C 和 C++ 代碼。
NDK是一系列工具的集合。

  • NDK提供了一系列的工具,幫助開發(fā)者快速開發(fā)C(或C++)的動態(tài)庫,并能自動將so和java應用一起打包成apk。這些工具對開發(fā)者的幫助是巨大的。
  • NDK集成了交叉編譯器,并提供了相應的mk文件隔離CPU、平臺、ABI等差異,開發(fā)人員只需要簡單修改mk文件(指出“哪些文件需要編譯”、“編譯特性要求”等),就可以創(chuàng)建出so。
  • NDK可以自動地將so和Java應用一起打包,極大地減輕了開發(fā)人員的打包工作。

NDK提供了一份穩(wěn)定、功能有限的API頭文件聲明。包含有:C11標準庫(libc)、標準數(shù)學庫(libm)、c++17庫、Log庫(liblog)、壓縮庫(libz)、Vulkan渲染庫(libvulkan)、openGl庫(libGLESv3)等。
NDK可以為我們生成C/C++動態(tài)鏈接庫。 我們對于native的開發(fā)是基于ndk的開發(fā)。

ndk和jni沒什么關系,只是基于ndk開發(fā)的動態(tài)庫,需要通過jni和java進行溝通。

最后

經(jīng)過這一節(jié)的學習,接下來面試中碰到jni問題的話,總算可以說個123了:

  1. jni的native代碼怎么關聯(lián)?通過靜態(tài)注冊和動態(tài)注冊方式。
  2. 加載so庫需要注意什么?System.loadLibrary之后再獲取實例調(diào)用native方法才能調(diào)用到對應實現(xiàn)。
  3. 怎么構建so庫?ndk支持通過cmake實現(xiàn)代碼編譯構建。
  4. ndk和jdk的區(qū)別?

只有學習才能是我成長,只有學習才能是我進步,我要好好學習,為建設祖國貢獻一份力量~~~

參考文章:

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容