JNI源码分析(并实现JNI动态注册)
2017-10-13 00:00
429 查看
今日科技快讯
近日,职场社交平台脉脉上有人发贴称,证实腾讯王者荣耀团队今年年终奖每人将发100个月工资,然后有腾讯员工在下面留言说:“也不全是啦,我就是120个月工资。”一时间引起轩然大波。要知道,人的一生如果按40年的工龄来算,总共也就只能领480个月的工资,而腾讯一下子就发了120个月的,怎能不让人眼红?但是至于这则消息的真实性到底如何呢?腾讯的官微进行了辟谣,说现在离年终还早着呢,怎么可能现在就确定年终奖了。不过,我也有托内部的员工打听了一下这个消息的真实性,对方回答消息确实是真的。想想也对,王者荣耀今年帮助腾讯赚得是盆满钵满,区区120个月工资又算得了什么呢?
作者简介
明天就是周末啦,提前祝大家周末愉快!
本篇来自 李樟清 的投稿,分析了Java和C++语言如何通过so文件交互的,希望对大家有所帮助!
[b][b][b][b][b][b][b][b]李樟清 [/b][/b][/b][/b][/b][/b][/b][/b]的博客地址:
http://blog.csdn.net/urrjdg
C/C++的编译和链接
c/c++ ========= 二进制文件
对于C/C++ 一般分为两个阶段
1. 编译
xxx.c ——> windows .obj ; Linux .o –》 语法检查
2. 链接
.o —–> log.so .dll .exe
举例:
a.c a.h b.c b.h
a.c –>b.h(test方法)
在编译阶段只会去找b.h有没有test方法,而在链接的阶段,他会在b.o当中去找这个test方法
如果没有test方法会 报 LinkErro 错误。而这个
LinkErro 错误一般是因为我们在一个文件当中引入了一个.h文件,并且使用了这个文件当中的这个方法,而这个对应的.h文件对应的.o文件(中间文件)里面没有这个方法的实现体。
编译器
将这个C/C++编译链接生成二进制文件的这个过程是谁做的?
是 编译器,编译规则:
Eclipse
GUN编译器 ----> 编译规则 Android.mk (log.so是android自带的)
Android Studio
LLVM编译器 ----> 编译规则 CMakeList.txt
使用Android Studio 创建工程
android studio 会给我们提供一个 exceptiosns support 异常支持
javah 生成头文件
public class FileUtils { public static native void diff(String path,String pattern_Path,int file_num); public static void javaDiff(String path,String pattern_Path,int file_num){} // Used to load the 'native-lib' library on application startup. static { System.loadLibrary("native-lib"); } }
jvm 是虚拟机内存,C/C++ 是 native内存,并且这个 so库 是放在 apk 的 lib 下面的
那这个so库 ,系统是怎么找到的?System.loadLibrary是怎么来找到的?并且系统是如何来区分(JVM是怎么来区分 native 方法(diff)和 javaDiff方法)
native
关键字起到什么作用?loadLibrary 做了什么?
当我们调用 javaDiff 的时候会到 Java虚拟机 的内存当中来处理找这个方法,而加了 native 关键字的时候他就会去到 C++ 的堆栈空间找这个 C++ 的实现。
为什么 native 会这样,起了什么作用?
先在看声明了 native 的方法和没有声明 native 方法之间的区别。
使用 javap -s -p -v FileUtils.class。找到这两个方法,可以看到这两个方法的区别在于 flag ,native 声明的方法 多了个 ACC_NATIVE 的 flag。也就是说 java 在执行这个文件的时候 ,对于有 ACC_NATIVE 的 flag 的方法,他就会去 native 区间去找,如果没有ACC_NATIVE 这个
flag 就在本地的虚拟机空间来找这个方法。
C:\Users\Zeking\Desktop\Lsn9\app\src\main\java\com\example\zeking\lsn9>javap -s -p -v FileUtils.class Classfile /C:/Users/Zeking/Desktop/Lsn9/app/src/main/java/com/example/zeking/lsn9/FileUtils.class Last modified 2017-9-2; size 469 bytes MD5 checksum 19201ed5479758e0dfffb63528653a65 Compiled from "FileUtils.java" public class com.example.zeking.lsn9.FileUtils minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#16 // java/lang/Object."<init>":()V #2 = String #17 // native-lib #3 = Methodref #18.#19 // java/lang/System.loadLibrary:(Ljava/lang/String;)V #4 = Class #20 // com/example/zeking/lsn9/FileUtils #5 = Class #21 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 diff #11 = Utf8 (Ljava/lang/String;Ljava/lang/String;I)V #12 = Utf8 javaDiff #13 = Utf8 <clinit> #14 = Utf8 SourceFile #15 = Utf8 FileUtils.java #16 = NameAndType #6:#7 // "<init>":()V #17 = Utf8 native-lib #18 = Class #22 // java/lang/System #19 = NameAndType #23:#24 // loadLibrary:(Ljava/lang/String;)V #20 = Utf8 com/example/zeking/lsn9/FileUtils #21 = Utf8 java/lang/Object #22 = Utf8 java/lang/System #23 = Utf8 loadLibrary #24 = Utf8 (Ljava/lang/String;)V { public com.example.zeking.lsn9.FileUtils(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 public static native void diff(java.lang.String, java.lang.String, int); descriptor: (Ljava/lang/String;Ljava/lang/String;I)V flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE // 这边多了个 ACC_NATIVE 代表是native public static void javaDiff(java.lang.String, java.lang.String, int); descriptor: (Ljava/lang/String;Ljava/lang/String;I)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=0, locals=3, args_size=3 0: return LineNumberTable: line 11: 0 static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #2 // String native-lib 2: invokestatic #3 // Method java/lang/System.loadLibrary:(Ljava/lang/String;)V 5: return LineNumberTable: line 15: 0 line 16: 5 } SourceFile: "FileUtils.java"
System.loadLibrary找到so库文件分析
native的方法栈为什么能被jvm调用到?从 System.loadLibrary 入手
System.loadLibrary("native-lib");
System.java
public static void loadLibrary(String libname) { Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname); }
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) { // 点进去发现是return null;找到so库的全路径 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<String> candidates = new ArrayList<String>(); 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); }
所以可以想到 应该是 ClassLoader的实现类去实现了这个 findLibrary方法。
怎么找是哪个实现类 实现的呢?
Log.i(TAG,this.getClassLoader().toString()); dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.zeking.lsn9-1/base.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_dependencies_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_0_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_1_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_2_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_3_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_4_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_5_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_6_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_7_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_8_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_9_apk.apk"], nativeLibraryDirectories=[/data/app/com.example.zeking.lsn9-1/lib/arm64, /data/app/com.example.zeking.lsn9-1/base.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_dependencies_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_0_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_1_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_2_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_3_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_4_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_5_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_6_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_7_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_8_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_9_apk.apk!/lib/arm64-v8a, /vendor/lib64, /system/lib64]]]
从上面可以看出是 PathClassLoader。PathClassLoader .java 这里面没有 findLibrary 继续进到 BaseDexClassLoader
public class PathClassLoader extends BaseDexClassLoader { ...... }
BaseDexClassLoader.java
DexPathList.java
首先我们先来看
DexPathList .java 中的 String fileName = System.mapLibraryName(libraryName);
System.java 看注释可以看出 ,是根据你的平台来找你的 so库
再继续看 for (Element element : nativeLibraryPathElements)
DexPathList .java 可以看到 nativeLibraryPathElements 是在 DexPathList的构造函数里面初始化的
public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { ...... // 找so库是从两个地方来找, // 1.在BaseDexClassLoader初始化的时候传入的目录 这个目录是 librarySearchPath,这个就是应用apk下面的解压的lib目录下 // 2. 在系统的环境变量里面,System.getProperty("java.library.path"):这个目录通过Log.i(TAG,System.getProperty("java.library.path")); // 打印出来是/vendor/lib64:/system/lib64 或者 /vendor/lib:/system/lib // dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.zeking.lsn9-1.apk"], // nativeLibraryDirectories=[/data/app-lib/com.example.zeking.lsn9-1, /system/lib]]] // /data/app-lib/com.example.zeking.lsn9-1, // /system/lib this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); // 这个是系统里面 java.library.path this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); // 就是在这边进行初始化的 this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,suppressedExceptions,definingContext); ...... }
System.loadLibrary加载so库分析
分析下他是怎么加载so库的
现在回到 Runtime.java 的 loadLibrary0 方法找到他的 doLoad 方法
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); // 找到so库的全路径 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<String> candidates = new ArrayList<String>(); 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); }
doLoad 方法
private String doLoad(String name, ClassLoader loader) { if (loader != null && loader instanceof BaseDexClassLoader) { BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader; librarySearchPath = dexClassLoader.getLdLibraryPath(); } synchronized (this) { // 这一边 return nativeLoad(name, loader, librarySearchPath); } } // 这一边 private static native String nativeLoad(String filename, ClassLoader loader, String librarySearchPath);
nativeLoad 方法 要去 runtime.c(java_lang_Runtime.cc)android-7.1.0_r1.7z\android-7.1.0_r1\libcore\ojluni\src\main\native\runtime.c
以下是 Runtime.c 的源码
#include "jni.h" #include "jni_util.h" #include "jvm.h" #include "JNIHelp.h" #define NATIVE_METHOD(className, functionName, signature) \ { #functionName, signature, (void*)(className ## _ ## functionName) } JNIEXPORT jlong JNICALL Runtime_freeMemory(JNIEnv *env, jobject this) { return JVM_FreeMemory(); } JNIEXPORT jlong JNICALL Runtime_totalMemory(JNIEnv *env, jobject this) { return JVM_TotalMemory(); } JNIEXPORT jlong JNICALL Runtime_maxMemory(JNIEnv *env, jobject this) { return JVM_MaxMemory(); } JNIEXPORT void JNICALL Runtime_gc(JNIEnv *env, jobject this) { JVM_GC(); } JNIEXPORT void JNICALL Runtime_nativeExit(JNIEnv *env, jclass this, jint status) { JVM_Exit(status); } // 这个就是 nativeLoad 方法 的实现 JNIEXPORT jstring JNICALL Runtime_nativeLoad(JNIEnv *env, jclass ignored, jstring javaFilename, jobject javaLoader, jstring javaLibrarySearchPath) { // JVM_NativeLoad 方法 在 OpenjdkJvm.cc 中 return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath); } static JNINativeMethod gMethods[] = { // 使用了一个 NATIVE_METHOD 的 宏替换 ,这个宏替换在这个类的顶部 NATIVE_METHOD(Runtime, freeMemory, "!()J"), NATIVE_METHOD(Runtime, totalMemory, "!()J"), NATIVE_METHOD(Runtime, maxMemory, "!()J"), NATIVE_METHOD(Runtime, gc, "()V"), NATIVE_METHOD(Runtime, nativeExit, "(I)V"), NATIVE_METHOD(Runtime, nativeLoad, "(Ljava/lang/String;Ljava/lang/ClassLoader;Ljava/lang/String;)" "Ljava/lang/String;"), }; void register_java_lang_Runtime(JNIEnv *env) { jniRegisterNativeMethods(env, "java/lang/Runtime", gMethods, NELEM(gMethods)); }
下面就是 OpenjdkJvm.cc
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env, jstring javaFilename, jobject javaLoader, jstring javaLibrarySearchPath) { ScopedUtfChars filename(env, javaFilename); if (filename.c_str() == NULL) { return NULL; } std::string error_msg; // 这边 有一个 JavaVMExt , 这个方法的参数有一个 JNIEnv 。 // 那好,JavaVM* 和 JNIEnv 有什么区别呢? // JavaVM* : 一个android应用的进程,有且仅有一个javaVm // JNIEnv :每个java线程都对应一个env的环境变量 // 虚拟机里面jvm 是怎么找到具体的so库的堆栈的? // 他调用了 JavaVM的loadNativeLibrary 方法里面, // 创建了一个结构体(这个结构体,包一个的指针,这个指针放我们真实加载完操作的文件地址) // 在这个结构体里面将我传进来的动态库()filename.c_str())加到结构体里面,然后保存到VM里面, // 那么对于我的android进程其他的地方,我只要拿到这个VM,就能找到这个结构体, // 通过这个结构体,就能找到这个so库里面的方法栈和引用内存 art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM(); // vm->LoadNativeLibrary 方法 在 java_vm_ext.cc bool success = vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, javaLibrarySearchPath, &error_msg); if (success) { return nullptr; } }
Java_vm_ext.h
关键是与JVM的联系:android进程,有且只有一个 JavaVMExt* 指针对象,当我们在 LoadNativeLibrary 的时候,new 了一个 SharedLibrary 的对象指针,而 SharedLibrary 保存了 handle 句柄,然后在找文件方法的时候,都是通过对象里面的 handle 句柄来进行操作的,library 有一个 FindSymbol
来找方法,找到 JNI_OnLoad 方法去做具体的调用,这就是JNI设计的流程
JNI动态注册
根据以上的分析进行实现
#include "com_example_zeking_FileUtils.h" #include <android/log.h> #include <assert.h> //int __android_log_print(int prio, const char* tag, const char* fmt, ...) #define TAG "Zeking_JNI" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) #define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0]))) /* * Class: com_example_zekign_FileUtils * Method: diff * Signature: (Ljava/lang/String;Ljava/lang/String;I)V */ JNIEXPORT void JNICALL native_diff (JNIEnv *env, jclass clazz, jstring path, jstring pattern_Path, jint file_num){ LOGI("JNI begin 动态注册的方法 "); } static const JNINativeMethod gMethods[] = { { "diff","(Ljava/lang/String;Ljava/lang/String;I)V",(void*)native_diff } }; static int registerNatives(JNIEnv* engv) { LOGI("registerNatives begin"); jclass clazz; clazz = (*engv) -> FindClass(engv, "com/example/zeking/FileUtils"); if (clazz == NULL) { LOGI("clazz is null"); return JNI_FALSE; } if ((*engv) ->RegisterNatives(engv, clazz, gMethods, NELEM(gMethods)) < 0) { LOGI("RegisterNatives error"); return JNI_FALSE; } return JNI_TRUE; } JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){ LOGI("jni_OnLoad begin"); JNIEnv* env = NULL; jint result = -1; if ((*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4) != JNI_OK) { LOGI("ERROR: GetEnv failed\n"); return -1; } assert(env != NULL); registerNatives(env); return JNI_VERSION_1_4; }
静态注册:
每个 class 都需要使用 javah 生成一个头文件,并且生成的名字很长书写不便;初次调用时需要依据名字搜索对应的JNI层函数来建立关联关系,会影响运行效率。用 javah 生成头文件方便简单
javah 生成一个头文件,操作简单
名字很长,书写不方便
初次调用的使用,需要依据名字搜索对应的 FindSymbol(具体看 Runctime.c)
来找到对应的方法,如果方法数较多的时候,效率不高
动态注册:
第一次调用效率高
使用一种数据结构 JNINativeMethod 来记录 java native函数 和 JNI函数 的对应关系
移植方便,便于维护(一个java文件中有多个native方法,只要修改下gMethods 的映射关系)
由于原文过长,本文进行了一些适当的修剪。想要阅读完整文章的朋友,请点击的下方阅读原文,到作者的博客当中查看。
欢迎长按下图 -> 识别图中二维码
或者 扫一扫 关注我的公众号
相关文章推荐
- JNI源码分析 (并实现JNI动态注册)
- JNI源码分析(并实现JNI动态注册)
- JNI源码分析 (并实现JNI动态注册)
- (七)JNI 源码分析、动态注册
- android 动态注册JNI函数过程源码分析
- android 动态注册JNI函数过程源码分析
- JavaSE JNI 动态注册本地方法(c语言实现native层)
- Android JNI动态注册Native 方法(实现IDA中改名)
- Android源码解析之动态注册广播接收器的过程分析
- MTK平台CPU/GPU动态调频的实现之PerfService的源码分析
- Java动态代理详解,以及底层JDK源码实现分析(基于Java8)。
- JNI通过动态注册实现native函数
- 修改MyBatis源码实现扫描注册枚举-分析源码
- mybatis源码分析(5)-----拦截器的实现原理(动态代理+责任链)
- OpenStack基于Libvirt的虚拟化平台调度实现----Nova虚拟机动态迁移源码分析
- Android Apk资源加载机制源码分析以及资源动态加载实现系列文章
- JavaSE JNI 动态注册本地方法(c语言实现native层)
- Dubbo源码分析(六):Dubbo内核实现之动态编译
- SprignMVC+myBatis整合+mybatis源码分析+动态代理实现流程+如何根据mapper接口生成其实现类
- OpenStack基于Libvirt的虚拟化平台调度实现----Nova虚拟机动态迁移源码分析