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)容和目的:
- 了解JNI 在開發(fā)中的基礎使用
- Java 代碼和 c++ 的native 方法鏈接原理
- JNI 框架是啥,都有哪些東西
- 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)注冊先:
-
創(chuàng)建demo jni sdk模塊
我們創(chuàng)建一個sdk模塊,承載native和jni代碼,目錄結構如下:
圖中展示的主要目錄如下:
-
src/main/javajava源碼 -
src/main/jninative源碼 -
src/main/jni/CMakeLists.txtcmake的配置文件
并且在build.gradle 中配置好jni源碼路徑:
sourceSets {
main {
jni.srcDirs = ['src/main/jni']
}
}
-
定義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,模擬對字符串進行簽名的方法。
-
生成對對應的頭文件
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文件被成功生成了
有了
.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:字符串。
-
完善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)物。
-
簡單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)用。
以此為基礎,我們在未來能深入很多
- 我們能夠慢慢了解跨平臺
nativesdk 如何在安卓中使用。 - 能夠為閱讀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了:
- jni的native代碼怎么關聯(lián)?通過靜態(tài)注冊和動態(tài)注冊方式。
- 加載so庫需要注意什么?System.loadLibrary之后再獲取實例調(diào)用native方法才能調(diào)用到對應實現(xiàn)。
- 怎么構建so庫?ndk支持通過cmake實現(xiàn)代碼編譯構建。
- ndk和jdk的區(qū)別?
只有學習才能是我成長,只有學習才能是我進步,我要好好學習,為建設祖國貢獻一份力量~~~