JNI—第一個程序HelloWorld
NDK開發流程
- 在Java裏面寫native代碼
- 在main目錄下創建jni目錄,寫C代碼—生成頭文件
- 配置動態鏈接庫的名稱
- 加載動態鏈接庫
//把.so加載進來
System.loadLibrary("hello");
- 使用
交叉編譯
-
在一個平臺上編譯出在另外一個平臺上可以運行的本地代碼
-
平臺CPU平臺:x86、arm、mips
-
操作系統平臺:Windows、Linux、Mac os、Unix
-
NDK:模擬另外一個平臺的特性進行編譯
NDK壓縮包下的文件
-
sources:NDK相關的源代碼
-
platforms:根據不同的api-level 有多個文件夾 每個文件夾中都有不同CPU架構的文件夾
-
include:跟Android下做jni開發相關的頭文件
-
lib:Google編譯好的jni開發可能用到的函數庫
-
toolchains:交叉編譯工具鏈
-
build/tools .sh linux的批處理命令 通過這些批處理命令調用交叉編譯工具鏈中的編譯工具
-
ndk-build.cmd:通過這個命令可以開啓交叉編譯的過程 如果想通過命令行來編譯可以在Android上運行的本地代碼 要吧ndk-build放到環境變量中
看到這個錯說明配置成功了
JNI的理解
現在是Java是程序入口,所以C代碼不用寫main方法,只用寫函數對native方法的具體實現
cd /d:直接進入到後面的目錄下
cd /d E:\AndroidDemo\JNIDemo
JNI_Helloworld的步驟
-
首先新建一個NDKDemo的項目,我們在包名目錄下建立一個調用C方法的類JNI
-
給AS配置關聯NDK
1). local.properties中添加配置
ndk.dir=G\:\\android-ndk-r10(=號後面爲ndk的解壓路徑)
2). gradle.properties中添加配置
兼容老的ndk(老的版本):android.useDeprecatedNdk=true
android.useDeprecatedNdk=true
- 在 JNI 中聲明一個native方法,native方法不用實現
//通過native關鍵字 聲明瞭一個本地方法 本地方法不用實現,需要用jni調用C的代碼來實現
public native String helloInc();
native關鍵字表明該Java方法由非Java語言實現
最後類的樣式如下:
我們爲準備生成的so文件起名爲 Hello ,JNI 會預加載該庫,平時使用so庫的小夥伴一定不會陌生了,這是一個標準的使用方法,說明我們的native方法來自於Hello.so庫,其實JNI也是最後生成so庫供以使用。
-
生成提供該方法的C類(可以先看這個:https://blog.csdn.net/weixin_42814000/article/details/105279704)
那麼既然這樣,我們就要用c語言實現一個同名方法用以調用,首先我們根據這個類生成一個c的頭文件(這是c語言的內容如果不瞭解的話先照着做),在android studio下面terminal窗口執行如下命令:
這裏我遇到了第一個坑,先把正確實現的步驟寫出來
生成頭文件的方式
點擊 Terminal 輸入命令,便可以生成一個JNI的 C 頭文件.
輸入第二個命令行提示錯誤的原因是註釋爲中文。所以在輸入命令行時不能出現註釋或者中文
javac -h ./ JNI.java
上面的命令的作用:根據Java中的 native 方法生成對應在 C 中的方法該怎麼寫(自動生成)
這樣爲正確的,輸入命令行沒問題
這樣會提示錯誤
下圖爲生成的頭文件
JNI的頭文件的代碼如下
坑1(繞路):有些資料給出的步驟是去/build/intermediates/classes目錄下對.class文件執行javah,其實需要在生成頭文件的文件夾下(cd app/src/main/java/com/lwm/ndkdemo)執行就可以。
坑2(找不到類文件):有些資料給出的命令是 javah -d jni 包名.類型,在不同的本地環境下可能出現找不到類文件的提示,網上有人給出的解決方法是在classpath中做配置,其實直接使用javah -d jni -classpath 命令就可以了。
坑3(JNI目錄問題):注意JNI目錄的位置是在/src/main之下,很多同學的JNI目錄生成不對(比如使用了坑1方法生成再挪過來),位置錯誤導致最後運行時一直崩潰。
- 生成完頭文件之後我們需要在寫一個.c 文件並引用該頭文件(還是c的內容,不瞭解的同學直接copy),在項目根目錄下創建 jni 文件夾 在 jni 目錄下創建.c 的代碼(New–>c/c++SourceFile---->後綴選擇.c),取名 Hello.c:
C函數命名規則: Java_包名_native方法所在類名_native方法名(JNIEnv* env,jobject jobj)
//
// Created by 林偉茂 on 2020/4/2.
//
#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
// 因爲 sayHello() 方法返回的爲String類型所以對應JNI中的jstring類型
// 然後把.改爲_,然後前面加一個 Java_
/**
jstring:返回值
本地函數命名規則:Java_包名—_native函數所在類的類名_native方法名
JNIEnv* env:相當於環境變量,裏面有很多方法
jobject jobj:誰調用了這個方法就是誰的實例
當前就是 JNI.this
*/
// 第一個參數 JNIEnv* JNIEnv是結構體 JNINativeInterface 這個結構體的一級指針
// env又是JNIEnv的一級指針 那麼 env就是 JNINativeInterface 的二級指針
// 結構體 JNINativeInterface 定義了大量的函數指針,這些函數指針在 JNI 開發中十分常用
// 第二個參數 jobject 就是調用當前 native 方法的 java 對象
jstring Java_com_lwm_ndkdemo_JNI_sayHello(JNIEnv* env,jobject jobj){
// jstring (*NewStringUTF)(JNIEnv*, const char*);
char* text = "I am from c";
//通過 NewStringUTF方法把C的字符串轉換成java的jstring類型
return (*env)->NewStringUTF(env,text);
}
- 在jni目錄下創建一個Android.mk、Application.mk
Android.mk文件
#makefile:作用就是向編譯系統描述 我要編譯的文件在什麼位置 要生成的文件叫什麼名字,是什麼類型
#call my-dir:獲取當前的目錄,因爲放在jni這個目錄,所以就找當前的目錄
LOCAL_PATH := $(call my-dir)
#把上次編譯的時候的信息清空,但是call my-dir裏的內容不會被清除掉的
include $(CLEAR_VARS)
#在這裏指定最後生成的文件叫什麼名字
LOCAL_MODULE := hello
#要編譯的C的代碼的文件名
LOCAL_SRC_FILES := hello.c
#要生成的是一個動態鏈接庫( 通過BUILD_SHARED_LIBRARY指定存儲的擴展名爲 .so)
include $(BUILD_SHARED_LIBRARY)
Application.mk 文件
APP_ABI := all
APP_STL := stlport_static
在工程上app目錄上右擊選擇link c++ project with Gradle,這個選項的意思是導入外部的c++工程,也就是把我們寫好的c當做外部工程導入:
在下拉框中選擇ndk-build,並選擇對應的Android.mk文件的位置:
這裏說明一下mk文件的作用,mk文件其實裏面也就是一些 ndk 的文件路徑配置、生成 so庫名字、生成平臺等的配置。
我們需要在 jni 目錄里加入如上兩個 mk 文件 Android.mk 和 Application.mk,如圖是最基本的配置,解釋下每個配置的注意的地方:
Android.mk:LOCAL_PATH 和兩個 include 照寫就行了,這三個配置 google 出來的意思比較生澀,解釋也比較難懂,我們要關注的是 LOCAL_MUDLE 和 LOCAL_SRC_FILES,LOCAL_MUDLE 其實就是我們之前配置的 moduleName,指定了生成 so 庫的名字,LOCAL_SRC_FILES則是我們引用的c文件位置。
Application.mk:APP_ABI 就是 abiFilters 了,所以之前我們做的配置都可以在這裏寫,賦值爲 all 表明全平臺生成,如果有多個用空格分開,APP_ABI:= armeabi-v7a armeabi。APP_STL指運行庫類型,通常都是stlport_static,表示以靜態鏈接方式使用的sttport版本的STL(寫出這個配置翻譯,是挺生澀的吧)。
項目build完之後,回想一下,我們選擇的mk文件是 Android.mk,Applicaiton.mk 還沒配置進去,打開app/build.gradle,你會發現多了一個配置:
所以Android.mk最後還是在gradle裏面引入,其實我們就可以直接在build.gradle里加入這句引入代碼,剛纔用的方式是可視化界面的操作方式。接着我們在裏面配置Application.mk:
externalNativeBuild {
ndkBuild{
// 指定 Application.mk 的路徑
arguments "NDK_APPLICATION_MK=src/main/jni/Application.mk"
//cFlags 和 cppFlags 是用來設置環境變量的,一般不需要動
cFlags "-DTEST_C_FLAG1","-DTEST_C_FLAG2"
cppFlags "-DTEST_CPP_FLAG2","-DTEST_CPP_FLAG2"
}
}
最後再build一下,終於大功告成了!
7. 通過ndk-build在項目根目錄下編譯.c文件 生成.so文件
如果生成了如上兩個文件,並且ndkBuild文件夾下有對應so文件則說明成功了。
但是其實還有最後一個坑
我發現Application.mk中APP_ABI的配置並不起作用,於是在build.gradle中做了最後一步修改:
ndkBuild下生成的so文件也變了,終於ok了!
JNI 的 sayHello 方法已經可以調用了,我們在c裏面讓它返回一個 I am from c 的字符串,趕快試試吧!
- 調用.so之前需要使用System.loadLibrary來加載.so文件
//把.so加載進來
System.loadLibrary("hello");
- 調用 C 的函數
public class MainActivity extends AppCompatActivity {
private Button cid;
private String result;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
result = new JNI().sayHello();
System.out.println("result" + result);
initView();
}
private void initView() {
cid = (Button) findViewById(R.id.cid);
cid.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "result=" + result, Toast.LENGTH_SHORT).show();
}
});
}
}
部分轉載:https://www.jianshu.com/p/eae320ee9b2d