前言
要實現一個在安卓中調用C++模塊的功能。通過查閱資料發現,網上的資料質量參差不齊,同時因爲Android Studio版本的不一致,很難跟着某篇博客操作後得到博客中預期的結果,導致自己走了很多彎路。通過查閱大量資料和多次實踐,終於走通安卓調用C++模塊的整個流程。特此記錄下整個詳細操作步驟,給大家提供參考,同時對自己也是一次總結。
準備工作:
1、安裝Android Studio3.5,配置Gradle。參考:Android studio下載及安裝方法
2、配置NDK環境,主要包含如下幾步:
1)下載ndk
2) 配置ndk環境變量
參考:android NDK——搭建Android Studio的NDK環境
3、創建安卓虛擬機。參考:android studio 創建虛擬機
4、安裝配置jdk。參考:如何下載並安裝JDK?我安裝的是
jdk-12.0.2_windows-x64_bin。
熱身運動
上述準備工作做好了,就可以創建一個安卓開發界的“Hello world”試試。
參考:使用Android Studio完成Hello World
正題:安卓調用C++模塊Demo
總體要經過如下幾步:
1、 新建一個空白工程
2、 配置工程ndk路徑、gradle.properties文件
3、 新建一個java類,只聲明,不實現
4、 新建一個jni文件夾:在這裏通過javac命令生成java類的頭文件;在這裏新建cpp文件把java類裏的函數實現;在這裏新建用於編譯和構建的Android.mk、Application.mk兩個mk文件。
5、 將java類和實現java類的cpp源文件鏈接起來
6、 配置app構建文件build.gradle,設置app構建信息
7、 運用ndk-build生成so文件
8、 在MainActivity.java中調用so文件,需要時修改佈局文件
9、 運行
1、新建一個工程,按照下圖操作:
2、新建好工程後,進入工程界面。點擊Android–》project,更改工程目錄的顯示方式。
更改顯示方式後的文件結構如下圖所示。app目錄是我們的主要編碼目錄。
3、配置工程ndk路徑:File–>Project structure–>SDK Location,進入如下界面,點擊下拉按鈕,選擇NDK安裝路徑。
此時可以看到local.properties文件中已經添加ndk路徑:
配置項目下的gradle.properties文件,在文件最後加上這句:
android.useDeprecatedNdk=true
4、新建jni類,依次點擊app–>src–>main–>java–>com.example.jnitest,右鍵com.example.jnitest–>new–>java class,新建一個JNITest類
在JNITest類中添加如下內容:
JNITest(){
//static {
System.loadLibrary("JniLib");
}
//定義一個方法,該方法在C中實現
public native String getString(); //native關鍵字指示以原生形式實現的方法.向編譯器告知實現在原生庫中
最後如下圖所示:
下面我們開始在C++中實現JNITest類中定義的getString方法。
5、右鍵main文件夾,新建一個jni文件夾:
6、通過javac命令生成.h頭文件。網上好多資料都是用javah命令生成的.h頭文件,無奈我是安裝的jdk12版本,沒有javah命令了。通過多番查找資料發現,javac可以實現javah的功能。
進入Terminal界面,進入main文件夾:(這裏有個簡便方法可以直接進入到main文件夾:鼠標左鍵選中main文件夾,按住不放,拖動到Terminal界面那裏,即可進入到main文件夾)
輸入javac命令後生成.h頭文件,可以看到jni目錄下已經有生成好的頭文件了:
javac 命令生成.h頭文件的格式爲:
javac -encoding utf8 -h 目標文件夾 源文件夾
目標文件夾:生成的.h文件存放的目錄
源文件夾:要生成頭文件的java源文件所在目錄
.\ :代表當前目錄
生成的.h文件內容如下,可以看到頭文件裏生成了一個沒有實現的函數聲明,待會我們就是要在cpp文件中實現這個函數的函數體。
仔細研究會發現,這個函數的命名是有規則的:Java_是固定前綴。com_example_jnitest是包名。JNITest是類名。getString是函數名。
7、在jni目錄下新建3個文件:JniLib.cpp 、Android.mk、Application.mk
.mk文件新建時,文件類型直接選File,如下圖:
JniLib.cpp 文件內容如下:
直接將剛生成的.h文件中的沒有實現的函數聲明拷貝過來進行實現。加上extern "C"之後,編譯時就不會修改這個函數的名字,這樣才能在JNITest.java文件中調用getstring函數時,找到這個函數在C++中的實現。
Android.mk:
LOCAL_PATH := $(call my-dir) #不用修改
include $(CLEAR_VARS) #不用修改
LOCAL_MODULE := JniLib #動態庫名稱
LOCAL_SRC_FILES =: JniLib.cpp #Cpp文件,裏面就是我們寫的Cpp代碼
include $(BUILD_SHARED_LIBRARY) #生成.so動態庫
Application.mk:
APP_MODULES := JniLib
APP_ABI := all
8、將java文件鏈接到C++文件:
右鍵java文件夾–》Link C++…
點擊下拉菜單,選擇ndk-build,在Project Path 中找到Android.mk文件所在地址,點擊OK
此時可以看到.h文件的圖標變了,cpp文件中getstring函數前面也多了個J圖標:
java文件夾getstring函數那裏也多了個C++圖標。兩個函數鏈接到一起了。
9、 修改app下的build.gradle文件:
加上ndk節點、sourceSets節點、task ndkBuild節點:
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.example.jnitest"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk{
moduleName "JniLib"
abiFilters "armeabi-v7a", "x86" //輸出指定的三種abi體系下的so庫"armeabi",只加載armabi架構(目錄下)的so庫,如果是別的架構,就會找不到
}
sourceSets{ //不配的話都會有一個默認值 可以指定哪些源文件(或文件夾下的源文件)要被編譯,哪些源文件要被排除
main{
jni.srcDirs = [] //禁用as自動生成mk
//jniLibs.srcDirs=["src/main/libs" ] //so包就去src/main/libs目錄下找
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
task ndkBuild(type:Exec,description:'Compile JNI source via NDK'){
commandLine "C\\:\\Users\\lmkhd\\AppData\\Local\\Android\\Sdk\\ndk\\21.0.6113669\\ndk-build.cmd",//配置ndk的路徑
'NDK_PROJECT_PATH=build/intermediates/ndk',//ndk默認的生成so的文件
'NDK_LIBS_OUT=src/main/libs',//配置的我們想要生成的so文件所在的位置
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',//指定項目以這個mk的方式
'NDK_APPLOCATION_MK=src/main/jni/Application.mk'//指定項目以這個mk的方式
}
externalNativeBuild {
ndkBuild {
path file('src/main/jni/Android.mk')
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
10、執行ndk-build生成so文件:
先配置ndk-build工具:
program裏面選擇ndk路徑,我的是:C:\Users\lmkhd\AppData\Local\Android\Sdk\ndk\21.0.6113669\ndk-build.cmd
working dictionary是指定生成的so文件的存放路徑,我的是:$ProjectFileDir$\app\src\main
可以點擊Insert Macro獲取項目的路徑$ProjectFileDir$
,然後後面加上\app\src\main
配置好ndk-build工具後,選中JNITest類右鍵->External Tools->ndk-build,生成so文件:
如圖,main文件夾下多了一個libs文件:
11、在MainActivity.java中調用so文件getString()函數:
MainActivity.java:
package com.example.jnitest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
Button button;
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
tv = findViewById(R.id.tv);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
tv.setText("結果:"+ new JNITest().getString()); #按鈕點擊時調用
//tv.setText("結果:");
}
});
}
}
此時會報錯說button和tv無法解析。需要在佈局文件activity_main.xml中進行修改:
先切換到design視圖下,選中Button拖動到佈局中,添加一個Button控件:
切換到Text視圖,對activity_main.xml代碼進行相關修改,最終activity_main.xml
內容如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv" #自己添加
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button" #自己添加
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="調用JNI" #修改
tools:layout_editor_absoluteX="25dp"
tools:layout_editor_absoluteY="37dp"
tools:ignore="MissingConstraints"/> #自己添加
</androidx.constraintlayout.widget.ConstraintLayout>
12、運行:
結果如下所示:
點擊“調用JNI”按鈕後:
總的來說,此Demo主要思路是:在java類中只聲明函數,不實現,然後在C++中實現函數。再將java類和其C++方式實現的函數體變成一個so包。最後在MainActivity.java中進行調用。