參考
一天掌握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引用類型對應
堆內存和棧內存的概念
棧內存
系統自動分配和釋放,保存全局,靜態,局部變量,在棧上分配內存叫今天內存,大小一般是固定的。
堆內存
程序員手動分配(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-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命令生成,不過命令要在你的類的所在包的根路徑執行
比如我這個:
可以看下頭文件的代碼:
/* 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文件現在有這個三個文件了
然后在此目錄下執行ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk
如圖:
然后再看我們的文件夾就是這樣了:
常見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目錄下
gradle配置庫文件目錄
在android塊下設置sourceSets塊即可,比如我這里設置的是libs文件夾,說明我的so庫文件在libs文件夾里。
sourceSets {
main {
jniLibs.srcDirs=["libs"] //指定庫文件的目錄,java代碼編譯時鏈接用
}
}
然后gradle同步一下,即可在Android模式目錄下看到jniLibs文件包,證明gradle已經識別了so庫文件
完善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方法調出來的。