Android JNI开发流程介绍
1.什么是JNI
JNI全稱是Java Native Interface,中文稱為Java本地接口。JNI是JAVA語言和C/C++語言溝通的協議,通過JNI,Java代碼可以調用C、C++等語言寫的代碼,或者反過來C、C++等語言代碼通過JNI調用Java 寫的代碼。
為什么使用JNI?
我們知道,Java語言的特性是一次編寫,到處運行,跨平臺是Java的優點,但有得就有失,跨平臺的特性導致了Java和底層交互能力不夠強大,而這正是C/C++語言所擅長的,另一方面,Java通過JNI可以復用大量現有的C/C++庫文件,避免重復造輪子。
2.什么是NDK
NDK全稱是Native Develop Kit,可譯為本地開發套件,NDK是Android提供的工具集,用于進行C/C++的開發。
JNI和NDK又是什么關系?
JNI是一個接口協議,NDK是一個開發套件,他們之間有個共同點是英文都包含Native(本地),都和C/C++語言扯上關系,不過以這兩點硬說他們有關系就有點太勉強了,事實上,JNI是Java的,NDK是Android的,兩者沒有相互依存關系,硬要說有什么關系的話,就是使用NDK,可在Android中快速開發符合JNI接口要求的C/C++動態庫,方便Java調用。
可能有人會問了,Android開發語言不是Java語言,為何還另外提供C/C++語言開發工具?
要知道,Android開發語言雖然是Java,但內核是Linux,而Linux核心庫是用C/C++編寫,因此Google提供NDK工具,方便開發者和核心庫交互。不過,一般開發純業務的應用,不會涉及到NDK,如果涉及到以下需求則需要用到NDK:
- 1、提供代碼安全性。因為so庫文件反編譯較難,將核心代碼封裝成so庫文件,大大提高了代碼的安全性;
- 2、便于平臺間移植其應用。通過NDK,開發人員可以方便生成指定平臺的動態庫;
- 3、重復使用現有C/C++開源庫;
- 4、提升程序執行效率,特別是一些計算密集型應用。
3.JNI開發流程
以兩個數相加作為例子,兩個數相加使用C/C++實現,再通過JNI調用。這和日常生活中的外包流程很類型,Java自己不想做,發了一個外包需求,C接下了這個活。
- 第一步:第一步是Java發布需求。首先創建包名為com.test.jnitest的工程,在工程中創建名為JNIUtils的class,并在類中聲明一個native方法。代碼如下:
native關鍵字表示這是一個要外包實現的函數,那么C完成外包工作后以什么形式給Java交差呢,用so庫文件,然后Java通過System.loadLibrary(“jni_method”)加載,so庫文件名jni_method值隨意,只要Java和C之間約定好就行。
- 第二步:這一步還是Java這邊的,是Java這邊拿到C交差工作后做的事。我們要實現的功能是在兩個文本框分別輸入兩個數字后,點擊相加按鈕,然后代碼調用C完成的兩個數進行相加功能,并把結果顯示在界面上,界面布局xml代碼如下:
Activity里調用本地相加代碼如下:
package com.test.jnitest;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView;public class MainActivity extends AppCompatActivity {Button Add;EditText Num1;EditText Num2;TextView Result;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Add=findViewById(R.id.bAdd);Num1=findViewById(R.id.etNum1);Num2=findViewById(R.id.etNum2);Result=findViewById(R.id.tvResult);Add.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {String sN1=Num1.getText().toString().trim();String sN2=Num2.getText().toString().trim();int iN1 = Integer.parseInt(sN1);int iN2 = Integer.parseInt(sN2);//調用本地方法進行相加int iResult=JNIUtils.add(iN1,iN2);//記得不要直接打印Result.setText(iResult),這樣代碼會認為iResult是個 資源IDResult.setText(“相加結果”+iResult);}});}}- 第三步:Java這邊代碼是準備好了,接下來的工作是把外包工作布置給C的過程,因為Java和C是兩種語言,他們之間不能直接溝通,需要做一下處理,以下就是溝通過程:首先編譯Java源文件得到.class文件,方法是點擊Visual Studio的Build菜單下的Make Project,然后在下圖的路徑找到生成的.class文件,注意,該路徑和有些博客所說的不一致,他們生成的.class文件路徑在intermediates的classes文件夾下。
-第四步:生成.class文件C語言還是看不懂,因此還是再做一次處理,處理方式是通過.class文件得到C的頭文件,這樣C語言就能看懂了,生成頭文件使用javah命令,可以在終端中執行(我的是MAC)或者使用Android Studio里的終端,兩者其實一樣。首先進入在終端里進入classes路徑下,注意一定要在classes這一級目錄,不然會提示找不到class文件。我的環境是:
/Users/lan/AndroidStudioProjects/JNITest/app/build/intermediates/javac/debug/classes,然后執行命令:
注:命令應包含完整的包名,并且.class文件不能帶“.class”后綴,另外之前按照網上其他博客的命令,一直報找不到類的錯誤,加上-cp參數解決
執行命令成功后,即可得到.h文件,如下圖所示。
生成得到的.h文件內容如下:
-第五步:得到頭文件后,就可以根據.h文件,實現對應的C代碼。在java目錄下,新建一個jni文件夾,將生產的.h文件拷貝到這個目錄下,然后新建一個名為JNIUtils的C文件,在該代碼中實現頭文件聲明的函數,代碼實現如下:
-第六步:C代碼實現后,已經實現了Java發布的外包需求,接下來的工作是C以Java看得懂的形式交差外包工作,在第一步中Java已經說明了,給他交差工作用so庫文件的形式,因此接下來的工作就是想辦法生成so庫文件。首先在jni目錄下創建Android.mk文件,Android.mk作用是指定源碼編譯的配置信息,Android.mk內容如下:
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := jni_calLOCAL_SRC_FILES := JNITool.cinclude $(BUILD_SHARED_LIBRARY)其中
-
LOCAL_PATH := $(call my-dir)得到Android.mk文件本身所在的路徑,宏my-dir則由編譯系統提供,返回當前目錄(Android.mk 文件本身所在的目錄)的路徑;
-
include $(CLEAR_VARS) 宏CLEAR_VARS 變量由編譯系統提供。并指向一個指定的GNU Makefile,由它負責清理LOCAL_PATH之外的LOCAL_xxx,例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES等。為什么要執行這個清理操作,因為所有的編譯控制文件由同一個GNU Make解析和執行,其變量是全局性的,清理后才能避免相互影響,因此在描述每個庫之前,必須有該聲明;
-
LOCAL_MODULE:=jni_method 表示的是要生成的庫,這個庫就是在java里加載的庫,每個庫名稱必須唯一,且不含任何空格。編譯系統在生成最終的庫名稱里自動添加lib前綴和so后綴。例如,上述示例會生成名為libjni_cal.so,如果在LOCAL_MODULE定義的名稱已經帶lib了,則編譯系統不會再添加lib前綴,例如名稱是libmodule,那么編譯系統輸出的是libmodule.so,而不是liblibmodule.so;
-
LOCAL_SRC_FILES :=JNIUtils.c包含要編譯到庫中的 C 和/或 C++ 源文件列表,不必列出頭文件,編譯系統會自動幫我們找出依賴文件;
-
include $(BUILD_SHARED_LIBRARY),其中BUILD_SHARED_LIBRARY 變量指向一個GNU Makefile腳本,該腳本會收集您自最近include以來在 LOCAL_XXX 變量中定義的所有信息。此腳本確定要編譯的內容以及編譯方式:
- BUILD_STATIC_LIBRARY:編譯為靜態庫
- BUILD_SHARED_LIBRARY:編譯為動態庫
- BUILD_EXECUTABLE:編譯為Native C 可執行程序
- BUILD_PREBUILT:該模塊已經預先編譯
最后一行幫助系統將所有內容連接到一起:
第七步,在jni目錄下創建Application.mk文件,其中內容就一句話:APP_ABI:=all。在上一步中,Android.mk解決的問題是編譯誰,但還沒解決編譯出來給哪個平臺用,這是Application.mk要做的工作,常見的平臺有Arm,x86,MIPS,配置方法是在APP_ABI字段設置成對應的值,例如如果想配置成基于Arm平臺的so文件,則APP_ABI := armeabi,至于要生成哪個平臺,這就看Java代碼準備運行在哪個平臺了,我這里設置成配置支持所有平臺,對應的字段是APP_ABI := all。
第八步,到目前為止,生成so文件的準備工作已經差不多了,接下來要做的則是使用NDK工具生成so文件。關于配置NDK環境不再贅述(折騰過程可以寫成一篇博客了),這里假設NDK環境已經配置好了。生成NDK過程很簡單:在終端進入到jni目錄,終端在Android Studio底部,進到目錄后,輸入ndk-build命令,編譯成功后,在src/main/會多了兩個文件夾libs & obj,其中libs下存放的是生成的so庫文件,因為我在Application.mk設置的全平臺,因此生成所有平臺的so文件,如果Application.mk設置成特定平臺,則只生成特定平臺的so文件。拿到so文件后,C可以給Java交差任務了。
第九步,這一步要做的工作把so庫文件交給Java。首先在src/main/中創建一個名為jniLibs的文件夾,并將上一步生成的so文件夾放到該目錄下,這里有兩點需要注意一下,一是因為要拷貝哪些so庫,需要看Android程序準備運行在哪些平臺上,如果拷貝的so庫文件不正確,則應用不能正常安裝,會提示ABI不匹配錯誤,因為我的模擬器是x86平臺,因此我拷貝了x86平臺的so庫文件到jniLibs下,二是拷貝文件是文件夾一起拷貝,不能單拷貝單個so文件到jniLibs下。
第十步,因為在第一步時,Java端的代碼已經準備好了,現在so庫文件也拷貝到指定目錄下,Java代碼可以直接運行,下圖是在模擬器上運行的效果。
4.總結
把JNI開發流程當成Java和C之間的一次外包需求開發理解起來就容易了,首先是Java發布需求,使用native關鍵字聲明函數由外包實現,并聲明了外包任務交差以so庫文件的形式,Java外包任務通過頭文件的形式交給C語言,之后C語言實現后,再想辦法生成so庫文件來交差任務,最后將so庫文件拷貝到指定目錄下,Java能正常加載,整個流程也到處結束。
總結
以上是生活随笔為你收集整理的Android JNI开发流程介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Schnorr身份识别方案
- 下一篇: Android回调的简单理解