Android中JNI&NDK入門(一) 之 初識NDK和JNI

1 NDK

NDK全稱是Native Develop Kit,翻譯作原生開發工具包。它允許你爲Android使用C/C++代碼來實現應用程序的功能。換言之Android的SDK之外,有一個工具叫NDK,用於進行C/C++的開發。一般情況,是用NDK工具把C/C++編譯爲.co文件,然後在Java中調用。NDK 可能不適合大多數 Android 編程初學者,這些初學者只需使用 Java 代碼和框架 API 來開發應用。然而,如果你需要完成一或多個以下事項,那麼 NDK 就能派上用場:

       提高代碼的安全性,因爲Java層代碼比較容易被反編譯,而so庫反編譯比較困難。

       重複使用目前已有的 C/C++ 庫。

       通過C/C++實現的動態庫可以很方便地在其他平臺間移植使用

       進一步提升設備性能,以實現低延遲時間,或運行計算密集型應用,如遊戲或物理模擬。

2 JNI

JNI全稱是Java Native Interface,即Java本地接口。JNI是Java調用Native 語言的一種特性,它是爲了方便Java和C/C++等本地代碼相互調用所封裝的一層接口。

3 配置NDK環境

第一步,下載NDK

在Android Studio的”Default Preferences”中勾選NDK項進行下載NDK,待下載成功後,便可在”Project Structure”中查看到NDK的目錄,操作如下面圖:

第二步,配置環境變量

打開“終端”,並輸入命令:echo $PATH 查看當前環境變量

 輸入:sudo vi ~/.bash_profile,按回車輸入密碼後用vi打開用戶目錄下的bash_profile文件。一定要用sudo,否則沒權限保存文件 

按i鍵,開始編輯,並將你電腦裏NDK路徑填寫到後面

編輯完之後,按ESC鍵,輸入:wq,就可以保存退出了,如果不想保存就輸入:q就可以了

4 Hello world

準備工作完成後,現在我們就來開始創建一個JNI的Demo,該Demo很簡單,就是使Java能正常調用到C++代碼而已。創建工程後,正常情況下會看到local.properties文件中有指定了ndk的目錄,如果沒有則要手動加上:

ndk.dir=/Users/liyizhi/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/liyizhi/Library/Android/sdk

第一步,創建JNIUtils類,在類中將執行加載so庫文件和定義一個native方法getInfo,並在MainActivity中調用getInfo方法:

JNIUtils.java

public class JNIUtils {
    static {
        System.loadLibrary("jni-demo");
    }
    public static native String getInfo();
}

MainActivity.java

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        String info = JNIUtils.getInfo();
        TextView tv = findViewById(R.id.tv1);
        tv.setText(info);
    }
}

第二步,執行命令,生成.h頭文件。如下圖,執行了兩條命令,首先通過命令:cd app/src/main/java跳轉到工程中java文件夾中,然後使用命令:javac -h . com/zyx/jnidemo/JNIUtils.java 生成了對應的.h文件。這裏筆者使用的是jdk1.8,如果是jdk較前的版本中,應該要使用命令:javah -jni com.zyx.jnidemo.JNIUtils

上面.h頭文件代碼需要做一下說明

JINEXPORTJNICALL:                                     是JNI中定義的宏,可以在jni.h這個頭文件中查找到

Jstring:                                                                 是代表的是getInfo方法的String返回類型

Java_com_zyx_jnidemo_JNIUtils_getInfo:       是函數名,格式遵循如下規則:Java_包名_類名_方法名

JNIEnv*:                                                                  表示一個指向JNI環境的指針,可以通過它來訪問JNI提供的接口方法

jclass:                                                                      表示Java對象中的this

第三步,創建JNI Folder,並將com_zyx_jnidemo_JNIUtils.h頭文件移至該文件夾中

第四步,在剛創建的jni文件夾中創建2個文件:JNIUtils.cpp和Android.mk,它們的實現如下所示:

JNIUtils.cpp

#include "com_zyx_jnidemo_JNIUtils.h"
#include <stdio.h>
#include <android/log.h>

JNIEXPORT jstring JNICALL Java_com_zyx_jnidemo_JNIUtils_getInfo(JNIEnv * env, jclass thiz) {
    __android_log_print(ANDROID_LOG_DEBUG, "zyx", "Hello world from JNI !");
    return env->NewStringUTF("Hello world from JNI !");
}

Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := jni-demo
LOCAL_SRC_FILES := JNIUtils.cpp

LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

上面代碼中,LOCAL_MODULE表示模塊的名稱,LOCAL_SRC_FILES表示需要參與編譯的源文件,LOCAL_LDLIBS表示在C++代碼中支持log日誌輸出。

第五步,修改app目錄下的build.gradle,如下代碼,其中說明請看註釋:

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.zyx.jnidemo"
        minSdkVersion 22
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        ndk {
            moduleName "jni-demo"                // so庫的名稱
            abiFilters "armeabi-v7a", "x86"      // 支持的cpu構架平臺類型,all表示編譯所有cpu平臺
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        externalNativeBuild {
            ndkBuild {
                path 'src/main/jni/Android.mk'    // 指向Android.mk文件
            }
        }
    }
}

第六步,執行編譯運行,這時就會在app/build/intermediates/ndkBuild/debug/obj/local目錄下生成對應兩類so文件,這裏兩類是對應着gradle中配置的cpu構架平臺類型。然後將程序運行到手機上便會看到Java成功調用了C++代碼返回了結果。

5 引用外部so庫

在實際開發過程中,往往C++工程是跟Android工程分離,或者Android工程中直接引用外部提供現成的so庫文件。現在我們就來模擬一下這種情況的發生。

首先在jni文件夾中再添加一下Application.mk文件,代碼如下:

Application.mk

APP_ABI := armeabi-v7a,x86

接着切換到jni目錄的父目錄,然後通過命令:ndk-build來手動編譯產生so庫,這時候NDK會創建一個和jni目錄平級的目錄libs,libs下面存放着就是so庫。

然後修改Gradle文件:

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.zyx.jnidemo"
        minSdkVersion 22
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//        ndk {
//            moduleName "jni-demo"               // so庫的名稱
//            abiFilters "armeabi-v7a", "x86"     // 支持的cpu構架平臺類型,all表示編譯所有cpu平臺
//        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
//        externalNativeBuild {
//            ndkBuild {
//                path 'src/main/jni/Android.mk'  // 指向Android.mk文件
//            }
//        }
        sourceSets.main {
            jni.srcDirs = []                      // 禁用自動NDK生成調用
            jniLibs.srcDirs = ['src/main/libs']   // so庫存放目錄
        }
    }
}

這裏要說明一下,因爲JNI的代碼是必須放在上面創建的jni文件夾裏,如果不想採用jni這個名稱,可以在Gradle通過jin.srcDirs指定JNI代碼的路徑。因爲我們上例中JNI的代碼是默認放在了創建的jni文件夾裏,就必須得加上jni.srcDirs=[],否則會編譯不通過。

最後就是再次執行編譯運行,可以再次看到程序運行到手機上也能展示同樣的結果

另外說明一下,如果在實際開發過程中存在舊版本兼容的問題,可以嘗試在gradle.properties文件中加上下面的一行代碼:

Android.useDeprecatedNdk=true 

 

 

點擊下載示例

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章