【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++文件。

一、阅读文档

不要着急,请认真读完下面的文档:

  1. 官方文档——Google官方文档-如何向项目中添加C/C++代码 这一部分写的很简单清楚的,阅读大约需要5~8分钟。
  2. 官方文档——Google官方文档-配置CMake 阅读大约需要--分钟。
  3. 官方文档——Google官方文档-向Gradle中导入配置好的CMakeList.txt 阅读大约需要--分钟。
  4. (非必需)Cmake官方文档 因为CMakeList.txt是基于Cmake进行的,如果有兴趣可以查阅Cmake中的相关指令。 阅读大约需要-----分钟

二、新建项目:因为是示例调研,所以当然从新项目开始入手啦

第一步的三个文档看过了嘛?看过了最好,下面的内容就会很好理解~
不过要是实在不想看……那就继续往下看吧,应该理解也是没问题的。

在Android Studio(版本号在文首说明)中点击 File -> new -> New Project创建一个新的项目,记得勾选上C++ Support就好,后面的选项按需求勾选,如果暂时不理解那些选项说明暂时也没有相关需求…按默认的来就好。直接看图吧:

创建项目1

创建项目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.cppCMakeList.txtMainActivity.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){
    ...
}

其中JNIEXPORTJNICALL以及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结果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335