Android JNI 开发
概述
几乎稍有经验的Android开发,都会在工作中用到JNI的开发。即使工作中没有涉及到JNI的开发,在我们使用第三方的库时,也经常需要引入.so文件。
最初我在学习JNI开发时,基本是懵的。因为大部分JNI开发的指南,其实是在教我们,如何生成.so文件和如何引入.so文件。
我们参照着博客的步骤,修改build.gradle,添加cmakelists,写JNI接口,写c++。但每一步,我们实际是在做啥,我们并没有清晰的认识。这也导致每次JNI的配置步骤,看一次忘一次。
本文旨在彻底写清楚,当我们在做JNI开发时,我们在做什么。
.so文件
so是shared object的缩写,见名思义就是共享的对象,机器可以直接运行的二进制代码。大到操作系统,小到一个专用软件,都离不开so。参见https://en.wikipedia.org/wiki/Library
so主要存在于Unix和Linux系统中。
A shared library or shared object is a file that is intended to be shared by executable files and further shared object files. Modules used by a program are loaded from individual shared objects into memory at load time or run time, rather than being copied by a linker when it creates a single monolithic executable file for the program.
我们通过C/C++开发的软件,如果以动态链接库的形式输出,那么在Android中它的输出就是一个.so文件。
相比于.a,.so文件是在运行时,才会加载的。所以,当我们将.so文件放入工程时,一定有一段Java代码在运行时,load了这个native库,并通过JNI调用了它的方法。
所以,当我们使用JNI开发时,我们就是在开发一个.so文件。不论我们是开发一个工程,还是开发一个库,只要当我们使用C++开发,都会生成对应的.so文件。
所以JNI开发的核心是,我们生成so的过程,和使用so的过程。
生成.so文件
当我们在新建工程过程中,选中support c++
时,系统会为我们写好一些配置。
build.gradle
android {
compileSdkVersion 26
defaultConfig {
minSdkVersion 14
targetSdkVersion 26
versionCode 1
versionName "1.0"
externalNativeBuild {
cmake {
// cpp 编译时的额外选项
cppFlags ""
}
}
// 设置 执行 gradle assembleRelease 时,支持的 SO 库构架
ndk{
abiFilters "armeabi" , "armeabi-v7a" , "arm64-v8a"
}
}
buildTypes {
release {
postprocessing {
removeUnusedCode false
removeUnusedResources false
obfuscate false
optimizeCode false
proguardFile 'proguard-rules.pro'
}
}
}
externalNativeBuild {
cmake {
// cmakelists文件的路径
path "CMakeLists.txt"
}
}
}
这是module的build.gradle中的一段。其中,两个externalNativeBuild
与ndk
是与JNI相关的配置。
我们写好的.cpp/.h文件需要经过编译才能生成.so,让apk得以调用。在编译生成.so文件的过程中,我们可以添加如下一些配置。
cppFlags
cmake时的额外参数,此时我们暂时没有额外参数,所以为空。具体使用参考https://developer.android.com/studio/projects/add-native-code?hl=zh-cn
abiFilters
设置 执行 gradle assembleRelease 时,支持的 SO 库构架。如果像上面的代码这样设置,我们打出来的包,就会包含三种架构的.so包。这必然会使APK包体积变大。可以考虑使用productFlavors
进行优化。
cmake.path
指定cmakelists文件的路径。
除了这些必须的标签外,externalNativeBuild
中还有很多可以配置的东西,因为不是必需,不在此赘述。如ndkBuild中可以设置c++的版本等配置。
CMakeLists.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.
native2-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.
native2-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
add_library
该指令的主要作用就是将指定的源文件生成链接文件,然后添加到工程中去。指令语法如下:
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[source1] [source2] [...])
find_library
该指令的作用为查找库,指令语法如下:
find_library (<VAR> name1 [path1 path2 ...])
target_link_libraries
该指令的作用为将目标文件与库文件进行链接。该指令的语法如下:
target_link_libraries(<target> [item1] [item2] [...]
[[debug|optimized|general] <item>] ...)
完成以上配置后,当你Make Project
时,我们就可以在build目录下,看到我们的.so文件了。
不论是给自己项目用,还是提供给其他项目用。当我们执行我们的C++代码时,我们都使用的是这个.so文件了。
使用.so文件。
将下来,我们就需要关注。Android是如何使用.so文件的了。
在很多如何使用so文件的博客中,我们可以看到下面这一句话:
jniLibs.srcDirs = ['libs']
这句话是将我们的so文件的目录指定为libs文件夹。这样,我们只需要将so文件放入libs即可。
这样,当我们构建APK包时,gradle就会帮我们,将这个.so文件,打入我们的APK文件中。
这样,当我们在代码中,执行到
System.loadLibrary("native2-lib");
我们就会加载这个libnative2-lib
库了。
有趣的问题
在开发中,我们会遇到一个有趣的情况。
比如,我们有工程ProjectA。有库LibraryA。
Project依赖库LibraryA。
LibraryA使用了NDK,生成了.so库。
由于ProjectA依赖了库LibraryA,所以当我们构建ProjectA时,一定会先构建LibraryA。此时,我们就已经生成了libnative2-lib.so
。
如果我们再将libnative2-lib.so
放入ProjectA的libs目录时,我们的工程下,其实就有两个libnative2-lib.so
了。此时,我们生成APK时,系统如何辨认两个libnative2-lib.so
呢。当我们调用
System.loadLibrary("native2-lib");
我们到底加载的是哪个libnative2-lib.so
呢?
经过试验,我们发现,APK中,只会有一个libnative2-lib.so
。并且,是ProjectA中的libnative2-lib.so
。这个地方,我们可以推断出gradle在为我们构建APK时的逻辑。
gradle在构建Android APK 时,一定是指定了好几个工程中存放.so文件的目录。在build时,会遍历这些目录,并将指定目录下的.so文件复制到APK包中。如果名字相同的.so文件,则会相互覆盖。
由于LibraryA是先build的,所以LibraryA中的.so文件会被ProjectA中的.so文件覆盖。
SDK如何输入.so
如果我们开发的就是一个Library。当其他Project集成我们的Library时,需要将我们的.so文件,拷贝到Project的so文件指定路径。
通常来说,我们会在build.gradle中,添加一个小小的脚本。将.so文件在build后,复制到我们的指定目录中。方便使用Project取用。
task(copySoFile) {
String fromDir = './build/intermediates/cmake/debug/obj'
String toDir = [target dir]
copy {
into toDir
from(fromDir) {
include '**'
}
}
println "------------ so files copied --------------"
}
总结
本文记录了当Android需要进行JNI开发,需要明白的一些基础知识。如有问题,欢迎指正。