【Android】在Android项目中添加C/C++代码
更新时间: 2018-10-31
由于现在网络上的博客混杂,很多版本错乱。本文的一开始首先贴出我的编译环境:
(你也可以在你的Android Studio中的Help菜单里面点选About
选项来查看)
################# Android Studio #################
Android Studio 3.2.1
Build #AI-181.5540.7.32.5056338, built on October 9, 2018
JRE: 1.8.0_152-release-1136-b06 amd64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0
##################################################
在文章正式开始之前,还是要说一句……就算Google的Android开发者文档有的部分是有中文版的,请务必!千万!一定!看英文文档!!!!!
中文文档经常会出现版本滞后的问题,就算你不想用最新版的也会出错,因为这些中文文档自己的滞后都不一定是看齐的…所以还是老老实实看英文文档吧,里面的英语还是很基础的,总比踩坑出无数个BUG好。
目录:
序: 【写作理由】
一、阅读文档 【推荐阅读一些前置知识,否则很难理解】
二、新建项目 【创建一个支持C/C++的项目】
三、理解默认创建的文件 【解读native-lib.cpp
MainActivity.java
CMakeList.txt
三个文件】
四、创建一个自己的cpp文件 【新建cpp文件,返回java的int值,并且修改cmakelist,将该文件打包成so库】
序:
实习的部门最近苦于.so库以及如何导入的一些知识。所以我调研了一下如何在Android项目中导入我的C/C++文件。
一、阅读文档
不要着急,请认真读完下面的文档:
- 官方文档——Google官方文档-如何向项目中添加C/C++代码 这一部分写的很简单清楚的,阅读大约需要5~8分钟。
- 官方文档——Google官方文档-配置CMake 阅读大约需要--分钟。
- 官方文档——Google官方文档-向Gradle中导入配置好的CMakeList.txt 阅读大约需要--分钟。
- (非必需)Cmake官方文档 因为CMakeList.txt是基于Cmake进行的,如果有兴趣可以查阅Cmake中的相关指令。 阅读大约需要-----分钟
二、新建项目:因为是示例调研,所以当然从新项目开始入手啦
第一步的三个文档看过了嘛?看过了最好,下面的内容就会很好理解~
不过要是实在不想看……那就继续往下看吧,应该理解也是没问题的。
在Android Studio(版本号在文首说明)中点击 File -> new -> New Project
创建一个新的项目,记得勾选上C++ Support就好,后面的选项按需求勾选,如果暂时不理解那些选项说明暂时也没有相关需求…按默认的来就好。直接看图吧:
创建项目2
没有每个步骤都截图,毕竟是在GUI里面,都很好理解。这样点击完成,就会得到你自己的支持C++的项目了。
现在app/src/main/
文件夹内出现了java/
,res/
之外的文件夹cpp/
。
其中有一个文件:native-lib.cpp
:
#include <jni.h>
#include <string>
extern "C" {JNIEXPORT jstring JNICALL
Java_com_xxxx_xx_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}}
刚开始看可能有点懵,不过我们后面会详细解释。
在项目的app/
文件夹下也出现了一个新的文件CMakeList.txt
文件:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib})
这个文件虽然现在看起来很可怕,但是实际上大部分都是注释。当个文档看吧~
还有Android正常的Activity对应的JAVA文件,在app/src/main/java/包名.../
文件夹下。默认为MainActivity.java
:
package com.xxxx.xx.xx;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
好的,这就是主要要注意的三个文件了。第二步创建项目很简单,基本上就讲述到这里。
不过还要说一句,如果你有心研究透彻的话,也可以注意一下native-lib.cpp
中Include的jni.h
文件
最后总结一下,最重要需要读懂的三个文件分别是:native-lib.cpp
,CMakeList.txt
,MainActivity.java
。
如果你有心理解底层逻辑,那么可以再加上一个jni.h
。
三、理解默认创建的文件
好的,第二步里面我们已经创建了一个完整的项目,并且将需要注意的文件都提取出来了,现在开始我们要静下心来仔细研读这几个文件中的内容。
首先来看看MainActivity.java
,我们最熟悉的Android Activity文件。语言使用的是JAVA……就是这么熟悉的一个文件,还是发现了其中有好多陌生的语句:
...
public class MainActivity extends AppCompatActivity {
...
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib"); /*********************注意②!*******************/
}
@Override
protected void onCreate(Bundle savedInstanceState) {
...
tv.setText(stringFromJNI());/*********************注意①!*******************/
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();/*********************注意③!*******************/
}
刨去注释一共还剩下三句新的语句,我用注意①
这样的字符标记出来了。
- 注意①:
tv.setText(stringFromJNI());
这一句只是对③语句的调用,无需纠结。 - 注意②:
System.loadLibrary("native-lib");
是用来加载动态库的语句,无需纠结原理,只要把你需要的动态库名字写进去就好了,至于这个native-lib名字是如何来的(并不是由另外一个文件名来的!!!!!),我们会在讲述CMakeList.txt
文件的时候详细叙述。 - 注意③:
public native String stringFromJNI();
这一句是最重要的,对Native函数的声明,切记函数名和参数一定要与cpp文件中函数名字一致(不是相同,cpp文件中的函数名字有一部分前缀,参数也会比java中多两个)。
记住这三个语句中的②和③需要与别的文件一致就可以(而①是③的实际使用,所以当然也得一致啦),那么第一个文件我们就理解完成了,可以说是很简单啦~
下面我们来看看第二个文件native-lib.cpp
:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxxx_xx_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
纯粹的C/C++代码,如果你专心于java以及Android开发,已经忘记了C/C++的知识,恐怕这短短的10行文件就是这篇文章中最大的难点啦~
不过我们一句一句来看,这个难点也就不那么难了(其实本来很简单的,主要是宏太多,换行太多才难的):
-
#include
语句:不多说,类似于java的import
。 -
extern "C"
:单独把这个拿出来你应该就会断句了吧。这个就是在C++里面修饰C代码的。不论是不是java引用,只要在C++里使用C代码最好就要有这么一个声明。如果你看着实在别扭,我习惯于在后面添加上大括号,变成这样:
extern "C" {JNIEXPORT jstring JNICALL
Java_com_xxxx_xx_MainActivity_stringFromJNI(
...) {
...
}}
是不是(稍微)易读了(那么一点点点点点)呢~?
- 函数
Java_..._stringFromJNI
声明部分:我们印象中的C/C++代码应该都是:
返回TYPE 函数名(参数类型1 参数名1, 参数类型2 参数名2...){
...
}
这样对吧?可是现在就算把extern "C"
这句拿走,也还是剩了一个这么臃肿的函数头。
JNIEXPORT jstring JNICALL Java_com_xxxx_xx_MainActivity_stringFromJNI(JNIEnv *env, jobject){
...
}
其中
JNIEXPORT
和JNICALL
以及JNIEnv
都是预处理(如果你连C/C++的预处理的基本知识都忘了那就稍微百度一下,或者看看这篇博客#define的用法和#typedef的百度百科,虽然#define
和#typedef
只是预处理命令的沧海一粟,但是理解这里足够了。)
提前说明这几个参数在理解代码的时候都是可以直接忽略的,如果你不care他们的含义,可以 跳过 下面这一段对他们的说明。
【OPTIONAL 可选阅读】
根据源码知道:
-
JNIEXPORT
是__attribute__ ((visibility ("default")))
的宏定义,可以参考这篇CSDN博客,得知它表示设置将本项目的函数作为库使用时的可见性。读代码的时候可以忽略。 - 对于
JNICALL
,查看源码时候发现了#define JNICALL
后面为空。不过参照百度知道的这个问题,同时点击回答中代码里面__stdcall的超链接,参考百度百科-stdcall,我猜测这个也有可能代表__attribute__((stdcall))
,或者干脆没什么含义,所以删掉也是完全可以正常运行的,可以忽略不计。 - 而
JNIEnv
指向了一个特别特别特别复杂的结构体,大概是在C++里面生成一些JAVA类型用的。 - 第二个参数
jobject
是一个空class。
总之:前面两个参数是必须要写的,如果你的函数需要别的输入参数,那就请将这些参数依次添加在这两个参数后面,并且在调用的时候传入你写的参数就好(不需要写这两个参数)。
值得注意的是函数的返回值是jstring,顾名思义就是JAVA中的String类型。不需要说明太多,只要知道这个是靠着函数体里面的env->NewStringUTF(hello.c_str());
生成的就好,这里是想要给你提供一个表格,是java类型在这边的转换,之后可以查表:
java-native类型对应
Java 类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++带符号的8位整型 |
char | jchar | C/C++无符号的16位整型 |
short | jshort | C/C++带符号的16位整型 |
int | jint | C/C++带符号的32位整型 |
long | jlong | C/C++带符号的64位整型e |
float | jfloat | C/C++32位浮点型 |
double | jdouble | C/C++64位浮点型 |
Object | jobject | 任何Java对象,或者没有对应java类型的对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
使用数组
函数 | Java 数组类型 | 本地类型 |
---|---|---|
GetBooleanArrayElements | jbooleanArray | jboolean |
GetByteArrayElements | jbyteArray | jbyte |
GetCharArrayElements | jcharArray | jchar |
GetShortArrayElements | jshortArray | jshort |
GetIntArrayElements | jintArray | jint |
GetLongArrayElements | jlongArray | jlong |
GetFloatArrayElements | jfloatArray | jfloat |
GetDoubleArrayElements | jdoubleArray | jdouble |
使用对象
函数 | 描述 |
---|---|
GetFieldID | 得到一个实例的域的ID |
GetStaticFieldID | 得到一个静态的域的ID |
GetMethodID | 得到一个实例的方法的ID |
GetStaticMethodID | 得到一个静态方法的ID |
这样第二个文件也解决啦~
下面我们看看最后的CMakeList.txt
:
删除注释后,文件形式如下:
cmake_minimum_required(VERSION 3.4.1)
add_library(
native-lib
SHARED
src/main/cpp/native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
${log-lib})
就是一些Cmake语句,如果你已经看过了他们的官方说明书,这段也可以跳过。
cmake_minimum_required()
和add_library()
语句是必要的。前者说明了CMake的最低版本要求,后者将C++源文件打包进了一个native的lib库中,这里这个库的名字叫native-lib,塞进去的文件是native-lib.cpp
。(第二个参数SHARED表示这个库是一个共享库。)
后面两个语句:其中find_library()
是找到已有的原生库,这里是log,并且以log-lib的名字导入。
最后一个语句target_link_libraries
是将我们的库和log-lib链接起来。(这两个语句删除了也只是会影响Log输出,不会影响正常运行)
好的至此我们也完成了对默认生成的三个文件的解读。这时候将程序打包成apk并选择analyze apk,就会发现其中出现了.so库。
四、创建一个自己的cpp文件
为了测试输入输出,我想模拟示例文件创建一个lib库。
首先在cpp文件夹新建MyMath.cpp
:
#include <jni.h>
extern "C"{
jint Java_com_xxx_addFromJNI(JNIEnv *env, jobject, jint a, jint b) {
return a+b;
}
}
在CMakeList.txt中打包成库:
add_library( ...
...
src/main/cpp/native-lib.cpp)
add_library(
my-math
SHARED
src/main/cpp/MyMath.cpp)
...
最后再在MainActivity
中引用:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
tv.setText(String.valueOf(addFromJNI(150, 356)));
}
public native String stringFromJNI();
public native int addFromJNI(int a, int b);
点击运行,发现屏幕上出现了506,结果正确。
这时候生成apk并分析,就会发现里面有两个.so库:
analyze apk结果