Android插件化探索(一)类加载器DexClassLoader
来源:互联网 发布:jdk 8u5 windows x64 编辑:程序博客网 时间:2024/05/18 23:57
本文部分内容参考自《Android内核剖析》
基本概念
在Java环境中,有个概念叫做“类加载器”(ClassLoader),其作用是动态装载Class文件。标准的JavaSDK中有一个ClassLoader类,借助它可以装载想要的Class文件,每个ClassLoader对象在初始化时必须指定Class文件的路径
没有使用过ClassLoader的读者可能会问:“在过去的程序开发中,当我们需要某个类时,只需使用import关键字包含该类就可以了,为什么还要类加载器呢?”简单的讲,import中所引用的类文件有两个特点:
- 必须存在于本地,当程序运行时需要该类时,内部类装载器会自动装载该类,这对程序员来讲是透明的,即程序员感知不到这一过程。
- 编译时必须在现场,否则编译不过会因为找不到引用文件而正常编译。
但在有些情况下,所需的类却不能满足以上两个条件。比如当该类时从远程下载并在本地执行时,典型的例子就是通过浏览器中的AppleLet执行的Java程序,这些要执行的程序是在服务器端。另一种情况是,要引用的Class文件不方便在编译时直接参与,而只能运行时动态调用。举例来讲,在Android Framework中,所包含的Class文件是一些通用的类文件,但对于一些设备商而言,他们需要扩充Framework,扩充的具体工作包括两点:
- 需要增加一些额外的类文件,这些类文件提供厂商自定义的功能,这些文件一般以独立的Jar包存在。
- 需要修改Framework中的已有类文件,比如WindowManagerServcie类,在该类中添加使用自定义Jar包中的代码。使用自定义Jar包的常用方法是使用import关键字包含的自定义的类,但为了保持和原生Framework的兼容性、对于原生Framework最少化修改,可以使类装载器动态装载自定义Jar包。
这就是使用ClassLoader的原因。
在一般情况下,应用程序不需要创建一个全新的ClassLoader对象,而是使用当前环境已经存在的ClassLoader。因为Javad的Runtime环境在初始化时,其内部会创建一个ClassLoader对象用于加载Runtime所需的各种Java类。
每个ClassLoader必须有一个父ClassLoader,在装载Class文件时,子ClassLoader会先请求父ClassLoader加载该Class文件,只有当其父ClassLoader找不到该Class文件时,子ClassLoader才会继续装载该类,这是一种安全机制。关系ClassLoader的内部过程,大家可以参考《Inside the Java Virtual Machine》一书,作者为Bill Venners,相关链接如下:
http://www.artima.com/insidejvm/ed2/index.html
- 1
- 1
对于Android的应用程序,本质上虽然也是用Java开发,并且使用标准的Java编译器编译出Class文件,但最终的APK文件中包含的却是dex类型的文件。dex文件是将所需的所有Class文件重新打包,打包的规则不是简单的压缩,而是完全对Class文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是dex文件。由于dex文件是一种经过优化的Class文件,因此要加载这样特殊的Class文件就需要特殊的类装载器,这就是DexClassLoader,Android SDK中提供的DexClassLoader类就是出于这个目的。
初始API
//DexClassLoader的构造方法DexClassLoader (String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
- dexPath: 指目标类所在的jar/apk文件路径, 多个路径使用 File.pathSeparator分隔, Android里面默认为 “:”
- optimizedDirectory: 解压出的dex文件的存放路径,以免被注入攻击,不可存放在外置存储。
下面来看DexClassLoader的使用方法。 - libraryPath :目标类中的C/C++库存放路径。
- parent: 父类装载器
使用方法
DexClassLoader的使用方法一般有两种:
1. 从已安装的apk中读取dex
2. 从apk文件中读取dex
假如有两个APK,一个是宿主APK,叫作HOST,一个是插件APK,叫作Plugin。Plugin中有一个类叫PluginClass,代码如下:
public class PluginClass { public PluginClass() { Log.d("JG","初始化PluginClass"); } public int function(int a, int b){ return a+b; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
现在如果想调用插件APK中PluginClass内的方法,应该怎么办?
从已安装的apk中读取dex
先来看第一种方法,这种方法必须建一个Activity,在清单文件中配置Action.
<activity android:name=".MainActivity"> <intent-filter> <action android:name="com.maplejaw.plugin"/> </intent-filter> </activity>
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
然后在宿主APK中如下使用
/** * 这种方式用于从已安装的apk中读取,必须要有一个activity,且需要配置ACTION */ private void useDexClassLoader(){ //创建一个意图,用来找到指定的apk Intent intent = new Intent("com.maplejaw.plugin"); //获得包管理器 PackageManager pm = getPackageManager(); List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0); if(resolveinfoes.size()==0){ return; } //获得指定的activity的信息 ActivityInfo actInfo = resolveinfoes.get(0).activityInfo; //获得包名 String packageName = actInfo.packageName; //获得apk的目录或者jar的目录 String apkPath = actInfo.applicationInfo.sourceDir; //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己 //目录下的文件 String dexOutputDir = getApplicationInfo().dataDir; //native代码的目录 String libPath = actInfo.applicationInfo.nativeLibraryDir; //创建类加载器,把dex加载到虚拟机中 DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath, this.getClass().getClassLoader()); //利用反射调用插件包内的类的方法 try { Class<?> clazz = calssLoader.loadClass(packageName+".PluginClass"); Object obj = clazz.newInstance(); Class[] param = new Class[2]; param[0] = Integer.TYPE; param[1] = Integer.TYPE; Method method = clazz.getMethod("function", param); Integer ret = (Integer)method.invoke(obj, 12,34); Log.d("JG", "返回的调用结果为:" + ret); } catch (Exception e) { e.printStackTrace(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
我们安装完两个APK后,在宿主中就可以直接调用,调用示例如下。
public void btnClick(View view){ useDexClassLoader(); }
- 1
- 2
- 3
- 1
- 2
- 3
可以看出控制台打印结果如下。
05-24 14:43:12.239 4068-4068/com.maplejaw.host D/JG: 初始化PluginClass05-24 14:43:12.240 4068-4068/com.maplejaw.host D/JG: 返回的调用结果为: 46
- 1
- 2
- 1
- 2
从apk文件中读取dex
这种方法由于并不需要安装,所以不需要通过Intent从activity中解析信息。换言之,这种方法不需要创建Activity。无需配置清单文件。我们只需要打包一个apk,然后放到SD卡中即可。
核心代码如下:
//apk路径 String path=Environment.getExternalStorageDirectory().getAbsolutePath()+"/1.apk"; private void useDexClassLoader(String path){ File codeDir=getDir("dex", Context.MODE_PRIVATE); //创建类加载器,把dex加载到虚拟机中 DexClassLoader calssLoader = new DexClassLoader(path, codeDir.getAbsolutePath(), null, this.getClass().getClassLoader()); //利用反射调用插件包内的类的方法 try { Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass"); Object obj = clazz.newInstance(); Class[] param = new Class[2]; param[0] = Integer.TYPE; param[1] = Integer.TYPE; Method method = clazz.getMethod("function", param); Integer ret = (Integer)method.invoke(obj, 12,21); Log.d("JG", "返回的调用结果为: " + ret); } catch (Exception e) { e.printStackTrace(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
运行结果如下:
05-24 14:45:12.239 4068-4068/com.maplejaw.host D/JG: 初始化PluginClass05-24 14:45:12.240 4068-4068/com.maplejaw.host D/JG: 返回的调用结果为: 33
- 1
- 2
- 1
- 2
插件概念
插件是一个逻辑概念,而不是什么技术标准。总的来讲插件的概念包含以下意思:
- 插件不能独立运行,必须运行与一个宿主程序中,即由宿主程序去调用插件程序。
- 插件一般可以独立安装。
- 宿主程序中可以管理不同的插件,包或查看插件的多少,禁用和使用某个插件,如果多个插件功能是互斥的,则可以切换插件。
- 宿主程序应该保证参见的向下兼容性,即新版本的宿主程序可以运行较老版本的插件,或者说较老版本的插件能够在新版本的宿主程序中运行。
- 由于ClassLoader具有动态装载程序的特点,因此,可以使用该技术来实现一种插件架构。
插件架构化
通过ClassLoader装载的类,调用其内部函数的过程有点繁琐,使用反射构造Method对象、构造参数等等。那么,有没有一种方法,既能通过动态装载,利用动态装载的灵活性,又能像直接类引用那样方便地调用其函数?答案是有的,接口(Interface)。
首先定义一个interface接口,interface仅仅定义函数的输入输出,不定义函数的具体实现。该interface类一方面存在于Plugin项目中,另一方面存在于HOST宿主项目中。
这种方法,需保证接口的完整类名(包名+类名)是一样的,否则将会报如下异常。
java.lang.ClassCastException: com.maplejaw.plugin.PluginClass cannot be cast to com.maplejaw.host.Comm
- 1
- 1
我们应该保证两者的完整类名是一致的。一般会建一个插件接口库,给两个项目分别引用即可。又或者,在两个工程中创建一个同样名字的用于存放插件接口的包,然后把插件接口类统一放到那个包下即可。
接口定义如下:
public interface Comm { int function(int a, int b);}
- 1
- 2
- 3
- 1
- 2
- 3
现将PluginClass修改成如下:
public class PluginClass implements Comm { public PluginClass() { Log.d("JG","初始化PluginClass"); } @Override public int function(int a, int b) { return a+b; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
相应的调用核心代码修改如下:
Class<?> clazz = calssLoader.loadClass(pacageName+".PluginClass"); Comm obj = (Comm) clazz.newInstance(); Integer integer=obj.function(33,44); Log.d("JG", "返回的调用结果为:" + integer);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
打印结果如下
05-24 16:17:22.033 12963-12963/com.maplejaw.host D/JG: 初始化PluginClass05-24 16:17:22.035 12963-12963/com.maplejaw.host D/JG: 返回的调用结果为:77
- 1
- 2
- 1
- 2
注意!!!如果你按照上面进行操作,会发现这种方法在Android5.0以上运行没有任何问题,但是在5.0以下运行。你会发现报错了!!!
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation at dalvik.system.DexFile.defineClassNative(Native Method) at dalvik.system.DexFile.defineClass(DexFile.java:222) at dalvik.system.DexFile.loadClassBinaryName(DexFile.java:215)
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
从字面意思可以知道Class预校验出错,为什么会报这个错呢?那是因为插件接口被同一个加载器装载了两次。由于插件接口存在于两个不同的dex文件中,每个dex文件有一个类型id,检测到不一致所以报错。如果想加载两个相同的类,一定要用两个加载器去分别装载。你可能心想,我不是new了一个类加载器吗?明明不一样啊。由于双亲委托原则,会请求父加载器去加载,所以导致加载器是一样的。
那么怎么解决这一问题呢?思路很简单。只需保证插件接口只被装载一次就行了,一般选择让宿主APK加载。。
先把插件接口打包成jar包plugin.jar。
然后在宿主apk中如下引用
compile files('libs/plugin.jar')
- 1
- 1
在插件apk中如下引用,这种方式在打包时不会将jar包一起打包进去
provided files('libs/plugin.jar')
- 1
- 1
获取资源文件
在了解了ClassLoader的基本用法后,那么问题来了,如果想访问插件中的资源文件怎么办?
获取资源的方式比较简单,首先得知道名字,这些名字最好要事先约定好,根据名字获取相应的id,最后用id取相应资源。Android中提供的获取Resource得API:
Resources res= pm.getResourcesForApplication(packageName);
- 1
- 1
- 取图片资源
首先在插件APK中的drawable文件夹中放进图片a.jpg。然后在宿主APK中编写核心代码如下。
private void useDexClassLoader(){ //创建一个意图,用来找到指定的apk Intent intent = new Intent("com.maplejaw.plugin"); //获得包管理器 PackageManager pm = getPackageManager(); List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0); if(resolveinfoes.size()==0){ return; } //获得指定的activity的信息 ActivityInfo actInfo = resolveinfoes.get(0).activityInfo; //获得包名 String packageName = actInfo.packageName; try { Resources res= pm.getResourcesForApplication(packageName); int id=res.getIdentifier("a","drawable",packageName);//根据名字取id mImageView.setImageDrawable(res.getDrawable(id));//设置给ImageView } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
取String
比如插件APK中的string中写了author信息。<resources> <string name="author">maplejaw</string></resources>
- 1
- 2
- 3
- 1
- 2
- 3
取出author信息
Resources res= pm.getResourcesForApplication(packageName);int id=res.getIdentifier("author","string",packageName);Log.d("JG", res.getString(id));
- 1
- 2
- 3
- 1
- 2
- 3
取颜色
Resources res= pm.getResourcesForApplication(packageName); int id=res.getIdentifier("colorPrimary","color",packageName); mImageView.setBackgroundColor( res.getColor(id));
- 1
- 2
- 3
- 1
- 2
- 3
源码解读
从上面的例子可以看出,一般使用Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass");
来加载类,那么这个类做了什么呢?
由于DexClassLoader继承自BaseDexClassLoader,且遵循着双亲委托,那我们先来看下BaseDexClassLoader中的源码。
构造方法如下,可见一个DexClassLoader包含一个DexPathList。DexPathList用一个来存放dex信息的列表
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
我们来简单看一下DexPathList的源码。
属性列表如下:
private static final String DEX_SUFFIX = ".dex"; //一个ClassLoader对象 private final ClassLoader definingContext; //一个存放dex元素列表,Element是DexPathList的一个内部类 private final Element[] dexElements; //本地库目录列表 private final File[] nativeLibraryDirectories; //创建dexElements抛出的异常集合 private final IOException[] dexElementsSuppressedExceptions;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
Element内部类的构造方法如下:
private final File file; private final boolean isDirectory; private final File zip; private final DexFile dexFile; public Element(File file, boolean isDirectory, File zip, DexFile dexFile) { this.file = file; this.isDirectory = isDirectory; this.zip = zip; this.dexFile = dexFile; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
DexPathList的构造方法如下
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { //.. //省略了异常相关源码 //直接赋值ClassLoader对象 this.definingContext = definingContext; //赋值数组,splitDexPath将多个路径拆分成集合,makeDexElements根据路径遍历存取 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions); //.. //省略了异常相关源码 //赋值本地库目录集合 this.nativeLibraryDirectories = splitLibraryPath(libraryPath); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
我们来看看makeDexElements方法,看看dexElements数组是怎么赋值的。
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) { ArrayList<Element> elements = new ArrayList<Element>(); //开始遍历保存到dexElements集合 for (File file : files) { File zip = null; DexFile dex = null; String name = file.getName(); if (file.isDirectory()) { // We support directories for looking up resources. // This is only useful for running libcore tests. elements.add(new Element(file, true, null, null)); } else if (file.isFile()){ if (name.endsWith(DEX_SUFFIX)) { dex = loadDexFile(file, optimizedDirectory); } else { zip = file; dex = loadDexFile(file, optimizedDirectory); } } else { System.logW("ClassLoader referenced unknown path: " + file); } if ((zip != null) || (dex != null)) { elements.add(new Element(file, false, zip, dex)); } } return elements.toArray(new Element[elements.size()]); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
我们现在回到Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass");
,从ClassLoader类中找到loadClass
源码,如下
//loadClass(String) public Class<?> loadClass(String className) throws ClassNotFoundException { return loadClass(className, false); }
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
内部调用了loadClass的重载方法。
//loadClass(String,boolean) protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { Class<?> clazz = findLoadedClass(className);//从已装载过的类中找。 if (clazz == null) { ClassNotFoundException suppressed = null; try { clazz = parent.loadClass(className, false);//由父类装载 } catch (ClassNotFoundException e) { suppressed = e; } if (clazz == null) { try { clazz = findClass(className);//由子类装载 } catch (ClassNotFoundException e) { e.addSuppressed(suppressed); throw e; } } } return clazz; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
可以看出,该方法分三步装载类:
* 从已装载过的类中找
* 如果从已装载过的列表中找不到,则从父类装载
* 如果父类找不到,从子类装载
先来看看findLoadedClass(已装载过的类)源码如下,最终调用虚拟机的装载器去寻找。
protected final Class<?> findLoadedClass(String className) { ClassLoader loader; if (this == BootClassLoader.getInstance()) loader = null; else loader = this; return VMClassLoader.findLoadedClass(loader, className); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
由于第一次装载一定会走findClass这个方法,我们来看下源码,可以看出,最终会去pathList中寻找
@Override protected Class<?> findClass(String name) { //.. //省略了部分源码 Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { //.. //省略了抛出异常的源码 } return c; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
找到DexPathList的findClass方法。
public Class findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) {//这里进行遍历查询 DexFile dex = element.dexFile; if (dex != null) { //从DexFile中试图加载Class,从这里看出,从第一个开始遍历,如果查到就返回,这就是热修复的基本原理。 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } //.. return null; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
最后
由于SDK中无法直接查看DexClassLoader相关源码。这里把源码链接贴出来。方便大家阅读。
- DexClassLoader相关源码链接
- Android插件化探索(一)类加载器DexClassLoader
- Android插件化探索(一)类加载器DexClassLoader
- Android插件化探索(一)类加载器DexClassLoader
- 类加载器(DexClassLoader)与插件化(动态加载)
- Android插件化(二):使用DexClassLoader动态加载assets中的apk
- Android插件化(二):使用DexClassLoader动态加载assets中的apk
- 基于类加载DexClassLoader的“插件”结构
- android基于类装载器DexClassloader设计“插件框架”
- android基于类装载器DexClassloader设计“插件框架”
- android基于类装载器DexClassloader设计“插件框架”
- android基于类装载器DexClassloader设计“插件框架”
- android基于类装载器DexClassloader设计“插件框架”
- android基于类装载器DexClassloader设计“插件框架”
- Android应用程序插件化研究之DexClassLoader
- Android应用程序插件化研究之DexClassLoader
- Android应用程序插件化研究之DexClassLoader
- Android插件技术(三)DexClassloader分析
- Android类加载之PathClassLoader和DexClassLoader
- selenium python 常用方法总结
- 快速排序及Java实现
- 润乾报表JSF FORM 标签中使用填报表解决方案
- codeforces round 346div2 Polycarp and Hay搜索+并查集
- iOS导入支付宝报错
- Android插件化探索(一)类加载器DexClassLoader
- java读取项目配置文件中配置的数据
- C语言小程序死机
- 理解RESTful架构
- Windows8.1+centos7双系统详细教程
- Hadoop YARN中内存和CPU两种资源的调度和隔离
- 项目管理利器(Maven)——依赖冲突
- java 重载 多个方法的调用顺序
- 一个初学者对于MVC架构的理解