问题描述:
在做一个安卓终端往服务器端的任务迁移系统时,遇到了这个问题。
安卓应用APK传至服务器后,解压得到dex文件,即可通过反射调用其中的方法。但是若此方法调用了自己的JNI动态链接库,直接用System.load()或System.loadlibrary()会出错。提示的大概如下:
总是提示native方法找不到,即使加载了库libFiboJNI.so还是找不到。很明显JNI自动生成的na?ve函数名字太特殊,java_包名_类名_函数名,其他包的类访问不了。
参见这篇文章:http://blog.newnaw.com/?p=1013
解决思路:
- 若通过反射调用原有类里的System.load()方法是可以的,对原有类依赖太大。
-
是否是lib查找路径的问题,这个可以输出来查看
System.getProperty("java.library.path")
然而,放进去并不管用。
- 可否修改反射得到的类,给其添加方法或变量?未知。
解决过程:
先是注意到了调试信息里打印出来的 (wrong CL),网上随便搜下,并没有解释这代表了什么,但是猜到了CL应该指的是ClassLoader,但是JNI调用so库的时候,跟ClassLoader有什么关系呢?先去看看JNI的执行的相关内容。
关于System.load()和System.loadLibrary()区别:http://blog.csdn.net/ring0hx/article/details/3242245,所以用loadlibray()似乎更方便点
System.loadLibrary()的执行过程,碰到这种问题,直接看源码最好了:
继续看Runtime.getRuntime().loadLibrary()的实现:
所以如何调用Runtime.getRuntime().loadLibrary()?因为它是private,不能直接调用,先试着直接复制这段代码,果然里面的很多函数都是private,不行。第二种办法,就是通过反射来,setAccessible后可以访问private方法,确实方便。修改后再次碰到问题,第6行的findLibrary函数返回null,只好继续看findLibray的实现。
05-11 08:18:57.857: V/QRCodeActivity(11556): classLoader = dalvik.system.PathClassLoader[dexPath=/data/app/com.qrcode.qrcode-1.apk,libraryPath=/data/app-lib/com.qrcode.qrcode-1]
其中的第三个路径好像不太对,应该是新建classloader时传进来的那个libFolderPath。源码也能看出来。
参考Java中System.loadLibrary() 的执行过程,写得非常好http://my.oschina.net/wolfcs/blog/129696
关于ClassLoader:
当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。
Android的Dalvik/ART虚拟机如同标准JAVA的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此,我们可以利用这一点,在程序运行时手动加载Class,从而达到代码动态加载可执行文件的目的。Android的Dalvik/ART虚拟机虽然与标准Java的JVM虚拟机不一样,ClassLoader具体的加载细节不一样,但是工作机制是类似的,也就是说在Android中同样可以采用类似的动态加载插件的功能,只是在Android应用中动态加载一个插件的工作要比Eclipse加载一个插件复杂许多(这点后面在解释说明)。
动态加载的基础是ClassLoader,从名字也可以看出,ClassLoader就是专门用来处理类加载工作的,所以这货也叫类加载器,而且一个运行中的APP 不仅只有一个类加载器。
其实,在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类,我们的Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。
此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。
所谓双亲代理模式就是装载一个类时,先由自己定义的类装载器请求其parent装载,parent再请求它自己的parent装载,直到顶级的Bootstrap ClassLoader。 若某一级的parent能装载则装载之,否则由它的"下级"自己尝试装载。
Android 也有自己的 ClassLoader,分为 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader,区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk(因为安装过的 apk 在 cache 中存在缓存的 dex 文件)。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。
PathClassLoader 会去读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候由 Dalvik 生成的。例如,如果包的名字是 com.qihoo360.test,Android 应用安装之后都保存在 /data/app 目录下,即 /data/app/com.qihoo360.test-1.apk,那么 /data/dalvik-cache 目录下就会生成 data@app@com.qihoo360.test-1.apk@classes.dex 文件。在调用 PathClassLoader 时,它就会按照这个规则去找 dex 文件,如果你指定的 apk 文件是 /sdcard/test.apk,它按照这个规则就会去读 /data/dalvik-cache/sdcard@test.apk@classes.dex 文件,显然这个文件不会存在,所以 PathClassLoader 会报错。
参考: https://segmentfault.com/a/1190000004062880
JNI大致编写流程:
ClassLoader 隔离问题:
大家觉得一个运行程序中有没有可能同时存在两个包名和类名完全一致的类?JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个"类"不是由一个 ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是 ClassLoader 隔离。 如 Android 中碰到如下异常
android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager
当碰到这种问题时可以通过 instance.getClass().getClassLoader(); 得到 ClassLoader,看 ClassLoader 是否一样。
Java虚拟机为了在JNI本地库中确保基于classloader的命名空间隔离,因而不允许一个JNI本地库被两个不同的classloader加载。
每个应用都有一个专属的classloader,这样多个应用时,就出现两个classloader加载同一JNI本地库的情况。