JNI學習筆記之ndk-build手動編譯並整合流程

weixin_34253539發表於2017-11-05

參考

一天掌握Android JNI本地程式設計 快速入門
Android開發實踐:常用NDK命令列引數
Secrets of Android.mk

JNI

JNI是啥?

JNI(Java Native Interface):Java本地開發介面,JNI是一個協議,用來溝通Java程式碼和外部的原生程式碼(c/c++),外部的c/c++程式碼也可以呼叫Java程式碼

為什麼使用JNI

  • 效率上C/C++是本地語言,比Java更高效
  • 程式碼移植,如果之前用C語言開發過模組,可以複用已經存在的C程式碼
  • Java反編譯比C語言更容易,一般加密演算法都是用C語言編寫,不容易被反編譯

Java基本資料型別與C語言基本資料型別的對應

Java基本資料型別與C語言基本資料型別的對應

Java引用型別對應

Java引用型別對應

堆記憶體和棧記憶體的概念

棧記憶體

系統自動分配和釋放,儲存全域性,靜態,區域性變數,在棧上分配記憶體叫今天記憶體,大小一般是固定的。

堆記憶體

程式設計師手動分配(mallc/new)和釋放(free/java不用手動釋放,有GC回收),在堆上分配記憶體叫動態分配,一般硬體記憶體有多大記憶體就有多大。

交叉編譯

交叉編譯的概念

交叉編譯記載一個平臺,編譯出另一個平臺能夠執行的二進位制程式碼
主流平臺:Windows,Mac os,Linux
主流處理器:X86,arm,mips

交叉編譯的原理

即在一個平臺上,模擬其它平臺的特性
編譯的流程:原始碼》編譯》連結》可執行程式

交叉編譯的工具鏈

多個工具的集合,一個工具使用完後接著呼叫下一個工具

常見的交叉編譯工具

NDK(Native Development Kit):開發JNI必備工具,就是模擬其它平臺特性類編譯程式碼的工具
CDP(C/C++ Development Tools):是Eclipse開發C語言的一個外掛,高亮顯示C語言的語法
Cygwin:一個Windows平臺的Unix模擬器

NDK的目錄結構

這裡我下的ndk版本是15.2.4203891 比較新了,配置NDK環境變數大家應該都會,這裡我就不提了


目前NDK最新的目錄

ndk-build方式手動編譯出so庫檔案

一個簡單的例子

1.編寫java程式碼

這個直接在工程目錄下正常編輯你的程式碼,比如我的這個JNIUtils是在com.newtrekwang.ndkpractice包下的,這個類宣告瞭一個方法,功能就是獲取從C層傳來的字串。方法的具體實現當然是在C層實現啦。所以這個就跟java的介面定義差不多。

package com.newtrekwang.ndkpractice;

public class JNIUtils {
    public static native String getStringFromC();
}

2.編寫c層程式碼

這裡如果想快速寫一個與之關聯的標頭檔案的話,(java7)可以使用javah命令生成,不過命令要在你的類的所在包的根路徑執行
比如我這個:

javah

可以看下標頭檔案的程式碼:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_newtrekwang_ndkpractice_JNIUtils */

#ifndef _Included_com_newtrekwang_ndkpractice_JNIUtils
#define _Included_com_newtrekwang_ndkpractice_JNIUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_newtrekwang_ndkpractice_JNIUtils
 * Method:    getStringFromC
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_newtrekwang_ndkpractice_JNIUtils_getStringFromC
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

可以看到裡面已經幫我們寫好了一個與Java類getStringFromC對應的Native函式Java_com_newtrekwang_ndkpractice_JNIUtils_getStringFromC,命名格式就是Java加完整類名加方法名

然後我把這個標頭檔案剪下到另一個新建的資料夾prac1裡,這個資料夾專門存放c/c++真個編譯過程的產物,比如最後我要得到so檔案,就從這裡面拿

然後我們新建一個hello.c實現標頭檔案裡面的函式

#include<com_newtrekwang_ndkpractice_JNIUtils.h>
#include<stdio.h>
#include<stdlib.h>

JNIEXPORT jstring JNICALL Java_com_newtrekwang_ndkpractice_JNIUtils_getStringFromC
  (JNIEnv* env, jclass obj){
      char* str="Hello from C!";
      jstring result=(*env)->NewStringUTF(env,str);
      return result;
  }

(1)JNIEXPORT :在Jni程式設計中所有本地語言實現Jni介面的方法前面都有一個"JNIEXPORT",這個可以看做是Jni的一個標誌,至今為止沒發現它有什麼特殊的用處。

(2)void :這個學過程式設計的人都知道,當然是方法的返回值了。

(3)JNICALL :這個可以理解為Jni 和Call兩個部分,和起來的意思就是 Jni呼叫XXX(後面的XXX就是JAVA的方法名)。

(4)Java_com_test01_Test_firstTest:這個就是被上一步中被呼叫的部分,也就是Java中的native 方法名,這裡起名字的方式比較特別,是:包名+類名+方法名。

(5)JNIEnv * env:這個env可以看做是Jni介面本身的一個物件,在上一篇中提到的jni.h標頭檔案中存在著大量被封裝好的函式,這些函式也是Jni程式設計中經常被使用到的,要想呼叫這些函式就需要使用JNIEnv這個物件。例如:env->GetObjectClass()。(詳情請檢視jni.h)

3.編寫Android.mk

Android.mk 裡面就是些設定編譯配置的指令碼
例如我的這個:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE :=hello
LOCAL_SRC_FILES := hello.c com_newtrekwang_ndkpractice_JNIUtils.h
include $(BUILD_SHARED_LIBRARY)

常見Android.mk語句解釋

  • LOCAL_PATH := $(call my-dir)
    一個Android.mk檔案必須以LOCAL_PATH變數的定義開始,它用於在開發樹中查詢原始檔。在這個例子中,構建系統提供的巨集函式'my-dir'被用來返回 當前目錄的路徑(即包含Android.mk檔案本身的目錄)。
  • include $(CLEAR_VARS)
    CLEAR_VARS變數由構建系統提供,並指向一個特殊的GNU Makefile,它將清除許多LOCAL_XXX變數 (例如LOCAL_MODULE,LOCAL_SRC_FILES,LOCAL_STATIC_LIBRARIES等),但除了LOCAL_PATH,這個是需要的,因為所有的構建控制檔案在單個GNU Make執行上下文中解析,其中所有變數都是全域性的。
  • LOCAL_MODULE :=hello
    定義LOCAL_MODULE變數來標識您在Android.mk中描述的每個模組,構建系統為自動為模組新增字首和字尾,例如我這個是hello,他最終會生成libhello.so
  • LOCAL_SRC_FILES := hello.c com_newtrekwang_ndkpractice_JNIUtils.h
    LOCAL_SRC_FILES變數必須包含C和/或C ++原始檔的列表,這些檔案將被構建並組裝到一個模組中。這裡可以只列出原始檔,不需要標頭檔案,標頭檔案構建系統會自動查詢

更多Android.mk功能請見Secrets of Android.mk

然後我的prac1檔案現在有這個三個檔案了

prac1

然後在此目錄下執行ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk
如圖:

image.png

然後再看我們的資料夾就是這樣了:

image.png
image.png

常見ndk-build命令列引數

  • NDK_LOG=1:配置log級別,列印ndk編譯時的詳細輸出資訊
  • NDK_PROJECT_PATH=.:制定NDK編譯的程式碼路徑為當前目錄,如果不配置,則必須把工程程式碼放到Android工程的jni目錄下
  • APP_BUILD_SCRIPT=./Android.mk:指定NDK編譯使用的Android.mk檔案
  • NDK_APP_APPLICATION_MK=./Application.mk:指定NDK編譯使用的Application.mk檔案
  • CLEAN:清除所有編譯出來的臨時檔案和目標檔案
  • -B:強制重新編譯已經編譯完成的的程式碼
  • NDK_DEBUG=1:執行 debug build
  • NDK_DEBUG=0:執行release build
  • NDK_OUT=./mydir:指定編譯生成的檔案的存放位置
  • -C /opt/myTest/:到指定目錄編譯native程式碼

4.在Android專案中整合so檔案

首先把那些so檔案拷到Android工程的libs目錄下

拷so檔案

gradle配置庫檔案目錄

在android塊下設定sourceSets塊即可,比如我這裡設定的是libs資料夾,說明我的so庫檔案在libs資料夾裡。

 sourceSets {
        main {
            jniLibs.srcDirs=["libs"] //指定庫檔案的目錄,java程式碼編譯時連結用
        }
    }

然後gradle同步一下,即可在Android模式目錄下看到jniLibs檔案包,證明gradle已經識別了so庫檔案

jniLibs

完善java程式碼

就是在原來的基礎上加了個靜態塊,裡面的System.loadLibrary("hello")功能就是載入hello這個庫檔案,不然下面的native方法不能正常執行

package com.newtrekwang.ndkpractice;

public class JNIUtils {
        static {
            System.loadLibrary("hello");
        }
    public static native String getStringFromC();
}

然後就可以愉快的使用調方法了
比如我這裡這個TextView顯示的字串就是就是從JNIUtils類的native方法調出來的。

例項

相關文章