专栏名称: 鸿洋
你好,欢迎关注鸿洋的公众号,每天为您推送高质量文章,让你每天都能涨知识。点击历史消息,查看所有已推送的文章,喜欢可以置顶本公众号。此外,本公众号支持投稿,如果你有原创的文章,希望通过本公众号发布,欢迎投稿。
目录
相关文章推荐
郭霖  ·  Android振动分析:从App层到HAL层 ·  昨天  
郭霖  ·  Android音视频基础能力之音频路由 ·  2 天前  
鸿洋  ·  系统Apk、普通Apk、core ... ·  昨天  
鸿洋  ·  一波深入的Android 性能优化 ·  2 天前  
郭霖  ·  activityGuard:Android ... ·  3 天前  
51好读  ›  专栏  ›  鸿洋

Android下玩JNI的新老三种姿势

鸿洋  · 公众号  · android  · 2017-05-26 07:28

正文

本文作者


本文由马北剑西投稿。

马北剑西的博客地址:

http://blog.csdn.net/mabeijianxi

https://github.com/mabeijianxi


说明:本篇不撸代码,只玩编译,其包含了Android studio 2.2最新的JNI玩法

编译环境:macOS 10.12.3

工具包含:Android Studio 2.2  NDK-r14 


在Android下要玩jni首先下载ndk是必须的,可以直接去https://developer.android.google.cn/ndk/downloads/index.html下载,当然我们家AS为开发者也提供了便捷


只需如图勾选然后OK即可,我的版本是r14,值得一提的是 google ndk-build 命令在 r13 后默认使用 Clang,并将在后续版本中移除 GCC,其编译速度更快、编译产出更小、出错提示更友好。


1

徒手编写Android.mk然后ndk-build编译


这种编译其实是用make工具来玩的,在 Linux 徒手写并编译过c的应该很清楚,通过编写makefile,然后再用make编译已经比不停的用gcc命令逐个编译要爽很多,但是 makefile 的编写还是有点蛋疼。


程序员都是化繁为简善解人意的,通过 ndk 工具我们无需自己写 makefile 了,现在你只要安心撸自己关心的代码就行了。

1、在main下新建 jni 目录,如图:



2、再新建一个 c 或者 c++ 文件,如图:



3、在Java里面声明个 native 方法


private native String jniTellMeWhy(String hiJni);


4、copy全类名



然后去我们新建的那个 hi_jni.cpp 里面去声明一个方法,这里就添加头文件了,直接干。


命名规则是死的,粘贴一下把"."换成" _ "再加上“ _方法名 ”,我们是有返回值的且是个 String 的,对应的就是 jstring ,最前面拼上固定的“ Java_ ”。


我们传入了一个参数,但是规定是每个函数默认都会两个参数,一个是 JNIEnv 指针类型的结构体,一个是调用者对象,比如我们这里就是MainActivity对象,其实玩过 c++ 的都知道里面每个函数其实默认也会传入个 this 指针的,不然一个类可以有那么多对象怎么知道是哪个对象调用的?


言归正传,还有一个参数就是我们传入的 String 值了。如下:



我这里用的是 c++ 所以得加上 extern "C" ,原因很简单,在 C++ 中函数在编译的时候会拼接上参数,这也是 c++ 中函数重载的处理机制,比如一个 set(int a) 和一个 set(int a,int b) ,在编译的时候就变成了 set_int 与 set_int_int ,我们加上extern ”C“ 就表示大爷想按照C来编译,所以函数名字后面就不会拼接上参数类型了。

5、在jni目录下新建两个文件一个叫 Android.mk ,一个叫 Application.mk 。

6、编写Android.mk,最简单的编写如下,后面将介绍一些稍微牛逼点点的。



LOCAL_PATH :是得最先配置的,它用于在开发tree中查找源文件。

include $(CLEAR_VARS) : CLEAR_VARS 变量指向特殊 GNU Makefile ,可为您清除许多 LOCAL_XXX 变量,例如:

LOCAL_MODULE 、 LOCAL_SRC_FILES 和 LOCAL_STATIC_LIBRARIES 。 请注意,它不会清除 LOCAL_PATH.。

LOCAL_PATH :此变量必须保留其值,因为系统在单一 GNU Make 执行环境(其中所有变量都是全局的)中解析所有构建控制文件。 在描述每个模块之前,必须声明(重新声明)此变量。

LOCAL_MODULE :存储您要构建的模块的名称,并指定想生成的 so 叫什么名字。当然生成产物的时候前面会自动拼接上 lib,后面会自动拼接上 .so 。

LOCAL_SRC_FILES :要编译的源文件,多个文件以空格分开即可。当导入 .a 或者 .so 文件的时候一个模块只能添加一个文件,后面将演示。

LOCAL_C_INCLUDES :可以使用此可选变量指定相对于 NDK root 目录的路径列表,以便在编译所有源文件(C、C++ 和 Assembly)时添加到 include 搜索路径,通常是原文件地址、头文件地址等。
LOCAL_LDLIBS :这里是添加一个本地依赖库,比如可以添加一个 log 库,当然我没用到就注释了。

include $(BUILD_SHARED_LIBRARY) :这一行帮助系统将所有内容连接到一起, BUILD_SHARED_LIBRARY 变量指向GNU Makefile 脚本,用于收集您自最近 include 后在 LOCAL_XXX 变量中定义的所有信息。 此脚本确定要构建的内容及其操作方法。 BUILD_SHARED_LIBRARY 代表动态库, BUILD_STATIC_LIBRARY 代表静态库 。

7、编写 Application.mk :


# 指定生成哪些cpu架构的库
APP_ABI := armeabi-v7a
# 此变量包含目标 Android 平台的名称
APP_PLATFORM := android-22


8、在 jni 目录下面打开命令行工具,然后执行 ndk-build ,即可在 libs 目录下得到产物:




9、把产物放到 jniLibs 下面(当然你可以在采用 builde.gradle 的 sourceSets 里面改变其路径, jniLibs.srcDirs=['src/main/libs']) 。



10、 Java 层调用:



结果是:



添加一预构建库编译: 

其实和咋们Android中的添加依赖差不多,我们要编译一个原生库,这个库的功能是可以按照 H264 编码视频,然后我们不想自己写那么多代码,所以我们引入开源的 libx264 ,我们拿到编译好的 libx264.a 或者 libx264.so 和其头文件,这个时候我们只需要导入一起编译即可: 目录结构如图



有了头文件我们即可include入我们的 hi_jni.cpp 里面自由蹂蹑。

接下来修改下 Android.mk 。



如果我们导入 .a 的静态库的话第一组就如上所写,每添加一组的时候必须执行 include $(CLEAR_VARS) ,LOCAL_MODULE 的值就各自喜好了,第一组的 LOCAL_SRC_FILES 我们指向想导入的静态库地址,第一组的LOCAL_C_INCLUDES 指向其头文件地址,然后 include $(PREBUILT_STATIC_LIBRARY) 代表生成静态预构建。 


我们在第二组中引用第一组的静态预构建也就是 LOCAL_STATIC_LIBRARIES := x264 ,引用动态预构建只需把 STATIC 修改为SHARED 即可。


配置完成即可在当前目录打开命令行执行 ndk-build 命令生成产物。如果第一组你指定的是 .so 的动态库,使用的时候也得在 java 层 System.loadLibrary("x264") 。


2

通过配置AS中build.gradle来编译


这种方式比上一中又简化了很多,无需再自己编写 Android.mk 了,但原理都是一样的。


 1、在 main 下新建 jni 目录,如图:



2、再新建一个 c 或者 c++ 文件,如图:



3、找到你项目的 gradle.properties ,添加一行 android.useDeprecatedNdk=true 

4、打开你主 Module 的 build.gradle ,在 defaultConfig 里添加:

ndk{
    moduleName 'hi_jni'
    abiFilter 'armeabi-v7a'
}


名如其实, moduleName 是你给生成的 So 取的名字,当然它会在前面拼接上 “ lib ”,会在后面拼接上 .so ,于是生成的名字就 libhi_jni.so , abiFilter 嘛就是你想保留哪些架构类型的 so ,一般 arm 的就够玩了,当然除了这些还有很多可配参数,比如想添加个日志库来玩玩,那就添加这行呗: IdLibs “log“ 。


5、在Java里面声明个 native 方法


private native String jniTellMeWhy(String hiJni);


6、copy全类名



然后去我们新建的那个 hi_jni.cpp 里面去声明一个方法,这里就添加头文件了,直接干。


命名规则是死的,粘贴一下把"."换成" _ "再加上“ _方法名 ”,我们是有返回值的且是个 String 的,对应的就是 jstring ,最前面拼上固定的“ Java_ ”。


我们传入了一个参数,但是规定是每个函数默认都会两个参数,一个是 JNIEnv 指针类型的结构体,一个是调用者对象,比如我们这里就是MainActivity对象,其实玩过 c++ 的都知道里面每个函数其实默认也会传入个 this 指针的,不然一个类可以有那么多对象怎么知道是哪个对象调用的?


言归正传,还有一个参数就是我们传入的 String 值了。如下:



我这里用的是 c++ 所以得加上 extern "C" ,原因很简单,在 C++ 中函数在编译的时候会拼接上参数,这也是 c++ 中函数重载的处理机制,比如一个 set(int a) 和一个 set(int a,int b) ,在编译的时候就变成了 set_int 与 set_int_int ,我们加上extern ”C“ 就表示大爷想按照C来编译,所以函数名字后面就不会拼接上参数类型了。


7、 Java 层调用:



结果是:



鸿洋:原文为同上3,4,10,这里直接copy了一份,避免翻到上面对应。


这种方式不仅 so easy,当配置好后编译器还能帮我们像在写 Java 代码一样做代码提示。我们可以打开 主 Mudoul下debug->ndk 文件夹:



看自动给我们生成了 Android.mk ,所以说原理都是一样的。


3

通过AS的cMake插件编译


没错,今天的重头戏就是这个,也是必须把AS升级到2.2以后才能玩的功能,有了它写原生代码可谓是如鱼得水,做到真走的徒手一千行,没有比友基!

怎么玩?


点点啊,我靠只需要在新建项目的时候打个钩:



没错选上 Incude C++ Support,然后呢?然后就是直接运行即可。

这是自动生成的部分 c++ 代码:



这是自动生成的部分Java代码:



自动生成的build.gradle里面多出了这两个东西 

android 里多了:


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


defaultConfig 里多了:


externalNativeBuild {
    cmake {
        cppFlags ""
    }
}


在主 Mudule 的跟目录下多了个 CMakeLists.txt ,我们定制自己的原生代码的时候主要就是修改 CMakeLists.txt 里面的配置,下面也会详细讲解.


在原生代码里面添加本地库:


这里以 log 库为例, log 库是 android 下的,如果我们新建项目的时候勾选上了 Incude C++ Support ,那么自动生成的CMakeLists.txt 里面默认会为我们添加 log 库,至于 CMakeLists.txt 的配置一会儿道来。

下面是导入 log 库的头文件,并且宏定义 log 打印函数的代码:



我们这里宏定义了个 LOG_TAG ,并宏定义打印函数 __android_log_print ,我们传入 ANDROID_LOG_ERROR ,所以是E级别。


使用:



输出结果: 



当然我们平时玩 log 总是有一个总开关的,这里我们也可以定义一个,这个开关我们放在 build.gradle 里面:



没错 -D 命令就是宏定义,这里我们宏定义了一个 Debug 。接下来我们在原生代码里面我们就可以根据是否定义了这个宏来决定是否输出日志。代码如下:



接下我们仔细研究下 CMakeLists.txt


我复制了里面自动生成的一些关键脚本:



add_library :我们需要在里面指定三个东西,首先给 lib 取一个名字,然后指定作为动态库还是静态库,最后指定源文件或者库。


使用 add_library() 向您的 CMake 构建脚本添加源文件或库时, Android Studio 还会在您同步项目后在 Project 视图下显示关联的标头文件。


不过,为了确保 CMake 可以在编译时定位您的标头文件,您需要将 include_directories() 命令添加到 CMake 构建脚本中并指定标头的路径:


add_library(...)

# Specifies a path to native header files.
include_directories(src/main/cpp/include/)


find_library :将 find_library() 命令添加到您的 CMake 构建脚本中以定位 NDK 库,并将其路径存储为一个变量。您可以使用此变量在构建脚本的其他部分引用 NDK 库。


target_link_libraries :指定要关联到原生库的库,第一个自然是我们 add_library 里面指定的库名字 hi_jni 库,然后可以看到${log-lib} ,也就是引用了 find_library 里面定义的日志库。


经过上面的脚本,基本可以玩起来了。


引入其他编译好的静态库或者动态库


我们需要在 add_library 里面把我们的库 add 进去,也是三个参数,指定库名字,指定库类型,第三个指定为 IMPORTED 关键字即可,然后我们需要再添加个 set_target_properties() 命令,里面 也是三个参数,要为其设置属性的库名称,指定其预构建类型,如 PROPERTIES IMPORTED_LOCATION ,最后指定 .a .so 库的绝对地址。

如图是我的目录结构:




由于我导入的 x264 静态库是 armeabi-v7a 架构的所以只能编译这个架构的,我需要在 builde.gradle 里面添加过滤:


ndk{
    abiFilters "armeabi-v7a"
}


配置完成了我们现在试试导入 x264 的头文件,调用下其默认参数配置函数玩玩,代码如下:



运行后输出结果如图:



上面演示的是添加外部静态库,添加动态 so 库也大同小异,把 add_library() 里面的 STATIC 改成 SHARED 即可编译。

编译就是怎么简单,我们的全部配置其实都写入了另外一个脚本文件 cmake_build_command.txt 它的位置如图:




我们点开看看:



参数的含义看官网的吧https://developer.android.google.cn/ndk/guides/cmake.html, 撸了七七四十九个小时,实在撸不动了,这些参数可以在build.gradle里面添加,如下:



总结


上面三种姿势肯定推荐第三种, CMake 工具确实不错,也是 AS 2.2 推出的功能。在实际项目中肯定会有一些这样那样的问题,特别是导入一些像 FFmpeg 啊 x264 这些外部库的时候,后面会写一些文章把编译 FFmpeg 与实际项目中运用的姿势做一个分享,欢迎一起交流。


如果你有想学习的文章直接留言,我会整理征稿。如果你有好的文章想和大家分享欢迎投稿,直接向我投递文章链接即可。


欢迎长按下图->识别图中二维码或者扫一扫关注我的公众号: