Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android NDK開發技術與技巧總結與心得

Android NDK開發技術與技巧總結與心得

編輯:關於Android編程

一、JNI到底是干嘛用的
百度都能查到的官方解釋我就不多說了。我自己的理解是兩方面,一方面主要用於各種復雜算法的執行,C的效率高自不必說,更重要的是so的破解難度要遠遠大於apk了,很多公司就靠算法活著的,沒有JNI用他們要死了~;另一方面,JavaNativeInterface,作為Java和C間的bridge,我們可以把很多java代碼可能無法實現的事情丟到C層去搞。

二、怎麼跑起來一個帶JNI代碼的工程
我用的是AS2.1,主要學姿勢,所以默認各位看官的環境已經折騰好,如果沒折騰好百度一找一洗臉盆,學一下就好了。AS現在對NDK的支持還是很感人的low,不過編譯很輕松,所以湊合著用吧。簡單步驟總結下,不一樣的同學不用噴,這不是重點:

1.寫個類,寫好自己需要的native方法,像這樣:

package com.amuro.ndkcompactdemo.chapter_1;

/**
 * Created by Amuro on 2016/6/23.
 */
public class Chapter1JNI
{
    static
    {
        System.loadLibrary("ndk_compact");
    }

    public native String stringFromJNI();
    public native int add(int a, int b);
    public native String addString(String str);
    public native int[] addArray(int[] array);
}

2.寫好之後,編譯你的代碼,保證通過。然後用AS自帶的Termial進入工程根目錄,我的根目錄是這樣的:
D:\DevelopAS\workspace\NDKCompactDemo
好,然後進入根目錄的以下目錄:
D:\DevelopAS\workspace\NDKCompactDemo\app\build\intermediates\classes\debug
然後輸入命令javah -jni com.amuro.ndkcompactdemo.chapter_1.Chapter1JNI
就這樣會在剛才那個目錄下生成一個.h頭文件。不要問我javah是什麼也不要問我h文件是什麼,自己百度。生成頭文件代碼如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI */

#ifndef _Included_com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
#define _Included_com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
 * Method:    stringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_amuro_ndkcompactdemo_chapter_11_Chapter1JNI_stringFromJNI
  (JNIEnv *, jobject);

/*
 * Class:     com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
 * Method:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_amuro_ndkcompactdemo_chapter_11_Chapter1JNI_add
  (JNIEnv *, jobject, jint, jint);

/*
 * Class:     com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
 * Method:    addString
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_amuro_ndkcompactdemo_chapter_11_Chapter1JNI_addString
  (JNIEnv *, jobject, jstring);

/*
 * Class:     com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
 * Method:    addArray
 * Signature: ([I)[I
 */
JNIEXPORT jintArray JNICALL Java_com_amuro_ndkcompactdemo_chapter_11_Chapter1JNI_addArray
  (JNIEnv *, jobject, jintArray);

#ifdef __cplusplus
}
#endif
#endif

3.在工程的src/main目錄下創建一個jni文件夾,把剛才生成的h文件復制過來。

4.對Gradle進行配置
1)app module的build.gradle下增加ndk配置,完整gradle文件如下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.amuro.ndkcompactdemo"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        ndk{
            moduleName "ndk_compact"
            ldLibs "log", "z", "m"  //添加依賴庫文件,因為有log打印等
            abiFilters "armeabi", "armeabi-v7a", "x86"
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.3.0'
}

第一個是你自己的so庫名稱,第二個看注釋,第三個復制粘貼就好
2)在gradle.properties中添加:
android.useDeprecatedNdk=true
不要問我為什麼。

5.針對之前生成的h文件,寫自己的源文件,c或cpp均可,記得要把h文件include進來。

6.然後就可以編譯執行啦,是不是好簡單,AS幫我們把以前Eclipse需要的Android.mk文件的編寫給自動化了。

三、用JNI你需要知道的一些知識

1.指針和引用
Java把C最復雜的知識給封裝了,而回到C的我們不得不把他們重新撿起來,記得我當年剛學也是被折騰的死去活來,其實想明白之後真的不復雜。如果你C的知識都還給譚老師了,建議你花個一兩天先去撿回來,再回頭搞會發現很多源碼理解起來就輕松多了。這裡舉個例子,看代碼:
先定義一個結構體:

struct Student
{
    int age;
    char* name;
};

然後定義一個指針指向它:

typedef const struct Student* Stu;

然後我們要怎麼用呢:

func(Stu* student)
{
    printf("%d", (*student)->age);
    printf("%d", (**student).age);
}

int main()
{
    struct Student s = {10, "amuro", 100};
    Stu stu = &s;

    printf("%d", stu->age);
    printf("%d", (*stu).age);

    func(&stu);
}

運行結果四者打印出來的是10。大概解釋一下:
stu是s的指針,也就是說stu裡是s的地址,所以stu在賦值時等號右邊必須是一個地址值,而&就是取地址符。
從指針中取值的時候有兩種方法,代碼裡都寫了。
函數傳遞指針的時候要特別小心func的參數其實是一個指向stu的指針,也就是指針的指針。所以傳值的時候應該把stu的地址傳過去。而函數中的student保存的是stu的地址,所以*student才是取出stu地址裡保存的s的地址,這時候就可以進行取值操作了。畫個圖大家就更好理解了:

這裡寫圖片描述

舉這個例子是為了方便大家理解JNIEnv *env這個玩意兒,其實我們自己函數中拿到的env就是剛才例子裡的student,所以我們要調用env中的方法的時候,才會這樣寫:(*env)->MethodName

2.Java標准類和JNI接口的對應關系
這裡寫圖片描述
這裡寫圖片描述
圖我就直接盜了,反正口訣就是java類前面加個j就好了,然後首字母改小寫。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjMuam5pLmjU2sTEwO88YnIgLz4NCrrctuDM+9fTtry74cHQ0ru20Wpuabqvyv2jrL+0wcvSu823zu3LrqOsxuTKtda70qrO0sPH1dK1vdS0wuujrLrctuDOysziu+G3x7OjyN3S173ivvaho2puaS5otcTUtMLr1NrO0sPHz8LU2LXETkRLsPzA76GjztK1xLXY1rfKx6O6PGJyIC8+DQpEOlxEZXZlbG9wQVNcYXNTREtcbmRrLWJ1bmRsZVxwbGF0Zm9ybXNcYW5kcm9pZC0yM1xhcmNoLWFybVx1c3JcaW5jbHVkZTxiciAvPg0Ky/nT0LXEt723qLa81NrA78PmwcujrMi7uvO4+b7d0OjH88ilwO/D5tXSvs26w8HLoaM8YnIgLz4NCs7EtbW1xLuwv7TV4sDvo7o8YnIgLz4NCjxhIGhyZWY9"http://docs.oracle.com/javase/7/docs/technotes/guides/jni">http://docs.oracle.com/javase/7/docs/technotes/guides/jni
看到這種密密麻麻的文檔當時就想死了。

4.函數簽名
很重要,在C層反射的時候,都需要傳這東西,百度能找到很多詳細的帖子,我這裡就取其精華了。
這裡寫圖片描述
注意注意,這個表寫錯了,long的簽名是J,我當時就被坑死了,查了好久才查出來!
舉個例子,比如
public String doSomething(int a, String x, long[] b);
簽名就是:(ILjava/lang/String;[J)Ljava/lang/String;
括號裡是參數列表,注意非原生類一定要在前面加L後面加;,括號後面是返回值。分號一定不要忘記!!!!
其實還能用javap命令來搞的,但是個人覺得還不如理解之後自己手寫,熟悉之後效率高多了。這裡就不贅述了。

5.JNIEnv是個啥
1)JNIEnv是一個線程相關的結構體, 該結構體代表了 Java 在本線程的運行環境 ;
2)JNIEnv 與 JavaVM : 注意區分這兩個概念;
– JavaVM : JavaVM 是 Java虛擬機在 JNI 層的代表, JNI 全局只有一個;
– JNIEnv : JavaVM 在線程中的代表, 每個線程都有一個, JNI 中可能有很多個 JNIEnv;
3)JNIEnv 作用 :
– 調用 Java 函數 : JNIEnv 代表 Java 運行環境, 可以使用 JNIEnv 調用 Java 中的代碼;
– 操作 Java 對象 : Java 對象傳入 JNI 層就是 Jobject 對象, 需要使用 JNIEnv 來操作這個 Java 對象;
4)JNIEnv 體系結構
線程相關 : JNIEnv 是線程相關的, 即 在 每個線程中 都有一個 JNIEnv 指針, 每個JNIEnv 都是線程專有的, 其它線程不能使用本線程中的 JNIEnv, 線程 A 不能調用 線程 B 的 JNIEnv;
– 當前線程有效 : JNIEnv 只在當前線程有效, JNIEnv 不能在 線程之間進行傳遞, 在同一個線程中, 多次調用 JNI層方法, 傳入的 JNIEnv 是相同的;
– 本地方法匹配多JNIEnv : 在 Java 層定義的本地方法, 可以在不同的線程調用, 因此 可以接受不同的 JNIEnv;
5)JNIEnv 結構 : 由上面的代碼可以得出, JNIEnv 是一個指針, 指向一個線程相關的結構, 線程相關結構指向 JNI 函數指針 數組, 這個數組中存放了大量的 JNI 函數指針, 這些指針指向了具體的 JNI 函數;

四、JNI要怎麼用
這個話題太大了,就我自己的總結來看,比較常用的其實無非就是四種,一種是操作基本類型,一種是String,一種是操作我們自定義的類型,一種就是數組。
1.常用核心API接口
媽蛋,做表格好蛋疼,就繼續用截圖代替吧……
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
基本看到名字都能猜到意思了,有了方法名字,想要知道怎麼用,傳哪些參數的話,去查文檔或者我剛才說的jni.h源文件就好啦。

2.還是回去用Eclipse吧
用了一下AS之後真是覺得弱爆了,雖然編譯方便了很多。要搞復雜的NDK開發還是要靠Eclipse,自己寫Android.mk文件和Application.mk文件吧。
下面總結下這兩個文件的常用配置:
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

3.我能想到最痛苦的事,是沒有代碼提示~
記憶力拔群的手寫代碼大神請繞道。
反正一開始用AS我是快瘋了,基本沒法寫代碼,寫一行就要去查一下jni.h的頭文件看看方法名和傳入參數的定義,同時也不能調試,只能跑起來之後看日志或者報錯,效率低下不能忍。那怎樣讓熟悉的代碼提示出現呢,這裡提供兩個方法。
方法一:
1)你需要一個微軟的VisualStudio,版本不必太高,我用的2008,建一個工程,選dll工程。
2)從前面說的jni.h的頭文件目錄下,把jni.h和另一個叫jni_md.h的文件,加上你自己javah生成的頭文件一起copy到studio項目目錄下,像這樣:
這裡寫圖片描述
然後在你的工程裡把這些頭文件導入:
這裡寫圖片描述
然後自動提示就出來啦啦啦啦~
這裡寫圖片描述
也沒有要死的各種紅線綠線。
3)編譯出的dll文件其實是可以拿來用的(學過操作系統的童鞋都知道,dll動態鏈接庫其實就是windows上的so),這裡有一個注意點,就是如果你安裝的是64位的java,記得一定要把VS的編譯條件改成x64,不然編譯出來的dll文件java是沒法解析的。把你生成的dll文件的路徑配置到環境變量的PATH下面,然後寫一個java程序就能用load這個dll啦。我的配置是這樣的:
D:\VisualStudio\Projects\NDKTest\x64\Debug;(這裡面有個NDKTest.dll文件)
4)寫個Java程序去測試這個JNI代碼吧,System.loadLibrary的時候,lib名就傳“NDKTest”就OK了。這個適合測試算法類的JNI,測試成功後直接拿到Android程序裡用就好了。畢竟對C的支持,VS是無出其右的。

方法2:Eclipse增加代碼提示
1)我相信這個才是很多童鞋想要的~為了把這個搞出來花了我一整天折騰各種環境和配置,Orz
2)確認你的eclipse安裝了cdt,如果沒有,去官網找下載地址去
注意這個地址是在eclipse的install new software中使用的,不要直接網頁打開。
3)下載MinGW,安裝MinGW,配置MinGW
4)新建C項目,選擇如圖,記得右邊一定要選MinGW
這裡寫圖片描述
5)好了,出現了我們熟悉的代碼界面,stdio.h,stdlib.h都能看到源碼了,爽,但是這時候並沒有jni的源碼,別急,右鍵項目,把ndk-bundle裡源碼的地址配置進去:
這裡寫圖片描述
具體看圖,我就不貼了。配置完成確認OK。
6)激動人心的時候終於到了,加上jni.h的include,看到變藍色沒,變就對了。這時候我們就可以直接F3啦~然後,我們就可以把我們的javah生成的頭文件拷過來了,然後的然後請看圖:
這裡寫圖片描述
這下想看什麼源碼,都輕松愉快了,有些小算法,還可以在下面的main函數裡自己測試。

差不多就是這些東西了,剩下的就是學習各種API的使用了,作為程序員這個是必備技能了。跪求谷歌盡快完美支持NDK開發,這樣折騰實在是太痛苦了。如果各位有更好更優秀的方式,歡迎留言討論~

就醬~

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved