JNI和NDK介紹
JNI(Java Native Interface),是方便Java調(diào)用C、C++等Native代碼所封裝的一層接口,相當(dāng)于一座橋梁。通過JNI可以操作一些Java無法完成的與系統(tǒng)相關(guān)的特性,尤其在圖像和視頻處理中大量用到。
NDK(Native Development Kit)是Google提供的一套工具,其中一個(gè)特性是提供了交叉編譯,即C或者C++不是跨平臺(tái)的,但通過NDK配置生成的動(dòng)態(tài)庫(kù)卻可以兼容各個(gè)平臺(tái)。比如C在Windows平臺(tái)編譯后生成.exe文件,那么源碼通過NDK編譯后可以生成在安卓手機(jī)上運(yùn)行的二進(jìn)制文件.so
在AS中使用ndk-build開發(fā)JNI示例
Android Studio2.2之前對(duì)于JNI開發(fā)的支持不是很好,開發(fā)一般使用Eclipse+插件編寫本地動(dòng)態(tài)庫(kù)。后面Google官方全面增強(qiáng)了對(duì)JNI的支持,包括內(nèi)置NDK。
1.在AS中新建一個(gè)項(xiàng)目
2.聲明一個(gè)native方法
package com.mercury.jnidemo;
public class JNITest {
public native static String getStrFromJNI();
}
3.通過javah命令生成頭文件
在AS的Terminal中,先進(jìn)入要調(diào)用本地代碼的類所在的目錄,也就是在項(xiàng)目中的具體路徑,比如這里是cd app\src\main\java 。然后通過javah命令生成該類的頭文件,注意包名+類名.這里是javah -jni com.mercury.jnidemo.JNITest ,生成頭文件com_mercury_jnidemo_JNITest.h
實(shí)際項(xiàng)目最終可以不包含此頭文件,不熟悉C的語法的開發(fā)人員,借助于該頭文件可以知道JNI的相關(guān)語法:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_mercury_jnidemo_JNITest */
#ifndef _Included_com_mercury_jnidemo_JNITest
#define _Included_com_mercury_jnidemo_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_mercury_jnidemo_JNITest
* Method: getStrFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_mercury_jnidemo_JNITest_getStrFromJNI
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
首先引入jni.h,里面包含了很多宏定義及調(diào)用本地方法的結(jié)構(gòu)體。重點(diǎn)是方法名的格式。這里的JNIEXPORT和JNICALL都是jni.h中所定義的宏。JNIEnv *表示一個(gè)指向JNI環(huán)境的指針,可通過它來訪問JNI提供的接口方法。jclass也是jni.h中定義好的,類型是jobject,實(shí)際上是一個(gè)不確定類型的指針,這里用來接收J(rèn)ava中的this。實(shí)際編寫中一般只要遵循Java_包名_類名_方法名 就好了。
4.實(shí)現(xiàn)JNI方法
像上面的頭文件只是定義了方法,并沒有實(shí)現(xiàn),就像一個(gè)接口一樣。這里就用C寫一個(gè)簡(jiǎn)單的無參的JNI方法。
先創(chuàng)建一個(gè)jni目錄,我直接在src的父目錄下創(chuàng)建的,也可以在其他目錄創(chuàng)建,因?yàn)樽罱K只需要編譯好的動(dòng)態(tài)庫(kù)。在jni目錄下創(chuàng)建Android.mk和demo.c文件。
Android.mk是一個(gè)makefile配置文件,安卓大量采用makefile進(jìn)行自動(dòng)化編譯。LOCAL_MODULE定義的名稱就是編譯好的so庫(kù)名稱,比如這里是jni-demo ,最終生成的動(dòng)態(tài)庫(kù)名稱就叫l(wèi)ibjni-demo.so。 LOCAL_SRC_FILES表示參與編譯的源文件名稱,這里就是demo.c
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jni-demo
LOCAL_SRC_FILES := demo.c
include $(BUILD_SHARED_LIBRARY)
這里的demo.c實(shí)現(xiàn)了一個(gè)很簡(jiǎn)單的方法,返回String類型。
#include<jni.h>
jstring Java_com_mercury_jnidemo_JNITest_getStrFromJNI(JNIEnv *env,jobject thiz){
return (*env)->NewStringUTF(env,"I am Str from jni libs!");
}
這時(shí)候NDK編譯生成的動(dòng)態(tài)庫(kù)會(huì)有四個(gè)CPU平臺(tái):arm64-v8a、armeabi-v7a、x86、x86_64。如果創(chuàng)建Application.mk就可以指定要生成的CPU平臺(tái),語法也很簡(jiǎn)單:
APP_ABI := all
這樣就會(huì)生成各個(gè)CPU平臺(tái)下的動(dòng)態(tài)庫(kù)。
5.使用ndk-build編程生成.so庫(kù)
切回到j(luò)ni目錄的父目錄下,在Terminal中運(yùn)行ndk-build指令,就可以在和jni目錄同級(jí)生成一個(gè)libs文件夾,里面存放相對(duì)應(yīng)的平臺(tái)的.so庫(kù)。同時(shí)生成的還有一個(gè)中間臨時(shí)的obj文件夾,和jni文件夾可以一起刪除。
需要注意,使用NDK一定要先在build.gradle下要配置ndk-build的相關(guān)路徑,這樣在編寫本地代碼時(shí)才會(huì)有相關(guān)的提示功能,并且可以關(guān)聯(lián)到相關(guān)的頭文件:
externalNativeBuild {
ndkBuild {
path 'jni/Android.mk'
}
}
還有一點(diǎn),網(wǎng)上很多資料都在build.gradle中加入以下代碼:
sourceSets{
main{
jniLibs.srcDirs=['libs']
}
}
這樣就指定了目標(biāo).so庫(kù)的存放位置。但在實(shí)際使用中,就算不指定,運(yùn)行時(shí)仍然可以加載正確的.so庫(kù)文件,并且如果添加該代碼后有時(shí)會(huì)報(bào)出以下錯(cuò)誤:
Error:Execution failed for task ':usejava:transformNativeLibsWithMergeJniLibsForDebug'.
> More than one file was found with OS independent path 'lib/x86/libjni-calljava.so'
>
6.加載.so庫(kù)并調(diào)用方法
在類初始化的時(shí)候要加載該.so庫(kù),一般會(huì)寫在靜態(tài)代碼塊里。名稱就是前面的LOCAL_MODULE。
static {
System.loadLibrary("jni-demo");
}
需要注意的是如果是有參的JNI方法,那么直接在參數(shù)列表里補(bǔ)充在jni.h預(yù)先typedef好的數(shù)據(jù)類型就可以了。
JNI調(diào)用Java
不同于JNI調(diào)用C,JNI調(diào)用Java的過程不是單獨(dú)存在的。而是編寫native方法,Java先通過JNI調(diào)用該方法,在方法內(nèi)部再去回調(diào)類中對(duì)應(yīng)的Java方法。步驟有些類似于Java中的反射。這里寫定義三個(gè)點(diǎn)擊事件,三個(gè)Native方法,三種Java的方法類型,根據(jù)相關(guān)的Log判斷是否成功。
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
static {
System.loadLibrary("jni-calljava");
}
public void noParamMethod() {
Log.i(TAG, "無參的Java方法被調(diào)用了");
}
public void paramMethod(int number) {
Log.i(TAG, "有參的Java方法被調(diào)用了" + number + "次");
}
public static void staticMethod() {
Log.i(TAG, "靜態(tài)的Java方法被調(diào)用了");
}
public void click1(View view) {
test1();
}
public void click2(View view) {
test2();
}
public void click3(View view) {
test3();
}
public native void test1();
public native void test2();
public native void test3();
}
1.調(diào)用Java無參方法
- JNI調(diào)用本地方法,根據(jù)類名找到類,注意類名用"/"分隔。
- 找到類后,根據(jù)方法名找到方法。該函數(shù)GetMethodID最后一個(gè)形參是該形參列表的簽名。不同于Java,C中是通過簽名標(biāo)識(shí)去找方法。
- 獲取方法的簽名:首先定位到該類的字節(jié)碼文件所在的父目錄,一般在
module\build\intermediates\classes\debug> ,通過javap -s com.mercury.usejava.MainActivity 獲取整個(gè)類所有的內(nèi)部類型簽名。無參方法test1()的簽名是()V 。
- 通過JNIEnv對(duì)象的CallVoidMethod來完成方法的回調(diào),最后一個(gè)形參是可變參數(shù)。
JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test1
(JNIEnv * env, jobject obj){
//回調(diào)MainActivity中的noParamMethod
jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity");
if (clazz == NULL) {
printf("find class Error");
return;
}
jmethodID id = (*env)->GetMethodID(env, clazz, "noParamMethod", "()V");
if (id == NULL) {
printf("find method Error");
}
(*env)->CallVoidMethod(env, obj, id);
}
2.調(diào)用Java有參方法
類似于無參方法,只是參數(shù)簽名和可變參數(shù)的不同
3.調(diào)用Java靜態(tài)方法
注意獲取方法名的方法是GetStaticMethodID ,調(diào)用方法的函數(shù)名是CallStaticVoidMethod ,并且由于是靜態(tài)方法,不應(yīng)該傳入jobject參數(shù),而直接是jclass.
JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test3
(JNIEnv * env, jobject obj){
jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity");
if (clazz == NULL) {
printf("find class Error");
return;
}
jmethodID id = (*env)->GetStaticMethodID(env, clazz, "staticMethod", "()V");
if (id == NULL) {
printf("find method Error");
}
(*env)->CallStaticVoidMethod(env, clazz, id);
}
相應(yīng)日志
使用CMake開發(fā)JNI
CMake是一個(gè)跨平臺(tái)的安裝(編譯)工具,通過編寫CMakeLists.txt,可以生成對(duì)應(yīng)的makefile或project文件,再調(diào)用底層的編譯。AS 2.2之后工具中增加了對(duì)CMake的支持,官方也推薦用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去構(gòu)建JNI項(xiàng)目.
1.創(chuàng)建使用CMake構(gòu)建的項(xiàng)目
開始前AS要先在SDK Manager中安裝SDK Tools->CMake
只要勾選Include C++ Support 。其中會(huì)提示配置C++支持的功能.
一般默認(rèn)就可以了,各個(gè)選項(xiàng)的具體含義:
- C++ Standard:指定編譯庫(kù)的環(huán)境。
- Exception Support:當(dāng)前項(xiàng)目支持C++異常處理
- Runtime Type Information Support:除異常處理外,還支持動(dòng)態(tài)轉(zhuǎn)類型(dynamic casting) 、模塊集成、以及對(duì)象I/O
2.工程的目錄結(jié)構(gòu)
創(chuàng)建好的工程主Module下直接就有.externalNativeBuild,多出一個(gè)CMakeLists.txt,相當(dāng)于以前的配置文件。并且在src/main目錄下多了一個(gè)cpp文件夾,里面存放的是C++文件,相當(dāng)于以前的jni文件夾。這個(gè)是工程創(chuàng)建后AS生成的示例JNI方法,返回了一個(gè)字符串。后面開發(fā)JNI就可以按照這個(gè)目錄結(jié)構(gòu)。
相應(yīng)的,build.gradle下也增加了一些配置。
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-std=c++14 -frtti -fexceptions"
}
}
}
buildTypes {
...
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
defaultConfig中的externalNativeBuild各項(xiàng)屬性和前面創(chuàng)建項(xiàng)目時(shí)的選項(xiàng)配置有關(guān),外部的externalNativeBuild則定義了CMakeLists.txt的存放路徑。
如果只是在自己的項(xiàng)目中使用,CMake的方式在打包APK的時(shí)候會(huì)自動(dòng)將cpp文件編譯成so文件拷貝進(jìn)去。如果要提供給外部使用時(shí),Make Project ,之后在libs 目錄下就可以看到生成的對(duì)應(yīng)配置的相關(guān)CPU平臺(tái)的.so文件。
CMakeLists.txt
CMakeLists.txt可以自定義命令、查找文件、頭文件包含、設(shè)置變量,具體可見 官方文檔。項(xiàng)目默認(rèn)生成的CMakeLists.txt核心內(nèi)容如下:
# 編譯本地庫(kù)時(shí)我們需要的最小的cmake版本
cmake_minimum_required(VERSION 3.4.1)
# 相當(dāng)于Android.mk
add_library( # Sets the name of the library.設(shè)置編譯生成本地庫(kù)的名字
native-lib
# Sets the library as a shared library.庫(kù)的類型
SHARED
# Provides a relative path to your source file(s).編譯文件的路徑
src/main/cpp/native-lib.cpp )
# 添加一些我們?cè)诰幾g我們的本地庫(kù)的時(shí)候需要依賴的一些庫(kù),這里是用來打log的庫(kù)
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 )
# 關(guān)聯(lián)自己生成的庫(kù)和一些第三方庫(kù)或者系統(tǒng)庫(kù)
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
使用CMakeLists.txt同樣可以指定so庫(kù)的輸出路徑,但一定要在add_library之前設(shè)置,否則不會(huì)生效:
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}) #指定路徑
#生成的so庫(kù)在和CMakeLists.txt同級(jí)目錄下的libs文件夾下
如果想要配置so庫(kù)的目標(biāo)CPU平臺(tái),可以在build.gradle中設(shè)置
android {
...
defaultConfig {
...
ndk{
abiFilters "x86","armeabi","armeabi-v7a"
}
}
...
}
需要注意的是,如果是多次使用add_library,則會(huì)生成多個(gè)so庫(kù)。如果想將多個(gè)本地文件編譯到一個(gè)so庫(kù)中,只要最后一個(gè)參數(shù)添加多個(gè)C/C++文件的相對(duì)路徑就可以
用C語言實(shí)現(xiàn)字符串加密
Java中實(shí)現(xiàn)字符串加密的一種比較簡(jiǎn)單的方法是異或,將字符串轉(zhuǎn)換為字符數(shù)組,遍歷對(duì)其中的每個(gè)字符用密鑰(可以是字符)進(jìn)行一次異或運(yùn)算,生成新的字符串。如果用JNI和C實(shí)現(xiàn),大致步驟如下(jstring是要加密的字符串):
1 獲取jstring的長(zhǎng)度
2 動(dòng)態(tài)開辟一個(gè)跟data長(zhǎng)度一樣的char*
3 將 jstring類型轉(zhuǎn)換為char數(shù)組(用char*接收)
4 遍歷char數(shù)組,進(jìn)行異或運(yùn)算
5 將char*轉(zhuǎn)換為jstring類型返回
6 釋放動(dòng)態(tài)開辟的堆內(nèi)存空間
效果圖
我是用的是5.0的模擬器,有時(shí)會(huì)閃退,查看系統(tǒng)日志,會(huì)報(bào)出一下錯(cuò)誤:
JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8
網(wǎng)上查了一下,JNI在調(diào)用NewStringUTF方法時(shí),遇到不認(rèn)識(shí)的字符就會(huì)退出,因?yàn)樘摂M機(jī)dalvik/vm/CheckJni.cpp里面的checkUTFString會(huì)對(duì)字符類型進(jìn)行檢查。替代方案是在開始轉(zhuǎn)換前,先檢查char*中是否含有非UTF-8字符,有的話返回空字符串。完整代碼如下:
#include<jni.h>
#include <stdlib.h>
jboolean checkUtfBytes(const char* bytes, const char** errorKind) ;
jstring Java_com_mercury_cmakedemo_MainActivity_encryptStr
(JNIEnv *env, jobject object, jstring data){
if(data==NULL){
return (*env)->NewStringUTF(env, "");
}
jsize len = (*env)->GetStringLength(env, data);
char *buffer = (char *) malloc(len * sizeof(char));
(*env)->GetStringUTFRegion(env, data, 0, len, buffer);
int i=0;
for (; i <len ; i++) {
buffer[i] = (char) (buffer[i] ^ 2);
}
const char *errorKind = NULL;
checkUtfBytes(buffer, &errorKind);
free(buffer);
if (errorKind == NULL) {
return (*env)->NewStringUTF(env, buffer);
} else {
return (*env)->NewStringUTF(env, "");
}
}
//把char*和errorKind傳入,如果errorKind不為NULL說明含有非utf-8字符,做相應(yīng)處理
jboolean checkUtfBytes(const char* bytes, const char** errorKind) {
while (*bytes != '\0') {
jboolean utf8 = *(bytes++);
// Switch on the high four bits.
switch (utf8 >> 4) {
case 0x00:
case 0x01:
case 0x02:
case 0x03:
case 0x04:
case 0x05:
case 0x06:
case 0x07:
// Bit pattern 0xxx. No need for any extra bytes.
break;
case 0x08:
case 0x09:
case 0x0a:
case 0x0b:
case 0x0f:
/*
* Bit pattern 10xx or 1111, which are illegal start bytes.
* Note: 1111 is valid for normal UTF-8, but not the
* modified UTF-8 used here.
*/
*errorKind = "start";
return utf8;
case 0x0e:
// Bit pattern 1110, so there are two additional bytes.
utf8 = *(bytes++);
if ((utf8 & 0xc0) != 0x80) {
*errorKind = "continuation";
return utf8;
}
// Fall through to take care of the final byte.
case 0x0c:
case 0x0d:
// Bit pattern 110x, so there is one additional byte.
utf8 = *(bytes++);
if ((utf8 & 0xc0) != 0x80) {
*errorKind = "continuation";
return utf8;
}
break;
}
}
return 0;
}
|