NDK开发(二)- JNI

JNI(Java Native Interface):Java调用C/C++的规范。

一、JNI数据类型

基本数据类型:
JAVA JNI
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
void void
引用类型:
JAVA JNI
Object jobject
Class jclass
String jstring
Object[] jobjectArray
boolean[] jbooleanArray
char[] jbyteArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdouble
Throwable jthrowable

二、JNI签名

数据签名:
JAVA JNI
byte B
char C
short S
int I
long L
float F
double D
void V
boolean Z
object L开头,然后以/分隔包的完整类型,后面再加; 比如String的签名就是 "Ljava/lang/String;"
数组 基本数据类型: [ + 其类型的域描述符 ,引用类型: [ + 其类型的域描述符 + ;
多维数组 N个[ 其余语法和数组一致

举例:
int[ ] [I
int[][] [[I
float[ ] [F
String[ ] [Ljava/lang/String;
Object[ ] [Ljava/lang/Object;

方法签名:

(参数的域描述符的叠加)返回

举例:
public int add(int index, String value,int[] arr)

签名:(ILjava/util/String;[I)I

括号内为参数描述符,依次为I、Ljava/util/String;、[I ,括号外为返回值I
通过方法签名和方法名来唯一确认一个JNI函数。

注:
如果不好确认对应方法的签名,可以通过javap命令去查看:
javap -s -p -v xxx.class 找到对应方法去查看具体签名。

三、native函数

extern "C" JNIEXPORT jstring JNICALL
Java_com_stan_jnidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
       jobject /* this */) {
    std::string hello = "print string";
   return env->NewStringUTF(hello.c_str());
}
  • extern “C” 使用C语言,如果使用C++则不需要添加。
  • JNIEXPORT 宏定义,在Linux平台即将方法返回值之前加入 attribute ((visibility (“default”))) 标识,使so对外可见,即保证在本动态库中声明的方法 , 能够在其他项目中可以被调用。
  • JNICALL 宏定义 Linux没有进行定义 , 直接置空。
  • jstring 返回参数
  • JNIEnv 以java线程为单位的执行环境,通过它来使用JNI API。
  • jclass/jobject jclass: 静态方法,jobject: 非静态方法

四、JNI使用模板及案例举例

native函数中使用JNI主要就是玩4类东西:类、对象、属性、方法以及C/C++相关数据操作,非常类似于java的反射。

4.1 类操作:

1)获取jclass通用方法:通过具体java类来获取

jclass claz1 = env->FindClass(“com/stan/base/network/NetworkFactory”);

2)在非静态方法中获取当前函数所在的类:通过native方法参数jobject来转换

jclass claz2 = env->GetObjectClass(jobject);
4.2 对象操作:

1)获取jobject通用型方法:通过jclass创建jobject

jobject obj = env->NewObject(jclass,jmethodID);//这里jmethodID对应的是类的构造方法

2)在非静态方法中获取当前函数所在的对象:直接用native方法参数jobject

4.3 属性操作:

1)获取属性id

jfieldID jfieldId = env->GetFieldID(jclazz, "key", "Ljava/lang/String;”);//获取数据id。

2)get属性值

jint  value = env->GetIntField(jobject obj, jfieldID fieldID);//非静态int数据获取。

3)set属性值

env->SetIntField(jobject obj, jfieldID fieldID,jint value);

这里获取属性id和get/set属性值都区分静态非静态。

4.4 方法操作:

1)获取方法id

jmethodID methodId = env->GetMethodID(network_cls, "", "()V”);

2)调用方法

jint result = env->CallIntMethod(jclass,jmethodID);

这里获取方法id和调用方法区别静态非静态。

4.5 数据操作:

这部分是JNI数据类型的创建和JNI数据类型和C/C++数据类型相互转换,这里以字符串举例

1)创建引用类型

jstring jstr = env->NewStringUTF(str.c_str());

2)JNI数据类型和C/C++数据类型相互转换

jboolean *iscopy;
//jstring 转char *
const char *c_str = env->GetStringUTFChars(str, iscopy);//str为方法传入的参数
if (iscopy == JNI_TRUE) {//重新开辟内存空间保存
    printf("is copy:true");
} else if (iscopy == JNI_FALSE) {//与str内存空间一致
    printf("is copy:false");
}

//释放字符串,如果是重新开辟内存空间的则直接释放,否则通知JVM可以释放,由JVM自行释放。
env->ReleaseStringUTFChars(str, c_str);

这里简单归纳了一些高频操作。JNIEnv中的方法非常多,这里肯定不会一一列举,玩api就是熟能生巧。

案例举例:

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
   }

    public String tips;
   public void showToast() {
        Toast.makeText(this, tips, Toast.LENGTH_SHORT).show();
   }

    public static native int[] sort(int[] arr);
   public native void show();

   @Override
   protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       int[] arr = {5, 2, 1, 4, 3};
       int[] sortArr = sort(arr);
       for (int i = 0; i < sortArr.length; i++) {
            Log.d("jnitest", sortArr[i] + "");
       }
        show();
   }
}

#native-lib.cpp

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_stan_jnidemo_MainActivity_sort(JNIEnv *env, jclass clazz, jintArray arr) {
    jboolean *isCopy;
   jint *intArr = env->GetIntArrayElements(arr, isCopy);
   int len = env->GetArrayLength(arr);
   for (int i = 0; i < len; i++) {
        for (int j = 0; j < len - i; j++) {
            if (intArr[j] > intArr[j + 1]) {
                int tmp = intArr[j + 1];
               intArr[j + 1] = intArr[j];
               intArr[j] = tmp;
           }
        }
    }
    env->ReleaseIntArrayElements(arr, intArr, 0);
   return arr;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_stan_jnidemo_MainActivity_show(JNIEnv *env, jobject jobject) {
    jclass jclaz = env->GetObjectClass(jobject);
   jfieldID fieldId = env->GetFieldID(jclaz, "tips", "Ljava/lang/String;");
   jstring jstr = env->NewStringUTF("suc");
   env->SetObjectField(jobject, fieldId, jstr);
   jmethodID jmethodId = env->GetMethodID(jclaz, "showToast", "()V");
   env->CallVoidMethod(jobject, jmethodId);
}

五、引用类型

1)局部引用

定义:jobject NewLocalRef(JNIEnv *env, jobject ref);//ref:全局或者局部引用 ,return:局部引用。
释放方式:1.jvm自动释放,2.DeleteLocalRef(JNIEnv *env, jobject localRef);。JNI局部引用表,512个局部引用,太依赖jvm自动释放会导致溢出。

2)全局引用

定义:jobject NewGlobalRef(JNIEnv *env, jobject obj); //obj:任意类型的引用,return:全局引用,如果内存不足返回NULL。
释放方式:无法垃圾回收,释放它需要显示调用void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

3)弱全局引用

定义:jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
释放方式:1.当内存不足时,可以被垃圾回收;2void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

env->IsSameObject(weakGlobalRef, NULL);//判断该对象是否被回收了

六、注册方式

public class MainActivity extends AppCompatActivity {
   static {
        System.loadLibrary("native-lib");
   }
   public native String stringFromJNI();
}
静态注册
extern "C" JNIEXPORT jstring JNICALL
Java_com_stan_jni_MainActivity_stringFromJNI(
        JNIEnv* env,
       jobject /* this */) {
    std::string hello = "Hello from C++";
   return env->NewStringUTF(hello.c_str());
}
动态注册

native端:

  • 编写C/C++代码, 实现JNI_Onload()方法;
  • 将Java 方法和 C/C++方法通过签名信息一一对应起来;
  • 通过JavaVM获取JNIEnv, JNIEnv主要用于获取Java类和调用一些JNI提供的方法;
  • 使用类名和对应起来的方法作为参数, 调用JNI提供的函数RegisterNatives()注册方法;

注:
javaVM:与进程对应;
JNIEnv:与线程对应;

//对应实现的native方法
jstring native_stringFromJNI(
        JNIEnv *env,
       jobject /* this */) {
    std::string hello = "Hello from dynamic C++";
   return env->NewStringUTF(hello.c_str());
}

//需要注册的函数列表,放在JNINativeMethod类型的数组中,以后如果需要增加函数,只需在这里添加就行了
static JNINativeMethod gMethods[] = {
        {"stringFromJNI", "()Ljava/lang/String;", (void *) native_stringFromJNI}
};

//此函数通过调用RegisterNatives方法来注册我们的函数
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *getMethods,
                                int methodsNum) {
    jclass clazz;
   //找到声明native方法的类
   clazz = env->FindClass(className);
   if (clazz == NULL) {
        return JNI_FALSE;
   }
    //注册函数 参数:java类 所要注册的函数数组 注册函数的个数
   if (env->RegisterNatives(clazz, getMethods, methodsNum) < 0) {
        return JNI_FALSE;
   }
    return JNI_TRUE;
}

static int registerNatives(JNIEnv *env) {
    //指定类的路径,通过FindClass 方法来找到对应的类
   const char *className = "com/stan/jni/MainActivity";
   return registerNativeMethods(env, className, gMethods,sizeof(gMethods) / sizeof(gMethods[0]));
}

//System.loadLibrary回调函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
   //获取JNIEnv
   if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
   }

    assert(env != NULL);
   //注册函数 registerNatives ->registerNativeMethods
   if (!registerNatives(env)) {
        return -1;
   }

    //返回jni 的版本
   return JNI_VERSION_1_6;
}

静态注册与动态注册的对比:

  • 静态注册:java的native方法与c/c++方法一一对应地书写。当需要修改包名、类名时需要逐个修改;
  • 动态注册:动态关联java的native方法与c/c++方法,第一次书写比较繁琐,之后修改类名包名、增加删除方法比较灵活。

七、System.loadLibrary源码简析

这里简单分析下System.loadLibrary(libName)如何加载so,代码基于Android 8.0。

System.loadLibrary(libName) 通过Runtime来加载:

libcore/ojluni/src/main/java/java/lang/Runtime.java

synchronized void loadLibrary0(ClassLoader loader, String libname) {
    if (libname.indexOf((int)File.separatorChar) != -1) {
        throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
   }
    String libraryName = libname;
   if (loader != null) {
        String filename = loader.findLibrary(libraryName);
       if (filename == null) {
            // It's not necessarily true that the ClassLoader used
           // System.mapLibraryName, but the default setup does, and it's
           // misleading to say we didn't find "libMyLibrary.so" when we
           // actually searched for "liblibMyLibrary.so.so".
           throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                           System.mapLibraryName(libraryName) + "\"");
       }
        String error = doLoad(filename, loader);
       if (error != null) {
            throw new UnsatisfiedLinkError(error);
       }
        return;
   }
    String filename = System.mapLibraryName(libraryName);
   List candidates = new ArrayList();
   String lastError = null;
   for (String directory : getLibPaths()) {
        String candidate = directory + filename;
       candidates.add(candidate);
       if (IoUtils.canOpenReadOnly(candidate)) {
            String error = doLoad(candidate, loader);
           if (error == null) {
                return; // We successfully loaded the library. Job done.
           }
            lastError = error;
       }
    }
    if (lastError != null) {
        throw new UnsatisfiedLinkError(lastError);
   }
    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

整个方法主要功能是首先通过BaseDexClassLoader.findLibrary去找,找不到则通过getLibPaths()去找,其中之一能找到则通过doLoad去加载so。那么整体看来,Runtime.loadLibrary0 主要分两步走:先找so,在加载so。

1.找so
1)通过ClassLoader.findLibrary来获取so绝对路径
ClassLoader.findLibrary调用时序图

路径包括:

/data/app/com.stan.cmakedemo-PgoTyGH7OVC_1VGtmnSlGg==/lib/arm64,  应用安装拷贝的目录寻找so
/data/app/com.stan.cmakedemo-PgoTyGH7OVC_1VGtmnSlGg==/base.apk!/lib/arm64-v8a, 对apk内部寻找so
/system/lib64, 系统目录
/system/product/lib64 系统设备目录

不同架构和Android版本的手机应该会有差异,这里是小米9,x64架构,Android 10系统,仅供参考。

2)通过getLibPaths()来获取

即String javaLibraryPath = System.getProperty("java.library.path");

路径包括:

/system/lib64, 系统目录
/system/product/lib64 系统设备目录

找so总结:

如果ClassLoader不为null,则通过ClassLoader去找,先找从应用内部找,然后再找系统目录,如果ClassLoader为null,则直接去系统目录找。

2.加载so

Runtime.doLoad

Runtime.doLoad调用时序图

OpenNativeLibrary最终通过dlopen方式打开so文件,返回文件操作符handle。

应用侧so加载重试:System.load(Build.CPU_ABI ) > System.loadLibrary(libName) > System.load(absPath);

八、引入三方so库

1.so在工程中存放位置选择:

  • app/libs
  • app/src/main/jniLibs

2.CMakeList.txt配置 ( third-party.so)

#1设置so库路径
#CMAKE_SOURCE_DIR :CMakeList.txt文件所在的绝对路径
set(my_lib_path ${CMAKE_SOURCE_DIR}/libs)

#2将第三方库作为动态库引用
add_library( 
             third-party
             SHARED
             IMPORTED )

#3指明第三方库的绝对路径
#ANDROID_ABI :当前需要编译的版本平台
set_target_properties( 
                        third-party
                       PROPERTIES IMPORTED_LOCATION
                       ${my_lib_path}/${ANDROID_ABI}/ third-party.so )

#2+3的另一种写法
add_library( # Sets the name of the library.
             third-party
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             # 也可以直接指定路径
             ${my_lib_path}/${ANDROID_ABI}/ third-party.so)

#4 链接对应的so库
target_link_libraries( # Specifies the target library.
                       third-party
                       ${log-lib} )

3 gradle配置

android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                abiFilters 'armeabi-v7a', 'arm64-v8a’ //选择有so支持的平台
                cppFlags ""
            }
        }
    }

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt” //CMakeLists.txt路径 也有放cpp目录的:src/main/cpp/CMakeLists.txt
        }
    }

    sourceSets {
        main {  
            jniLibs.srcDir('libs’)//指定so文件夹路径
            jni.srcDirs = []
    }
}
}

你可能感兴趣的:(NDK开发(二)- JNI)