1. 程式人生 > >NDK開發 從入門到放棄(一:基本流程入門瞭解)

NDK開發 從入門到放棄(一:基本流程入門瞭解)

一、前言

● NDK

Native Development Kit(NDK)是一系列工具的集合。它提供了一系列的工具,幫助開發者快速開發C/C++的動態庫,並能自動將so和java一起打包成apk。

● JNI

Java Native Interface(JNI)標準是java平臺的一部分,JNI是Java語言提供的Java和C/C++相互溝通的機制,Java可以通過JNI呼叫C/C++程式碼,C/C++的程式碼也可以呼叫java程式碼。

● JNI與NDK的關係

NDK可以為我們生成了C/C++的動態連結庫,JNI是java和C/C++溝通的介面,兩者與android沒有半毛錢關係,只因為安卓是java程式語言開發,然後通過JNI又能與C/C++溝通,所以我們可以使用NDK+JNI來實現“Java+C”的開發方式。

● 為什麼要NDK開發

NDK開發具有以下優點: 
1. 專案需要呼叫底層的一些C/C++的一些東西(java無法直接訪問到作業系統底層(如系統硬體等)),或者已經在C/C++環境下實現了功能程式碼(大部分現存的開源庫都是用C/C++程式碼編寫的。),直接使用即可。NDK開發常用於驅動開發、無線熱點共享、數學運算、實時渲染的遊戲、音視訊處理、檔案壓縮、人臉識別、圖片處理等。 
2. 為了效率更加高效些。將要求高效能的應用邏輯使用C/C++開發,從而提高應用程式的執行效率。但是C/C++程式碼雖然是高效的,在java與C/C++相互呼叫時卻增大了開銷; 
3. 基於安全性的考慮。防止程式碼被反編譯,為了安全起見,使用C/C++語言來編寫重要的部分以增大系統的安全性,最後生成so庫(用過第三方庫的應該都不陌生)便於給人提供方便。(任何有效的程式碼混淆對於會smail語法反編譯你apk是分分鐘的事,即使你加殼也不能倖免高手的攻擊) 
4. 便於移植。用C/C++寫得庫可以方便在其他的嵌入式平臺上再次使用。

二、安裝與配置

首先我們在Android Studio下新建一個安卓專案。然後開啟Project Structure介面,如下: 


在SDK Location目錄下,有SDK和NDK的路徑,而這裡我們暫時還未下載配置過NDK,故我們需要點選Download Android NDK來進行下載(Android Studio還是很強大的,相比Eclipse能省不少事)。這裡Android Studio會下載最新版本的NDK進行安裝,預設會下載儲存在SDK的路徑下。我們在上圖中還能看到有一段介紹文字,說SDK以及NDK的路徑配置會儲存在local.properties檔案內,安裝完成後我們重新整理Project,進local.properties檔案檢視也能看到SDK與NDK的路徑。 
 
 
NDK下載配置完成之後,需要在gradle.properties檔案中加上一行:
 

android.useDeprecatedNdk=true

接下來,我們藉助強大的Android Studio的外掛功能,在External Tools下配置兩個非常有用的外掛。進入Settings–>Tools–>ExternalTools,點選+號增加。 

javah -jni命令,是根據java檔案生成.h標頭檔案的,會自動根據java檔案中的類名(包含包名)與方法名生成對應的C/C++裡面的方法名。下面是引數配置及其含義: 
1. Program: $JDKPath$\bin\javah.exe 這裡配置的是JDK目錄下的javah.exe的路徑。 
2. Parametes: -classpath . -jni -d $ModuleFileDir$/src/main/jni $FileClass$ 這裡$FileClass$指的是要執行操作的類名(即我們操作的檔案),$ModuleFileDir$/src/main/jni表示生成的檔案儲存在這個module目錄的src/main/jni目錄下。 
3. Working: $ModuleFileDir$\src\main\java module目錄下的src\main\java目錄(不是很理解)。 
使用方式:選中java檔案—>右鍵—>External Tools—>javah-jni,將生成jni資料夾以及資料夾下的 包名.類名的.h標頭檔案 (名字過長,我們可以自己重新命名)。 


ndk -build命令,是根據C/C++檔案生成so檔案的。下面是引數配置及其含義: 
1. Program: F:\apk\sdk\ndk-bundle\ndk-build.cmd 這裡配置的是ndk下的ndk-build.cmd的路徑(根據實際情況填寫)。 
2. Working: $ModuleFileDir$\src\main\ 
使用方式:選中C/C++檔案—>右鍵—>ExternalTools—>ndk-build,將在main資料夾下生成libs資料夾以及多個so檔案,我們可以移動至jniLibs目錄下去。

三、簡單例項

接下來我們建立一個訪問本地C/C++方法的java類。

public class JniTest {
    /**
     * 將用C++程式碼實現,在android程式碼中呼叫的方法:獲取當前app的包名
     * @param o
     * @return
     */
    public static native String getPackname(Object o);

    /**
     * 載入so庫或jni庫,在使用到該庫之前載入就行,不一定非要寫在這個類內
     * 系統自己會判斷副檔名是dll還是so,這裡載入libJNI_ANDROID_TEST.so
     */
    static {
        System.loadLibrary("JNI_ANDROID_TEST");
    }
}

注意JNI_ANDROID_TEST這個Library名字,之後還會需要用到,要保持一致。該類提供了一個static的native方法,該方法將用來獲取app的包名。然後對該檔案執行javah -jni操作,生成對應的.h標頭檔案。 

如圖,已經根據我們的java類生成了對應的.h檔案,檔名為包名_類名.h,我們可以手動改名為jnitest.h,裡面只有一個方法,返回值為String(jstring),方法名為Java_類的包名_類名_方法名(包名中的分級不是用.而是_),前面兩個引數是C++裡面必須有的(JNIEnv代表指向JVM的指標,jclass是呼叫該方法的java物件),第三個就是我們java類的方法裡面的引數Object。注意,這是java函式與C++函式對應的靜態註冊方法,即通過特定的規則來寫,此處方法名可以隨意起名字,然後還可以用動態註冊的方式關聯兩個方法(顯然,靜態註冊要簡單一些)。 
然後我們新建一個C++檔案,取名為jnitest.cpp,寫上需要include的檔案,從.h檔案中複製方法過來(方法名、引數型別、返回值等必須一致!血與淚的教訓)。 

至此,.h檔案和c++檔案均已完成,接下來還需要在這個jni目錄下增加兩個檔案,Android.mk和Application.mk。 
Android.mk,注意LOCAL_MODULE的值與之前的名字相對應,LOCAL_SRC_FILES的值寫c++檔案的名字,這兩個值成對設定,可設定多組。(:=是賦值的意思,$是引用某變數的值。)
 

裡面的符號正確的應該是:=,程式碼中已更正,圖片裡面的更換麻煩就沒改了。
很奇怪,我當初寫的時候編譯執行好像是沒出錯是正常的…(Tips.20170519)
LOCAL_PATH := $(call my-dir)     // 設定當前的編譯目錄(Android.mk所在的目錄) 

include $(CLEAR_VARS)            // 清除LOCAL_XX變數(LOCAL_PATH除外)
LOCAL_MODULE := JNI_ANDROID_TEST  // 指定當前編譯模組的名稱  
LOCAL_SRC_FILES := jnitest.cpp    // 編譯模組需要的原始檔
include $(BUILD_SHARED_LIBRARY) // 指定編譯出的庫型別,BUILD_SHARED_LIBRARY:動態庫;BUILD_STATIC_LIBRARY:靜態庫, BUILD_EXECUTEABLE指:可執行檔案

這裡寫圖片描述
在一個Android.mk檔案中配置多個Module的方式如下(include$(CLEAR_VARS)、include$(BUILD_SHARED_LIBRARY)兩個語句也需要加上):

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := JNI_STATIC_ANDROID_TEST
LOCAL_SRC_FILES := jnistaticutils.cpp
include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := JNI_DYNAMIC_ANDROID_TEST
LOCAL_SRC_FILES := jnidynamicutils.cpp
include $(BUILD_SHARED_LIBRARY)

Application.mk,APP_ABI有四種類型(預設armeabi),armeabi、armeabi-v7a、x86、mips,設定時以空格隔開,all表示所有。該檔案中有個可選配置的APP_MODULES,類似於上面Android.mk檔案中的LOCAL_MODULE,以空格隔開,且會覆蓋掉Android.mk檔案中的LOCAL_MODULE設定(比如Android.mk檔案中的寫了兩個jni庫的配置,LOCAL_MODULE := JNI1、LOCAL_MODULE := JNI2,而Application.mk中設定的APP_MODULES := JNI1,則只能生成JNI1的so檔案,要生成JNI2的so檔案的時候會報錯,除非寫成APP_MODULES := JNI1 JNI2,這裡我們直接省略預設使用Android.mk中的)。

APP_ABI := all

接下來我們需要對C++檔案執行ndk-build操作,生成相應的so檔案。 
 
如圖,在main/libs目錄下生成了多個so檔案,名字為lib+我們指定的庫名(同時還生成了obj資料夾,不知是什麼東西)。 
這時候我們可以在main目錄下新建jniLibs資料夾,把生成的libs資料夾內的東西均複製過去,刪除新生成的jni、libs、obj三個資料夾。然後在Activity中測試呼叫,在TextView上顯示我們通過C++程式碼實現的方法getPackname獲取app的包名了。
 

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = (TextView) findViewById(R.id.tv_app_package_name);
        tv.setText("packageName: " + JniTest.getPackname(MainActivity.this));
    }
}

這裡寫圖片描述 
測試能正確得到包名,說明呼叫成功了。我們可以把JniTest類以及so檔案給別人去使用,這樣別人是看不到我們的程式碼實現的,能很好的保護我們的原始碼。