读书笔记:《Android 高级进阶》
NDK简介
NDK 是 Native Developmentit的缩写,是Google在Android开发中提供的一套用于快速创建native工程的一个工具。
使用这个工具可以很方便的编写和调试JNI的代码。
- NDK是一系列工具的集合
NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的.
- NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。
NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。
为什么使用NDK,优点是什么?
- 代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反编译难度较大。
- 可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的。
- 提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。
- 便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。
ABI的基本概念
早起的Android系统几乎只支持ARMv5的CPU架构,而发展到现在,Android系统目前支持一下7种不同的CPU架构
- ARMv5
- ARMv7(从2010年起)
- x86(从2011年起)
- MIPS(从2012年起)
- ARMv8
- MIPS6
- x86_64(从2014年起)
每一种架构关联着一种ABI,那么什么是ABI呢?ABI是Application Binary Interface的缩写,即应用程序二进制接口,它定义了二进制文件(Android平台上专指.so文件)如何运行在相应的系统平台上,包括使用的指令集、内存对齐到可用的系统函数库。在Android系统上,每一个CPU架构对应一个ABI,如前所述,总共有7种,对应到Android Studio中的目录结构如下
[module_name]
[src]
[main]
[jniLibs]
[armeabi]
[armeabi-v7a]
[x86]
[mips]
注意jniLibs目录是放在module下面,在Android Studio中效果如下:
在Android Studio中为工程添加C++代码的方式有两种。
- 引入预编译的二进制(自己编译好或者第三方提供的)
- 在Android Studio中直接从C/C++源码编译
引入预编译的二进制C/C++函数库
添加预编译的二进制库很简单,默认情况下,Android Studio会到jniLibs目录中查找并拷贝所有的二进制库。假设我们在上面每个ABI目录中都有一个名为libhellondk.so的二进制库,在Android中要使用这个库很简单。
String libName = "helloNDK"; // 库名, 注意没有前缀lib和后缀.so
System.loadLibrary( libName );
直接从C/C++源码编译
Android Studio中对C/C++源码编译成.so文件的步骤主要如下:
- 配置ndk.dir变量
- 在Gradle中配置NDK模块
- 添加C/C++文件到指定目录
配置ndk.dir变量
进行NDK开发第一件要做的事情就是打开工程根目录的local.properties文件,并在其中配置ndk的目录,以便让Android Studio能够做到NDK的可执行文件等。
sdk.dir= /Users/guhaoxin/Library/Android/sdk
ndk.dir= /Users/guhaoxin/Library/Android/ndk
在Gradle中配置NDK模块
为了让Gradle对C/C++代码进行编译,需要配置module的build.gradle文件,打开build.gradle,在其中defaultConfig段落中添加如下代码:
ndk{
moduleName "moduleName"
}
其中,moduleName要替换成我们自己的C/C++模块名,例如某个NDK项目中配置如下
android {
compileSdkVersion 20
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.example.ndksample"
minSdkVersion 9
targetSdkVersion 20
versionCode 1
versionName "1.0"
ndk {
moduleName "helloNDK" // <-- This is the name of my C++ module!
}
}
// ... more gradle stuff here ...
} // end of android section
ndk还可以配置更多选项,如下:
ndk {
moduleName "myEpicGameCode"
cFlags "-DANDROID_NDK -D_DEBUG DNULL=0" // Define some macros
ldLibs "EGL", "GLESv3", "dl", "log" // Link with these libraries!
stl "stlport_shared" // Use shared stlport library
}
添加C/C++文件到指定的目录
默认情况下,Gradle会到:
[module]/src/main/jni/
效果如下图:
目录中查找C/C++文件进行编译,我们只需要把C/C++文件放到这个目录中即可。当然我们也可以修改jni的目录,例如希望把C/C++文件放到source目录中,可以修改gradle文件如下:
android {
// .. android settings ..
sourceSets.main {
jni.srcDirs 'src/main/source'
}
}
分平台配置编译(可选)
这一步不是必须的,你可以根据需要,对各个平台进行不同的编译配置,可以设置覆盖前面的编译选项(例如cFlags)。例如你只想编译指定平台的.so,而不是所有的平台。如下:
android {
// .. android settings ..
productFlavors {
x86 {
ndk {
abiFilter "x86"
}
}
arm {
ndk {
abiFilter "armeabi-v7a"
}
}
mips {
ndk {
abiFilter "mips"
}
}
}
} // android
使用.so文件的注意事项
处理.so文件时有一条简单却不知名的重要法则:你应该尽可能地提供专为每个ABI优化过的.so文件,要么全部支持,要么都不支持。我们不应该混合着使用,而应该为每个ABI目录提供对应的.so文件。
使用高平台版本编译的.so文件运行在低版本的设备上
这是大家习以为常的做法,但是这是错误的。因为NDK平台不是向后兼容的,而是向前兼容的。推荐使用App的minSdkVersion对应的编译平台,因此,当我们引入一个预编译好的.so文件时,首先需要检查它被编译所用的平台版本。
混合使用不同的C++运行时编译的.so文件
.so文件可以依赖于不同的C++运行时,静态编译或者动态加载。混合使用不同版本的C++运行时可能导致很多奇怪的Crash。作为一个经验法则,当只有一个.so文件时,静态编译C++运行时是没有问题的,但存在多个.so文件时,应该让所有的.so文件都动态链接相同的C++运行时。
没有为每个支持的CPU架构提供对应的.so文件
这一点前文已经说到,但应该特别注意它,它可能发生在根本没有注意的情况下。例如:你的APP支持armeabi-v7a和x86架构,然后使用Android Studio新增一个函数库依赖,这个函数库包含.so文件并支持更多的CPU架构,例如新增android-gif-drawable函数库。
compile 'pl.droidsonroids.gif:android-gif-drawable:1.1.+'
发布我们的APP后,发现在某些设备上会发生Crash。解决办法是重新编译我们的.so文件使其支持确实的ABIs,或者设置
ndk.abiFilters
显示地指定支持的ABIs。
将.so文件放在错误的地方
- Android Studio工程放在jniLibs/ABI目录中(当然也可以通过在build.gradle文件中的设置jniLibs.srcDir属性自己指定)。
- Eclipse工程放在libs/ABI目录中(这也是ndk-build命令默认生成.so文件的目录)。
- AAR压缩包中位于jni/ABI目录中(.so文件会自动包含到引用AAR压缩包的APK中)。
- 最终APK文件中的lib/ABI目录中
- 通过PackageManager安装后,在小于Android 5.0的系统中,.so文件位于APP的nativeLibraryPath目录中;在大于等于Android 5.0的系统中,.so文件位于APP的nativeLibraryRootDir/CPU_ARCH目录中。
只提供armeabi架构的.so文件而忽略其他ABIs的
所有的x86/x86_64/armeabi-v7a/arm64-v8a设备都支持armeabi架构的.so文件,因此,似乎移除其他ABIs的.so文件时一个减少APK大小的好技巧。但事实上并不是:这将影响到函数库的性能和兼容性。
以减少APK包大小为由是一个错误的借口,因为你可以选择在应用市场上传指定ABI版本的APK,生成不同ABI版本的APK。