- Welcome
- Requirement
- Create a Native Project
- Directory structure
- CMake build script
- Reduce size of APK
- JNI
- References
Welcome
NDK全称Native Development Kit.
是一个允许在Android应用里引用本地代码的工具集.
多用于以下两种情况:
- 压榨设备性能以达到低延迟或者运行密集计算,比如游戏或者物理仿真.
- 复用C或者C++库(好像还可以使用汇编,不过我没试过).
Android Studio
Android Studio 2.2及以上版本,可以直接NDK编译C或者C++代码成本地库,并打包进apk(使用Gradle,IDE集成了NDK编译系统).
CMake
CMake是一个开源、跨平台的脚本工具,可用于构建、测试以及打包软件.
Android Studio编译本地库时默认使用CMake构建.
Android Studio也同样支持ndk-build,Android开发早期在使用,其配置文件是Android.mk.
当然,如果是新建一个本地库,建议大家用CMake.
Requirement
- Android Studio 前面已经说了,2.2及以上版本.
- NDK
- CMake
- LLDB全称Low Level Debugger.用于调试本地代码.
在安装好了Android Studio之后可以用SDK Manager直接下载后面的组件.
Create a Native Project
创建一个native项目和创建一个其他Android项目有点类似,除了一些额外步骤.
依次点击File -> New -> New Project,在Create New Project界面上勾选上Include C++ Support,填好其他选项后一路Next,直到出现Customize C++ Support界面.
- C++ Standard:默认选项Toolchain Default,使用默认的CMake设置;如果想使用标准C++就下拉选择C++11.
-
Exceptions Support:启用支持C++异常处理.如果启用,Android Studio会在module的build.gradle文件里添加 -fexceptions 标志给cppFlags.
-f 指force的意思,强行启用,默认不启用. -
Runtime Type Information Support启用支持RTTI.如果启用,Android Studio会在module的build.gradle文件里添加 -frtti 标志给cppFlags.
android { ··· defaultConfig { ··· externalNativeBuild { cmake { path "CMakeLists.txt" cppFlags "-std=c++11 -frtti -fexceptions" } } }
当然,你也可以不勾选,待项目创建好后直接在module的build.gradle里添加需要的标志.Gradle把这些标志作为参数传递给CMake.
externalNativeBuild
CMake Variables
选好后点Finish,项目就创建好了.
Directory structure
.
├── app
│ ├── build
│ │ ├── intermediates
│ │ │ ├── cmake
│ │ │ │ └── debug
│ │ │ │ └── obj
│ │ │ │ ├── arm64-v8a
│ │ │ │ │ └── libnative-lib.so
│ │ │ │ ├── armeabi
│ │ │ │ │ └── libnative-lib.so
│ │ │ │ ├── armeabi-v7a
│ │ │ │ │ └── libnative-lib.so
│ │ │ │ ├── mips
│ │ │ │ │ └── libnative-lib.so
│ │ │ │ ├── mips64
│ │ │ │ │ └── libnative-lib.so
│ │ │ │ ├── x86
│ │ │ │ │ └── libnative-lib.so
│ │ │ │ └── x86_64
│ │ │ │ └── libnative-lib.so
│ │ │ ├── jniLibs
│ │ │ └── ···
│ │ └── ···
│ ├── CMakeLists.txt
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── cpp
│ │ │ └── native-lib.cpp
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── bob
│ │ │ └── ndkdemo
│ │ │ └── MainActivity.java
│ │ └── res
│ └── ···
└── ···
一个NDK项目的结构大致如上(便于观察,这里不太关注的结构都被我省略掉了).
在/src/main目录下多了cpp文件夹,这里用来存放native code.注意,native代码不支持instant run,以后不清楚,至少目前是这样.
在module根目录下多了CMakeLists.txt,该文件类似ndk-build的Android.mk.
在编译后的中间文件里主要多了cmake和jniLibs文件夹.
CMake build script
我们通过修改CMakeLists.txt文件来控制Cmake编译.
还是以默认生成的NDK项目demo为例:
cmake_minimum_required(VERSION 3.4.1)
add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp )
find_library( log-lib
log )
target_link_libraries( native-lib
library
${log-lib} )
cmake_minimum_required,设置最低要求的CMake版本号.
- cmake_minimum_required()应该尽可能的放在顶级CMakeLists.txt文件的开头,甚至比project()还早.
之所以说顶级CMakeLists.txt文件,是因为CMakeLists.txt还可以include其他文件或者模块,以及add_subdirectory. - cmake_minimum_required()如果只是声明在局部函数里边function(),则只对该函数生效,不会影响全局.
add_library,包含三个参数,分别指定库名、库的创建类型、库对应的源码路劲.
- 这里的库名只是编译和运行期间逻辑上的库名(必须是该项目里全局唯一的),而生成的库文件命名:lib库名.so
当然,在代码里加载库的时候依然是使用库名.
find_library,搜索给定名字的库.
- 如果找到了就把路劲赋值给缓存的变量,除非该变量被清除,否则不会重新搜索这个库.
没找到库,则变量值为NOTFOUND,且下一次调用的时候还会再次搜索.
上面这个例子就是去搜索名为log的库,并把结果赋给变量log-lib. - 由于CMake默认包含了系统库的搜索路劲,所以只需要指定NDK库的名字就行了.
target_link_libraries,为每个目标分别指定需要链接的库文件(指定部分目标专用的库文件).
区别于link_libraries,后者为所有目标统一指定需要的库文件(指定所有目标都用的库文件),老命令,若无特殊理由不建议使用.
include_directories,指定native code的头文件路劲.
更多命令参考CMake commands.
注意,和修改了Gradle脚本需要Sync Project 一样,修改了CMake脚本需要点击菜单栏 Build > Refresh Linked C++ Projects 更新.
Reduce size of APK
Specify ABIs
Configure multiple APKs for ABIs
JNI
JNI全称Java Native Interface.
JNI与NDK的区别:
NDK是为便于开发基于JNI的应用而提供的一套开发和编译工具集;而JNI则是一套编程接口,可以运用在应用层,也可以运用在应用框架层,以实现Java代码与本地代码的互操作。
native方法的声明
在java方法的基础上去掉方法体,并加上native关键字:
public native String hello();
loadLibrary 两种方式
Runtime.getRuntime().loadLibrary("native-lib");
System.loadLibrary("native-lib");
其实System.loadLibrary()最终也是调用的Runtime.getRuntime().loadLibrary()。
javah 生成头文件
进入\src\main\java
目录下,在命令行输入:
javah -jni com.step2hell.jnisample.MainActivity
后面一定要输入类的全名,包括包名。然后就会生成.h头文件了,生成的.h头文件在当前目录下,默认命名的文件名很长(可以修改)。
.h头文件的内容主要是根据JNI方法规则生成对应的native层方法,为了保证每个函数的唯一性,所以JNI层的方法命名比较长,规则如下:
Java_包名_类名_函数名
Java_包名_类名_函数名__函数签名 // 函数签名仅出现在函数重载情况下,函数签名与函数名之间是 2 个下划线
在main/下创建cpp文件夹,将该.h文件加入,并创建新的cpp文件或者c文件,include .h头文件,实现.h头文件的方法即可。
Android Studio创建一个支持c++的项目,会自动生成一个JNI的模板,并且模版只有一个cpp文件,并不需要什么.h头文件,这也是可以的,只要保证cpp的方法命名满足JNI方法规则。当然还是推荐使用javah,可以防止方法名太长而导致的手动输入错误。
- 使用头文件
main.h头文件:/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_step2hell_jnisample_MainActivity */ #ifndef _Included_com_step2hell_jnisample_MainActivity #define _Included_com_step2hell_jnisample_MainActivity #ifdef __cplusplus extern "C" { #endif /* * Class: com_step2hell_jnisample_MainActivity * Method: hello * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_step2hell_jnisample_MainActivity_hello (JNIEnv *, jobject); /* * Class: com_example_step2hell_jnisample_MainActivity * Method: helloWorld * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_step2hell_jnisample_MainActivity_helloWithArgs (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
main.cpp文件:
#include <string> #include "main.h" jstring Java_com_step2hell_jnisample_MainActivity_hello( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from JNI"; return env->NewStringUTF(hello.c_str()); } jstring Java_com_step2hell_jnisample_MainActivity_helloWithArgs( JNIEnv *env, jobject /* this */, jstring) { std::string hello = "Hello from JNI"; return env->NewStringUTF(hello.c_str()); }
- 不使用头文件
main.cpp文件:#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_step2hell_jnisample_MainActivity_hello( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } extern "C" JNIEXPORT jstring JNICALL Java_com_step2hell_jnisample_helloWithArgs( JNIEnv *env, jobject /* this */, jstring) { std::string hello = "Hello from C++2"; return env->NewStringUTF(hello.c_str()); }
javap 生成函数签名
java支持多态,通过函数签名可以确定函数唯一性,在native调用java方法的时候用以区分重载函数。
javap是Java class文件分解器,可以反编译(即对javac编译的文件进行反编译),也可以查看java编译器生成的字节码。用于分解class文件。
javap -s -p -classpath . xxx.class // 更多参数及用法可以查询 javap -help
在Android Studio中我们需要先build得到对应的class文件,再进入class文件所在目录使用javap指令:
$ gradle clean assembleDebug
$ cd build/intermediates/classes/debug/com/step2hell/jnisample
$ pwd
/Users/bob/Projects/JNIsample/app/build/intermediates/classes/debug/com/step2hell/jnisample
$ ls
BuildConfig.class MainActivity.class R.class
$ javap -s -p -classpath . MainActivity.class
Compiled from "MainActivity.java"
public class com.step2hell.jnisample.MainActivity extends android.support.v7.app.AppCompatActivity {
public com.example.step2hell.jnisample.MainActivity();
descriptor: ()V
protected void onCreate(android.os.Bundle);
descriptor: (Landroid/os/Bundle;)V
public native java.lang.String hello();
descriptor: ()Ljava/lang/String;
static {};
descriptor: ()V
}
对应的MainActivity源码:
package com.step2hell.jnisample;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(hello());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String hello();
}
可以看到descriptor
对应的括号内为函数参数签名,括号外为函数返回值签名。
JNI Types
- 基本类型
Java Type | Native Type | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
-
引用类型
-
类型签名
Type Signature | Java Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L fully-qualified-class; | fully-qualified-class |
[ type | type[] |
( arg-types ) ret-type | method type |
extern “C”
extern代表声明的方法和变量为全局变量,和java的static一样。
”C”则代表{}内的内容以C语言方式编译和连接。
如果是cpp文件则需要加上该修饰符以兼容C调用:
#ifdef __cplusplus
extern "C" {
#endif
··· // 需要采用C编译器编译的C语言代码段
#ifdef __cplusplus
}
#endif
JNIEXPORT & JNICALL
JNIEXPORT & JNICALL 都是定义在jni.h中的宏:
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL
-
JNIEXPORT
JNIEXPORT 用于控制对应的native方法在so库是否导出可见。
如果native方法前不加关键字JNIEXPORT,该方法默认依然是可见,如果在编译时设置参数-fvisibility=hidden
,则不加JNIEXPORT关键字的方法才不可见。 - JNICALL
由上述定义可知JNICALL是一个空的宏,猜测作用有如下几个:- 注释说明这是一个JNI调用
- 开关作用
- 方便移植
虽然 JNIEXPORT & JNICALL 修饰符基本上都可以去掉而不影响程序运行,但是建议保留 遵循规范。
JNIEnv在c和c++中的区别
.cpp文件是c++的语法,.c是c的语法,文件的类型决定了JNIEnv的语法。
- cpp用法:
env->FindClass("com/step2hell/jnisample/MainActivity");
- c用法:
(*env)->FindClass(env,"com/step2hell/jnisample/MainActivity");
两者区别,在jni.h中查看JNIEnv定义:
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
这段话的意思是在c++中,定义_JNIEnv是JNIEnv,其他情况(c)下,定义const struct JNINativeInterface*是JNIEnv。
继续追踪_JNIEnv和JNINativeInterface:
struct JNINativeInterface {
... // 省略代码
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
};
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
... // 省略代码
};
可以看到JNINativeInterface 其实定义了很多方法,都是对Java的数据进行操作,而_JNIEnv则封装了一个JNINativeInterface的指针,并且声明与JNINativeInterface中一模一样的方法,并且都是通过JNINativeInterface的指针来调方法,其实就是对JNINativeInterface做了一层封装.c++是面向对象的语言,不需要再用指针方式来调用,并且_JNIEnv中的每个方法都比JNINativeInterface少一个参数,就是JNIEnv。
JNI native调用Java
Native调用java方法,使用的依然是反射原理。
- 通过FindClass方法获取到对应的class
- 再通过class获取对应的方法id,这里需要根据是否是静态类型分为
GetMethodID
和GetStaticMethodID
- 通过method id调用java方法
- 返回值类型不同的java方法,native调用的方式也不一样:
CallVoidMethod
,CallIntMethod
,CallFloatMethod
等等; - 又根据是否是静态java方法,同一类型返回值的方法 获取的方式分:
CallStaticIntMethod
,CallIntMethod
(这里以Int返回值为例)。
- 返回值类型不同的java方法,native调用的方式也不一样:
Native还可以调用java类的属性,操作与方法基本一致,只是获取method id更换为获取feild id,CallMethoID变为GetFieldID,获取的方式依然区分是否是静态,依然区分类型(变成了field的类型)。
e.g. 假设有POJO Book:
package com.step2hell.jnisample;
import java.io.Serializable;
public class Book implements Serializable {
private String name;
private float price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
}
在MainActivity里定义需要调用的java方法:
public native float getPriceOfBook(Book book);
在native层调用Book的方法setPrice()
以及getPrice()
:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jfloat JNICALL Java_com_example_step2hell_jnisample_SubActivity_getPriceOfBook
(JNIEnv *env, jobject, jobject bookObj) {
// 通过类的路径获取对应的jclass
jclass bookClass = env->FindClass("com/step2hell/jnisample/Book");
// 通过获取的jclass获取对应的methodID,第一个参数是jclass类,第二个参数是需要获取的java函数名,第三个参数是需要获取的java函数签名
jmethodID setPriceMethodID = env->GetMethodID(bookClass, "setPrice", "(F)V");
// 通过methodID调用方法,返回值为Void的使用CallVoidMethod()
env->CallVoidMethod(bookObj, setPriceMethodID, 23.0); // 这里必须是float型,如果是整型则会赋值失败,最后结果为0.0
jmethodID getPriceMethodID = env->GetMethodID(bookClass, "getPrice", "()F");
// 对于返回值为float类型的使用CallFloatMethod()
jfloat price = env->CallFloatMethod(bookObj, getPriceMethodID);
return price;
}
通过在native调用java方法setPrice()
,将默认为0.0的book价格设置成了23.0,并调用getPrice()
方法将价格返回;只需要在java层调用native方法getPriceOfBook()
,便可以得到23.0的价格结果。
References
https://developer.android.google.cn/studio/projects/add-native-code https://developer.android.google.cn/training/articles/perf-jni#java