【Android面試速學(xué)】JNI 了解一下瓮钥?

標(biāo)題圖

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

編寫:guuguo  校對:guuguo

名詞解釋

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

這篇文章講什么?

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

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

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

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

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

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

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

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

我們創(chuàng)建一個sdk模塊冠跷,承載native和jni代碼,目錄結(jié)構(gòu)如下:

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 中身诺,使用關(guān)鍵字external標(biāo)識該方法是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. 生成對對應(yīng)的頭文件

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

javah -h
用法: 
  javah [options] <classes>
其中, [options] 包括:
  -o <file>                輸出文件 (只能使用 -d 或 -o 之一)
  -d <dir>                 輸出目錄
  -v  -verbose             啟用詳細(xì)輸出
  -h  --help  -?           輸出此消息
  -version                 輸出版本信息
  -jni                     生成 JNI 樣式的標(biāo)頭文件 (默認(rèn)值)
  -force                   始終寫入輸出文件
  -classpath <path>        從中加載類的路徑
  -cp <path>               從中加載類的路徑
  -bootclasspath <path>    從中加載引導(dǎo)類的路徑
<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中完成對應(yīng)方法的實現(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源碼的編譯氢橙,當(dāng)前有兩種方案:cmake 和 ndk-build结窘。CMake會更加流行一些,現(xiàn)在介紹一下CMake充蓝。
CMake 是一個跨平臺構(gòu)建工具隧枫,支持C/C++/Java等語言的工程構(gòu)建喉磁。通過配置CMake 構(gòu)建腳本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++編譯期標(biāo)識 賦值
#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 {
            ///編譯目標(biāo)名
            targets 'jni-test'
            //預(yù)編譯行為配置 :-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版本和配置文件路徑

編譯:

接下來的編譯中會自動 編譯出相關(guān)類庫孕暇,也可以通過以下的gradle命令直接打包出對應(yīng)的so庫和aar包

./gradlew :sdk:aR

也就是使用aR(assembleRelease)命令編譯release包,在build/intermediates/cmake/release中能找到對應(yīng)產(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()
        }
    }
}

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

這一塊有一點需要注意W帷每瞒!

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

小結(jié):

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

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

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

java調(diào)用native方法的時候,由art虛擬機對應(yīng)做特殊處理识啦。
參考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結(jié)構(gòu)的源碼:

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結(jié)構(gòu)體的一層封裝齿税。
我們JNI的大多數(shù)操作都是通過其進行。

NDK是啥炊豪,和jni什么關(guān)系凌箕?

ndk:Native Development Kit

Android NDK 支持使用 CMake 編譯應(yīng)用的 C 和 C++ 代碼拧篮。
NDK是一系列工具的集合。

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

NDK提供了一份穩(wěn)定疗锐、功能有限的API頭文件聲明坊谁。包含有:C11標(biāo)準(zhǔn)庫(libc)费彼、標(biāo)準(zhǔn)數(shù)學(xué)庫(libm)、c++17庫口芍、Log庫(liblog)箍铲、壓縮庫(libz)、Vulkan渲染庫(libvulkan)鬓椭、openGl庫(libGLESv3)等颠猴。
NDK可以為我們生成C/C++動態(tài)鏈接庫。 我們對于native的開發(fā)是基于ndk的開發(fā)小染。

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

最后

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

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

只有學(xué)習(xí)才能是我成長蓝晒,只有學(xué)習(xí)才能是我進步腮出,我要好好學(xué)習(xí),為建設(shè)祖國貢獻一份力量~~~

參考文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芝薇,一起剝皮案震驚了整個濱河市利诺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剩燥,老刑警劉巖慢逾,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異灭红,居然都是意外死亡侣滩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門变擒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來君珠,“玉大人,你說我怎么就攤上這事娇斑〔咛恚” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵毫缆,是天一觀的道長唯竹。 經(jīng)常有香客問我,道長苦丁,這世上最難降的妖魔是什么浸颓? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮旺拉,結(jié)果婚禮上产上,老公的妹妹穿的比我還像新娘。我一直安慰自己蛾狗,他們只是感情好晋涣,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沉桌,像睡著了一般谢鹊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蒲牧,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天撇贺,我揣著相機與錄音,去河邊找鬼冰抢。 笑死松嘶,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挎扰。 我是一名探鬼主播翠订,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼巢音,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了尽超?” 一聲冷哼從身側(cè)響起官撼,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎似谁,沒想到半個月后傲绣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡巩踏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年秃诵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塞琼。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡菠净,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出彪杉,到底是詐尸還是另有隱情毅往,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布派近,位于F島的核電站攀唯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏构哺。R本人自食惡果不足惜革答,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一战坤、第九天 我趴在偏房一處隱蔽的房頂上張望曙强。 院中可真熱鬧,春花似錦途茫、人聲如沸碟嘴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娜扇。三九已至,卻和暖如春栅组,著一層夾襖步出監(jiān)牢的瞬間雀瓢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工玉掸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留刃麸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓司浪,卻偏偏與公主長得像泊业,于是被迫代替她去往敵國和親把沼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容