Android 之 JNI 開發 詳解 - NDK從入門到精通

NDK項目源碼地址 : html

-- 第一個JNI示例程序下載 :  GitHub - https://github.com/han1202012/NDKHelloworld.git java

 

 

一. JNI介紹

 

1. JNI引入

 

JNI概念 : Java本地接口, Java Native Interface, 它是一個 協議, 該協議用來溝通Java代碼和外部的本地C/C++代碼, 經過該協議 Java代碼能夠調用外部的本地代碼, 外部的C/C++ 代碼能夠調用Java代碼;linux

 

C和Java的側重 : android

-- C語言 : C語言中最重要的是 函數 function; git

-- Java語言 : Java中最重要的是 JVM, class類, 以及class中的方法;github

 

C與Java如何交流 : 算法

-- JNI規範 : C語言與Java語言交流須要一個適配器, 中間件, 即 JNI, JNI提供了一種規範; windows

-- C語言中調用Java方法 : 可讓咱們在C代碼中找到Java代碼class中的方法, 而且調用該方法; api

-- Java語言中調用C語言方法 : 同時也能夠在Java代碼中, 將一個C語言的方法映射到Java的某個方法上; 數組

-- JNI橋樑做用 : JNI提供了一個橋樑, 打通了C語言和Java語言之間的障礙;

 

2. Android中的應用程序框架

 

正常狀況下的Android框架 : 最 頂層是 Android的應用程序代碼, 是純Java代碼, 中間有一層的 Framework框架層代碼是 C/C++代碼, 經過Framework進行系統調用, 調用底層的庫 和 linux 內核;

 

 

使用JNI時的Android框架 : 繞過Framework提供的調用底層的代碼, 直接調用本身寫的C代碼, 該代碼最終會編譯成爲一個庫, 這個庫經過JNI提供的一個Stable的ABI 調用linux kernel;ABI是二進制程序接口 application binary interface.

 

 

3. JNI做用

 

JNI做用 : 

-- 擴展: JNI擴展了JVM能力, 驅動開發, 例如開發一個wifi驅動, 能夠將手機設置爲無限路由;

-- 高效 : 本地代碼效率高, 遊戲渲染, 音頻視頻處理等方面使用JNI調用本地代碼, C語言能夠靈活操做內存;

-- 複用 : 在文件壓縮算法 7zip開源代碼庫, 機器視覺 openCV開放算法庫 等方面能夠複用C平臺上的代碼, 沒必要在開發一套完整的Java體系, 避免重複發明輪子;

-- 特殊 : 產品的核心技術通常也採用JNI開發, 不易破解;

 

Java語言執行流程 : 

-- 編譯字節碼 : Java編譯器編譯 .java源文件, 得到.class 字節碼文件;

-- 裝載類庫 : 使用類裝載器裝載平臺上的Java類庫, 並進行字節碼驗證;

-- Java虛擬機 : 將字節碼加入到JVM中, Java解釋器 和 即時編譯器 同時處理字節碼文件, 將處理後的結果放入運行時系統;

-- 調用JVM所在平臺類庫 : JVM處理字節碼後, 轉換成相應平臺的操做, 調用本平臺底層類庫進行相關處理;

 

 

Java一次編譯處處執行 : JVM在不一樣的操做系統都有實現, Java能夠一次編譯處處運行, 字節碼文件一旦編譯好了, 能夠放在任何平臺的虛擬機上運行;

.

 

二. NDK詳解

 

1. 交叉編譯庫文件

 

C代碼執行 : C代碼被編譯成庫文件以後, 才能執行, 庫文件分爲 動態庫 和 靜態庫 兩種;

-- 動態庫 : unix環境下 .so 後綴的是動態庫, windows環境下 .dll 後綴的是動態庫; 動態庫能夠依賴靜態庫加載一些可執行的C代碼;

-- 靜態庫 : .a 後綴是靜態庫的擴展名;

 

庫文件來源 : C代碼 進行 編譯 連接操做以後, 纔會生成庫文件, 不一樣類型的CPU 操做系統 生成的庫文件是不同;

-- CPU分類 : arm結構, 嵌入式設備處理器; x86結構, pc 服務器處理器; 不一樣的CPU指令集不一樣;

-- 交叉編譯 : windows x86編譯出來的庫文件能夠在arm平臺運行的代碼;

-- 交叉編譯工具鏈 : Google提供的 NDK 就是交叉編譯工具鏈, 能夠在linux環境下編譯出在arn平臺下執行的二進制庫文件;

 

NDK做用 : 是Google提供了交叉編譯工具鏈, 可以在 linux平臺編譯出在 arm平臺下執行的二進制庫文件;

 

NDK版本介紹 : android-ndk-windows 是在windows系統中的cygwin使用的, android-ndk-linux 是在linux下使用的;

 

2. 部署NDK開發環境

 

(1) 下載Cygwin安裝器

 

下載地址 : http://cygwin.com/setup-x86.exe , 這是下載器, 可使用該下載器在線安裝, 也能夠將cygwin下載到本地以後, 在進行安裝;

 

安裝器使用 : Cygwin的下載, 在線安裝, 卸載 等操做都有由該安裝器進行;

-- 本地文件安裝 : 選擇安裝文件所在的目錄, 而後選擇所要安裝的安裝包;

-- 在線安裝 : 選擇在線安裝便可, 而後選擇須要的安裝包;

-- 卸載 : windows上使用其它軟件例如360, 控制面板中是沒法卸載Cygwin的, 只能經過安裝器來卸載;

 

(2) 安裝Cygin

 

雙擊安裝器 setup-x86.exe 下一步 : 

 

 

選擇安裝方式 : 

-- 在線安裝 : 直接下載, 而後安裝;

-- 下載安裝文件 : 將安裝文件下載下來, 能夠隨時安裝, 注意安裝文件也須要安裝器來進行安裝;

-- 從本地文件安裝 : 即便用下載的安裝文件進行安裝;

 

 

選擇Cygwin安裝位置 : 

 

 

選擇下載好安裝文件位置 : 以前我下了一個徹底版的Cygwin, 包括了全部的Cygwin組件, 所有加起來有5.23G, 下載速度很快, 使用網易的鏡像, 基本能夠全速下載;

 

 

選擇須要安裝Cygwin組件 : 這裏咱們只須要如下組件 : binutils , gcc , gcc-mingw , gdb , make , 不用下所有的組件;

 

 

以後點擊下一步等待完成安裝便可;

.

安裝完以後, 打開bash命令窗口, 能夠設置下顯示的字體, 使用 make -version 查看是否安裝成功 : 

 

(3) Cygwin目錄介紹

 

如下是Cygwin安裝目錄的狀況 : 該安裝目錄就是所模擬的 linux 的根目錄;

 

對應的linux目錄 : 這兩個目錄進行對比發現, 兩個目錄是同樣的, Cygwin的安裝目錄就是 linux根目錄;

 

cygdrive目錄 : 該目錄是Cygwin模擬出來的windows目錄結構, 進入該目錄後, 會發現windows的盤符目錄, 經過該目錄能夠 訪問windows中的文件;

 

 

 

(4) 下載NDK工具 

 

從Google的Android開發者官網上下載該工具, 注意NDK工具分類 : 下載地址 - http://developer.android.com/tools/sdk/ndk/index.html -;

-- windows版本NDKandroid-ndk-r9c-windows-x86.zip (32位), android-ndk-r9c-windows-x86_64.zip (64位) 該版本是用在windows上的Cygwin下, 不能直接在windows上直接運行;

-- linux版本NDK : android-ndk-r9c-linux-x86.tar.bz2(32位) , android-ndk-r9c-linux-x86_64.tar.bz2 (64位) , 該版本直接在linux下執行便可;

 

在這裏下載windows版本的NDK, 運行在Cygwin上;

 

 

(4) NDK環境介紹

 

NDK工具的文件結構 : 

 

 

ndk-build腳本 : NDK build 腳本是 gun-make 的簡單封裝, gun-make 是編譯C語言代碼的工具, 該腳本執行的前提是linux環境下必須安裝 make 程序;

 

 

NDK安裝在Cygwin中 : 將NDK壓縮文件拷貝到Cygwin的根目錄中, 解壓 : android-ndk-r9c 目錄就是NDK目錄;

執行如下NDK目錄下的 ndk-build 命令 : ./ndk-build ;

執行結果 :

 

Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
/android-ndk-r9c/build/core/build-local.mk:148: *** Android NDK: Aborting    。 中止。

 

 

 

 

三. 開發第一個NDK程序

 

1. 開發NDK程序流程

 

a. 建立Android工程

首選建立一個Android工程, 在這個工程中進行JNI開發;

 

b. 聲明native方法 : 

注意方法名使用 native 修飾, 沒有方法體 和 參數, eg : public native String helloFromJNI();

 

c. 建立C文件 : 

在工程根目錄下建立 jni 目錄, 而後建立一個c語言源文件, 在文件中引入 include <jni.h> , C語言方法聲明格式 jstring Java_shuliang.han.ndkhelloworld_MainActivity_helloFromJNI(JNIEnv *env) , jstring 是 Java語言中的String類型, 方法名格式爲 : Java_完整包名類名_方法名();

-- JNIEnv參數 : 表明的是Java環境, 經過這個環境能夠調用Java裏面的方法;

-- jobject參數 : 調用C語言方法的對象, thiz對象表示當前的對象, 即調用JNI方法所在的類;

 

d. 編寫Android.mk文件 : 

如何寫 查看文檔, NDK根目錄下有一個 documentation.html 文檔, 點擊該html文件就能夠查看文檔, 查看 Android.mk File 文檔, 下面是該文檔給出的 Android.mk示例 : 

 

 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c

include $(BUILD_SHARED_LIBRARY)

 

 

--  LOCAL_PATH : 表明mk文件所在的目錄;

--  include $(CLEAR_VARS) : 編譯工具函數, 經過該函數能夠進行一些初始化操做;

--  LOCAL_MODULE : 編譯後的 .so 後綴文件叫什麼名字;

--  LOCAL_SRC_FILES: 指定編譯的源文件名稱;

--  include $(BUILD_SHARED_LIBRARY) : 告訴編譯器須要生成動態庫;

 

e. NDK編譯生成動態庫 : 

進入 cygdrive 找到windows目錄下對應的文件, 編譯完成以後, 會 自動生成so文件並放在libs目錄下, 以後就能夠在Java中調用C語言方法了;

 

f. Java中加載動態庫 : 

在Java類中的靜態代碼塊中使用System.LoadLibrary()方法加載編譯好的 .so 動態庫;

 

 

NDK平臺版本 : NDK腳本隨着 android-sdk 版本不一樣, 執行的腳本也是不一樣的, 不一樣平臺會引用不一樣的頭文件, 編譯的時候必定注意 sdk 與 ndk 版本要一致;

so文件在內存中位置 : apk文件安裝到手機上以後, .so動態庫文件存在在 data/安裝目錄/libs 目錄下;

 

2. 開發實例

 

 

按照上面的步驟進行開發

 

 

(1) 建立Android工程

 

Android工程版本 : 建立一個Android工程, minSdk 爲 7 即 android-2.1, 編譯使用的 sdk爲 10 即 android-2.3.3 ;

<uses-sdk
        android:minSdkVersion="7"
        android:targetSdkVersion="10" />


 

NDK編譯原則 : 編譯NDK動態庫是 按照最小版本進行編譯, 選擇編譯的平臺的時候, 會選擇 NDK 7 平臺進行編譯;

 

        

 

(2) 聲明native方法

 

聲明native方法, 注意該方法沒有方法體 和 參數, 以下 :

 

/*
	 * 聲明一個native方法
	 * 這個方法在Java中是沒有實現的, 沒有方法體
	 * 該方法須要使用C語言編寫
	 */
	public native String helloFromJNI();


 

(3) 建立C文件

 

引入頭文件: 首先要包含頭文件 jni.h, 該 頭文件位置定義在 android-ndk-r9c\platforms\android-5\arch-arm\usr\include目錄下的 jni.h, 下面是該頭文件中定義的一些方法, 包括本項目中使用的 NewString 方法;

jstring     (*NewString)(JNIEnv*, const jchar*, jsize);
jsize       (*GetStringLength)(JNIEnv*, jstring);
const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);
void        (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);
jstring     (*NewStringUTF)(JNIEnv*, const char*);
jsize       (*GetStringUTFLength)(JNIEnv*, jstring);


 

 

調用Java類型 : C中調用Java中的String類型爲 jstring;

 

C語言方法名規則 : Java_完整包名類名_方法名(JNIEnv *env, jobject thiz), 注意完整的類名包名中包名的點要用 _ 代替;

 

參數介紹 : C語言方法中有兩個重要的參數, JNIEnv *env, jobject thiz ;

--  JNIEnv參數 : 該參數表明Java環境, 經過這個環境能夠調用Java中的方法;

--  jobject參數 : 該參數表明調用jni方法的類, 在這裏就是MainActivity;

 

調用jni.h中的NewStringUTF方法 : 該方法的做用是在C語言中建立一個Java語言中的String類型對象, jni.h中是這樣定義的 jstring (*NewStringUTF)(JNIEnv*, const char*), JNIEnv 結構體中包含了 NewStringUTF 函數指針, 經過 JNIEnv 就能夠調用這個方法;

 

C語言文件源碼 : 

#include <jni.h>

/*
 * 方法名稱規定 : Java_完整包名類名_方法名()
 * JNIEnv 指針
 *
 * 參數介紹 :
 * env : 表明Java環境, 經過這個環境能夠調用Java中的方法
 * thiz : 表明調用JNI方法的對象, 即MainActivity對象
 */
jstring Java_shuliang_han_ndkhelloworld_MainActivity_helloFromJNI(JNIEnv *env, jobject thiz)
{
	/*
	 * 調用 android-ndk-r9c\platforms\android-8\arch-arm\usr\include 中jni.h中的方法
	 * jni.h 中定義的方法  jstring (*NewStringUTF)(JNIEnv*, const char*); 
	 */
	return (*env)->NewStringUTF(env, "hello world jni");
}

 

 

(4) 編寫Android.mk文件

 

 

查詢NDK文檔 : NDK的文檔在NDK工具根目錄下, 點擊 documentation.html 文件, 就能夠在瀏覽器中打開NDK文檔;

 

上面的開發流程中詳細的介紹了Android.mk 五個參數的詳細用處, 這裏直接給出源碼 : 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello
LOCAL_SRC_FILES := hello.c

include $(BUILD_SHARED_LIBRARY)



 

(5) 編譯NDK動態庫

 

 

進入Cygwin相應目錄 : 從Cygwin中的cygdrive 中進入windows的工程jni目錄 ;


 

編譯hello.c文件 : 注意Android.mk文件 與 hello.c 文件在同一目錄中;

 

編譯完成後的狀況 : 編譯完以後 會成成一個obj文件, 在obj文件中會生成 libhello.so, 系統會自動將該 so後綴文件放在libs目錄下;

 

 

 

(6) Java中加載動態庫

 

靜態代碼塊中加載 : Java中在靜態代碼塊中加載庫文件, 調用 System.loadLibrary("hello") 方法, 注意 libs中的庫文件名稱爲 libhello.so咱們加載的時候 將 lib 去掉, 只取hello 做爲動態庫名稱, 這是規定的;

//靜態代碼塊加載C語言庫文件
	static{
		System.loadLibrary("hello");
	}

 

 

(7) 其它源碼

 

MainActivity源碼 : 

package shuliang.han.ndkhelloworld;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends Activity {

	//靜態代碼塊加載C語言庫文件
	static{
		System.loadLibrary("hello");
	}
	
	/*
	 * 聲明一個native方法
	 * 這個方法在Java中是沒有實現的, 沒有方法體
	 * 該方法須要使用C語言編寫
	 */
	public native String helloFromJNI();
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        System.out.println(helloFromJNI());
    }

    public void onClick(View view) {
    	//點擊按鈕顯示從jni調用獲得的字符串信息
    	Toast.makeText(getApplicationContext(), helloFromJNI(), 1).show();
	}
    
}


XML佈局文件 : 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/bt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="顯示JNI返回的字符串" />

</RelativeLayout>


 

(8) 將源碼上傳到GitHub中

 

 

在上一篇博客  http://blog.csdn.net/shulianghan/article/details/18812279 中對GitHub用法進行了詳解;

 

在GitHub上建立工程 : 

 

項目地址 

-- HTTP: https://github.com/han1202012/NDKHelloworld.git 

-- SSH : git@github.com:han1202012/NDKHelloworld.git

 

生成的命令 :  

touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:han1202012/NDKHelloworld.git
git push -u origin master


打開 Git Bash 命令行窗口 : 

-- 從GitHub上克隆項目到本地 : git clone git@github.com:han1202012/NDKHelloworld.git , 注意克隆的時候直接在倉庫根目錄便可, 不用再建立項目根目錄 ;

 


-- 添加文件 : git add ./* , 將目錄中全部文件添加;

 
-- 查看狀態 : git status ;

-- 提交緩存 : git commit -m '提交';

 
-- 提交到遠程GitHub倉庫 : git push -u origin master ;

 

 

GitHub項目 : 

 

 

3. 項目講解 

 

(1) Android.mk文件講解

 

Android.mk文件內容 : 

 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello
LOCAL_SRC_FILES := hello.c

include $(BUILD_SHARED_LIBRARY)

 

獲取當前文件內容 : $(call my-dir) 是編譯器中的宏方法, 調用該宏方法, 就會 返回前的目錄路徑

賦值符號 : " := " 是 賦值符號, 第一句話 是 返回當前文件所在的當前目錄, 並將這個目錄路徑賦值給 LOCAL_PATH;

初始化編譯模塊參數 : $(CLEAR_VARS) 做用是將編譯模塊的參數初始化, LOCAL_MODULE LOCAL_SRC_FILES 也是這樣的參數;

指定編譯模塊 : LOCAL_MODULE    := hello , 指定編譯後的 so 文件名稱, 編譯好以後系統會在該名稱前面加上 "lib", 後綴加上 ".so";

指定編譯源文件 : LOCAL_SRC_FILES := hello.c 告訴編譯系統源文件, 若是有多個文件那麼就依次寫在後面便可; 

編譯成靜態庫 : include $(BUILD_SHARED_LIBRARY), 做用是高速系統, 編譯的結果編譯成 .so 後綴的靜態庫;

 

靜態庫引入 : NDK的platform中有不少 ".a" 結尾的動態庫, 咱們編譯動態庫的時候, 能夠將一些靜態庫引入進來;

 

 

(2) 自動生成方法簽名

 

 

使用javah工具 : 在C中實現Java調用的jni方法, 方法的簽名很複雜, 須要將完整的包名類名方法名都要使用 "_" 鏈接起來, 很麻煩, jdk提供的生成簽名方法的工具;

 

遺留問題 : 目前查到的方法是 在bin目錄下 執行 javah -jni 包名類名 命令, 可是執行不成功, 暫時沒找到解決方案;

-- Android中會自動生成 .class文件嗎, 沒發現啊, PY人!

 

 

 

問題解決 : 在bin目錄下有一個classes目錄, 該目錄在eclipse中看不到, 可是實際沒目錄中是存在的; 

 

 

 

(3) NDK開發中亂碼問題

 

解決亂碼思路 : C語言編譯的時候用的是 ISO-8859-1 碼錶進行編碼, 若是咱們使用C語言jni開發, 須要進行轉碼操做;

-- 將ISO-8859-1轉爲UTF-8字符: String string = new String(str.getBytes("iso8859-1"), "UTF-8");

 

 

示例 : 

 

添加中文jni調用 : 將jni中的hello.c 中返回的字符串修改成中文, 從新編譯 .so 靜態庫文件;

-- 修改後的hello.c文件以下 : 只改變了返回的字符串, 添加了中文;

#include <jni.h>

/*
 * 方法名稱規定 : Java_完整包名類名_方法名()
 * JNIEnv 指針
 *
 * 參數介紹 :
 * env : 表明Java環境, 經過這個環境能夠調用Java中的方法
 * thiz : 表明調用JNI方法的對象, 即MainActivity對象
 */
jstring Java_shuliang_han_ndkhelloworld_MainActivity_helloFromJNI(JNIEnv *env, jobject thiz)
{
	/*
	 * 調用 android-ndk-r9c\platforms\android-8\arch-arm\usr\include 中jni.h中的方法
	 * jni.h 中定義的方法  jstring (*NewStringUTF)(JNIEnv*, const char*);
	 */
	return (*env)->NewStringUTF(env, "hello world jni 中文");
}


使用NDK從新編譯hello.c文件 : 修改了C源碼以後, 從新將該c文件編譯成so文件;

-- 編譯過程: 打開cygwin, 進入cygdrive/ 下對應windows中源碼項目中的jni目錄, 執行 /android-ndk-r9c/ndk-build 命令;

 

 

運行Android代碼報錯 : 由於jni中c文件有中文, 中文不能被識別;

01-31 14:36:04.803: W/dalvikvm(389): JNI WARNING: illegal continuation byte 0xd0
01-31 14:36:04.803: W/dalvikvm(389):              string: 'hello world jni ����'
01-31 14:36:04.803: W/dalvikvm(389):              in Lshuliang/han/ndkhelloworld/MainActivity;.helloFromJNI ()Ljava/lang/String; (NewStringUTF)
01-31 14:36:04.834: I/dalvikvm(389): "main" prio=5 tid=1 NATIVE
01-31 14:36:04.834: I/dalvikvm(389):   | group="main" sCount=0 dsCount=0 obj=0x4001f1a8 self=0xce48
01-31 14:36:04.834: I/dalvikvm(389):   | sysTid=389 nice=0 sched=0/0 cgrp=default handle=-1345006528
01-31 14:36:04.844: I/dalvikvm(389):   | schedstat=( 257006717 305462830 51 )
01-31 14:36:04.844: I/dalvikvm(389):   at shuliang.han.ndkhelloworld.MainActivity.helloFromJNI(Native Method)
01-31 14:36:04.844: I/dalvikvm(389):   at shuliang.han.ndkhelloworld.MainActivity.onCreate(MainActivity.java:26)
01-31 14:36:04.844: I/dalvikvm(389):   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
01-31 14:36:04.853: I/dalvikvm(389):   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1611)
01-31 14:36:04.853: I/dalvikvm(389):   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1663)
01-31 14:36:04.853: I/dalvikvm(389):   at android.app.ActivityThread.access$1500(ActivityThread.java:117)
01-31 14:36:04.864: I/dalvikvm(389):   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:931)
01-31 14:36:04.864: I/dalvikvm(389):   at android.os.Handler.dispatchMessage(Handler.java:99)
01-31 14:36:04.864: I/dalvikvm(389):   at android.os.Looper.loop(Looper.java:123)
01-31 14:36:04.864: I/dalvikvm(389):   at android.app.ActivityThread.main(ActivityThread.java:3683)
01-31 14:36:04.864: I/dalvikvm(389):   at java.lang.reflect.Method.invokeNative(Native Method)
01-31 14:36:04.874: I/dalvikvm(389):   at java.lang.reflect.Method.invoke(Method.java:507)
01-31 14:36:04.874: I/dalvikvm(389):   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
01-31 14:36:04.874: I/dalvikvm(389):   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
01-31 14:36:04.874: I/dalvikvm(389):   at dalvik.system.NativeStart.main(Native Method)
01-31 14:36:04.884: E/dalvikvm(389): VM aborting

 

 

 

四. Java傳遞數據給C語言與日誌打印

 

 

1. JNI數據類型

 

 

Java數據類型 C數據類型 JNI數據類型對比 : 32位 與 64位機器可能會有出入;

 

Java數據類型 C本地類型 JNI定義別名
int long jint/jsize
long __int64 jlong
byte signed char jbyte
boolean unsigned char jboolean
char unsigned short jchar
short short jshort
float float jfloat
double doyble jdouble
object'  _jobject jobject

 

數據類型表示方法 : int數組類型 jintArray , boolean數組 jbooleanArray ...

 

頭文件定義類型 : 這些基本的數據類型在 jni.h 中都有相應的定義 : 

jobject     (*CallObjectMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jboolean    (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);
    jboolean    (*CallBooleanMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jboolean    (*CallBooleanMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jbyte       (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...);
    jbyte       (*CallByteMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jbyte       (*CallByteMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jchar       (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);
    jchar       (*CallCharMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jchar       (*CallCharMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jshort      (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...);
    jshort      (*CallShortMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jshort      (*CallShortMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
    jint        (*CallIntMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jint        (*CallIntMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jlong       (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...);
    jlong       (*CallLongMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jbooleanArray (*NewBooleanArray)(JNIEnv*, jsize);
    jbyteArray    (*NewByteArray)(JNIEnv*, jsize);
    jcharArray    (*NewCharArray)(JNIEnv*, jsize);
    jshortArray   (*NewShortArray)(JNIEnv*, jsize);
    jintArray     (*NewIntArray)(JNIEnv*, jsize);
    jlongArray    (*NewLongArray)(JNIEnv*, jsize);
    jfloatArray   (*NewFloatArray)(JNIEnv*, jsize);
    jdoubleArray  (*NewDoubleArray)(JNIEnv*, jsize);


 

2. JNI在Java和C語言之間傳遞int類型

 

 

Java中定義的方法 : 

//將Java中的兩個int值 傳給C語言, 進行相加後, 返回java語言 shuliang.han.ndkparameterpassing.DataProvider
	public native int add(int x, int y);


C語言中定義的方法 : 

#include <jni.h>

//方法簽名, Java環境 和 調用native方法的類 必不可少, 後面的參數就是native方法的參數
jint Java_shuliang_han_ndkparameterpassing_DataProvider_add(JNIEnv * env, jobject obj, jint x, jint y)
{
	return x + y;
}


使用NDK工具變異該c類庫 : 

在cygwin中進入cygdrive, 而後 進入windows中相應的目錄, 執行  /android-ndk-r9c/ndk-build 命令, 便可完成編譯;

 

 

3. NDK中C代碼使用LogCat

 

 

(1) 引入頭文件

 

NDK中斷點調試 : 斷點調試在NDK中實現極其困難, 所以在這裏咱們通常都是打印日誌;

 

引入頭文件 : 在C代碼中引入下面的頭文件;

#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)


頭文件介紹 : log.h 是關於調用 LogCat日誌文件;

-- log.h頭文件路徑 : android-ndk-r9c\platforms\android-9\arch-arm\usr\include\android\log.h;

-- 主要方法 :  __android_log_write, 下面有該方法的解析, 傳入參數 日誌等級 日誌標籤 日誌內容;

-- 宏定義 : __android_log_write 方法太麻煩, 這裏作出一個映射, LOGD(...) 輸出debug級別的日誌, LOGI(...) 輸出Info級別的日誌;

-- LogCat日誌級別 : verbose < debug < info < warn < error < assert;

 

使用到的log.h文件內容解析 : __android_log_write 方法中的日誌等級參數就使用 枚舉中的內容 

/*
 * Android log priority values, in ascending priority order. 日誌等級
 */
typedef enum android_LogPriority {
    ANDROID_LOG_UNKNOWN = 0,
    ANDROID_LOG_DEFAULT,    /* only for SetMinPriority() */
    ANDROID_LOG_VERBOSE,
    ANDROID_LOG_DEBUG,
    ANDROID_LOG_INFO,
    ANDROID_LOG_WARN,
    ANDROID_LOG_ERROR,
    ANDROID_LOG_FATAL,
    ANDROID_LOG_SILENT,     /* only for SetMinPriority(); must be last */
} android_LogPriority;

/*
 * Send a simple string to the log. 向LogCat中輸出日誌 
	參數介紹: 日誌優先級 , 日誌標籤 , 日誌內容
 */
int __android_log_write(int prio, const char *tag, const char *text);


C語言中輸入輸出函數佔位符介紹 : 

佔位符 數據類型
%d int
%ld long int
%c char
%f float
&lf double
%x 十六進制
%O 八進制
%s 字符串

 

.

.

 

(2) Android.mk增長liblog.so動態庫

 

在該make配置文件中, 增長一行 : LOCAL_LDLIBS += -llog , 該語句添加在 LOCAL_SRC_FILES 語句下面一行;

 

完整的Android.mk文件 : 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := DataProvider
LOCAL_SRC_FILES := DataProvider.c
#增長log函數對應的函數庫 liblog.so  libthread_db.a
LOCAL_LDLIBS += -llog -lthread_db

include $(BUILD_SHARED_LIBRARY)


函數庫位置 : android-ndk-r9c\platforms\android-9\arch-arm\usr\lib;

函數庫截圖 : 從該目錄下的 liglog.so能夠看出, 存在該庫;

 

引入函數庫方法 : 使用 LOCAL_LDLIBS += -l函數庫名, 注意 函數庫名不帶 lib前綴 和 .so 後綴, 同時能夠添加多個庫, 使用 -l庫1 -l庫2 -庫3 ;

 

 

(3) 編譯執行

 

根據(1) 中的佔位符, 編寫打印日誌代碼

//Java中的int對應的是C語言中的long類型, 對應JNI中的jint類型, C語言中
	LOGI("JNI_日誌 : x = %ld , y = %ld" , x , y);


最終的包含打印日誌的完整代碼 : 注意, 這裏有一處可能錯誤, 若是是32位機器, int類型佔位符使用 %d 便可;

#include <jni.h>
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)


//方法簽名, Java環境 和 調用native方法的類 必不可少, 後面的參數就是native方法的參數
jint Java_shuliang_han_ndkparameterpassing_DataProvider_add(JNIEnv * env, jobject obj, jint x, jint y)
{
	//Java中的int對應的是C語言中的long類型, 對應JNI中的jint類型, C語言中
	LOGI("JNI_日誌 : x = %ld , y = %ld" , x , y);
	return x + y;
}


從新編譯C文件 : 執行  /android-ndk-r9c/ndk-build命令;

-- 第一次編譯 : 出現警告, long int佔位符行不通, 注意區分機器位長, 64位 與 32位不一樣, 這樣編譯出現的結果就不會打印日誌;

-- 第二次編譯 : 將佔位符改成 %d ;

 

執行按鈕以後打印的日誌 : 雖然有亂碼, 不過顯示出來了;

 

 

 

4. 字符串處理

 

Java中的String轉爲C語言中的char字符串 : 下面的工具方法能夠在C程序中解決這個問題;

// java中的jstring, 轉化爲c的一個字符數組
char*   Jstring2CStr(JNIEnv*   env,   jstring   jstr)
{
	 char*   rtn   =   NULL;
	 jclass   clsstring   =   (*env)->FindClass(env,"java/lang/String");
	 jstring   strencode   =   (*env)->NewStringUTF(env,"GB2312");
	 jmethodID   mid   =   (*env)->GetMethodID(env,clsstring,   "getBytes",   "(Ljava/lang/String;)[B");
	 jbyteArray   barr=   (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode); // String .getByte("GB2312");
	 jsize   alen   =   (*env)->GetArrayLength(env,barr);
	 jbyte*   ba   =   (*env)->GetByteArrayElements(env,barr,JNI_FALSE);
	 if(alen   >   0)
	 {
	  rtn   =   (char*)malloc(alen+1);         //new   char[alen+1]; "\0"
	  memcpy(rtn,ba,alen);
	  rtn[alen]=0;
	 }
	 (*env)->ReleaseByteArrayElements(env,barr,ba,0);  //釋放內存

	 return rtn;
}


C語言方法 : 注意調用Jstring2CStr方法以後要強轉, 不然會出錯, Jstring2CStr方法要定義在該方法的前面, C語言中的方法要先聲明才能使用;

jstring Java_shuliang_han_ndkparameterpassing_DataProvider_sayHelloInc(JNIEnv *env, jobject obj, jstring str)
{
	char *p = (char*)Jstring2CStr(env, str);
	//打印Java傳遞過來的數據
	LOGI("Java JNI string parameter is : %s", p);
	
	char *append = "append";
	
	//strcat(dest, source) 函數能夠將source字符串 添加到dest字符串後面
	return (*env)->NewStringUTF(env, strcat(p, append));
}

-- 若是沒有強轉會出現下面的錯誤 : char *p = Jstring2CStr(env, str);

-- 將Jstring2CStr方法定義在主方法下面會出現下面錯誤 : 

 

Java源碼 : 

case R.id.sayHelloInc:
				Toast.makeText(getApplicationContext(), dataProvider.sayHelloInc("Hello"), Toast.LENGTH_LONG).show();
				break;

 

編譯以後運行結果 : 

 
 

5. 開發JNI程序流程

 

  • C語言類庫接口 : 存在C語言類庫, 調用接口爲login_server(char* address, char* username, char* password);
  • Java定義本地方法 : public native void LoginServer(String address, String user, String pwd);
  • C語言JNI代碼 : Java_包名_類名_LoginServer(JNIEnv* env, jobject obj, jstring address, jstring user, jstring pwd){...調C接口};

 

注意跨語言字符串轉換: JNI方法中, 要將Java的String字符串轉爲C中的char*字符串;

 

首先驗證C碼農提供的代碼是否可用 : 驗證該api是否可用, 在一個 int main() 函數中進行測試, 根據該測試代碼查看方法執行相關的狀況;

 

6. 數組參數處理

模塊講解 : 在該模塊中, Java語言傳遞一個int數組參數給C語言, C語言將這一組參數讀取出來, 而且輸出到Android的LogCat中, 這裏涉及到了兩個重要的JNI方法, 一個數 獲取數組長度方法, 一個是 獲取數組中每一個元素的方法;

 

獲取數組長度方法 : jni中定義 - jsize (*GetArrayLength)(JNIEnv*, jarray);

建立數組相關方法 : 

jbooleanArray (*NewBooleanArray)(JNIEnv*, jsize);
    jbyteArray    (*NewByteArray)(JNIEnv*, jsize);
    jcharArray    (*NewCharArray)(JNIEnv*, jsize);
    jshortArray   (*NewShortArray)(JNIEnv*, jsize);
    jintArray     (*NewIntArray)(JNIEnv*, jsize);
    jlongArray    (*NewLongArray)(JNIEnv*, jsize);
    jfloatArray   (*NewFloatArray)(JNIEnv*, jsize);
    jdoubleArray  (*NewDoubleArray)(JNIEnv*, jsize);


獲取數組元素相關方法 : 

jboolean*   (*GetBooleanArrayElements)(JNIEnv*, jbooleanArray, jboolean*);
    jbyte*      (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);
    jchar*      (*GetCharArrayElements)(JNIEnv*, jcharArray, jboolean*);
    jshort*     (*GetShortArrayElements)(JNIEnv*, jshortArray, jboolean*);
    jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
    jlong*      (*GetLongArrayElements)(JNIEnv*, jlongArray, jboolean*);
    jfloat*     (*GetFloatArrayElements)(JNIEnv*, jfloatArray, jboolean*);
    jdouble*    (*GetDoubleArrayElements)(JNIEnv*, jdoubleArray, jboolean*);

 

C語言代碼 : 

jintArray Java_shuliang_han_ndkparameterpassing_DataProvider_intMethod(JNIEnv *env, jobject obj, jintArray arr)
{
	//獲取arr大小
	int len = (*env)->GetArrayLength(env, arr);
	
	//在LogCat中打印出arr的大小
	LOGI("the length of array is %d", len);
	
	//若是長度爲0, 返回arr
	if(len == 0)
		return arr;
		
	//若是長度大於0, 那麼獲取數組中的每一個元素
	jint* p = (*env)->GetIntArrayElements(env, arr, 0);
	
	//打印出數組中每一個元素的值
	int i = 0;
	for(; i < len; i ++)
	{
		LOGI("arr[%d] = %d", i, *(p + i));
	}
	
	return arr;
	
}


Java語言代碼 : 

case R.id.intMethod:
				int[] array = {1, 2, 3, 4, 5};
				dataProvider.intMethod(array);
				break;


執行結果 : 上面的那種LogCat居然啓動失敗, 只能將就着用這個了;

 

.

7. 上傳代碼到GitHub

 

建立新項目 : han1202012/NDKParameterPassing ;

-- SSH地址 : git@github.com:han1202012/NDKParameterPassing.git ;

-- HTTP地址 : https://github.com/han1202012/NDKParameterPassing.git ;

.

相關文章
相關標籤/搜索