Android NDK 开发
NDK 开发其实就是在Android开发中使用C++这种底层开发语言。
环境依赖
- Android Studio
- 在Android Studio里使用SDK Manager安装sdk和ndk(建议r16版本)
NDK 开发
最简单的示例
在Android Studio中新建工程,选择Native C++模板;选择这个模板,他会给出一个最基本的能够运行的NDK应用模板。
基本原理
- cpp源码会编译成so库,并在apk安装后被复制到一个特定的路径
- java通过加载这个库,然后使用jni调用so里面定义的接口。
就这么简单!
原理讲解
工程结构,如下图:相比普通的Android工程,这个工程里面多出了cpp这个文件夹。
当你打包成apk的时候,这里的CPP源码会首先编译成共享库(.so),然后放进apk里。然后,Java源码通过如下代码加载这个库:
注意这里面的字符串不是CPP源文件的名称,而是编译后的共享库的名称。CPP源码编译成的so库具体是什么名称,就是在CMakeLists.txt里面定义的。如下图:注意这里的“Sets the name of the library”。
这个就是需要在System.loadLibrary()
里面填的字符串。不过,实际上所有编译出来的共享库,前面都必须要带一个“lib”,所以最终的真实的共享库的名称应该是“libnative-lib.so”。但是你在CMakeLists.txt和System.loadLibrary()
里面不需要在前面加上lib前缀,它会自动帮你加的。
还有一个问题,就是System.loadLibrary()
里面只是填写库的名称,那么,这个方法具体是去哪里查找这个库呢?查找路径可以通过以下语句获取:
System.getProperty("java.library.path");
在上面这个最简单的Android APP上,这个接口打印出来的是:
/system/lib64:/vendor/lib64
推测如果是很老的那种32bit手机,那么目录就是lib,而不是lib64.
再回头看一下Java源码那张图,加载库是放在一个Java静态块里面执行的,表示整个过程只加载一次。然后29行的代码:
public native String stringFromJNI();
你需要声明这样的用native关键字修饰的方法,它对应CPP源码里面定义的一个方法,然后才能像普通java方法那样调用。
所以这个对应关系是怎么样的呢?查看CPP源码:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_bytedance_myapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
这个对应的就是这个超级无敌长的函数名:Java+包名+类名+真正函数名:
Java_com_bytedance_myapplication_MainActivity_stringFromJNI
上面的java代码中的方法名stringFromJNI就是这么来的。
这个cpp源码中,第一行引入jni.h头文件,这个头文件里面定义了JNI专属的方法,数据类型和数据结构,比如上面用到的JNIEXPORT
, jstring
,JNICALL
,JNIEnv
,jobject
。还有extern "C"
是必须的。它表示下面这个是一个C方法,而不是C++方法。原因是因为C++为了实现重载,它会自动更改掉函数名,导致Java最后找不到这个方法。当然,如果这个方法不是对Java暴露的接口,就不用加这个修饰了。
运行Apk
打包成apk在手机上运行。此时,你可以用adb查看手机下的这个目录:
adb shell ls /system/lib64 | findstr native
你会看到。。。不,你看不到libnative-lib.so 。你也可以试一下/venfor/lib64,也没有。但是网上确实是说,System.loadLibrary()的加载路径是"java.library.path",怎么回事呢?
现在,你把System.loadLibrary()里面的名字改成一个错误的名字,再打包运行。应用闪退之后,查看logcat,你会看到这么一句话:
看到“nativeLibraryDirectories”这个词没有?这个就是真正的加载路径,它表示System.loadLibrary()会从这里罗列的路径去找,包括:
/data/app/com.qtc.test-2/lib/arm64
/data/app/com.qtc.test-2/base.apk!/lib/arm64-v8a
/system/lib64
/vendor/lib64
注意,每个手机,lib目录下的arm的名称可能是不同的,你可以通过adb shell ls
查看。至于app包名后面为什么有个-2
,是因为我的手机上已经有一个名为com.qtc.test
的包了,如果没有,那么应该是-1
。
(注意,这个后缀不一定是这么一个规律。我在用最新的华为P30手机测试的时候,后缀就是一串无规律的字母串,像是一个哈希值。一般来说,/data/app/这个目录不root是没有权限访问的,但是却可以访问/data/app/com.qtc.test-1/。大概是为了保护隐私,一些手机在加这个包的后缀的时候不遵循-1, -2的规律。你如果想知道当前包在这个目录下的具体名称,可以在代码里打印。安卓代码是Context.getPackageCodePath(),如果是Unity工程,则是Application.datapath)。
现在我们再把库名称改回来(顺便我把原来的com.qtc.test卸载了),运行,然后查看一下第一个路径:
找到了!!!
说明,System.loadLibrary()并不只是从“java.library.path”里面查找。关于实际的搜索路径,这篇博客写得很好!
我们再做一件事情:解压apk,得到的目录结构会是这样:
注意在目录arm64-v8a
,armeabi-v7a
,x86
,x86_64
下都各有一份libnative-lib.so
。这说明,如果你不手动用ndk-build将cpp编译成so库,那么Android Studio会自动帮你编译出四份so库,它分别对应四种指令集平台。其中arm64-v8a
对应64位的CPU,armeabi-v7a
对应32位的CPU。x86
就只在桌面CPU上应用,一般是模拟器使用。apk在安装的时候会自动判断当前CPU指令集,如果是64位,就只拷贝arm64-v8a
目录下的so到手机上的arm64下,当然如果是64位的CPU但是没有64位的so库,就拷贝32位的so库,理论上v8a是兼容v7a的,反之则不行。如果是32位CPU,就只拷贝armeabi-v7a
的so到arm下。