Android之 JNI详解,Android JNI深度解析

马肤

温馨提示:这篇文章已超过412天没有更新,请注意相关的内容是否还可用!

摘要:,,本文详细解析了Android中的JNI(Java Native Interface)技术。JNI是Java与本地代码(如C/C++)之间的桥梁,它允许Java应用程序调用本地代码以提高性能或访问特定系统资源。本文介绍了JNI的基本原理、使用方法和注意事项,帮助开发者更好地理解和应用JNI技术,优化Android应用程序的性能和功能。

一、JNI简介

Android之 JNI详解,Android JNI深度解析 第1张
(图片来源网络,侵删)

JNI 是Java Native Interface的缩写,表示Java本地调用,通过JNI技术可以实现Java调用C程序和C程序调用Java代码。

二、JNI函数注册

Android之 JNI详解,Android JNI深度解析 第2张
(图片来源网络,侵删)

2.1 静态注册:

  • 静态注册的方式我们平时用的比较多。我们通过javac和javah编译出头文件,然后再实现对应的cpp文件的方式就是属于静态注册的方式。这种调用的方式是由于JVM按照默认的映射规则来匹配对应的native函数,如果没匹配,则会报错。
  • 优缺点: 系统默认方式,使用简单; 灵活性差(如果修改了java native函数所在类的包名或类名,需手动修改C函数名称(头文件、源文件))
     //Java 层代码JniSdk.java
        public class NativeDemo {
            static {
                System.loadLibrary("jnisdk");
            }
            public native String showJniMessage();
        }
        //Native层代码 jnidemo.cpp
        extern "C"
        JNIEXPORT jstring JNICALL Java_com_dong_example_JnidemoClass_showJniMessage
          (JNIEnv* env, jobject job) {
            return env->NewStringUTF("hello world");
        }
    
    JNIEXPORT :在Jni编程中所有本地语言实现Jni接口的方法前面都有一个"JNIEXPORT",这个可以看做是Jni的一个标志,至今为止没发现它有什么特殊的用处。
    jstring :这个学过编程的人都知道,当然是方法的返回值了,对应java的String类型,无返回值就是void
    JNICALL :这个可以理解为Jni 和Call两个部分,和起来的意思就是 Jni调用XXX(后面的XXX就是JAVA的方法名)。
    Java_NativeDemo_sayHello:这个就是被上一步中被调用的部分,也就是Java中的native 方法名:包名+类名+方法名。
    JNIEnv * env:这个env可以看做是Jni接口本身的一个对象,jni.h头文件中存在着大量被封装好的函数,这些函数也是Jni编程中经常被使用到的,要想调用这些函数就需要使用JNIEnv这个对象,例如:env->GetObjectClass()。
    jobject obj:代表着native方法的调用者,本例即new NativeDemo();但如果native是静态的,那就是NativeDemo类。
    

    2.2 动态注册

    • 动态注册不再按照特定的规则去实现native函数,只要在.c文件里面根据对应的规则声明函数即可,所以我们可以不用默认的映射规则,直接由我们告诉JVM,java的native函数对应的是C文件里面的哪个函数。
    • 优缺点: 函数名看着舒服一些,但是需要在C代码中维护Java Native函数与C函数的对应关系; 灵活性稍高(如果修改了java native函数所在类的包名或类名,仅调整Java native函数的签名信息)
       //Java 层代码JniSdk.java
          public class JniSdk {
              static {
                  System.loadLibrary("jnisdk");
              }
              public static native int numAdd(int a, int b);
              public native void dumpMessage();
          }
          //Native层代码 jnidemo.cpp
          JNINativeMethod g_methods[] = {
                  {"numAdd", "(II)I", (void*)add},
                  {"dumpMessage","()V",(void*)dump},
          };
          jint JNI_OnLoad(JavaVM *vm, void *reserved) {
              j_vm = vm;
              JNIEnv *env = NULL;
              if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
                  return -1;
              }
              jclass clazz = env->FindClass("com/dong/example/JnidemoClass");
              //第一个参数clazz:是对应的类名的完整路径,(把.换成/) 
              //第一个参数g_methods:是定义的全局变量     
              //第一个参数2:是g_methods的数组长度,也可以用sizeof(g_methods)/sizeof(g_methods[0])
              jint ret = env->RegisterNatives(clazz, g_methods, 2);
              if (ret != 0) {
                  LOGI("register native methods failed");
              }
              return JNI_VERSION_1_6;
          }
      

      上面的JNI_OnLoad函数是在我们通过System.loadlibrary函数的时候,JVM会回调的一个函数,我们就是在这里做的动态注册的事情,通过env->RegisterNatives注册。

      2.3 这里主要讲解一下g_methods对象,下面是JNINativeMethods结构体的定义

        typedef struct {
               const char* name;   //对应java中native的函数名
               const char* signature;  //java中native函数的函数签名
               void*       fnPtr;  //C这边实现的函数指针
           } JNINativeMethod;
      

      三、函数签名

      3.1 什么是函数签名:

      所谓函数签名,简单点的理解可以理解成一个函数的唯一标识,一个签名对应着一个函数的签名。这个是一一对应的关系。有些人可能会问:函数名不能作为标识么?答案当然是否定的

      3.2 为什么需要函数的签名:

      我们知道,java是支持函数重载的。一个类里面可以有多个同名但是不同参数的函数,所以函数名+参数名才唯一构成一个函数标识,因此我们需要针对参数做一个签名标识。这样jni层才能唯一识别到一个函数

      3.3 如何获取函数的签名

      函数的签名是针对函数的参数以及返回值进行组成的。它遵循如下格式(参数类型1;参数类型2;参数类型3…)返回值类型。例如我们上面的numAdd函数一样。他在java层的函数声明是:

      public static native int numAdd(int a, int b);

      两个参数都是int,并且返回值也是int,所以的函数签名是(II)I。

      public native void dumpMessage();

      而dumpMessage函数没有任何参数,并且返回值也是空,所以它的签名是()V

      3.6 函数类型对应的签名的映射关系:

      类型标识                       Java类型
      Z                             boolean
      B                             byte
      C                             char
      S                             short
      I                             int
      J                             long
      F                             float
      D                             double
      L/java/language/String        String
      [I                            int[]
      [Ljava/lang/object            Object[]
      V                             void
      

      四、JNIEnv

      4.1 JNIEnv介绍

      JNIEnv贯穿了整个JNI技术的核心,java层调用native函数主要通过映射关系的建立,但jni函数调用java层的函数就要通过JNIEnv了

      4.2 何为JNIEnv

      JNIEnv是JVM内部维护的一个和线程相关的代表JNI环境的结构体,这个结构体是和线程相关的。并且C函数里面的线程与java函数中的线程是一一对应关系。也就是说,如果在java里的某个线程调用jni接口,不管调用多少个JNI接口,传递的JNIEnv都是同一个对象。因为这个时候java只有一个线程,对应的JNI也只有一个线程,而JNIEnv是跟线程绑定的,因此也只有一个

      4.2 通过JNIEnv调用java对象方法,通过JNIEnv调用方法大致可以分为以下两步:

      a、获取到对象的class,并且通过class获取成员属性

      b、通过成员属性设置获取对应的值或者调用对应的方法

      c、注意如果jni方法是通过static方式调用的话,这边的jobject表示的是jclass对象,需要进行强转,并不表示一个独立的对象

        public class JniSdk {
              private int mIntArg = 5;
              public int getArg() {
                  return mIntArg;
              }
          }
          void dump(JNIEnv *env, jobject obj) {
              LOGI("this is dump message call: %p", obj);
              jclass jc = env->GetObjectClass(obj);
              jmethodID  jmethodID1 = env->GetMethodID(jc,"getArg","()I");
              jfieldID  jfieldID1 = env->GetFieldID(jc,"mIntArg","I");
              jint  arg1 = env->GetIntField(obj,jfieldID1);
              jint arg = env->CallIntMethod(obj, jmethodID1);
              LOGI("show int filed: %d, %d",arg, arg1);
          }
      

      4.3 跨线程如何调用java方法

      • 上面可以直接调用的原因是java调用到jni层的时候始终都在同一个线程,因此再jni层可以直接操作从java层传递下来的JNIEnv对象来实现各种操作。但是如果是在JNI层创建的一个额外的线程想调用Java方法呢?这个时候又该如何操作呢?
      • 一个java线程和一个jni线程共同拥有一个JNIEnv,如果java线程调用native函数的时候,JVM还没有为这两个线程建立起映射关系,那么就会新创建一个JNIEnv并且传递到jni线程
      • 如果之前已经有创建过映射关系。那么就直接采用原来的JNIEnv 。如上面所描述的那样,两个JNIEnv的对象是相同的
      • 反之也一样,如果jni调用java线程的话,那么需要向JVM申请获取到已经映射的JNIEnv,如果之前未映射过的话。那么就重新创建一个,这个方法就是AttachCurrentThread。
        JNIEnv *g_env;
        void *func1(void* arg) {
            //进入另一个新线程
            //使用全局保存的g_env,进行操作java对象的时候程序会崩溃
            jmethodID  jmethodID1 = g_env->GetMethodID(jc,"getArg","()I");
            jint arg = g_env->CallIntMethod(obj, jmethodID1);
            //通过这种方法获取的env,然后再进行获取方法进行操作不会崩溃
            JNIEnv *env;
            j_vm->AttachCurrentThread(&env,NULL);
        }
        void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj) {
            //打印线程
            LOGI("on dump arg function, env :%p", env);
            g_env = env;
            pthread_t *thread;
            pthread_create(thread,NULL, func1, NULL);
        }
        

        上面表示JNIEnv跟每个线程是捆绑的,无法在线程B访问到线程A的JNIEnv,所以通过保存g_env的方式去使用是不行的。而是应该要通过AttachCurrentThread方法进行获取新的JNIEnv,然后再进行调用。

        五、销毁

        • java创建的对象是由垃圾回收器来回收和释放内存的,但java的那种方式在jni那边是行不通的。
        • 在JNI层,如果使用ObjectA = ObjectB的方式来保存变量的话,这种是没办法保存变量的,随时会被回收,我们必须要通过env->NewGlobalRef和env->NewLocalRef的方式来创建,还有一个env->NewWeakGlobalRef(这种很少使用)
        • 两种的生命周期的情况如下:
          • NewLocalRef创建的变量再函数调用结束后会被释放掉
            • NewGlobalRef创建的变量除非手动delete掉,否则会一直存在

              六、JNIEnv操作Java端的代码,主要方法:

              函数名称作用
              NewObject创建Java类中的对象
              NewString创建Java类中的String对象
              NewArray创建类型为Type的数组对象
              GetField获得类型为Type的static的字段
              SetField创建Java类中的对象
              GetStaticField创建Java类中的对象
              SetStaticField设置类型为Type的static的字段
              CallMethod调用返回值类型为Type的static方法
              CallStaticMethod调用返回值类型为Type的static方法

              七、Java 、C/C++中的常用数据类型的映射关系表

              JNI中定义的别名Java类型C/C++类型
              jint / jsizeintint
              jshortshortshort
              jlonglonglong / long long (__int64)
              jbytebytesigned char
              jbooleanbooleanunsigned char
              jcharcharunsigned short
              jfloatfloatfloat
              jdoubledoubledouble
              jobjectObject_jobject*
              jbyteArraybyte[]signed byte[]
              jcharArraychar[]unsigned char[]
              jdoubleArraydouble[]unsigned double[]
              jfloatArrayfloat[]unsigned float[]
              jintArrayint[]unsigned int[]
              jshortArrayshort[]unsigned short[]
              jlongArraylong[]unsigned long[]
              jbooleanArrayboolean[]unsigned bool[]

              八、函数类型对应的签名的映射关系:

              Java类型字段描述符(签名)备注
              intIint的首字母、大写
              floatFfloat的首字母、大写
              doubleDdouble的首字母、大写
              shortSshort的首字母、大写
              longLlong的首字母、大写
              charCchar的首字母、大写
              byteBbyte的首字母、大写
              booleanZ因B已被byte使用,所以JNI规定使用Z
              objectL + /分隔完整类名String 如: Ljava/lang/String
              array[ + 类型描述符int[] 如:[I
              voidVi无返回值类型
              Method(参数字段描述符…)返回值字段描述符int add(int a,int b) 如:(II)I
              byte[][B相比普通类型多了“[”来表示数组
              char[][C同上…
              double[][D
              float[][F
              int[][I
              short[][S
              long[][J
              boolean[][Z

              九、JNI 打印日志

              9.1 Cmake文件中有log模块引用,不然编译不通过

               # 编译一个库
                  add_library(
                      native-lib   # 库的名字
                      SHARED      # 动态库(.so库)
                      native-lib.cpp  # 需要编译的C++文件
                  )
                  # 相当于定义一个变量log-lib,引用安卓的打印模块
                  find_library(
                      log-lib
                      log
                  )
                  # 将变量log-lib连接到so库(我这边的so库名字是native-lib)中,这样这个库就能使用日志了
                  target_link_libraries(
                     native-lib
                     ${log-lib}
                  )
              

              9.2 然后在cpp文件中加入:

              #include 
              #define TAG "kang"
              #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
              #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
              #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
              

              9.3 使用日志打印:

               extern "C" JNIEXPORT jstring JNICALL
               Java_com_example_jnidemo_MainActivity_stringFromJNI(JNIEnv* env,jobject) {
                   std::string hello = "Hello from C++";
                   LOGD("jni打印(LOGD)")
                   LOGE("jni打印(LOGE)")
                   LOGI("jni打印(LOGI)")
                   return env->NewStringUTF(hello.c_str());
               }
              

              十、完整源码

              10.1,静态注册:

              Native层:NativeLib.java

              package com.bob.nativelib;
              public class NativeLib {
                  static {
                      System.loadLibrary("nativelib");
                  }
                  public native String stringFromJNI();
              }
              

              C++层:nativelib.cpp

              #include 
              #include 
              #include 
              #define TAG "kang"
              #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
              #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
              #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
              extern "C" JNIEXPORT jstring JNICALL
              Java_com_bob_nativelib_NativeLib_stringFromJNI(
                      JNIEnv* env,
                      jobject /* this */) {
                  std::string hello = "Hello from C++";
                  LOGE("Hello from C++");
                  return env->NewStringUTF(hello.c_str());
              }
              

              java调用层

              //静态注册
              public static void main(String[] args) {
                  NativeLib nativeLib=new NativeLib();
                  System.out.println(nativeLib.stringFromJNI());
              }
              

              10.2,动态注册:C语言

              native层:JNITools.java

              package com.bob.nativelib;
              public class JNITools {
                  static {
                      System.loadLibrary("dynamicnativelib");
                  }
                  public static native int  add(int a,int b);
              }
              

              C层:dynamicnativelib.c

              #include "jni.h"
              //日志打印
              #include 
              #define TAG "kang"
              #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
              #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
              #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
              //加
              jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
                  return a+b;
              }
              //三个参数,java层函数名,java 层方法签名,C 层方法指针
              //获取签名方法: javap -s -p DynamicRegister.class
              static const JNINativeMethod methods[]={
                      {"add","(II)I",(void*)addNumber},
              };
              //java层load时,便会自动调用该方法
              JNIEXPORT jint JNICALL
              JNI_OnLoad(JavaVM *vm, void *reserved){
                  JNIEnv* env = NULL;
                  //获得 JniEnv
                  int r = (*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6);
                  if(r != JNI_OK){
                      return  -1;
                  }
                  //FindClass,反射,通过类的名字反射
                  jclass mainActivityCls = (*env)->FindClass(env, "com/bob/nativelib/JNITools");//注册 如果小于0则注册失败
                  //注册方法
                  r = (*env)->RegisterNatives(env,mainActivityCls,methods,sizeof(methods)/sizeof(methods[0]));
                  if(r != JNI_OK){
                      return -1;
                  }
                  return JNI_VERSION_1_6;
              }
              

              java调用层:

              //动态注册
              public static void main(String[] args) {
                   //动态注册c库
                  JNITools jniTools=new JNITools();
                  System.out.println(String.valueOf(jniTools.add(100,100)));
              }
              

              10.3,动态注册:C++语言

              native层:JNITools2.java

              package com.bob.nativelib;
              public class JNITools2 {
                  static {
                      System.loadLibrary("dynamicnativelib2");
                  }
                  public static native int  add(int a,int b);
              }
              

              C++层:dynamicnativelib2.cpp

              #include 
              //加
              jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
                  return a+b;
              }
              //三个参数,java层函数名,java 层方法签名,C 层方法指针
              //获取签名方法: javap -s -p DynamicRegister.class
              static const JNINativeMethod methods[]={
                      {"add","(II)I",(void*)addNumber},
              };
              //java层load时,便会自动调用该方法
              JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){
                  //获得 JniEnv
                  JNIEnv *jniEnv{nullptr};
                  if (vm->GetEnv((void **) &jniEnv, JNI_VERSION_1_6) != JNI_OK) {
                      return -1;
                  }
                  //FindClass,反射,通过类的名字反射
                  jclass mainActivityCls = jniEnv->FindClass("com/bob/nativelib/JNITools2");//注册 如果小于0则注册失败
                  //注册方法
                  jint ret=jniEnv->RegisterNatives(mainActivityCls,methods,sizeof(methods)/sizeof(methods[0]));
                  if (ret != 0) {
                      return -1;
                  }
                  return JNI_VERSION_1_6;
              }
              

              java调用层:

              //动态注册
              public static void main(String[] args) {
                  //动态注册c++库
                  JNITools2 jniTools2=new JNITools2();
                  System.out.println(String.valueOf(jniTools2.add(100,100)));
              }
              

              10.4 jin调用java层

              native层:TestCallBack.java

              package com.bob.nativelib;
              public class TestCallBack {
                  static {
                      System.loadLibrary("jnitojava");
                  }
                  //回调方法 里面调用了add
                  public native void callBackAdd();
                  public int add(int x,int y){
                      return x+y;
                  }
              }
              

              C++层:jnitojava.cpp

              #include 
              #include 
              #include 
              #define TAG "kang"
              #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
              extern "C" JNIEXPORT void JNICALL
              Java_com_bob_nativelib_TestCallBack_callBackAdd(JNIEnv* env,jobject) {
                  //1得到字节码 包名:com.bob.nativelib
                  jclass jclazz = env->FindClass("com/bob/nativelib/TestCallBack");
                  //2得到方法
                  jmethodID jmethodIds = env->GetMethodID(jclazz,"add","(II)I");
                  //3实例化
                  jobject object = env->AllocObject(jclazz);
                  //4调用方法
                  jint result= env->CallIntMethod(object,jmethodIds,100,1);
                  //5打印结果
                  LOGE("result:%d",result);
              }
              

              java调用层:

              public static void main(String[] args) {
                      TestCallBack testCallBack=new TestCallBack();
                      testCallBack.callBackAdd();
                    System.out.println(String.valueOf(testCallBack.add(600,600)));
              }
              

              10.5,完整的CMakeLists.txt

              原来的JNI项目是需要自己手动配置的,有了CMake就简单多了,会自动帮我们配置项目,第11节将讲述CMake的优势

              # 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.10.2)
              # Declares and names the project.
              project("nativelib")
              # 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.
                      nativelib
                      # Sets the library as a shared library.
                      SHARED
                      # Provides a relative path to your source file(s).
                      nativelib.cpp)
              add_library(
                      # 库的名字
                      dynamicnativelib
                      # 动态库(.so库)
                      SHARED
                      # 需要编译的C++文件
                      dynamicnativelib.c)
              add_library(
                      # 库的名字
                      dynamicnativelib2
                      # 动态库(.so库)
                      SHARED
                      # 需要编译的C++文件
                      dynamicnativelib2.cpp)
              add_library(
                      # 库的名字
                      jnitojava
                      # 动态库(.so库)
                      SHARED
                      # 需要编译的C++文件
                      jnitojava.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.
              # 将变量log-lib连接到so库(我这边的so库名字是native-lib)中,这样这个库就能使用日志打印功能了
              target_link_libraries( # Specifies the target library.
                      nativelib
                      # Links the target library to the log library
                      # included in the NDK.
                      ${log-lib} )
                      
              target_link_libraries( # Specifies the target library.
              	     dynamicnativelib
              	     # Links the target library to the log library
              	     # included in the NDK.
              	      ${log-lib} )
              target_link_libraries( # Specifies the target library.
                      dynamicnativelib2
                      # Links the target library to the log library
                      # included in the NDK.
                      ${log-lib} )
                      
              target_link_libraries( # Specifies the target library.
                      jnitojava
                      # Links the target library to the log library
                      # included in the NDK.
                      ${log-lib} )
              

              10.5:总结

              c库和c++库还是有些区别的,下面要注意的几点,不然编译不通过

              a,头部引用

              C语言 #include "jni.h"
              C++   #include 
              

              b,JNIEnv指针引用和FindClass函数参数有区别

              //C语言,FindClass,反射,通过类的名字反射
              jclass mainActivityCls = (*env)->FindClass(env, "com/bob/nativelib/JNITools");
                  
              //C++语言,FindClass,反射,通过类的名字反射
              jclass mainActivityCls = env->FindClass("com/bob/nativelib/JNITools2");
              

              十一,CMake

              10.1 CMake的优势:

              • 可以直接的在C/C++代码中加入断点,进行调试
              • java引用的C/C++中的方法,可以直接ctrl+左键进入
              • 对于include的头文件或者库,也可以直接进入
              • 不需要配置命令行操作,手动的生成头文件,不需要配置android.useDeprecatedNdk=true属性

                10.2 传统JNI方式步骤:

                1. 新建jni目录,写好C/C++代码,注册JNI时我们使用了javah -jni对JAVA类进行操作,自动生成了jni目录以及对应的头文件,然后根据头文件写了C/C++代码。
                2. 在jni目录下创建且配置好Android.mk和Application.mk两个文件。
                3. build.gradle文件中根据情况进行配置,可不进行配置使用默认值。
                4. 通过ndk-build操作,我们能得到对应的so文件,放置在相应位置,java代码中即可调用C/C++代码,运行程序。

                10.3 CMake方式步骤:

                1. 新建cpp目录,写好C/C++代码。
                2. 创建且配置CMakeLists.txt文件。
                3. build.gradle文件中根据情况进行配置,CMakeLists.txt文件的路径必须配置。
                4. java代码中即可调用C/C++代码,运行程序。
                5. project的build.gradle文件中,gradle版本不能低于2.2,否则会报错。

                10.4 CMake和传统JNI的主要区别

                1. 以前的jni目录改成cpp,名字更换了,下面还是存放C/C++文件。
                2. 之前对C/C++文件的编译配置Android.mk、Application.mk文件放在jni目录下,现在改成CMakeLists.txt文件。事实上这些文件的位置是可任意存放的,只需要配置好就行。但最好还是按照默认习惯放置。

0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码