Android中的JNI入門實戰

yeyustudy發表於2020-11-15

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程式碼。
Android系統框架
JNI與NDK的區別:JNI提供了一系列的API實現Java與C/C++相互操作,NDK提供了一系列的工具快速開發C/C++動態庫,並能自動將so和Java應用一起打包給APK。概況的來說就是JNI負責Java和C/C++相互呼叫,而NDK提供工具方便在Androi使用JNI。

ubuntu下編譯執行JNI例項

實現JNI的流程:

  1. 在Java中宣告一個native方法;
  2. 通過javac編譯Java原始檔,生成.class檔案;
  3. 通過javah -jin命令匯出JNI的.h檔案;
  4. 實現在Java中宣告的Native方法;
  5. 將原生程式碼編譯成動態庫(Windows系統下是.dll檔案,Linux系統下是.so檔案,Mac系統下是.jnilib檔案);
  6. 通過Java命令執行Java程式,最終實現Java呼叫原生程式碼。

步驟:

  1. 首先建立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
  1. 使用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操作

  1. 建立一個Android專案,包名為jni.ndkdemo;
  2. 在main/java/jni.ndkdemo/建立一個class為NDKTools,程式碼如下:
package jni.ndkdemo;
	public class NDKTools {
	        static {
	                System.loadLibrary("ndkdemotest-jni");
	        }
	        public static native String getStringFromNDK();
	}
  1. 修改相關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);
	        }
	}
  1. 獲取classes檔案,點選Build中的Make Project或者Rebuild Project來編譯獲取中介軟體檔案,生成目錄如下圖:
    在這裡插入圖片描述
  2. 點選Android Studio的終端,跳轉到~/AndroidStudioProjects/jni/app/build/intermediates/javac/debug下,執行javah -jni jni.ndkdemo.NDKTools,生成jni_ndkdemo_NDKTools.h檔案。
  3. 增加對應的c檔案,在main目錄下建立一個名字為jni的目錄,將剛才的h檔案剪下過來,在jni目錄下新建c檔案,命名為ndkdemotest.c,此時目錄如下:
    在這裡插入圖片描述
  4. 實現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的第一行程式碼");
	    }
  1. 同樣在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'
	}
  1. 修改引用類,即在NDKTools類中新增靜態初始化程式碼,如下:
package jni.ndkdemo;
	
	public class NDKTools {
	        static {
	                System.loadLibrary("ndkdemotest-jni");
	        }
	        public static native String getStringFromNDK();
	}

最後run一下即可。

Android Studio使用CMake快捷編譯JNI

  1. 選擇Android studio中的Native C++建立專案,可直接建立大部分檔案,目錄如下:
    在這裡插入圖片描述
  2. 在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中的日誌庫

相關文章