NDK开发入门(二)
1. 基本项目的介绍
1.1 项目结构
1.1.1 项目目录结构
新建带C++支持的项目自动生成的目录结构,没什么好说的。切到Project模式的目录结构下,主要说两点。
- 根目录->app->.externalNativeBuild->cmake->debug->armeabi-v7a->cmake_build_command.txt.可以查看编译命令。
- 根目录->app->build->intermediates->cmake->debug->obj->armeabi-v7a可以查看本地cpp文件生成的so库。
1.1.2 cpp文件的集成方式
在app目录下的build.gradle定义了外部构建工具,且指定了相应配置文件CMakeLists的路径。
在cpp文件加入构建后,才使Java层能找到native层的具体实现。
1.2 各个文件的说明
1.2.1 MainActivity.java
package com.txs.ndktest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
//在类加载的时候加载名为native-lib的库
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 调用native方法
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* native方法
*/
public native String stringFromJNI();
}
在这个文件中,加载了名为native-lib动态链接库,而且写在了static代码块中,对于static代码块中的内容,jvm会在加载类的时候执行,并且仅执行一次。也就是说保证在进行调用时,native方法已经被加载进JVM了。
1.2.2 CMakeLists.txt
# 设置编译所需的cmake的最小版本号
cmake_minimum_required(VERSION 3.4.1)
# 设置需编译的库名称以及所涉及的源文件
add_library( # 设置库的名称
native-lib
# 设置库是静态链接库STATIC还是动态链接库SHARED
SHARED
# 提供相关的源文档文件们,如果属于同一个功能模块建议放在同一个
native-lib.cpp
)
#设置预编译系统库
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)
# 将多个库连接到 native-lib,第一个库为目标库,将其他库连接到目标库上
target_link_libraries( # 目标库.
native-lib
# 后续可以添加好多连接在上面的库,但是第一个为
# System.loadLibrary("native-lib")加载的库名称。
${log-lib})
1.2.3 native-lib.cpp
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_txs_ndktest_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
- 在这个文件中extern "C"关键词代表了后续的编译方式都采用c语言的编译方式进行编译。这是因为C++在进行编译的时候会对函数名称进行一定程度的重命名。C++存在着重载机制,故而对于同名函数要进行一定的区分,所以会在函数名前加一些前缀区分。对这个有兴趣的同学可以查看一下,C++的符号修饰与函数签名。
- 因为在native层的函数名是需要有映射的,所以不能让编译器给函数改名字,这也是这里extern "C"关键词的作用所在。由于这个特性,所以NDK开发大多也倾向于采用C++,在需要C特性的场景里将代码用extern "C"关键字,示例如下:
extern "C" {
//your c code;
}
- JNIEXPORT与JNICALL关键字是让JVM能识别这个函数。
- JNIEnv存着一些与JVM相联系的函数,这个在源码中是一个结构体,在后续章节中会提及。
2. JNI的简略原理
2.1 前置知识点
2.1.1 编译的具体流程
上图可知,主要包括.dex文件和so库。其中.dex是Java代码生成的字节码,而so库则是C++/C形成的动态链接库。
这里在回顾一下编译的详细过程。平时我们所提及的编译,包括了预处理,编译和汇编三个过程。
2.1.2 ART(Android Runtime)
在Lollipop之后,谷歌就已经放弃了Dalvik采用ART模式了,在启用ART模式后,系统在安装应用的时候会进行一次预编译,在安装应用程序时会先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。DVM是采用just in time的模式进行编译执行,ART是在安装的时候就将他们变成机器语言(以Lollipop为基准,不考虑后续优化),其示意图如下。 对此详见扒一扒Android运行时。
2.2 一次代码的执行过程
下图主要描述了一个工程从构建到运行的大体流程。
2.2.1 构建后的APK
首先我们先看一下,APK解压之后都有哪些文件,主要涉及到Java源码编译出的字节码文件和C/C++源码编译出的SO文件。
2.2.2 安装时所做的工作
将dex文件翻译成二进制,并引入so库的直接引用。
2.2.3 执行时的动态链接
在执行代码时进行动态库的链接。
2.3 源码加载so库的具体流程
3. 参考文献
[1]周志明. 深入理解Java虚拟机[M]. 机械工业出版社, 2013.
[2]https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
[3]俞甲子, 石凡, 潘爱民. 程序员的自我修养[M]. 电子工业出版社, 2009.