最近刚好做到NKD编程,就来系统记录一下这段时间的收获。
首先你要明白一件事情,任何语言都有局限性,比如JAVA的执行效率是不如C的。因此为了更好地实现程序效果,或是引入其他语言编译好的库,都需要跨语言的操作。
在JAVA和C语言之间通讯的技术,称为JNI。安卓中,将C语言编译成库打包,JAVA中去调用库的一系列原生开发工具包,统称为NKD。
前置工作:需要先下载NDK开发工具及CMake

1、什么是库
我们在编写接口或者使用第三方应用时,都会打包或者引入.so或者.a文件。这个so就是动态库,.a文件就是静态库。
动态库和静态库是在C语言中用来组织和共享代码的两种方式。
程序的编译要经过四个阶段,预处理,编译,汇编,链接。在汇编之后就会生成对应的二进制文件,这个时候它的后缀是为".o",比如test.c文件,在编译之后会生成test.o文件。 在汇编之后生成的二进制文件还是不可执行的,要想执行它,必须再经过链接阶段。
对于频繁用到的许多源文件,我们可以先将它们生成对于的二进制目标文件,然后进行打包,这样做的好处是,之后需要用到这些文件的时候就可以直接链接这个包中的目标文件即可。打包之后,这些目标文件就可以称为一个库。
而本质上看,静态库和动态库都是众多.o文件的集合,是可执行程序的"半成品"
2、简单了解JNI
JNI具有一个单独的jni.h文件,存放了所有JAVA和C之间沟通的桥梁函数
使用Android Studio简单创建一个Native工程即可得到一个JNI的Demo示例:

MainActivity中即可看到如下代码:
java
public native String stringFromJNI();
native的方法,代表需要从调用C语言执行的方法
java
static {
System.loadLibrary("ndkfirst");
}
这段代码则表示当前项目已经打包成了动态库ndkfirst并加载,若注释,则会闪退,并报错如下: java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.example.ndkfirst.MainActivity.stringFromJNI() (tried Java_com_example_ndkfirst_MainActivity_stringFromJNI and Java_com_example_ndkfirst_MainActivity_stringFromJNI__) - is the library loaded, e.g. System.loadLibrary?
告诉你关于stringFromJNI的方法没有实现,对应的库没有加载
native-lib.cpp文件中,则展示了JNI方法的创建过程,它创建了一个std命名空间下的string字符串 "Hello from c++" 并返回
c
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkfirst_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
具体的代码先不展开做讲解,你可能此时有疑问,这里命名是JNI层创建的产物,怎么和C通信了呢?
没错,这里只是展示了JNI层的用法,你可以在同级目录下定义任何C/C++文件,并在native-lib.cpp中引入并调用其中的C/C++函数。
2、修改为通用SO
因为JNI中的方法名称,和JAVA的包名路径有强制一对一的关系,所以该项目下直接创建的native-lib.cpp不能直接被其他项目所使用,修改方法名如下:
c
Java_com_nativefun_NativeTest_stringFromJNI
修改后本项目又找不到jni方法了,所以还需要做一点调整 java.com下新建文件夹nativefun,再新建文件NativeTest,大小写需要严格一致!

并将MainActivity中的代码移动到这里
java
public class NativeTest {
static {
System.loadLibrary("ndkfirst");
}
public static native String stringFromJNI();
}
java
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(NativeTest.stringFromJNI());
}
}
2、CMakeLists的打包过程
关于cmake的基本用法,以及CamkeLists的语法规则,先不展开讲解。我们先看一下这个文件:
CMakeLists
#申明了CMake的版本
cmake_minimum_required(VERSION 4.0.2)
#项目名称
project("ndkfirst")
#将 native-lib.cpp 打包成为动态库(SHARED),${CMAKE_PROJECT_NAME}为当前的项目名
add_library(${CMAKE_PROJECT_NAME} SHARED
native-lib.cpp)
#关于其他的链接库
target_link_libraries(${CMAKE_PROJECT_NAME}
android
log)
由此可知,是它的作用,将 native-lib.cpp 打包进了so库,所以如果你写了其他的c文件,也需要在这里加入
3、如何打包出so库
继续上面的CmakeLists文件,加入一行代码
bash
# 设置库输出路径,ANDROID_ABI 的意思为所有平台架构,也可指定具体的架构
SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
然后点build->make model xxx ,即可将我们的so文件导出到指定目录:
至此,打包的so库的过程我们已经全部完成了。
4、使用刚打包的so库文件
新建一个非NDK的项目
将刚才项目的文件jniLibs下面的文件全部复制过来(这里我知道自己使用的手机为arm64-v8a的架构,所以就只复制了该文件夹)
将刚才项目的nativefun文件夹,放在java.com文件下

此时再调用native下的方法,即可成功
java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.tv);
tv.setText(NativeTest.stringFromJNI());
}
}
恭喜你,已经完全掌握了打包so到应用so跑起来的全过程。
可能你还有个疑问,假如 NativeTest 中的方法不叫 stringFromJNI 呢? 我改个名字叫stringFromJNI2,当然就会发生运行时崩溃,因为在刚才打包so的 native-lib.cpp 的文件里,没有一个 Java_com_nativefun_NativeTest_stringFromJNI2 的方法!再次强调,JNI的方法名 与 android JAVA中的方法名,一一对应!