今天分析一个app的老版本时,无意发现JNI_OnLoad不存在,但是so的确是java层load加载,各native函数也有声明和调用,以为又遇到什么黑科技。查找发现最早期的ndk开发版本中的确是没有这个函数的。
现期各版本的ndk中JNI_OnLoad函数都是load时自动调用的,如果未发现则去调用dvmResolveNativeMethod。以下时一份详细的流程解释。
以下摘自 http://yanbober.github.io/2015/02/25/android_studio_jni_3/
Android OS加载JNI Lib的方法有两种:
- 通过JNI_OnLoad。
- 如果JNI Lib实现中没有定义JNI_OnLoad,则dvm调用dvmResolveNativeMethod进行动态解析。
PS:咱们上面第一部分就是dvm调用dvmResolveNativeMethod进行动态解析,所以log打印No JNI_OnLoad found。
从网上查到的深入解析(此解析模块代码引用自网络)
JNI_OnLoad机制分析
System.loadLibrary调用流程如下所示:
System.loadLibrary->Runtime.loadLibrary->(Java)nativeLoad->(C: java_lang_Runtime.cpp)Dalvik_java_lang_Runtime_nativeLoad->dvmLoadNativeCode->(dalvik/vm/Native.cpp)
接着如下:
- dlopen(pathName, RTLD_LAZY) (把.so mmap到进程空间,并把func等相关信息填充到soinfo中)
- dlsym(handle, “JNI_OnLoad”)
- JNI_OnLoad->RegisterNatives->dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName, const char* signature, void* fnPtr)->dvmUseJNIBridge(method, fnPtr)->(method->nativeFunc = func)
JNI函数在进程空间中的起始地址被保存在ClassObject->directMethods中。
struct ClassObject : Object { /* static, private, and <init> methods */ int directMethodCount; Method* directMethods; /* virtual methods defined in this class; invoked through vtable */ int virtualMethodCount; Method* virtualMethods; }
此ClassObject通过gDvm.jniGlobalRefTable或gDvm.jniWeakGlobalRefLock获取。
dvmResolveNativeMethod延迟解析机制
如果JNI Lib中没有JNI_OnLoad,即在执行System.loadLibrary时,无法把此JNI Lib实现的函数在进程中的地址增加到ClassObject->directMethods。则直到需要调用的时候才会解析这些javah风格的函数 。这样的函数dvmResolveNativeMethod(dalvik/vm/Native.cpp)来进行解析,其执行流程如下所示:
void dvmResolveNativeMethod(const u4* args, JValue* pResult, const Method* method, Thread* self)->(Resolve a native method and invoke it.)
接着如下:
- void* func = lookupSharedLibMethod(method)(根据signature在所有已经打开的.so中寻找此函数实现)dvmHashForeach(gDvm.nativeLibs, findMethodInLib,(void*) method)->findMethodInLib(void* vlib, void* vmethod)->dlsym(pLib->handle, mangleCM)
- dvmUseJNIBridge((Method*) method, func)
- (*method->nativeFunc)(args, pResult, method, self);(调用执行)
说完蛋疼Load基础后该准么办?
答案其实就是推荐Android OS加载JNI Lib的方法的通过JNI_OnLoad。因为通过它你可以干许多自定义的事,譬如实现自己的本地注册等。 因为在上面的解析中已经看到了JNI_OnLoad->RegisterNatives->…这两个关键方法。具体细节咱们现在再说说。
先来看JNI_OnLoad函数
JNI_OnLoad()函数主要的用途有两点:
- 通知VM此C组件使用的JNI版本。如果你的.so文件没有提供JNI_OnLoad()函数,VM会默认该.so使用最老的JNI 1.1版本。 而新版的JNI做了许多扩充,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer, 就必须藉由JNI_OnLoad()函数来告知VM。
- 因为VM执行到System.loadLibrary()函数时,会立即先调运JNI_OnLoad(),所以C组件的开发者可以由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization)。
既然有JNI_OnLoad(),那就有相呼应的函数,那就是JNI_OnUnload(),当VM释放JNI组件时会呼叫它,因此在该方法中进行善后清理,资源释放的动作最为合适。
再来看RegisterNatives函数
在上面第一部分时我们看见通过javah命令生成的io_github_yanbober_ndkapplication_NdkJniUtils.h里函数的名字好长,看着就蛋疼。你肯定也想过怎么这么长, 而且当有时候项目需求原因导致类名变了的时候,函数名必须一个一个的改,更加蛋疼。我第一次接触时那时候自己经验不足,就遇上了这个蛋疼问题。泪奔啊!
既然这样那就有解决办法的,那就是RegisterNatives大招。接下来来看下这个大招:
App的Java程序寻找c本地方法的过程一般是依赖VM去寻找*.so里的本地函数,如果需要连续调运很多次,每次都要寻找一遍, 会多花许多时间。因此为了解决这个问题我们可以自行将本地函数向VM进行登记,然后让VM自行调registerNativeMethods()函数。
VM自行调registerNativeMethods()函数的作用主要有两点:
- 更加有效率去找到C语言的函数
- 可以在执行期间进行抽换,因为自定义的JNINativeMethod类型的methods[]数组是一个名称-函数指针对照表,在程序执行时, 可以多次调运registerNativeMethods()函数来更换本地函数指针,从而达到弹性抽换本地函数的效果。
上面提到的JNINativeMethod结构是c/c++方法和Java方法之间映射关系的关键结构,该结构定义在jni.h中,具体定义如下:
typedef struct { const char* name;//java方法名称 const char* signature; //java方法签名 void* fnPtr;//c/c++的函数指针 } JNINativeMethod;
所谓自定义的JNINativeMethod类型的methods[]数组自然也就类似长下面这样了:
static JNINativeMethod methods[] = { {"generateKey", "(Ljava/lang/String;)Ljava/lang/String;", (void*)generateKey}, };
以上也就是所谓的动态注册JNI了。