这是一篇让你少走弯路的 JNI/NDK 实例教程

作者: 夏至 欢迎转载,但保留这段申明
http://blog.csdn.net/u011418943/article/details/79449108

关于 JNI 的基础就不多说了,这篇文章主要讲解如何在 AS 中用 ndk-build 和 用 cmake 去构建我们的 JNI 工程,并总结他们的特点以及优缺点。

本文代码链接:https://github.com/LillteZheng/JniDemo.git

通过这篇文章,你讲学习到:

  • 用 AS 构建自己的 JNI 工程
  • 学会使用 mk 去加载自己的 so 文件
  • 学会调用第三方 so 或 .a 的方法 (工程提供测试的 so )
  • 学会使用 camke,体验丝般顺滑的 C/C++ 编写体验
  • 3.5.1 as 之后的变化

一、ndk-build

先用传统的方式,即 ndk-build 的方式
首先,新建一个工程,配置 ndk 的环境:
这里写图片描述

然后,新建一个工程,在 gradle.properties 中,添加如下:
android.useDeprecatedNdk=true
这里写图片描述

接着,先使用 AS 自带的功能,在 module 中的 build.gradle 添加 so 库的名字:

这里写图片描述

新建一个类,用来生成 native 方法:

public class JniUtils {

    static {
        System.loadLibrary("JNIDemo");
    }
    public static native String getName();
}

接着,就是生成 class 文件了,先 build module 一下
(如果嫌麻烦,可以跳到快捷设置,不用写这么麻烦,不过我建议你还是操作一遍)

打开 cmd,或者用 as 的 Terminal ,这里用cmd演示,去到你的工程路径下,生成我们需要的 .h 文件 :
这里写图片描述

首先,我们需要设置 src 的根路径 ,如果不先设置根路径,一般会提示找不到类,用 set classpath 的命令,指向你的 java 文件:

这里写图片描述

然后,再使用 javah 去生成 .h 文件,即上面的 JniUtils:

这里写图片描述

就可以看到生成了 .h 文件,如下图:

这里写图片描述

接着,我们新建一个 jni 的文件夹:
这里写图片描述

把 .h 文件复制过去,然后复制多一份 .h 文件,后缀名改为.cpp ,如下:

#ifdef __cplusplus
#endif
#include <jni.h>
extern "C"
JNIEXPORT jstring JNICALL Java_com_zhengsr_jnidemo_JniUtils_getName
        (JNIEnv *env, jobject obj) {
    return env->NewStringUTF("这是个 jni 测试");
}

make module 一下,会发现,已经生成了 so 库:

这里写图片描述

最后再 MainActivity 中调用即可看到效果。

1.1、配置快捷方式

如果每次都这样,想想都觉得崩溃,这个时候,我们就可以配置快捷方式,这样就不用每次都开终端去输入,怎么配置呢?

去到 Setting 选择 external tools ,新建一个 ,命名为 javah,(忽略我配置的 ndk_build,后面会用到):
这里写图片描述

配置以下参数:

这里写图片描述

  • program 为要执行的命令
  • parameters ,先设置路径,然后就是把命令敲一遍,注意是 /src/main/jni ,如果你的路径不一样,记得修改
  • working directory 是 .h 的生成路径

然后在你的 jni 类中,按住右键:

这里写图片描述

之后会弹出一个弹窗,可以自己输入 .h 的名字 (ps:先把以前的去掉):

这里写图片描述

效果如下:

这里写图片描述

接下来的步骤,就跟上面的差不多了,这里就不赘述了。

1.2、编写自己的 mk

上面已经说过,我们并没有 mk 的文件,这是因为 as 用了自身的mk,如果我们需要引入第三方的so或者.a,或者需要特殊配置时,就需要编写自己的 mk 文件了。
关于 mk 的学习,可以参考这篇文章 (写得还不错),这里就不多说了:
http://blog.csdn.net/mynameishuangshuai/article/details/52577228

回到 build.gradle ,先把上面的 ndk 的属性去掉,然后添加:

这里写图片描述

在 jni 路径,添加 Android.mk 和 application.mk :

这里写图片描述

首先,先编写 Android.mk :

#设置路径
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := jniutils
LOCAL_SRC_FILES := jniutils.cpp

include $(BUILD_SHARED_LIBRARY)

可以看到,我们把 jni 的 so 的名字改成了 jniutils,用于区别,记得改 JniUtils 中 loadLibrary 的名字,不然报错了,别怪我没提醒;

Application.mk 则如下:

APP_ABI:=all

指定生成所有平台下的 so。

由于我们使用了 mk 编译了,as 并不知道,我们要像刚才配置 javah 那样,配置一下 ndk-build ,配置信息如下:

这里写图片描述

参数已经解释过了,然后在 jni 的文件夹上右键,编译一下:

这里写图片描述

可以看到,生成的 so 包如下:

这里写图片描述

这样,我们就完成了我们的编译了,run 一下,就可以看到你想要的结果了。

1.3、在 build.gradle 中配置编译

从上面中,我们可以看到,如果改动了 .cpp 的方法,每次都要 ndk-build 一下,其实是很烦的;
所以我们可以在 build.gradle 中,添加任务,在每次 run 的时候,自动编译。

build 应该这样配置:

这里写图片描述

完整 build.gradle 文件如下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.2"
    defaultConfig {
        applicationId "com.zhengsr.jnidemo"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    sourceSets {
        main{
            jni.srcDirs=[]; //禁用as自动生成mk
            jniLibs.srcDirs 'src/main/jniLibs' //这里设置 so 生成的位置
        }
    }
    //设置编译任务,编译ndkBuild
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn 'ndkBuild'
    }
}
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
    //应该都看得明白,就不解释了
    commandLine "C:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk\\ndk-bundle\\ndk-build.cmd",
            'NDK_PROJECT_PATH=build/intermediates/ndk',
            'NDK_LIBS_OUT=src/main/jniLibs',
            'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
            'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}

....

接下来,我们在 jniutils.cpp 中,把返回的字符串改一下:

这里写图片描述

直接run,可以看到效果:
这里写图片描述

1.4、引入第三方 so,.a 包

很多时候,像一些比较涉及加密或者核心代码,都是用 so 库来实现,java 只要编写对应的 jni 即可,这里就涉及到引入第三方包的问题,怎么写呢?
首先,我们需要有个第三方的 so 库,这里我从网上下载了一个,下载地址在 github 的demo 中;目录如下:
这里写图片描述

在引入第三方 so 库的时候,需要特别注意的是,这个 so 你要选择好版本,如果你的 so 是32的,而你在 appliaction.mk 的API版本中,选择了 all 或者 arm64-v8a等,那么编译肯定是报错的;
一般手机是 armeabi ,模拟器是 x86 ,机顶盒等板子是 arm64-v8a 的, 我的模拟器刚好是 x86_64 的,所以,这里引入的 so 库是 x86_64 下的,导入之后,目录如下:
这里写图片描述

重新编写 mk 文件:

LOCAL_PATH := $(call my-dir)
#引入第三方 so 
include $(CLEAR_VARS)
LOCAL_MODULE    := vvw
#这里的so名字叫做 vvw,规则是lib 与 so 之间的名字,在加载时使用 vvw,如果是
# libvvw1.0.so,则在 loadlibaray 用 "vvw1.0",module 名字只是给下面加载的
LOCAL_SRC_FILES := libvvw.so
LOCAL_EXPORT_C_INCLUDES := include
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE    := jniutils
LOCAL_SRC_FILES := jniutils.cpp
LOCAL_LDLIBS :=-llog

#引入第三方编译模块
LOCAL_SHARED_LIBRARIES := \
vvw

include $(BUILD_SHARED_LIBRARY)

与前面相比,多了一个第三方模块的引入。接着,我们要指定 application.mk 的 API:

#模拟器是 x86_64 的
APP_ABI := x86_64

如果导入的工程报错,可以试着 APP_ABI 为 x86 ,替换相应的 so 。
接着,我们在 java 类这里,添加一个 调用 so 方法的 java 方法 getIntValue :

public class JniUtils {

    static {
        System.loadLibrary("jniutils");
        System.loadLibrary("vvw");
    }

    public static native String getName();

    public static native int getIntValue(int a,int b);
}

JniUtils.cpp 的代码如下:

#include <jni.h>
#include <string>
#include "include/vvwUtils.h"

extern "C" jstring Java_com_zhengsr_jnidemo_JniUtils_getName(
        JNIEnv* env,
        jobject /* this */) {
    return env->NewStringUTF("获取两数字之和:");
}

extern "C" jint Java_com_zhengsr_jnidemo_getIntValue(
        JNIEnv* env,
        jobject obj,jint a,jint b) {
	# addMethod 为 libvvw.so 的方法
    return addMethod(a,b);
}

修改一下 MainActivity.java

这里写图片描述

效果如下;
这里写图片描述

二、使用 cmake 的方式

上面的 demo 中,写 c/c++ 的时候,并没有任何提示,这真的是让人崩溃啊,写了都不知道写对了没有。所以,在 as 2.2.2 之后,as 就支持用 cmake 的方式去编写 jni 了,而使用 camke,除了 c/c++ 有提示之外,在 jni 的配置上,也更加的人性化,如果是新建项目,我是推荐你用 camke 的构建方式去编写。
官方中文文档如下
https://developer.android.google.cn/studio/projects/add-native-code.html

首先,在新建工程的时候,勾选上 c++ support ( 3.0 往下拉才有)

这里写图片描述

一路 next ,然后有两个提示框:

这里写图片描述
这两个也勾选上,解释如下:

  • Exceptions Support:如果您希望启用对 C++ 异常处理的支持,请选中此复选框。如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
  • Runtime Type Information Support:如果您希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。

工程已经给了我们一个 jni 的例子,而它的编译方式就是通过 CMakeLists.txt 来构建的。
下面是对 CMakeLists.txt 的解释,由于篇幅,这里会删掉一些注释:

cmake_minimum_required(VERSION 3.4.1)
#这里会把  native-lib.cpp 转换成共享库,并命名为  native-lib
add_library( # 库的名字
             native-lib

             # 设置成共享库
             SHARED

             # 库的原文件
             src/main/cpp/native-lib.cpp )

#如果需要使用第三方库,则可以使用 find_library 来找到,比如这里的 log 这个库
find_library( 
              # so库的变量路径名字,在关联的时候是使用
              log-lib
              #你需要关联的so名字
              log )

#因为使用了第三方库,所以,这里我们通过 link 这这个库添加进来
target_link_libraries( # 关联的so的路径变量名
                       native-lib
                       #把上面的 log 中的关联的变量名 log-lib 添加进来即可
                       ${log-lib} )

如果要添加库,则使用 add_library,括号以空格区分,如果要使用第三方库,比如打印的 log 这个库,就通过 find_library 的方式添加,最后通过 target_link_libraries 把源文件的库,和第三方的库变量名引进来,注意第三方库是个路径变量名,所以 ${}的方式引用。

相较传统配置,如果对 mk 不熟悉的小伙伴,估计会很喜欢 cmake 的方式.

2.1 用 cmake 写 jni

按照上面的方式,新建 JniUtils.java 这个类:

public class JniUtils {
    static {
        System.loadLibrary("jniutils");
    }
    public static native String getName();
}

然后编写,jniutils.cpp,你会惊喜地发现,竟然有提示!!

#include <jni.h>
#include <string>
extern "C"
jstring
Java_com_zhengsr_jnidemo_camke_JniUtils_getName(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "这是使用 camke 的编译方式啦";
    return env->NewStringUTF(hello.c_str());
}

接下来就是 用 add_library 的方式,我们把 jniutils 加进来:

这里写图片描述

同步一下即可,修改一下 mainactivity,运行,效果如下:

这里写图片描述

可以看到,使用 cmake 的方式,除了有代码提示,在添加类上,简直不能太方便了。

2.2、引入第三方 so 库

官方推荐,每次库变动之前,先 clean project 一下,所以,先clean 一下,免得出现找不到 so 的情况;
接着,我们添加一下第三方so,还是上面的 libvvw.so ,目录如下:

这里写图片描述

接着,我们需要制定一下 ndk 编译时的 类型,不然会增加一个 mips 的类型,这个是编不过的。

这里写图片描述

接着,则是配置最重要的 CMakeLists.txt 了 (as最新版的配置有所不同,参考2.3),具体如下:

cmake_minimum_required(VERSION 3.4.1)

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 )

#导入第三方so包,并声明为 IMPORTED 属性,指明只是想把 so 导入到项目中
add_library( vvw
             SHARED
             IMPORTED )
#指明 so 库的路径,CMAKE_SOURCE_DIR 表示 CMakeLists.txt 的路径
set_target_properties( 
            vvw
            PROPERTIES IMPORTED_LOCATION
            ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libvvw.so )
            
#指明头文件路径,不然会提示找不到 so 的方法
include_directories(src/main/cpp/include/ )

add_library(jniutils SHARED src/main/cpp/jniutils.cpp)

target_link_libraries( # Specifies the target library.
                        jniutils
                        #关联第三方 so
                        vvw
                       ${log-lib} )

注释已经写得很清楚了,关键是要写对 so 的路径,不然会提示 missing and no rules to make 等错误;
jniutils.cpp 的代码如下:

#include <jni.h>
#include <string>
#include "include/vvwUtils.h"

extern "C" jstring Java_com_zhengsr_jnidemo_1camke_JniUtils_getName(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "这是使用 camke 的编译方式啦,还获取到两数之和啦: ";
    return env->NewStringUTF(hello.c_str());
}

extern "C" jint Java_com_zhengsr_jnidemo_1camke_JniUtils_getIntValue(
        JNIEnv* env,
        jobject obj,jint a,jint b) {

    return addMethod(a,b);
}

效果如下:
这里写图片描述

2.3 3.5.1 之后的配置 (2020/06/08更新)

最新版的 CMakeLists.txt 放在了 cpp 的目录下,如果按照上面的路径去配置,肯定也会报 missing and no rules to make ,因为 so 的路径配置错了。
因此,我们把 jniLibs 放到 cpp 下,如:
在这里插入图片描述
后面,我们的 CMakeLists.txt 修改如下:

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).
        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.

#导入第三方so包,并声明为 IMPORTED 属性,指明只是想把 so 导入到项目中
add_library( vvw
        SHARED
        IMPORTED )
#指明 so 库的路径,CMAKE_SOURCE_DIR 表示 CMakeLists.txt 的路径
set_target_properties( # Specifies the target library.
        vvw

        # Specifies the parameter you want to define.
        PROPERTIES IMPORTED_LOCATION

        # Provides the path to the library you want to import.
        ${CMAKE_SOURCE_DIR}/jniLibs/${ANDROID_ABI}/libvvw.so )

#指明头文件路径,不然会提示找不到 so 的方法
include_directories( ${CMAKE_SOURCE_DIR}/myInclude/ )

target_link_libraries( # Specifies the target library.
        native-lib
        #关联自己的库
        vvw
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

三、总结

不管是 ndk-build 传统的方式,还是 cmake 的方式,都有一定的可取之处,当然,在我看来, cmake 无论在学习成本还是代码编写提示上都要优于 ndk-build。
如果是新建项目,我建议还是用 cmake 的方式,毕竟只 c/c++ 有提示这一点,我相信你也拒绝不了的。
当然,实际项目上,还有动态加载 so 的方法,这里就不深入了,这里就当做个 入门介绍吧。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值