Android中的JNI入門實戰
JNI與NDK概述
JNI的全稱為Java Native Interface,即Java本機介面,用於Java跟C/C++的相互呼叫。對Android系統而言,上層應用都是用Java語言編寫,但Android系統又是基於Linux系統,因此不可避免的會與使用C/C++編寫的本地庫進行互動,除此之外,為了提高計算效率,許多程式碼都是使用C/C++編寫。JNI的出現正是基於上述原因,為了方便Java能夠方便呼叫底層的C/C++程式碼。需要注意的是,JNI是Java呼叫C/C++的規範,並不僅僅限於Android系統。
NDK的全稱是Native Develop Kit,顧名思義,是一套用於本地開發的編譯工具。NDK的功能就是為上層應用提供一種呼叫核心庫的方式,簡單來說,就是為了更加方便的在Android系統下使用JNI。
Android的系統框架圖如下所示,上層通過JNI來呼叫NDK層,使用這個工具可以很方便的編寫和除錯JNI程式碼。
JNI與NDK的區別:JNI提供了一系列的API實現Java與C/C++相互操作,NDK提供了一系列的工具快速開發C/C++動態庫,並能自動將so和Java應用一起打包給APK。概況的來說就是JNI負責Java和C/C++相互呼叫,而NDK提供工具方便在Androi使用JNI。
ubuntu下編譯執行JNI例項
實現JNI的流程:
- 在Java中宣告一個native方法;
- 通過javac編譯Java原始檔,生成.class檔案;
- 通過javah -jin命令匯出JNI的.h檔案;
- 實現在Java中宣告的Native方法;
- 將原生程式碼編譯成動態庫(Windows系統下是.dll檔案,Linux系統下是.so檔案,Mac系統下是.jnilib檔案);
- 通過Java命令執行Java程式,最終實現Java呼叫原生程式碼。
步驟:
- 首先建立HelloWorld.java例項,程式碼如下:
class HelloWorld {
private native void print();
public static void main(String[] args) {
new HelloWorld().print();
}
static
{
System.loadLibrary("HelloWorld");
}
}
其中,private native void print()
宣告一個本地函式,後續會在本地實現,而static { System.loadLibrary("HelloWorld"); }
則用來載入本地庫。
2. 編譯該類併產生標頭檔案
javac HelloWorld.java
javah -jni HelloWorld
這個過程之後,會產生HelloWorld.h標頭檔案跟HelloWorld.class。
3. 新建HelloWorld.cpp實現本地方法,程式碼如下:
#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jobject obj)
{
printf("Hello World!\n");
}
其中,void代表返回型別為空,JNICALL和JNIEXPORT是不固定保留的關鍵字,Java是是包名,HelloWorld是類名,print是方法名。JNIEnv *env代表一個介面指標,而JNIEnv結構是一個函式表在原生程式碼中通過這個函式表操作Java資料或者呼叫Java方法,obj則代表在本地方法中對宣告物件的引用。
4. 把C/C++函式編譯成一個本地庫,生成libHelloWorld.so檔案
g++ -I /usr/lib/jvm/java-8-openjdk-amd64/include/linux/ -I /usr/lib/jvm/java-8-openjdk-amd64/include/ -fPIC -shared -o libHelloWorld.so HelloWorld.cpp
- 使用Java命令執行HelloWorld程式
java HelloWorld
此時可能出現如下的錯誤:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
at java.lang.Runtime.loadLibrary0(Runtime.java:871)
at java.lang.System.loadLibrary(System.java:1124)
at HelloWorld.<clinit>(HelloWorld.java:9)
解決方式是臨時指定共享庫libHelloWorld.so的路徑,使用命令export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
Android系統編譯執行JNI示例
本例項使用android studio操作
- 建立一個Android專案,包名為jni.ndkdemo;
- 在main/java/jni.ndkdemo/建立一個class為NDKTools,程式碼如下:
package jni.ndkdemo;
public class NDKTools {
static {
System.loadLibrary("ndkdemotest-jni");
}
public static native String getStringFromNDK();
}
- 修改相關UI顯示,在MainActivity對應的xml中的textview新增id,如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
同時修改MainActivity,在裡面呼叫NDKTools的getStringFromNDK()方法:
package jni.ndkdemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String text = NDKTools.getStringFromNDK();
Log.i("jni", "text="+text);
((TextView)findViewById(R.id.tv)).setText(text);
}
}
- 獲取classes檔案,點選Build中的Make Project或者Rebuild Project來編譯獲取中介軟體檔案,生成目錄如下圖:
- 點選Android Studio的終端,跳轉到
~/AndroidStudioProjects/jni/app/build/intermediates/javac/debug
下,執行javah -jni jni.ndkdemo.NDKTools
,生成jni_ndkdemo_NDKTools.h檔案。 - 增加對應的c檔案,在main目錄下建立一個名字為jni的目錄,將剛才的h檔案剪下過來,在jni目錄下新建c檔案,命名為ndkdemotest.c,此時目錄如下:
- 實現c檔案,內容如下:
#include "jni_ndkdemo_NDKTools.h"
JNIEXPORT jstring JNICALL Java_jni_ndkdemo_NDKTools_getStringFromNDK
(JNIEnv *env, jobject obj){
return (*env)->NewStringUTF(env,"Hellow World,這是jni的NDK的第一行程式碼");
}
- 同樣在jni目錄下,新增一個Android.mk檔案,內容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := ndkdemotest-jni
LOCAL_SRC_FILES := ndkdemotest.c
include $(BUILD_SHARED_LIBRARY)
每個Android.mk檔案必須從定義開始,LOCAL_PATH := $(call my-dir)
用於在開發tree中查詢原始檔,巨集my-dir由Build System提供,此語句返回包含Android.mk目錄路徑;include $(CLEAR_VARS)
中CLEAR_VARS變數由Build System提供,此語句作用是指向一個指定的GNU.Makefile,由其負責清理很多的LOCAL_xxx;LOCAL_MODULE := ndkdemotest-jni
指定模組,為生成動態庫做準備,例如本例會生成libndkdemotest-jni.so;LOCAL_SRC_FILES := ndkdemotest.c
指定要打包的原始碼,不必列出標頭檔案,builid System會自動找出依賴檔案;include $(BUILD_SHARED_LIBRARY):BUILD_SHARED_LIBRARY
指定編譯的型別,BUILD_SHARED_LIBRARY代表動態庫。
9. 修改相應的配置檔案,local.properties內容如下:
ndk.dir=/home/user/Android/Sdk/ndk/21.3.6528147
sdk.dir=/home/user/Android/Sdk
修改src中的build.gradle,如下:
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "jni.ndkdemo"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
- 修改引用類,即在NDKTools類中新增靜態初始化程式碼,如下:
package jni.ndkdemo;
public class NDKTools {
static {
System.loadLibrary("ndkdemotest-jni");
}
public static native String getStringFromNDK();
}
最後run一下即可。
Android Studio使用CMake快捷編譯JNI
- 選擇Android studio中的Native C++建立專案,可直接建立大部分檔案,目錄如下:
- 在src/main/cpp/目錄中有原始檔native-lib.cpp,負責提供本地實現,這已經是一個完整的專案,直接run即可;
以下再來簡單介紹以下CMake檔案,在app/.cxx資料夾下,內容如下:
cmake_minimum_required(VERSION 3.4.1)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
- Cmake_minimum_required(VERSION 3.4.1):指定Cmake的最小版本;
- Add_library:建立一個靜態或者動態庫,提供其有關聯的原始檔路徑,開發者可以定義多個庫,CMake會自動構建它們,其中
- Native-lib:庫的名稱
- SHARED:庫的類別,是動態還是靜態
- Src/main/cpp/native-lib.cpp:庫的原始檔的路徑 - Find_library:找到一個預編譯的庫,並作為一個變數儲存起來,由於CMake在搜尋路徑的時候會包含系統庫,並且CMake會檢查它自己之前編譯的庫的名字,所以開發者需要保證開發者自行新增的庫的名字的獨特性。第一個引數log-lib設定路徑變數的名稱,log指定NDK庫的名字,這樣CMake就可以找到這個庫。
- Target_link_libraries:指定CMake連結到目標庫。第一個引數native-lib指定的目標庫,第二個引數${log-lib}將目標庫連結到NDK中的日誌庫
相關文章
- Android Studio jni - 入門篇Android
- Android JNI 中的執行緒操作Android執行緒
- podman 入門實戰
- 密集計算場景下的 JNI 實戰
- Android JNI原理分析Android
- Android Studio NDK:二、JNI 返回JAVA 實體AndroidJava
- React實戰入門指南React
- Flutter For Web入門實戰FlutterWeb
- Linux入門到實戰Linux
- Android入門(五):實踐技巧Android
- Android版kotlin協程入門(四):kotlin協程開發實戰AndroidKotlin
- Android JNI 之 Bitmap 操作Android
- shiro實戰系列(二)之入門實戰續
- 逆向入門分析實戰(二)
- Redis 從入門到實戰Redis
- Gin + GORM 入門到實戰GoORM
- Locust 從入門到實戰
- Webpack 實戰:入門、進階與調優(中卷)Web
- Android入門教程 | RecyclerView實際使用AndroidView
- 在 Android 中使用 JNI 的總結Android
- ElasticSearch實戰系列六: Logstash快速入門和實戰Elasticsearch
- 鴻蒙手機版JNI實戰(JNI開發、SO庫生成、SO庫使用)鴻蒙
- JDBC+MySQL入門實戰(實現CURD的例子)JDBCMySql
- 滲透測試入門實戰
- Docker從入門到實戰pdfDocker
- Docker實戰-從入門到跑路Docker
- Spring Security 入門原理及實戰Spring
- Docker入門實戰 (四) - Docker NetworkDocker
- Pulsar 入門實戰(6)--Rest APIRESTAPI
- Pulsar 入門實戰(3)--安裝
- 《Flink入門與實戰》簡介
- 深度學習:TensorFlow入門實戰深度學習
- webpack 快速入門 系列 —— 實戰一Web
- Gulp4.0入門和實戰
- Pytorch實戰入門(一):搭建MLPPyTorch
- Android入門教程 | RecyclerView使用入門AndroidView
- Android Bluetooth 入門Android
- Android入門教程 | Kotlin協程入門AndroidKotlin