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)容和目的:
- 了解JNI 在開發(fā)中的基礎(chǔ)使用
- Java 代碼和 c++ 的native 方法鏈接原理
- JNI 框架是啥,都有哪些東西
- 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)注冊先:
-
創(chuàng)建demo jni sdk模塊
我們創(chuàng)建一個sdk
模塊冠跷,承載native和jni代碼,目錄結(jié)構(gòu)如下:
圖中展示的主要目錄如下:
-
src/main/java
java源碼 -
src/main/jni
native源碼 -
src/main/jni/CMakeLists.txt
cmake的配置文件
并且在build.gradle 中配置好jni
源碼路徑:
sourceSets {
main {
jni.srcDirs = ['src/main/jni']
}
}
-
定義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
蜂挪,模擬對字符串進行簽名的方法。
-
生成對對應(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文件被成功生成了
有了
.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:
字符串。
-
完善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)物赤兴。
-
簡單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了:
- jni的native代碼怎么關(guān)聯(lián)踊赠?通過靜態(tài)注冊和動態(tài)注冊方式呵扛。
- 加載so庫需要注意什么?System.loadLibrary之后再獲取實例調(diào)用native方法才能調(diào)用到對應(yīng)實現(xiàn)筐带。
- 怎么構(gòu)建so庫今穿?ndk支持通過cmake實現(xiàn)代碼編譯構(gòu)建。
- ndk和jdk的區(qū)別伦籍?
只有學(xué)習(xí)才能是我成長蓝晒,只有學(xué)習(xí)才能是我進步腮出,我要好好學(xué)習(xí),為建設(shè)祖國貢獻一份力量~~~