要实现一个在安卓中调用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
总体要经过如下几步:
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、 运行
更改显示方式后的文件结构如下图所示。app目录是我们的主要编码目录。
此时可以看到local.properties文件中已经添加ndk路径:
配置项目下的gradle.properties文件,在文件最后加上这句:
android.useDeprecatedNdk=true
JNITest(){
//static {
System.loadLibrary("JniLib");
}
//定义一个方法,该方法在C中实现
public native String getString(); //native关键字指示以原生形式实现的方法.向编译器告知实现在原生库中
下面我们开始在C++中实现JNITest类中定义的getString方法。
进入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是函数名。
.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
右键java文件夹–》Link C++…
点击下拉菜单,选择ndk-build,在Project Path 中找到Android.mk文件所在地址,点击OK
此时可以看到.h文件的图标变了,cpp文件中getstring函数前面也多了个J图标:
java文件夹getstring函数那里也多了个C++图标。两个函数链接到一起了。
加上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'
}
先配置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文件:
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
内容如下:
#自己添加
结果如下所示:
点击“调用JNI”按钮后:
总的来说,此Demo主要思路是:在java类中只声明函数,不实现,然后在C++中实现函数。再将java类和其C++方式实现的函数体变成一个so包。最后在MainActivity.java中进行调用。
Demo链接
参考:
Android Studio 3.0 JNI的实现