【Android】在Android项目中添加C/C++代码

2018-10-31  本文已影响80人  任冉rr

更新时间: 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();/*********************注意③!*******************/
}

刨去注释一共还剩下三句新的语句,我用注意①这样的字符标记出来了。

记住这三个语句中的②和③需要与别的文件一致就可以(而①是③的实际使用,所以当然也得一致啦),那么第一个文件我们就理解完成了,可以说是很简单啦~

下面我们来看看第二个文件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行文件就是这篇文章中最大的难点啦~
不过我们一句一句来看,这个难点也就不那么难了(其实本来很简单的,主要是宏太多,换行太多才难的):

extern "C" {JNIEXPORT jstring JNICALL
Java_com_xxxx_xx_MainActivity_stringFromJNI(
        ...) {
    ...
}}

是不是(稍微)易读了(那么一点点点点点)呢~?

返回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 可选阅读】
根据源码知道:

总之:前面两个参数是必须要写的,如果你的函数需要别的输入参数,那就请将这些参数依次添加在这两个参数后面,并且在调用的时候传入你写的参数就好(不需要写这两个参数)。

值得注意的是函数的返回值是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结果
上一篇下一篇

猜你喜欢

热点阅读