前言
在开发过程中,有一些底层库,算法、加解密之类的功用,不是用Java写的,而是C或许C++,而咱们需求在Android工程中调用C/C++的函数到达抱负的要求,那么这个时分你就需求知道怎样运用它们。
正文
在之前我其实就遇到过这个问题,一顿操作之后能够掉用了,可是忘记记录了,导致我再次遇到这样的问题时,人傻了,便是那种似曾相似又解决不了的感觉,痛定思痛之下,我决定记录一下,好记性不如烂笔头。
而编译C和C++项目只有两种状况,一种是已知的状况,另一种是不知道的状况。分别阐明一下,便是有一天老板告知我要做一个项目,里边会用到一些C/C++的底层库,NDK等内容,你去了解一下,这归于已知状况,那么你在创立项意图时分就能够做好。可是不部分都是不知道的状况,仍是有一天老板告知你之前的某个项目需求增加新的功用,软硬件相结合,硬件给你提供了C/C++的代码,让你在项目中运用,这归于不知道状况。
相对来说已知比不知道要好,兵书有云:运筹帷幄之中,决胜千里之外, 所以两种状况我都会阐明一下怎样处理,对你来说也许有用,也许没有用,交给缘分吧。
一、基本知识
在写代码之前咱们需求先知道要做的是什么?一些名词是否了解里边的含义,例如JNI是什么?NDK是什么?Java怎样调用C/C++?不知道没有联系,当场百度一下就知道了,有一个概念是很重要的,你就不会像无头苍蝇一样。
① 要做什么?
咱们终究的意图是经过Java能够调用C/C++的函数,获取返回值显现在Activity中,这是咱们所需求的结果。
② JNI是什么?
JNI
是Java Native Interface
的缩写,经过运用 Java本地接口书写程序,能够保证代码在不同的渠道上便利移植。从Java1.1开始,JNI标准成为java渠道的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,可是它并不阻碍你运用其他编程语言,只需调用约定受支撑就能够了。运用java与本地已编译的代码交互,通常会丧失渠道可移植性。可是,有些状况下这样做是能够接受的,乃至是有必要的。例如,运用一些旧的库,与硬件、操作系统进行交互,或许为了进步程序的功能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。(PS:本段来自百度百科)
咱们总结一下:经过JNI,Java和C/C++的代码能相互调用。
③ NDK是什么?
NDK(Native Development Kit缩写)一种根据原生程序接口的软件开发东西包,能够让您在 Android 运用中运用 C 和 C++ 代码的东西。经过此东西开发的程序直接在本地运转,而不是虚拟机。在Android中,NDK是一系列东西的调集,首要用于扩展Android SDK。NDK提供了一系列的东西能够帮助开发者快速的开发C或C++的动态库,并能主动将so和Java运用一起打包成apk。同时,NDK还集成了穿插编译器,并提供了相应的mk文件隔离CPU、渠道、ABI等差异,开发人员只需求简略修正mk文件(指出“哪些文件需求编译”、“编译特性要求”等),就能够创立出so文件。(PS:本段来自知乎)
咱们总结一下:经过NDK,能够创立so文件,能够将so文件和Java代码一起打包成apk。
二、装备NDK
假如你是新装置的Android Studio,那么它里边默许是没有NDK装备,File → Settings… → Android SDK 。
切换到SDK Tools,然后找一下NDK和CMake两个东西,我这儿是现已下载了,这儿Apply是不可用的,你假如没有下载装备就勾选上,点击Apply会有弹窗提示你要下载那些内容。假如你想下载指定版别,就勾选上Show Package Details ,就能看到其他的东西版别了,例如:
三、创立新工程
下面咱们创立支撑 C/C++ 的新项目,这儿咱们在创立工程的时分挑选Native C++,如下图所示:
点击Next,输入工程名,这儿有这样一句话:Creates a new project with an Empty Activity configured to use JNI,意思是创立一个装备为运用jni的空活动的新项目。
点击Next,然后挑选C++的版别,你能够运用默许,也能够用其他的版别。
这儿咱们就运用默许的,点击Finish完结工程创立。
创立工程呈现问题了,这儿的过错意思是在Android Studio中运用SDK办理器装置缺少的组件cmake 3.18.1。也便是说咱们虽然装置了Cmake,可是装置的版别不符合这个NDK的编译要求。
勾选上这个需求的版别,点击Apply,然后呈现弹窗提示,点击OK,之后便是下载了。
下载完点击Finish,在回到SDK办理窗口点击Apply,最终看到工程窗口,并没有主动去编译。
你能够点击这个图标或许Try Again,再编译一次。
① 工程目录阐明
呈现这样的字样就代表编译成功了,也意味着咱们的项目创立成功了,咱们来看看工程目录。
- cpp 这儿边便是关于C++的一些装备,咱们能够在这儿边写C/C++的代码。
- includes 它里边便是项目所运用ndk的版别,可能会多个ndk版别对应一个cmake版别。
- CMakeLists.txt 这是一个装备文件,你能够理解为build.gradle文件,首要效果便是装备C++。
- native-lib.cpp 这是一个C++文件,里边便是C++代码了,咱们需求详细的了解这个文件。
- app下的build.gradle 在这个gradle也装备Native,在里边能够找到这样一段代码:
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.18.1'
}
}
便是装备一下C++的Build文件,下面咱们先运转一下看看。
运转没有问题,下面咱们需求剖析一下,打开MainActivity。
public class MainActivity extends AppCompatActivity {
// Used to load the 'studynative' library on application startup.
static {
System.loadLibrary("studynative");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'studynative' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
这儿的代码有几个关键点,第一个是加载库,System.loadLibrary("studynative");
,这儿的studynative你能够在CMakeList.txt中看到,然后咱们写了一个stringFromJNI()
办法,用于调用C++的代码,得到一个String的返回值,然后设置在TextView上,MainActivity基本的内容剖析完结了,下面咱们需求剖析一下这个stringFromJNI函数是怎样调用C++的,看一下native-lib.cpp文件。
② 剖析cpp文件
首要咱们看一下这个cpp文件的完好代码,如下所示:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_llw_studynative_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
#include
,是包含头文件命令,这两个便是头文件,这两句代码便是说在这个cpp文件的这个方位插入这两个头文件的代码。
extern "C"
,用于避免编绎器依照C++的办法去编绎C函数。
JNIEXPORT
,用来表明该函数是否可导出(即:办法的可见性,有一些类似于public修饰符)。
JNICALL
,用来表明函数的调用规范。
你按住Ctrl键,点击一下JNIEXPORT
或JNICALL
,会跳转到jni.h
文件中,如下图所示:
你会看到JNIEXPORT
或JNICALL
有一个#define
。
#define
,在C语言中,能够用 #define
界说一个标识符来表明一个常量。其特点是:界说的标识符不占内存,仅仅一个暂时的符号,预编译后这个符号就不存在了。那么关于
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL
这儿的JNIEXPORT和JNICALL表明宏界说,宏可这样理解:
① 宏 JNIEXPORT 代表的便是右侧的表达式: __attribute__ ((visibility ("default")))
② JNIEXPORT 是右侧表达式的别名,宏可表达的内容许多,如:一个详细的数值、一个规矩、一段逻辑代码等;
然后便是visibility表明是否可见,default表明外部可见,类似于public,能够被外部调用。假如改成hidden :表明躲藏,类似于private修饰符,只能被内部调用。
jstring
,这是一个数据类型,是 Java中String数据类型在 JNI 中的代表,宏JNICALL 右边是空的,阐明仅仅个空界说,空界说是能够去掉的,咱们试一下去掉再运转,如下图所示:
Java_com_llw_studynative_MainActivity_stringFromJNI
,这是一个函数名,有没有似曾相识的感觉,如同和咱们在MainActivity看到的stringFromJNI()
函数相似,可是呢,姓名没有这么长啊,而实际上是同一个函数,你能够按住Ctrl键,点击Java_com_llw_studynative_MainActivity_stringFromJNI
跳转到MainActivity中的stringFromJNI()
,也能够点击stringFromJNI()
跳转到native-lib.cpp中的Java_com_llw_studynative_MainActivity_stringFromJNI
。
那么回到之前的问题,为什么函数姓名变长了,这跟JNI native函数的注册办法有关,JNI Native函数有两种注册办法:① 静态注册:依照JNI接口规范的命名规矩注册;② 动态注册:在.cpp的JNI_OnLoad办法里注册;
JNI接口规范的命名规矩:Java_<PackageName>_<ClassName>_<MethodName>
,例如Java_com_llw_studynative_MainActivity_stringFromJNI,下面咱们来体会一下这种注册办法,在MainActivity中增加这样一行代码:
public native String testFromJNI();
代码方位如下图所示:
这个函数咱们还有在cpp文件中创立中创立,所以爆红很正常,鼠标点击这个函数,Alt + Enter,会呈现一个提示框。
第一项的意思是创立名为testFromJNI的JNI函数,回车一下就会快速在native-lib.cpp中创立了。
能够看到这个函数主动生成了,是不是很便利呢?然后咱们在这个函数体里边写几行代码,如下图所示:
然后修正一下MainActivity中的onCreate()办法中的代码,调用testFromJNI函数。
再运转一下:
代码可行,阐明咱们刚才写的函数没有问题,下面咱们持续剖析函数中的参数。
JNIEnv
,代表了Java环境,经过JNIEnv*就能够对Java端的代码进行操作,如:① 创立Java目标;② 调用Java目标的办法;③ 获取Java目标的属性等;
jobject
,代表了界说native函数的Java类 或 Java类的实例:假如native函数是static,则代表类Class目标;假如native函数非static,则代表类的实例目标。咱们能够经过jobject访问界说该native办法的成员办法、成员变量等。
③ JNI数据类型
前面提到jstring表明Java中的String类型,那么其他的数据类型在JNI中怎样表明呢,进入jni.h
,找到最上方的方位,咱们能够看到一些数据类型的界说。
这儿能够看到常见的变量,int、boolean、float、double、byte、char等,这儿是JNI所界说的,他们之间有一个映射联系,参考下表:
JNI | Java | C/C++ |
---|---|---|
jint / jsize | int | int |
jshort | short | short |
jlong | long | long / long long (__int64) |
jbyte | jbyte | signed char |
jboolean | boolean | unsigned char |
jchar | char | unsigned short |
jfloat | float | float |
jdouble | double | double |
jobject | Object | _jobject* |
jstring | String | string |
那么新项目中怎样样运用C++就说完了,下面咱们阐明一下在现有的项目中怎样增加C++的运用。
四、现有工程运用C++
这儿咱们将项目结构改成Project
然后右键StudyNative,点击New → Module ,呈现弹窗。
点击Next,这儿便是默许选中Empty Activity的。
点击Next。
点击Finish。
工程创立完结,这个功用结构你应该很熟悉了,那么怎样增加C++进去呢?
① 创立C++文件
右键点击old工程的 main 目录,然后顺次挑选 New > Directory,会呈现一个小窗口。
输入cpp然后回车。
右键点击 cpp 目录,然后顺次挑选 New > C/C++ Source File,呈现弹窗,输入C++文件名。
点击OK。
② 创立CMake
右键点击old目录,然后顺次挑选 New > File。输入“CMakeLists.txt”作为文件名,然后回车。
这个文件的姓名不能乱改的,假如你不习惯的话能够把这个文件再移动到cpp文件夹下。
然后咱们装备一下old的build.gradle文件,增加如下代码:
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.18.1'
}
}
这儿的途径很重要,假如你的CMakeLists.txt不在cpp下面,你就要改成对应的途径,Sync Now。
③ 运用C++
下面咱们在MainActivity中加载运用C++,首要需求这样的代码:
static {
System.loadLibrary("old");
}
这是加载C++的库,这个姓名便是CMakeList.txt中的project()中的姓名。
下面咱们增加一个native函数。
public native String oldString();
这儿要注意一点便是你要生成的JNI函数是在那个cpp文件中,这儿有两个,咱们需求在old下的old-lib.cpp中生成,选中回车,在old-lib.cpp中生成如下代码:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_llw_old_MainActivity_oldString(JNIEnv *env, jobject thiz) {
std::string result = "This old project used C++";
return env->NewStringUTF(result.c_str());
}
然后咱们调用运转一下,就在onCreate()打印一下看看。
OK,这样咱们在老项目中就能够运用C++了。
这儿能够自己切换运用那个工程,都能够正常运转的。
五、源码
欢迎 Star 和 Fork
源码地址:StudyNative