探究Android中的ClassLoader
1.什么是ClassLoader?
ClassLoader就是类加载器,作用是将编译后的class文件加载到虚拟机中,使之成为java类
2.Android中的ClassLoader
- BootClassLoader:主要加载Android Framework层的字节码文件
- PathClassLoader:主要加载已经安装到系统中的apk文件中的字节码文件
- DexClassLoader:主要加载没有安装到系统中的apk,jar文件中的字节码文件
- BaseDexClassLoader:PathClassLoader和DexClassLoader的父类,真正实现功能的代码都在这个ClassLoader中
3.ClassLoader的双亲委托
Android中的ClassLoader基本继承了Java中ClassLoader的特点,双亲委托的特点就是从java继承过来。何为双亲委托?就是同一个ClassLoader继承树,如果父ClassLoader已经加载了某个类,那么子Classloader就不会再去加载这个类。那么这种双亲委托的设计有什么好处吗?
- 共享性(同一个ClassLoader树对于一个类,只会加载一次,做到了一次加载,一起使用)
- 隔离性(一些敏感的类会被父ClassLoader先加载,子ClassLoader不会再去加载这些类,保证不会被串改)
我们通过查看ClassLoader的源码就可以很清楚这个双亲委托的特点:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
这个方法的逻辑是这样的:
- 先去判断当前的ClassLoader是否加载目标的类
- 再去判断父ClassLoader是否加载过目标类(如果有父ClassLoader的话)
- 如果都没有在去真正去加载这个类,调用的findClass这个方法(因为ClassLoader是个抽象类,所有findClass的实现在子ClassLoader中,例如PathClassLoader,DexClassLoader)
4.Android中的ClassLoader的工作流程
这节我们将通过具体ClassLoader源码的阅读去探究一下工作流程(注:这部分源码在AS中无法查看,需要到Android源码网站上去学习,网址如下:http://androidxref.com/)
DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
慕名而来,尴尬的发现,这两个类除了构造方法一无所有,真正的逻辑代码都在他们的父类中,也就是BaseDexClassLoader中,那我们就来看看BaseDexClassLoader这个类。先看构造方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
介绍上构造方法的参数含义:
- dexPath:需要加载含有dex文件的路径,一般是jar,apk
- optimizedDirectory:解压dex文件后临时存放内容的文件夹路径,一般放在内存储中,PathClassLoader不需要这个参数,所以传null
- librarySearchPath:包含native lib的目录路径,没有传null
- parent:父类加载器
构造方法中就干了一件事情,就是初始化了DexPathList对象,这个对象是用来存储一个或多个dex文件的信息,很重要,后面做详细了解。
还记得上面在将双亲委托时,ClassLoader最后去真正完成加载工作的是findClass这个方法,那我们就来看看BaseDexClassLoader的findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
然后发现这个方法只是一个中转,他调用了pathList的findClass方法去完成加载任务,没想到这么快就要去看看DexPathList这个类了...
看DexPathList的源码不能直接看findClass这个方法了,要先看下他的成员变量,重点介绍下dexElements,他是一个Element数组,如下
private Element[] dexElements;
又出现一个新类,Element是什么鬼...他是DexPathList的内部类,下面是Element的成员变量
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
看到了一个dexFile,它代表的是一个dex文件,那么dexElements其实就是一个存储着多个dex文件信息的数组。在DexPathList的构造方法中有这么一行代码:
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext);
从物理的dex文件到dexFile的转化就是通过这个方法,这个过程的代码在DexPathList的makeElements方法中,如下:
private static Element[] makeElements(List<File> files, File optimizedDirectory,
285 List<IOException> suppressedExceptions,
286 boolean ignoreDexFiles,
287 ClassLoader loader) {
288 Element[] elements = new Element[files.size()];
289 int elementsPos = 0;
290 /*
291 * Open all files and load the (direct or contained) dex files
292 * up front.
293 */
294 for (File file : files) {
295 File zip = null;
296 File dir = new File("");
297 DexFile dex = null;
298 String path = file.getPath();
299 String name = file.getName();
300
301 if (path.contains(zipSeparator)) {
302 String split[] = path.split(zipSeparator, 2);
303 zip = new File(split[0]);
304 dir = new File(split[1]);
305 } else if (file.isDirectory()) {
306 // We support directories for looking up resources and native libraries.
307 // Looking up resources in directories is useful for running libcore tests.
308 elements[elementsPos++] = new Element(file, true, null, null);
309 } else if (file.isFile()) {
310 if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
311 // Raw dex file (not inside a zip/jar).
312 try {
313 dex = loadDexFile(file, optimizedDirectory, loader, elements);
314 } catch (IOException suppressed) {
315 System.logE("Unable to load dex file: " + file, suppressed);
316 suppressedExceptions.add(suppressed);
317 }
318 } else {
319 zip = file;
320
321 if (!ignoreDexFiles) {
322 try {
323 dex = loadDexFile(file, optimizedDirectory, loader, elements);
324 } catch (IOException suppressed) {
325 /*
326 * IOException might get thrown "legitimately" by the DexFile constructor if
327 * the zip file turns out to be resource-only (that is, no classes.dex file
328 * in it).
329 * Let dex == null and hang on to the exception to add to the tea-leaves for
330 * when findClass returns null.
331 */
332 suppressedExceptions.add(suppressed);
333 }
334 }
335 }
336 } else {
337 System.logW("ClassLoader referenced unknown path: " + file);
338 }
339
340 if ((zip != null) || (dex != null)) {
341 elements[elementsPos++] = new Element(dir, false, zip, dex);
342 }
343 }
344 if (elementsPos != elements.length) {
345 elements = Arrays.copyOf(elements, elementsPos);
346 }
347 return elements;
348 }
这个方法的实现好长...简要的说,这个方法就是遍历目标文件夹中的所有文件,找出那些dex后缀的文件,转化成DexFile,存到Element数组中,并且找到那些压缩的文件,解压他们,找到他们内部的dex文件,也转化成DexFile文件,存到Element数组中。其实就一个作用,将目标文件中可以转化成DexFile文件的文件全部转成DexFile,存到Element数组中,供findClass用。
现在我们来看下DexPathList的findClass方法
public Class findClass(String name, List<Throwable> suppressed) {
414 for (Element element : dexElements) {
415 DexFile dex = element.dexFile;
416
417 if (dex != null) {
418 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
419 if (clazz != null) {
420 return clazz;
421 }
422 }
423 }
424 if (dexElementsSuppressedExceptions != null) {
425 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
426 }
427 return null;
428 }
遍历了整个Element数组,调用每个dexfile的loadClassBinaryName的方法去加载类。然后我们来看下DexFile的loadClassBinaryName,感觉越来越接近真相了...
288 public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
289 return defineClass(name, loader, mCookie, this, suppressed);
290 }
291
292 private static Class defineClass(String name, ClassLoader loader, Object cookie,
293 DexFile dexFile, List<Throwable> suppressed) {
294 Class result = null;
295 try {
296 result = defineClassNative(name, loader, cookie, dexFile);
297 } catch (NoClassDefFoundError e) {
298 if (suppressed != null) {
299 suppressed.add(e);
300 }
301 } catch (ClassNotFoundException e) {
302 if (suppressed != null) {
303 suppressed.add(e);
304 }
305 }
306 return result;
307 }
387 private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
388 DexFile dexFile)
389 throws ClassNotFoundException, NoClassDefFoundError;
层层调用,最后调用了defineClassNative整个native方法完成加载工作,流程结束...
总结下,流程其实不复杂,只是嵌套的比较多,真正复杂的逻辑在natvie层,DexFile很重要,接触过热更新,插件化框架的朋友,可以去看看框架源码,DexFile露脸的机会不少...
5.实验
做个关于ClassLoader的小实验,将一个未安装的apk文件通过ClassLoader加载到系统中,并调用其方法。let's go!!!
QQ截图20170715185055.jpg上图是这个小demo的项目结构,app是我们将安装到系统中的module,bundle就是我们准备通过ClassLoader去加载到系统的module。
基本的实验流程如下:
- 在bundle这个module中写个普通的类和普通的方法
- 将bundle打包,debug,release都行
- 将bundle.apk push到系统的存储上
- 在app中写代码加载bundle.apk,并通过反射技术生成实例对象,再调用其方法
bundle中的实验代码如下:
public class Printer {
public void print(){
Log.i("info","i am printer from bundle");
}
}
非常简单!
app中MainActivity代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String apkPath = getExternalCacheDir().getAbsolutePath() + "/bundle.apk";
Log.i("info", "apkPath=" + apkPath);
loadApk(apkPath);
}
private void loadApk(String apkPath) {
File optFile = getDir("opt", MODE_PRIVATE);
Log.i("info", "optFile=" + optFile);
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optFile.getAbsolutePath(), null, getClassLoader());
try {
Class clz = dexClassLoader.loadClass("com.loubinfeng.www.boundle.Printer");
if (clz != null) {
Object instance = clz.newInstance();
Method method = clz.getMethod("print");
method.invoke(instance);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行后,真的在Logcat中看到了打印信息
Paste_Image.png