第一個JNI入門步驟
概述
寫這篇文章的目的就是讓懵懵懂懂的新同學可以快速的體驗和理解JNI的通信流程,從應用層面上理解底層(driver layer)、JNI、framework、applications之間的通信過程。 避免在整個過程中陷入泥坑耗費精力和時間。
什麼是JNI
關於這個概念網上有很多解釋,大白話就是語言轉換,可以讓java與c/c++之間進行相互通信的一種格式約定。這樣的好處就是APP開發者不用關心頂層硬件如何實現的都可以通過java去控制硬件的響應。反之硬件反饋的數據也可以傳遞給上層app。
開發準備環境
- Ubuntu16.04 - 用來編譯Android源碼及其模塊,在這裏我們主要用來編譯jni so庫和jar包。
- Android Studio - 用來編譯Application上層應用程序,進行驗證jni的調用結果, 當前使用的版本爲3.5.3
- 目標設備 - 用來運行整個測試程序,當然也可以通過模擬器來測試。
編寫JNI工程
可能有很多新同學剛接觸JNI的時候就會疑惑它到底是個什麼東西,其實就是一個so庫(專指Linux下的動態庫),只是這個so庫是使用了一些jni語法進行編寫然後編譯形成的庫。對於嵌入式的同學應該就會很容易理解了。 不理解的同學也沒有關係,我們將通過實際操作來進行認識。
下面我們將進行第一個JNI練習。
目標爲: 從jni層獲得一個字符串,在屏幕上顯示,當然是手機屏幕或者模擬器
我們要完成這一過程需要經歷幾個步驟:
首先:需要有jni層,當app調用jni給出的接口時, jni可以通過這個接口返回數據給到app。
其次,需要framework層, 當app調用jni接口時需要經過中間轉換,否則不能直接調用到jni層。
最後,app就可以通過framework作用的jni接口,jni進而將會作用到硬件接口,如加載wifi,驅動馬達,點亮一個led等操作。
再此我們只到jni這一步就結束了,如果有興趣的話可以通過填充接口去作用相應的硬件。
在Android源碼下創建JNI工程
首先我們將在AOSP源碼樹下創建工作目錄,目錄佈局如下所示:
# 創建用於存放JNI源碼的目錄jni,並在該目錄下創建Android.mk文件和jnidemo.cpp文件及onload.cpp
mkdir -p vendor/mediatek/proprietary/frameworks/base/core/jni
cd vendor/mediatek/proprietary/frameworks/base/core/jni
touch Android.mk
touch jnidemo.cpp
touch onload.cpp
然後編寫jnidemo.cpp對外接口函數:
// jnidemo.cpp
#include <utils/Log.h>
#include "JNIHelp.h"
#include "jni.h"
#define LOG_TAG "Service-JNI"
namespace android {
static jint jni_nativeOpen(JNIEnv* env,jobject obj){
ALOGE("JNI test nativeOpen");
return 10;
}
static jstring jni_displayString(JNIEnv* env, jobject obj) {
ALOGE("JNI test native string");
return env->NewStringUTF("Hello Sven! I am from JNI!");
}
/**
* 在這裏method_table定義了兩個開放接口,一般要查看JNI的接口直接開個類型就可以快速瞭解
* 提供了哪些接口, 其中JNINativeMethod的類型爲:
* typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
* @param name 就是供framework開放的接口。
* @param signature 是對該接口屬性描述,比如返回值和參數的描述。具體關鍵字含義請自行查看。
* @param fnPtr 是對framework接口被調用後真正在jni被執行的函數。這是一一對應的關係。
*/
static JNINativeMethod method_table[] = {
{"nativeOpen","()I",(void*)jni_nativeOpen },
{"displayString","()Ljava/lang/String;",(void*)jni_displayString }
};
/**
* 這個register_android_jnidemo_Service()方法,是用於註冊JNI文件,在該方法中用到了兩個
* 關鍵的參數。一個是"com/example/test/Demo",對應着java代碼的包名和類名,即調用JNI的
* java代碼所在的包是“com.example.test”,類名是Demo;另一個參數是method_table,即是
* 上面初始化的JNINativeMethod結構體。
*/
int register_android_jnidemo_Service(JNIEnv *env){
return jniRegisterNativeMethods(env,"com/example/test/Demo",
method_table,NELEM(method_table));
}
}
最後增加JNI導入裝載函數入口:
在這裏我們使用JNI_OnLoad()函數進行註冊JNI。代碼實現如下:
// onload.cpp
#include "JNIHelp.h"
#include "jni.h"
#include "utils/Log.h"
#include "utils/misc.h"
namespace android {
int register_android_jnidemo_Service(JNIEnv* env);
};
using namespace android;
extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("GetEnv failed!");
return result;
}
ALOG_ASSERT(env, "Could not retrieve the env!");
register_android_jnidemo_Service(env);
return JNI_VERSION_1_4;
}
以上就是JNI相關源碼部分的實現了。
接下來我們需要實現編譯控制,因此需要編寫Android.mk將上述JNI源碼編譯成so庫文件:
# Android.mk
# Sven JNI Test
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
# 加入指定JNI源碼,在這我們直接指定具體文件, 也可以通過匹配方式加入
LOCAL_SRC_FILES:= \
jnidemo.cpp \
onload.cpp
LOCAL_SHARED_LIBRARIES := \
libnativehelper \
libcutils libutils \
liblog
LOCAL_MODULE:= libjnidemo
include $(BUILD_SHARED_LIBRARY)
這樣我們就可以進行編譯了。
在jni當前目錄執行mm 或者在Android源碼頂層目錄執行make這樣將編譯生成libjnidemo.so文件到/system/lib目錄下。
到這裏我們的第一步jni庫文件就產生了,我們可以將它push到我們的設備中,放到/system/lib目錄下。
adb push ${AOSP}/out/target/product/sm3_bsp/system/lib/libjnidemo.so /system/lib
在IDE開發環境中創建JNI工程
常用的IDE開發環境主要是AS和Eclipse這兩個開發環境,在本文中將不再表述如何創建jni工程,有興趣的同學可以自行查看資料學習吧!
framework層java代碼的實現
接下來我們需要實現java調用jni的接口層供app應用層調用。通常也有兩種方式實現:
一種:將framework實現部分與app分離,以jar包的形式提供給app調用。
第二種:不以jar包的形式提供,將framework實現部分整合到app中進行實現
jar包方式提供
首先我們通過如果實現jar包的方式給app調用,這種方式也是比較常用的方法,特別是跨部門合作或團隊合作基本都是這種方式。
在這裏我們只通過android源碼來進行實現,當然也可以通過IDE這樣的開發環境進行實現產出jar包。
和前面一樣首先準備源碼目錄:
# 在AOSP中創建java源碼目錄
mkdir -p vendor/mediatek/proprietary/frameworks/base/core/java
cd vendor/mediatek/proprietary/frameworks/base/core/java
# 創建包的目錄,需要和註冊jni時傳入的包名一致 'com/example/test', 類名爲Demo
mkdir -p com/example/test
cd com/example/test
# 創建framework源碼文件
touch Demo.java
# 編寫Demo.java
package com.example.test;
import android.util.Log;
public class Demo {
String TAG="frameworkSven Demo";
static {
// 引入上述實現的libjnidemo.so
System.loadLibrary("jnidemo");
}
//以native 申明JNI函數,該函數要和前面提到的method_table.name相一致
public native int nativeOpen();
public native String displayString();
public Demo(){
Log.d(TAG,"get from jni = "+nativeOpen());
}
}
# 在當前目錄中創建Android.mk文件, 製作出jar包好提供給app開發使用。
touch Android.mk
LOCAL_PATH:= $(call my-dir)
#make jar
include $(CLEAR_VARS)
LOCAL_JACK_ENABLED := disabled
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_MODULE := svendemo # 編譯出來的jar包就是svendemo.jar
include $(BUILD_STATIC_JAVA_LIBRARY)
最後開始編譯產出svendemo.jar,如此將該jar包給到app開發就可以使用了。
不以jar包方式提供
這種方式是直接將前面jni部分實現的libjnidemo.so提供給app,讓app開發者來實現java接口調用的接口導入。實際上它是將前面實現jar的部分直接放在了app中進行實現,相比前面那種方法省去了jar包。 這種模式集成度高通常內部分工沒有那麼細化或不用提供給第三方很多使用的情況下采用的方法。
首先通過Android studio3.5創建一個工程 - package name:com.runoob.myjniload。
第二步, 創建一個Demo.java類,該類就是實現framework調用jni的導入接口,其目的和前面的jar包一樣。
在main.java.com.example.test目錄下創建Demo.java並編輯如下:
// 這個包名一定要和jni所指定的包一致,否則加載so時將會出現不能找到包的問題
package com.example.test;
import android.util.Log;
public class Demo {
String TAG="AndroidSven Demo";
static {
System.loadLibrary("jnidemo");
}
public native int nativeOpen(); //以native 申明JNI函數
public native String displayString();
public Demo(){
Log.d(TAG,"get from jni = ");
Log.d(TAG,"get from jni = "+nativeOpen()); // 調用jni native api
}
}
是不是和前面的方式一樣呢。
注意:
包名不一致會提示的錯誤:
jni_internal.cc:593] JNI FatalError called: Native registration unable to find class ‘com/example/test/Demo’; aborting…
第三步:需要將前面實現的libjnidemo.so導入到工程中
需要在src目錄中創建一個jniLibs目錄來存放這個so。我的工程目錄結構如下:
以上就是不單獨提供jar包的方式。 接下來就是在該工程繼續實現app調用邏輯即可。
第四步, app調用framework層實現jni調用
編寫MainActivity.java
package com.runoob.myjniload;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import android.util.Log;
import com.example.test.Demo; // 導入Demo.java的包
public class MainActivity extends AppCompatActivity {
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("AndroidSven :", "onCreate() event");
System.out.println(System.getProperty("java.library.path"));
Demo instance = new Demo(); // 創建Demo事例,通過構造函數調用jni native api
tv = (TextView)findViewById(R.id.tv);
// 從jni接口獲取的string顯示到ui文本框中
tv.setText("myJniLoad test native displayString() : " + instance.displayString());
Log.d("AndroidSven :", "after onCreate() event");
}
}
編譯產出myDemo.apk及libjnidemo.so,將它們push到設備,然後運行apk在logcat中將會輸出jni調用信息:
2019-06-07 07:20:21.400 19357-19357/com.runoob.myjniload D/AndroidSven :: onCreate() event
2019-06-07 07:20:21.401 19357-19357/com.runoob.myjniload I/System.out: /system/lib:/vendor/lib
2019-06-07 07:20:21.403 19357-19357/com.runoob.myjniload D/AndroidSven Demo: get from jni =
2019-06-07 07:20:21.403 19357-19357/com.runoob.myjniload E/Service-JNI: JNI test nativeOpen
2019-06-07 07:20:21.404 19357-19357/com.runoob.myjniload D/AndroidSven Demo: get from jni = 10
2019-06-07 07:20:21.404 19357-19357/com.runoob.myjniload E/Service-JNI: JNI test native string
2019-06-07 07:20:21.404 19357-19357/com.runoob.myjniload D/AndroidSven :: after onCreate() event
UI試圖顯示:
APP上層調用framework
前面已經講到如何生成jar包給到app使用。
現在將開始如何導入這個jar包供app開發調用。
首先,通過Android studio3.5創建一個工程
第二步, 導入上面產出的svendemo.jar包放入到工程src\libs目錄下
第三步, 編寫app邏輯MainActivity.java
package com.runoob.myjniload;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import com.example.test.Demo;
public class MainActivity extends AppCompatActivity {
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("AndroidSven :", "onCreate() event");
System.out.println(System.getProperty("java.library.path"));
Demo instance = new Demo();
tv = (TextView)findViewById(R.id.tv);
tv.setText("Demo test native displayString() : " + instance.displayString());
Log.d("AndroidSven :", "after onCreate() event");
}
}
編譯後產出MyDemoJni.apk,放到設備運行
logcat日誌信息:
2019-06-07 07:36:02.247 19427-19427/com.example.mydemojni D/AndroidSven :: onCreate() event
2019-06-07 07:36:02.249 19427-19427/com.example.mydemojni E/Service-JNI: JNI test nativeOpen
2019-06-07 07:36:02.249 19427-19427/com.example.mydemojni D/frameworkSven Demo: get from jni = 10
2019-06-07 07:36:02.250 19427-19427/com.example.mydemojni E/Service-JNI: JNI test native string
2019-06-07 07:36:02.305 19427-19427/com.example.mydemojni D/WindowClient: Add to mViews: DecorView@64
總結
使用jni的流程大體有兩種方式:
一種: 生成jar包給第三方app開發使用,app不用關係jni的接口聲明,只需指定jar提供了哪些接口,這種是應用和接口分離的方法也是主流的方式
二種:不生成jar包,只提供native api文件(so文件)給app開發使用,這樣app開發人員還需要做nativ接口聲明,需要指定jni提供了哪些接口,這種相對於第一種稍微複雜一些