使用CMake构建Android JNI工程

Android Studio(2.2+)构建native库可以使用原生构建工具包ndk-build,也可以使用外部构建工具CMake,搭配Gradle插件可以方便的构建原生库,进行Android JNI的开发。使用Android Studio创建的native工程默认使用的是CMake构建工具,本文从零开始介绍使用CMake搭建一个JNI工程。

为了阐述方便,我们以创建一个默认的Android工程为例,不使用创建向导里的Include C++ Support或者创建C++工程。现在我们在native代码(C++)中实现一个获取字符串并返回的操作,然后使用java jni来调用。

一、下载NDK和构建工具

打开Android Studio -> Perferences -> Appearance&Behavoir -> System Setting -> Android SDK,或者直接在左侧搜索Android SDK,选择SDK Tools,下载NDK、CMake、LLDB这三个工具包。



新建的native工程(Include C++ Support)在local.properties中都会配置ndk的默认的路径:

ndk.dir=/Users/derek/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/derek/Library/Android/sdk

如果没有,或者ndk在别的目录下,需要手动添加或修改路径。

二、在java类中声明native方法

在java中声明要使用的native方法,这些方法以native前缀,只需声明,无需实现。这些方法可以声明为static或非static方法,可以是任何访问权限。

package com.tsia.example.jnitest;
...
public class MainActivity extends Activity {
  ...
  public native String stringFromJNI(String str);
}

三、添加C/C++代码

在c++代码中需添加和native方法对应的函数,注意如下几点:

  1. 文件名称可随意指定,可以只有源文件
  2. 头文件或源文件中要#include <jni.h>
  3. 方法声明要和java中的native方法对应:
  • 方法名称。Java_包名_类名_方法名,包名也使用_分隔。
  • 参数。
    • 第一个参数为JNIEnv *
    • 如果为static方法,第二个参数为jclass;如果非static方法,第二个参数为jobject
    • 从第三个参数开始,和java的native方法的参数类型和顺序要一一对应
  • 返回值。
    • 返回值类型要对应java中的类型
    • 需要JNIEXPORT、JNICALL前缀

方法格式可以概括为:JNIEXPORT <返回类型> JNICALL Java_<包名><类名><方法名>(JNIEnv *, jobject,<参数>); 包名中的“.”用下划线“_”代替。

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring JNICALL Java_com_tsia_example_jnitest_MainActivity_stringFromJNI
  (JNIEnv *env, jobject jobj, jstring str) {

  const char* c_name = env->GetStringUTFChars(str, NULL);

  std::string hello = "Hello, ";
  std::string name(c_name);

  return env->NewStringUTF((hello+name).c_str());
  }

如果担心函数声明写错,可以使用命令行生成native方法对应的C++函数头文件,只需要到java文件所在的包名目录下执行javah命令,比如在com所在目录下执行:

tsias-MacBook-Pro:java tsia$ javah -jni com.tsia.example.jnitest.MainActivity

执行后会在该目录下生成一个头文件:com_tsia_example_jnitest_MainActivity.h,自动生成的格式为包名+类名.h,中间会使用_分隔。(这个文件名为命令自动生成的格式,可以随意修改)

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_tsia_example_jnitest_MainActivity */

#ifndef _Included_com_tsia_example_jnitest_MainActivity
#define _Included_com_tsia_example_jnitest_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_tsia_example_jnitest_MainActivity
 * Method:    stringFromJNI
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_tsia_example_jnitest_MainActivity_stringFromJNI
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

可以在.h文件中看到所有native方法的声明,格式就是我们上文讲的一样。手动编写时也可参考自动生成头文件的格式,避免出错。

在调用native方法的时候会匹配函数名和参数,需要按照格式书写,不可随意修改。

四、编写CMakeLists.txt文件

接下来就是创建CMake的构建脚本,它是一个纯文本文件,必须命名为CMakeLists.txt。构建脚本用来告诉CMake将如何创建一个so库,例子中我们要将c++代码编译成一个名为native-lib的库,给jni调用。

cmake_minimum_required(VERSION 3.4.1)
add_library(
       # 设置so文件名称.
       native-lib

       # 设置这个so文件为动态库(SHARED)。静态库使用STATIC
       SHARED

       # c/c++源文件的相对路径(相对于CMakeLists.txt)
       src/main/java/jnitest.cpp)

当工程编译的时候,Gradle会自动将动态库native-lib库打包到APK中。脚本中指定的库名称为native-lib,但实际CMake生成的名称为libnative-lib.so。
CMake 使用以下规范来为库文件命名:

lib库名称.so

五、配置Gradle关联

在构建应用时,Gradle 会以依赖项的形式运行CMake,并将共享的库打包到的 APK中,因此我们需要提供一个指向 CMake脚本文件的路径。

手动配置

externalNativeBuild {} 块添加到模块级 build.gradle 文件中,并使用 cmake {} 对其进行配置

android {
  ...
  defaultConfig {...}
  buildTypes {...}

  externalNativeBuild {
    cmake {
      path "CMakeLists.txt"
    }
  }
}

这里的path为相对于build.gradle文件的路径,需正确配置。

使用Android Studio配置关联

从IDE左侧打开 Project 窗格并选择 Android视图,右键点击您想要关联到原生库的模块(例如 app 模块),并从菜单中选择 Link C++ Project with Gradle。


选择使用CMake构建,并制定CMakeLists的路径。

完成后可以看到模块级build.gradle 文件中会增加externalNativeBuild {} 块配置,和我们手动配置的结果是一样的。因为gradle配置有修改,sync project下。

此时执行build -> Make Module 'app',可以看到所有架构的so都会打到APK的lib目录下。


五、在java文件中加载so库

在Java代码中加载so库时,请使用您在 CMake 构建脚本中指定的名称。如CMake生成的而文件是libnative-lib.so,只要指定加载native-lib即可。

package com.tsia.example.jnitest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        String ret = stringFromJNI("world");
        Log.i("jnitest", ret+"");
    }
    
    public native String stringFromJNI(String str);
}

然后在代码中调用native方法打印结果。运行后打印结果: Hello, world

上述就是一个简单的jni工程搭建步骤,如下是在构建和运行时的基本过程:

  1. Gradle调用外部构建脚本CMakeLists.txt
  2. CMake按照构建脚本中的命令将 C++源文件jnitest.cpp 编译到共享的对象库中,并命名为libnative-lib.so,Gradle随后在编译的时候会将其打包到APK中。
  3. 运行时,应用的 MainActivity 会使用 System.loadLibrary() 加载原生库。这时候应用可以使用库的原生函数 stringFromJNI(String str)
  4. MainActivity.onCreate() 调用 stringFromJNI("world"),这将返回“Hello, world”并打印。

参考:https://developer.android.com/studio/projects/add-native-code

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

推荐阅读更多精彩内容