您的位置:首页 > 移动开发 > Android开发

JNI源码分析(并实现JNI动态注册)

2017-10-16 13:57 489 查看
本篇来自 微信公众号郭霖 中 李樟清 的投稿,分析了Java和C++语言如何通过so文件交互的,希望对大家有所帮助!

李樟清 的博客地址: 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 的映射关系)

由于原文过长,本文进行了一些适当的修剪。想要阅读完整文章的朋友,请点击的下方阅读原文,到作者的博客当中查看。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android android studio JNI