什么是NDK开发(一)

作者:代码大婶

在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开发(一)_第1张图片

JNI的前世今生

说到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代码,看下图:
什么是NDK开发(一)_第2张图片

一个NDK的小DEMO

俗话说的好,“纸上得来终觉浅,觉知此事要躬行”。那我们就看一个简单的列子吧。

    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有两种注册方式。分别是静态注册,和动态注册。为啥需要注册,注册就是让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++文件,需要在这里添加。

jni动态注册方式

  • 优点: 灵活性高, 更改类名,包名或方法时, 只需对更改模块进行少量修改, 效率高
  • 缺点: 对新手来说稍微有点难理解, 同时会由于搞错签名, 方法, 导致注册失败

废话不多说,直接上代码。

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。不知道怎么下载,看这么这张图。

什么是NDK开发(一)_第3张图片








下载插件完了之后,直接选中你要查看的文件,右键–>Show bytecode outLine,享受插件给我们带来的便利吧。
顺便说一下,这个插件我最开始是用来写ASM字节码插桩的时候用的,发现在这儿也很好用。




什么是NDK开发(一)_第4张图片

Java,C++相互调用

下面给出简单的例子来说明怎么实现代码之间的相互调用。

JAVA调用C++函数。

这个不要说了,上面写的两个例子都是写的这个,参考上面。

C++函数调用JAVA代码

初学者可能都是用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开发(二)

你可能感兴趣的:(NDK)