作者:代码大婶
在Android的官方文档上是这么解释NDK的:“原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,您可使用这些平台库管理原生 Activity 和访问物理设备组件,例如传感器和轻触输入。”NDK是一个Android官方提供的一个开发套件与Android SDK相对应,NDK是原生开发套件,而SDK是JAVA开发套件。NDK使得Android应用可以利用C++语言高效的计算性能,进一步提升App的性能,降低延迟。说道这里,大家肯定要问NDK有那些应用场景,我为什么要用NDK呢,用JAV不行吗?下面列举一些NDK 的应用场景:
- 重用一些现成的库,例如已经用C/C++编写好的openCV库
- 前面提到的高性能计算,例如很多Bitmap的处理到放在NDK进行处理。
- 一些敏感信息的保护,例如密钥等信息(密钥等信息还是要经过加盐才能放到NDK中,不然还是
会有别反编译的风险)
知道了应用场景,大家肯定已经摩拳擦掌准备试一试了,先bie别着急。欲善其事,先利其器。以下给出了开发NDK的三大利器。
下面罗列NDK的三大开发组件:
- NDK Android原生开发套件
- CMAKE 外部编译工具
- LLDB 原生代码调试工具
看到了上面的三大利器,我们要从哪儿获取呢,其实下载一个Android Studio就够了。当然下载完了要进行配置。说到配置,也很简单,就是:
说到NDK,如果不说JNI那不就是耍流氓嘛。为啥耍流氓??因为NDK是开发套件,JNI才是调用的框架。所以与其说是NDK开发,不如说是JNI的开发。不过NDK是Android提供的开发套件。JNI可不是,JNI全称Java Native Interface,已经不是新鲜的事情了。他可是有专门的网站来介绍它的,是不是觉得很牛逼。
下面来简单的介绍一个JNI,JNI,全称为Java Native Interface,即Java本地接口,JNI是Java调用Native 语言的一种特性。通过JNI可以使得Java与C/C++机型交互。即可以在Java代码中调用C/C++等语言的代码或者在C/C++代码中调用Java代码。由于JNI是JVM规范的一部分,因此可以将我们写的JNI的程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码JNI是一种在Java虚拟机机制下的执行代码的标准机制。代码被编写成汇编程序或者C/C++程序,并组装为动态库。也就允许非静态绑定用法。这提供了一个在Java平台上调用C/C++的一种途径,反之亦然。
是不是有点懵,那我就来总结一下,JAVA通过jni调用C++代码,C++代码通过jni也可以调用java代码,看下图:
俗话说的好,“纸上得来终觉浅,觉知此事要躬行”。那我们就看一个简单的列子吧。
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 = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
#include
#include
extern "C" JNIEXPORT jstring JNICALL
Java_com_nanguiyu_testcpp_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
以上就是官方给出的Demo,我在这里稍微介绍一下,上面给出的是java代码调用C++代码,把“Hello from C++”
这个字符串赋值给TextView,从而完成了java代码对C++的调用。顺便说一下,上面的这种注册方式属于静态注册,下面会对jni的两种注册方式进行介绍。
jni有两种注册方式。分别是静态注册,和动态注册。为啥需要注册,注册就是让jni知道你这个函数的存在呗。下面分别从两种注册方式的优点和缺点,怎么进行注册来介绍两种注册方式。
- 优点: 理解和使用方式简单, 属于傻瓜式操作, 使用相关工具按流程操作就行, 出错率低
- 缺点: 当需要更改类名,包名或者方法时, 需要按照之前方法重新生成头文件, 灵活性不高
package com.nanguiyu.jnitest;
public class JniTest {
static {
System.loadLibrary("JniTest-lib");
}
public native String stringFromJNI();
}
上面的Java代码首先加载静态库,然后定义native方法,注意这个方法没有实现,且前面加上标志native.
#include
#include
extern "C" JNIEXPORT jstring JNICALL
Java_com_nanguiyu_jnitest_JniTest_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
上面是C++代码,他是上面stringFromJNI方法的C++实现,功能非常简单,我们只需要关注C++中函数的名称,它的名称由com.nanguiyu.jnitest+类名+方法名,然后用下划线隔开。差点忘了,还有一个重要的东西CMakelist.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
JniTest-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
JniTest.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
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)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
JniTest-lib
# Links the target library to the log library
# included in the NDK.
${log-lib})
这个文件当中add_library(JniTest-lib SHARED JniTest.cpp),第一个参数是库的名称,这个名称由我们自己来取,第二个是模式,第三个是你要编译的C++代码,新添加的C++文件,需要在这里添加。
- 优点: 灵活性高, 更改类名,包名或方法时, 只需对更改模块进行少量修改, 效率高
- 缺点: 对新手来说稍微有点难理解, 同时会由于搞错签名, 方法, 导致注册失败
废话不多说,直接上代码。
package com.nanguiyu.dymanicjni;
public class Test {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
public native String getStr();
}
C++部分代码
#include
#include
jstring getStr(JNIEnv *env){
std::string str = "Hello World";
return env->NewStringUTF(str.c_str());
}
static JNINativeMethod getMethods[] = {
{"getStr","()Ljava/lang/String;",(void*)getStr},
};
static int registerNativeMethods(JNIEnv* env, const char* className,JNINativeMethod* getMethods,
int methodNum)
{
jclass clazz;
clazz = env->FindClass(className);
if(clazz== NULL)
{
return JNI_FALSE;
}
if(env->RegisterNatives(clazz,getMethods,methodNum)<0)
{
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNatives(JNIEnv* env)
{
const char* className = "com/nanguiyu/dymanicjni/Test";
return registerNativeMethods(env,className,getMethods, sizeof(getMethods)/ sizeof(getMethods[0]));
}
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM* vm,void* reserved){
JNIEnv* env = NULL;
if(vm->GetEnv(reinterpret_cast<void**>(&env),JNI_VERSION_1_6)!=JNI_OK)
{
return -1;
}
assert(env!=NULL);
if(!registerNatives(env)){
return -1;
}
return JNI_VERSION_1_6;
}
动态注册相较于静态注册,是比较麻烦的,但是它有个好处具体调用的函数名称有我们自己定义,而不用按照静态注册那样把包名等等七七八八的东西全部要写上。不过有个麻烦的东西要写java方法的签名,各个签名的基本变量如下表
签名符号 | C/C++ | java |
---|---|---|
V | void | void |
Z | jboolean | boolean |
I | jint | int |
J | jlong | long |
D | jdouble | double |
F | jfloat | float |
B | jbyte | byte |
C | jchar | char |
S | jshort | short |
[Z | jbooleanArray | boolean[] |
[I | jintArray | int[] |
[J | jlongArray | long[] |
[D | jdoubleArray | double[] |
[F | jfloatArray | float[] |
[B | jbyteArray | byte[] |
[C | jcharArray | char[] |
[S | jshortArray | short[] |
L完整包名加类名 | jobject | class |
各位看到这些,肯定觉得麻烦的很,难道要本大婶记住这些。这是不可能地。所以我有神器啊。这里给大家介绍一个Android Studio插件 bytecodeOutline。不知道怎么下载,看这么这张图。
下载插件完了之后,直接选中你要查看的文件,右键–>Show bytecode outLine,享受插件给我们带来的便利吧。
顺便说一下,这个插件我最开始是用来写ASM字节码插桩的时候用的,发现在这儿也很好用。
下面给出简单的例子来说明怎么实现代码之间的相互调用。
这个不要说了,上面写的两个例子都是写的这个,参考上面。
初学者可能都是用java调用C++代码。还有C++代码调用java代码这种骚操作吗?当然有,大婶亲自撸代码给出具体的例子。各位看官们不要眨眼。
#include
#include
#include
#include "jnilog.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_nanguiyu_testcpp_MainActivity_stringFromJNI(JNIEnv* env,jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_nanguiyu_testcpp_MainActivity_test(JNIEnv *env, jobject instance, jstring val_,
jint intVal) {
LOGI("TAG","---Java_com_nanguiyu_testcpp_MainActivity_test---");
const char *val = env->GetStringUTFChars(val_, 0);
std::cout<<*val<<std::endl;
std::cout<<intVal<<std::endl;
LOGI("TAG","%s",val);
LOGI("TAG","%d",intVal);
std::string hello = "wo shi zhongguoren";
jstring from = env->NewStringUTF(hello.c_str());
jclass mainActivityInterface = env->FindClass("com/nanguiyu/testcpp/MainActivity");
jfieldID testField = env->GetFieldID(mainActivityInterface,"intVal","I");
env->SetIntField(instance,testField,1);
jmethodID testMethodId = env->GetMethodID(mainActivityInterface,"testToast","()V");
env->CallVoidMethod(instance,testMethodId);
env->ReleaseStringUTFChars(val_, val);
env->DeleteLocalRef(mainActivityInterface);
}
java代码
package com.nanguiyu.testcpp;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
public String val;
private int intVal;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
val = "zhangfeng";
test("nihao",100);
}
});
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
public native void test(String val,int intVal);
public void testToast(){
Toast.makeText(this,val,Toast.LENGTH_LONG).show();
Log.d("TAG",intVal+"");
}
}
代码运行之后成功的调起了toast。仔细查看代码,是不是像反射。还真是有点像啊。
本文简单介绍了jni最基本的用法,从开发工具,到注册方式,再从注册方式到例子说明。但是更重要的JNIEnv,和本地代码内存是怎么占用的我并没有讲。留在下一遍文章中来解说吧。对了,如果需要完整的工程代码的。评论区留言或者私信给我。
NDK系列
什么是NDK开发(二)