前言
之前一直是用Eclipse開發(fā)的,后來轉(zhuǎn)AndroidStudio的時候遇到了一些坑,其中比較麻煩的就是NDK的編譯。
代碼已上傳至GitHub
myExample是我創(chuàng)建專門用來放小例子的,以后我寫文章需要的例子就都放這里面了。
正文
1.原理
之前用Eclipse的時候都是手動創(chuàng)建一個jni文件夾,然后自己添加Android.mk和Application.mk2個文件,最后通過執(zhí)行ndk-build命令生成so庫。
但是AndroidStudio自帶編譯so庫功能,它會通過build.gradle里面的android.ndk項自動生成Android.mk文件然后生成so庫,我們需要做的就是禁止它自帶的功能,使用我們自己的mk文件。
實現(xiàn)步驟
- 禁止AndroidStudio自帶的ndk功能。
- 添加gradle task自動調(diào)用
ndk-build命令。
首先看下文件目錄

2.配置
1.禁止AndroidStudio自帶的ndk功能
完整配置請看build.gradle sourceSets
android {
//... 省略其他配置
sourceSets {
main {
jni.srcDirs = [] //禁止自帶的ndk功能
jniLibs.srcDir 'src/main/libs' //重定向so目錄為src/main/libs
}
}
}
2.添加gradle task自動調(diào)用ndk-build命令
完整配置請看build.gradle task
android {
//... 省略其他配置
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
task ndkBuild(type: Exec, description: 'Compile JNI source with NDK') {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def ndkDir = properties.getProperty('ndk.dir')//獲得ndk目錄
//執(zhí)行ndk-build命令
if (Os.isFamily(Os.FAMILY_WINDOWS)) {//如果是win系統(tǒng)
//commandLine "$ndkDir/ndk-build.cmd", 'NDK_DEBUG=1', '-C', file('src/main/jni').absolutePath //debug命令,如果需要做調(diào)試可以使用這條命令
commandLine "$ndkDir/ndk-build.cmd", '-C', file('src/main/jni').absolutePath//這里指定了jni的目錄位置
} else {
//commandLine "$ndkDir/ndk-build", 'NDK_DEBUG=1', '-C', file('src/main/jni').absolutePath
commandLine "$ndkDir/ndk-build", '-C', file('src/main/jni').absolutePath
}
}
task ndkClean(type: Exec, description: 'Clean NDK Binaries') {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def ndkDir = properties.getProperty('ndk.dir')//獲得ndk目錄
//執(zhí)行ndk-build clear命令
if (Os.isFamily(Os.FAMILY_WINDOWS)) {//如果是win系統(tǒng)
commandLine "$ndkDir/ndk-build.cmd", 'clean', '-C', file('src/main/jni').absolutePath
} else {
commandLine "$ndkDir/ndk-build", 'clean', '-C', file('src/main/jni').absolutePath
}
}
clean.dependsOn 'ndkClean'
}
- 在生成so庫后可以把它注釋了,不然jni文件多了生成會比較慢。
- 千萬別忘了在
local.properties里配置ndk.dir不然會找不到ndk。
3.實現(xiàn)
在build.gradle配置了上述語句后就可以自動生成so庫了。
接下來讓我們來實現(xiàn)一個HelloJni試下。
1.創(chuàng)建Application.mk和Android.mk文件
Application.mk文件非常簡單,就是一句APP_ABI := all,它的意思是生成所有架構的so庫,在平常使用中我們一般會有選擇性的設置為APP_ABI := armeabi armeabi-v7a x86。
Android.mk文件比較復雜,推薦看《Android.mk、Application.mk》寫的比較詳細,我這里寫的比較簡單。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := HelloJni#模塊的名稱,會在生成的so庫前面加上lib,最終名稱就是libHelloJni.so
LOCAL_SRC_FILES := HelloJni.cpp#要打包的源碼
include $(BUILD_SHARED_LIBRARY)
2.創(chuàng)建Java文件
我們創(chuàng)建一個HelloJni.java文件,用于ndk交互,這個文件也特別簡單,要注意的是我這里的hello()方法用的static,在jni里對應第二個參數(shù)為jclass,如果不是靜態(tài)方法,就會對應jobject。
public class HelloJni {
public static native String hello();
}
3.創(chuàng)建C++文件
首先需要創(chuàng)建.h文件,有二種創(chuàng)建方法,第一種就是手動創(chuàng)建,第二種就是利用javah.exe這個工具創(chuàng)建,我們這里就利用第二種方法來創(chuàng)建這個.h文件。
javah.exe這個工具在jdk的bin目錄下,我們打開AndroidStudio的Terminal窗口然后輸入javah.exe -d app/src/main/jni -cp app/src/main/java com.xiuyukeji.ndk_config.HelloJni回車后我們就能生成com_xiuyukeji_ndk_config_HelloJni.h文件。
這里有點需要注意,函數(shù)名對應java層的包名+類名+方法名,如果不是自動生成,要檢查下函數(shù)名是否對應,不然會報UnsatisfiedLinkError錯誤。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_xiuyukeji_ndk_config_HelloJni */
#ifndef _Included_com_xiuyukeji_ndk_config_HelloJni
#define _Included_com_xiuyukeji_ndk_config_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_xiuyukeji_ndk_config_HelloJni
* Method: hello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_xiuyukeji_ndk_1config_HelloJni_hello
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif


我來解釋下javah.exe這條命令的意思,-d代表輸出目錄在app/src/main/jni,-cp代表加載目錄在app/src/main/java,最后的com.xiuyukeji.ndk_config.HelloJni就是文件名了。

javah.exe這個工具可以看下這篇文章《超級簡單的Android Studio jni 實現(xiàn)(無需命令行)》
- 如果提示找不到
javah.exe就說明你的環(huán)境變量需要在path里多加一句%JAVA_HOME%\bin。
輸出.h文件后我們就可以開始寫HelloJni.cpp了,代碼特別簡單,返回一個字符串Hello jni,我這里加了個extern "C",意思是告訴編譯器按照C進行編譯,不加也可以,具體解釋看這里《extern "c"用法解析》。
#include "com_xiuyukeji_ndk_config_HelloJni.h"
extern "C" {
JNIEXPORT jstring JNICALL Java_com_xiuyukeji_ndk_1config_HelloJni_hello(JNIEnv * env, jclass jc){
return (env)->NewStringUTF("Hello jni");
}
}
然后我們可以開始生成so庫了,執(zhí)行Build->Build APK命令。


最終會在lib下生成各個架構的so庫。

4.調(diào)用So庫
上面我們已經(jīng)生成了so庫,下面我們就需要在java層調(diào)用libHelloJni.so里面的hello函數(shù)。
- 在MainActivity里動態(tài)加載so庫,一般加載so庫都是利用
static {},這種寫法叫靜態(tài)代碼塊優(yōu)先于其他代碼執(zhí)行。
static {
System.loadLibrary("HelloJni");
}
- 調(diào)用HelloJni.hello()語句。
public class MainActivity extends AppCompatActivity {
//... 省略其他代碼
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, HelloJni.hello(), Snackbar.LENGTH_LONG).show();
}
});
}
}

結尾
如果它有解決你的問題的話,請下點個贊,謝謝。
這是我個人的第三篇文章,寫于2017年4月19日。