Android插件化原理和开发实战
一、前言
用过Android手机的人都知道,安装Android应用时都是安装一个个的apk文件,那么这个apk文件是怎么加载然后运行到手机上的呢?
开发Java的人都知道要想把Java文件编译成机器能识别的机器码文件需要依靠Java虚拟机,Android同样如此,不同的是Android并不是直接加载Java文件,而是加载apk安装包中dex文件和资源文件,将它们加载后然后显示到手机上给用户使用。
二、类加载机制
说到加载,就少不了类加载器,Android中的类加载器示意图如下:
BootClassLoader:用来加载SDK类 也就是framework的class类
PathClassLoader:加载应用的类 如第三方jar以及我们程序的class类,由系统调用
DexClassLoader:给我们手动加载class类,功能和PathClassLoader差不多,一般都选用这个来手动进行类加载。
DexPathList:dexfile文件的容器,里面的dexElements属性用于存放所有dex文件
双亲委托机制
要想加载一个类,不是自己直接通过loadclass()
去加载,而是先去请求父类去进行加载,如果父类加载了,就直接返回,如果没有加载到再交给子类自己去加载。
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) {
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.
c = findClass(name);
}
}
return c;
}
好处:
1、避免重复加载,提高加载效率
2、保证加载类的安全性。(例子 自己新建java.lang.String类,加载的仍然是系统api的String类)
三、源码展示
下面展示各个类的源码:(以Android API Level: 28 为例)
BaseDexClassLoader.java
package dalvik.system;
import java.io.File;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
/**
* Hook for customizing how dex files loads are reported.
*
* This enables the framework to monitor the use of dex files. The
* goal is to simplify the mechanism for optimizing foreign dex files and
* enable further optimizations of secondary dex files.
*
* The reporting happens only when new instances of BaseDexClassLoader
* are constructed and will be active only after this field is set with
* {@link BaseDexClassLoader#setReporter}.
*/
/* @NonNull */ private static volatile Reporter reporter = null;
private final DexPathList pathList;
/**
* Constructs an instance.
* Note that all the *.jar and *.apk files from {@code dexPath} might be
* first extracted in-memory before the code is loaded. This can be avoided
* by passing raw dex files (*.dex) in the {@code dexPath}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android.
* @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}
/**
* @hide
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
/**
* Reports the current class loader chain to the registered {@code reporter}.
* The chain is reported only if all its elements are {@code BaseDexClassLoader}.
*/
private void reportClassLoaderChain() {
ArrayList<BaseDexClassLoader> classLoadersChain = new ArrayList<>();
ArrayList<String> classPaths = new ArrayList<>();
classLoadersChain.add(this);
classPaths.add(String.join(File.pathSeparator, pathList.getDexPaths()));
boolean onlySawSupportedClassLoaders = true;
ClassLoader bootClassLoader = ClassLoader.getSystemClassLoader().getParent();
ClassLoader current = getParent();
while (current != null && current != bootClassLoader) {
if (current instanceof BaseDexClassLoader) {
BaseDexClassLoader bdcCurrent = (BaseDexClassLoader) current;
classLoadersChain.add(bdcCurrent);
classPaths.add(String.join(File.pathSeparator, bdcCurrent.pathList.getDexPaths()));
} else {
onlySawSupportedClassLoaders = false;
break;
}
current = current.getParent();
}
if (onlySawSupportedClassLoaders) {
reporter.report(classLoadersChain, classPaths);
}
}
/**
* Constructs an instance.
*
* dexFile must be an in-memory representation of a full dexFile.
*
* @param dexFiles the array of in-memory dex files containing classes.
* @param parent the parent class loader
*
* @hide
*/
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
// TODO We should support giving this a library search path maybe.
super(parent);
this.pathList = new DexPathList(this, dexFiles);
}
@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;
}
/**
* @hide
*/
public void addDexPath(String dexPath) {
addDexPath(dexPath, false /*isTrusted*/);
}
/**
* @hide
*/
public void addDexPath(String dexPath, boolean isTrusted) {
pathList.addDexPath(dexPath, null /*optimizedDirectory*/, isTrusted);
}
/**
* Adds additional native paths for consideration in subsequent calls to
* {@link #findLibrary(String)}
* @hide
*/
public void addNativePath(Collection<String> libPaths) {
pathList.addNativePath(libPaths);
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
/**
* Returns package information for the given package.
* Unfortunately, instances of this class don't really have this
* information, and as a non-secure {@code ClassLoader}, it isn't
* even required to, according to the spec. Yet, we want to
* provide it, in order to make all those hopeful callers of
* {@code myClass.getPackage().getName()} happy. Thus we construct
* a {@code Package} object the first time it is being requested
* and fill most of the fields with dummy values. The {@code
* Package} object is then put into the {@code ClassLoader}'s
* package cache, so we see the same one next time. We don't
* create {@code Package} objects for {@code null} arguments or
* for the default package.
*
* <p>There is a limited chance that we end up with multiple
* {@code Package} objects representing the same package: It can
* happen when when a package is scattered across different JAR
* files which were loaded by different {@code ClassLoader}
* instances. This is rather unlikely, and given that this whole
* thing is more or less a workaround, probably not worth the
* effort to address.
*
* @param name the name of the class
* @return the package information for the class, or {@code null}
* if there is no package information available for it
*/
@Override
protected synchronized Package getPackage(String name) {
if (name != null && !name.isEmpty()) {
Package pack = super.getPackage(name);
if (pack == null) {
pack = definePackage(name, "Unknown", "0.0", "Unknown",
"Unknown", "0.0", "Unknown", null);
}
return pack;
}
return null;
}
/**
* @hide
*/
public String getLdLibraryPath() {
StringBuilder result = new StringBuilder();
for (File directory : pathList.getNativeLibraryDirectories()) {
if (result.length() > 0) {
result.append(':');
}
result.append(directory);
}
return result.toString();
}
@Override public String toString() {
return getClass().getName() + "[" + pathList + "]";
}
/**
* Sets the reporter for dex load notifications.
* Once set, all new instances of BaseDexClassLoader will report upon
* constructions the loaded dex files.
*
* @param newReporter the new Reporter. Setting null will cancel reporting.
* @hide
*/
public static void setReporter(Reporter newReporter) {
reporter = newReporter;
}
/**
* @hide
*/
public static Reporter getReporter() {
return reporter;
}
/**
* @hide
*/
public interface Reporter {
/**
* Reports the construction of a BaseDexClassLoader and provides information about the
* class loader chain.
* Note that this only reports if all class loader in the chain are BaseDexClassLoader.
*
* @param classLoadersChain the chain of class loaders used during the construction of the
* class loader. The first element is the BaseDexClassLoader being constructed,
* the second element is its parent, and so on.
* @param classPaths the class paths of the class loaders present in
* {@param classLoadersChain}. The first element corresponds to the first class
* loader and so on. A classpath is represented as a list of dex files separated by
* {@code File.pathSeparator}.
*/
void report(List<BaseDexClassLoader> classLoadersChain, List<String> classPaths);
}
}
DexPathList .java
package dalvik.system;
import android.system.ErrnoException;
import android.system.StructStat;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import libcore.io.ClassPathURLStreamHandler;
import libcore.io.IoUtils;
import libcore.io.Libcore;
import static android.system.OsConstants.S_ISDIR;
/**
* A pair of lists of entries, associated with a {@code ClassLoader}.
* One of the lists is a dex/resource path — typically referred
* to as a "class path" — list, and the other names directories
* containing native code libraries. Class path entries may be any of:
* a {@code .jar} or {@code .zip} file containing an optional
* top-level {@code classes.dex} file as well as arbitrary resources,
* or a plain {@code .dex} file (with no possibility of associated
* resources).
*
* <p>This class also contains methods to use these lists to look up
* classes and resources.</p>
*/
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";
/** class definition context */
private final ClassLoader definingContext;
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private Element[] dexElements;//装载dex文件
/** List of native library path elements. */
// Some applications rely on this field being an array or we'd use a final list here
/* package visible for testing */ NativeLibraryElement[] nativeLibraryPathElements;
/** List of application native library directories. */
private final List<File> nativeLibraryDirectories;
/** List of system native library directories. */
private final List<File> systemNativeLibraryDirectories;
/**
* Exceptions thrown during creation of the dexElements list.
*/
private IOException[] dexElementsSuppressedExceptions;
/**
* Construct an instance.
*
* @param definingContext the context in which any as-yet unresolved
* classes should be defined
*
* @param dexFiles the bytebuffers containing the dex files that we should load classes from.
*/
public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexFiles == null) {
throw new NullPointerException("dexFiles == null");
}
if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) {
throw new NullPointerException("dexFiles contains a null Buffer!");
}
this.definingContext = definingContext;
// TODO It might be useful to let in-memory dex-paths have native libraries.
this.nativeLibraryDirectories = Collections.emptyList();
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
/**
* Constructs an instance.
*
* @param definingContext the context in which any as-yet unresolved
* classes should be defined
* @param dexPath list of dex/resource path elements, separated by
* {@code File.pathSeparator}
* @param librarySearchPath list of native library directory path elements,
* separated by {@code File.pathSeparator}
* @param optimizedDirectory directory where optimized {@code .dex} files
* should be found and written to, or {@code null} to use the default
* system directory for same
*/
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
// Native libraries may exist in both the system and
// application library paths, and we use this search order:
//
// 1. This class loader's library path for application libraries (librarySearchPath):
// 1.1. Native library directories
// 1.2. Path to libraries in apk-files
// 2. The VM's library path from the system property for system libraries
// also known as java.library.path
//
// This order was reversed prior to Gingerbread; see http://b/2933456.
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
@Override public String toString() {
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
File[] nativeLibraryDirectoriesArray =
allNativeLibraryDirectories.toArray(
new File[allNativeLibraryDirectories.size()]);
return "DexPathList[" + Arrays.toString(dexElements) +
",nativeLibraryDirectories=" + Arrays.toString(nativeLibraryDirectoriesArray) + "]";
}
/**
* For BaseDexClassLoader.getLdLibraryPath.
*/
public List<File> getNativeLibraryDirectories() {
return nativeLibraryDirectories;
}
/**
* Adds a new path to this instance
* @param dexPath list of dex/resource path element, separated by
* {@code File.pathSeparator}
* @param optimizedDirectory directory where optimized {@code .dex} files
* should be found and written to, or {@code null} to use the default
* system directory for same
*/
public void addDexPath(String dexPath, File optimizedDirectory) {
addDexPath(dexPath, optimizedDirectory, false);
}
public void addDexPath(String dexPath, File optimizedDirectory, boolean isTrusted) {
final List<IOException> suppressedExceptionList = new ArrayList<IOException>();
final Element[] newElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptionList, definingContext, isTrusted);
if (newElements != null && newElements.length > 0) {
final Element[] oldElements = dexElements;
dexElements = new Element[oldElements.length + newElements.length];
System.arraycopy(
oldElements, 0, dexElements, 0, oldElements.length);
System.arraycopy(
newElements, 0, dexElements, oldElements.length, newElements.length);
}
if (suppressedExceptionList.size() > 0) {
final IOException[] newSuppressedExceptions = suppressedExceptionList.toArray(
new IOException[suppressedExceptionList.size()]);
if (dexElementsSuppressedExceptions != null) {
final IOException[] oldSuppressedExceptions = dexElementsSuppressedExceptions;
final int suppressedExceptionsLength = oldSuppressedExceptions.length +
newSuppressedExceptions.length;
dexElementsSuppressedExceptions = new IOException[suppressedExceptionsLength];
System.arraycopy(oldSuppressedExceptions, 0, dexElementsSuppressedExceptions,
0, oldSuppressedExceptions.length);
System.arraycopy(newSuppressedExceptions, 0, dexElementsSuppressedExceptions,
oldSuppressedExceptions.length, newSuppressedExceptions.length);
} else {
dexElementsSuppressedExceptions = newSuppressedExceptions;
}
}
}
/**
* Splits the given dex path string into elements using the path
* separator, pruning out any elements that do not refer to existing
* and readable files.
*/
private static List<File> splitDexPath(String path) {
return splitPaths(path, false);
}
/**
* Splits the given path strings into file elements using the path
* separator, combining the results and filtering out elements
* that don't exist, aren't readable, or aren't either a regular
* file or a directory (as specified). Either string may be empty
* or {@code null}, in which case it is ignored. If both strings
* are empty or {@code null}, or all elements get pruned out, then
* this returns a zero-element list.
*/
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
List<File> result = new ArrayList<>();
if (searchPath != null) {
for (String path : searchPath.split(File.pathSeparator)) {
if (directoriesOnly) {
try {
StructStat sb = Libcore.os.stat(path);
if (!S_ISDIR(sb.st_mode)) {
continue;
}
} catch (ErrnoException ignored) {
continue;
}
}
result.add(new File(path));
}
}
return result;
}
private static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
List<IOException> suppressedExceptions) {
Element[] elements = new Element[dexFiles.length];
int elementPos = 0;
for (ByteBuffer buf : dexFiles) {
try {
DexFile dex = new DexFile(buf);
elements[elementPos++] = new Element(dex);
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + buf, suppressed);
suppressedExceptions.add(suppressed);
}
}
if (elementPos != elements.length) {
elements = Arrays.copyOf(elements, elementPos);
}
return elements;
}
/**
* Makes an array of dex/resource path elements, one per element of
* the given array.
*/
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader) {
return makeDexElements(files, optimizedDirectory, suppressedExceptions, loader, false);
}
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files up front.
*/
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if
* the zip file turns out to be resource-only (that is, no classes.dex file
* in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
/**
* Constructs a {@code DexFile} instance, as appropriate depending on whether
* {@code optimizedDirectory} is {@code null}. An application image file may be associated with
* the {@code loader} if it is not null.
*/
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
/**
* Converts a dex/jar file path and an output directory to an
* output file path for an associated optimized dex file.
*/
private static String optimizedPathFor(File path,
File optimizedDirectory) {
/*
* Get the filename component of the path, and replace the
* suffix with ".dex" if that's not already the suffix.
*
* We don't want to use ".odex", because the build system uses
* that for files that are paired with resource-only jar
* files. If the VM can assume that there's no classes.dex in
* the matching jar, it doesn't need to open the jar to check
* for updated dependencies, providing a slight performance
* boost at startup. The use of ".dex" here matches the use on
* files in /data/dalvik-cache.
*/
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
/*
* TODO (dimitry): Revert after apps stops relying on the existence of this
* method (see http://b/21957414 and http://b/26317852 for details)
*/
@SuppressWarnings("unused")
private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
return makeDexElements(files, optimizedDirectory, suppressedExceptions, null);
}
/**
* Makes an array of directory/zip path elements for the native library search path, one per
* element of the given array.
*/
private static NativeLibraryElement[] makePathElements(List<File> files) {
NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
int elementsPos = 0;
for (File file : files) {
String path = file.getPath();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
File zip = new File(split[0]);
String dir = split[1];
elements[elementsPos++] = new NativeLibraryElement(zip, dir);
} else if (file.isDirectory()) {
// We support directories for looking up native libraries.
elements[elementsPos++] = new NativeLibraryElement(file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
/**
* Finds the named resource in one of the zip/jar files pointed at
* by this instance. This will find the one in the earliest listed
* path element.
*
* @return a URL to the named resource or {@code null} if the
* resource is not found in any of the zip/jar files
*/
public URL findResource(String name) {
for (Element element : dexElements) {
URL url = element.findResource(name);
if (url != null) {
return url;
}
}
return null;
}
/**
* Finds all the resources with the given name, returning an
* enumeration of them. If there are no resources with the given
* name, then this method returns an empty enumeration.
*/
public Enumeration<URL> findResources(String name) {
ArrayList<URL> result = new ArrayList<URL>();
for (Element element : dexElements) {
URL url = element.findResource(name);
if (url != null) {
result.add(url);
}
}
return Collections.enumeration(result);
}
/**
* Finds the named native code library on any of the library
* directories pointed at by this instance. This will find the
* one in the earliest listed directory, ignoring any that are not
* readable regular files.
*
* @return the complete path to the library or {@code null} if no
* library was found
*/
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
/**
* Returns the list of all individual dex files paths from the current list.
* The list will contain only file paths (i.e. no directories).
*/
/*package*/ List<String> getDexPaths() {
List<String> dexPaths = new ArrayList<String>();
for (Element e : dexElements) {
String dexPath = e.getDexPath();
if (dexPath != null) {
// Add the element to the list only if it is a file. A null dex path signals the
// element is a resource directory or an in-memory dex file.
dexPaths.add(dexPath);
}
}
return dexPaths;
}
/**
* Adds a collection of library paths from which to load native libraries. Paths can be absolute
* native library directories (i.e. /data/app/foo/lib/arm64) or apk references (i.e.
* /data/app/foo/base.apk!/lib/arm64).
*
* Note: This method will attempt to dedupe elements.
* Note: This method replaces the value of {@link #nativeLibraryPathElements}
*/
public void addNativePath(Collection<String> libPaths) {
if (libPaths.isEmpty()) {
return;
}
List<File> libFiles = new ArrayList<>(libPaths.size());
for (String path : libPaths) {
libFiles.add(new File(path));
}
ArrayList<NativeLibraryElement> newPaths =
new ArrayList<>(nativeLibraryPathElements.length + libPaths.size());
newPaths.addAll(Arrays.asList(nativeLibraryPathElements));
for (NativeLibraryElement element : makePathElements(libFiles)) {
if (!newPaths.contains(element)) {
newPaths.add(element);
}
}
nativeLibraryPathElements = newPaths.toArray(new NativeLibraryElement[newPaths.size()]);
}
/**
* Element of the dex/resource path. Note: should be called DexElement, but apps reflect on
* this.
*/
/*package*/ static class Element {
/**
* A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
* (only when dexFile is null).
*/
private final File path;
private final DexFile dexFile;
private ClassPathURLStreamHandler urlHandler;
private boolean initialized;
/**
* Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
* should be null), or a jar (in which case dexZipPath should denote the zip file).
*/
public Element(DexFile dexFile, File dexZipPath) {
this.dexFile = dexFile;
this.path = dexZipPath;
}
public Element(DexFile dexFile) {
this.dexFile = dexFile;
this.path = null;
}
public Element(File path) {
this.path = path;
this.dexFile = null;
}
/**
* Constructor for a bit of backwards compatibility. Some apps use reflection into
* internal APIs. Warn, and emulate old behavior if we can. See b/33399341.
*
* @deprecated The Element class has been split. Use new Element constructors for
* classes and resources, and NativeLibraryElement for the library
* search path.
*/
@Deprecated
public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
System.err.println("Warning: Using deprecated Element constructor. Do not use internal"
+ " APIs, this constructor will be removed in the future.");
if (dir != null && (zip != null || dexFile != null)) {
throw new IllegalArgumentException("Using dir and zip|dexFile no longer"
+ " supported.");
}
if (isDirectory && (zip != null || dexFile != null)) {
throw new IllegalArgumentException("Unsupported argument combination.");
}
if (dir != null) {
this.path = dir;
this.dexFile = null;
} else {
this.path = zip;
this.dexFile = dexFile;
}
}
/*
* Returns the dex path of this element or null if the element refers to a directory.
*/
private String getDexPath() {
if (path != null) {
return path.isDirectory() ? null : path.getAbsolutePath();
} else if (dexFile != null) {
// DexFile.getName() returns the path of the dex file.
return dexFile.getName();
}
return null;
}
@Override
public String toString() {
if (dexFile == null) {
return (path.isDirectory() ? "directory \"" : "zip file \"") + path + "\"";
} else {
if (path == null) {
return "dex file \"" + dexFile + "\"";
} else {
return "zip file \"" + path + "\"";
}
}
}
public synchronized void maybeInit() {
if (initialized) {
return;
}
if (path == null || path.isDirectory()) {
initialized = true;
return;
}
try {
urlHandler = new ClassPathURLStreamHandler(path.getPath());
} catch (IOException ioe) {
/*
* Note: ZipException (a subclass of IOException)
* might get thrown by the ZipFile constructor
* (e.g. if the file isn't actually a zip/jar
* file).
*/
System.logE("Unable to open zip file: " + path, ioe);
urlHandler = null;
}
// Mark this element as initialized only after we've successfully created
// the associated ClassPathURLStreamHandler. That way, we won't leave this
// element in an inconsistent state if an exception is thrown during initialization.
//
// See b/35633614.
initialized = true;
}
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
public URL findResource(String name) {
maybeInit();
if (urlHandler != null) {
return urlHandler.getEntryUrlOrNull(name);
}
// We support directories so we can run tests and/or legacy code
// that uses Class.getResource.
if (path != null && path.isDirectory()) {
File resourceFile = new File(path, name);
if (resourceFile.exists()) {
try {
return resourceFile.toURI().toURL();
} catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
}
return null;
}
}
/**
* Element of the native library path
*/
/*package*/ static class NativeLibraryElement {
/**
* A file denoting a directory or zip file.
*/
private final File path;
/**
* If path denotes a zip file, this denotes a base path inside the zip.
*/
private final String zipDir;
private ClassPathURLStreamHandler urlHandler;
private boolean initialized;
public NativeLibraryElement(File dir) {
this.path = dir;
this.zipDir = null;
// We should check whether path is a directory, but that is non-eliminatable overhead.
}
public NativeLibraryElement(File zip, String zipDir) {
this.path = zip;
this.zipDir = zipDir;
// Simple check that should be able to be eliminated by inlining. We should also
// check whether path is a file, but that is non-eliminatable overhead.
if (zipDir == null) {
throw new IllegalArgumentException();
}
}
@Override
public String toString() {
if (zipDir == null) {
return "directory \"" + path + "\"";
} else {
return "zip file \"" + path + "\"" +
(!zipDir.isEmpty() ? ", dir \"" + zipDir + "\"" : "");
}
}
public synchronized void maybeInit() {
if (initialized) {
return;
}
if (zipDir == null) {
initialized = true;
return;
}
try {
urlHandler = new ClassPathURLStreamHandler(path.getPath());
} catch (IOException ioe) {
/*
* Note: ZipException (a subclass of IOException)
* might get thrown by the ZipFile constructor
* (e.g. if the file isn't actually a zip/jar
* file).
*/
System.logE("Unable to open zip file: " + path, ioe);
urlHandler = null;
}
// Mark this element as initialized only after we've successfully created
// the associated ClassPathURLStreamHandler. That way, we won't leave this
// element in an inconsistent state if an exception is thrown during initialization.
//
// See b/35633614.
initialized = true;
}
public String findNativeLibrary(String name) {
maybeInit();
if (zipDir == null) {
String entryPath = new File(path, name).getPath();
if (IoUtils.canOpenReadOnly(entryPath)) {
return entryPath;
}
} else if (urlHandler != null) {
// Having a urlHandler means the element has a zip file.
// In this case Android supports loading the library iff
// it is stored in the zip uncompressed.
String entryName = zipDir + '/' + name;
if (urlHandler.isEntryStored(entryName)) {
return path.getPath() + zipSeparator + entryName;
}
}
return null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof NativeLibraryElement)) return false;
NativeLibraryElement that = (NativeLibraryElement) o;
return Objects.equals(path, that.path) &&
Objects.equals(zipDir, that.zipDir);
}
@Override
public int hashCode() {
return Objects.hash(path, zipDir);
}
}
}
DexClassLoader.java
package dalvik.system;
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>Prior to API level 26, this class loader requires an
* application-private, writable directory to cache optimized classes.
* Use {@code Context.getCodeCacheDir()} to create such a directory:
* <pre> {@code
* File dexOutputDir = context.getCodeCacheDir();
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
PathClassLoader.java
package dalvik.system;
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
public class PathClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
*
* <ul>
* <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
* <li>Raw ".dex" files (not inside a zip file).
* </ul>
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
四、插件化实现原理
1)、Android class类加载流程
①、读取dex文件
②、将读取的dex文件通过addDexPath()
方法加入到DexPathList 的dexElements对象中,而BaseDexClassLoader类中持有DexPathList对象的引用,为接下来的加载做准备。
③、通过类加载机制加载所有已添加到dexElements中的dex文件。
2)、插件化实现原理分析
插件化正是基于类加载流程而实现的一项技术,其主要原理分为以下几步:
①、通过反射获取到宿主的dexElements对象,由于这个dexElements属性是对象属性,所以还需要获取DexPathList 实例对象,这个对象是BaseDexClassLoader类的实例属性,所以要先获取 BaseDexClassLoader对象,通过getClassLoader()
即可获取
②、获取插件的dexElements对象,获取方法和获取宿主的基本差不多,唯一的区别是类加载器不能通过getClassLoader()
来获取,而是需要自己创建一个DexClassLoader
类加载器。
/**
* @param dex_path dex文件路径
* @param optimizedDirectory 优化目录 this parameter is deprecated and has no effect * since API level 26. 这个参数从API 26以后被废弃了
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be {@code null}
* @param parent 自己传入classloader
*/
DexClassLoader dexClassLoader = new DexClassLoader(dex_path, context.getCacheDir().getAbsolutePath(), null, classLoader);
③、创建新的dexElements对象newDexElements,将宿主的dexElements和插件的dexElements对象合并成一个。
④、将宿主的dexElements对象替换成新创建的newDexElements。
五、插件化开发实战
LoadPluginUtils.java
package com.example.classloaderdemo;
import android.content.Context;
import android.util.Log;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
/**
* Created by suns on 2021/09/07 09:21 .
*/
public class LoadPluginUtils {
//定义插件dex文件路径 需要sdcard卡权限,一般这个路径不会这些写死。
private static final String dex_path = "/sdcard/app-debug.apk";
public static void loadPlugin(Context context) {
try {
ClassLoader classLoader = context.getClassLoader();
Class pathClassLoaderClazz = Class.forName("dalvik.system.BaseDexClassLoader");
Field dexPathField = pathClassLoaderClazz.getDeclaredField("pathList");
dexPathField.setAccessible(true);
Object objDexPath = dexPathField.get(classLoader);
Class dexPathListClazz = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClazz.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//拿到宿主的dexElements 对象
Object[] elements = (Object[]) dexElementsField.get(objDexPath);
//----------------------------------------------------------------
DexClassLoader dexClassLoader = new DexClassLoader(dex_path, context.getCacheDir().getAbsolutePath(), null, classLoader);
//拿到插件的pathList
Object pluginDexPath = dexPathField.get(dexClassLoader);
//拿到插件的dexElements 对象
Object[] pluginElements = (Object[]) dexElementsField.get(pluginDexPath);
// 通过Array.newInstance()创建新的dexElements对象 长度为宿主dexElements的长度加上插件dexElements的长度
Object[] newElements = (Object[]) Array.newInstance(elements.getClass().getComponentType(), elements.length + pluginElements.length);
// 通过 System.arraycopy() 方法将宿主的dexElements复制到新创建的newElements对象中
System.arraycopy(elements, 0, newElements, 0, elements.length);
// 通过 System.arraycopy() 方法将插件的dexElements复制到新创建的newElements对象中,注意第四个参数起始位置是 elements.length
System.arraycopy(pluginElements, 0, newElements, elements.length, pluginElements.length);
// 将宿主的dexElements 替换成新的newElements,即完成了插件化的调用。
dexElementsField.set(objDexPath, newElements);
//Log.e("log", "loadPlugin: " + elements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
APP.java
package com.example.classloaderdemo;
import android.app.Application;
import android.util.Log;
/**
* Created by suns on 2021/09/07 10:21 .
*/
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
// ClassLoader classLoader = getClassLoader();
// Log.e("log", "loadPlugin: " + classLoader);
// Log.e("log", "loadPlugin: " + classLoader.getParent());
// 加载插件
LoadPluginUtils.loadPlugin(this);
}
}
MainActivity.java
public void loadPlugin(View view) {
// ClassLoader classLoader = getClassLoader();
//
// while (classLoader != null) {
// classLoader = classLoader.getParent();
// Log.e("log", "loadPlugin: " + classLoader);
// }
//
// Log.e("TAG", "loadPlugin: "+AppCompatActivity.class.getClassLoader() );
try {
//加载的插件里面定义的静态方法
Class<?> aClass = Class.forName("yc.com.myapplication.Test");
Method method = aClass.getDeclaredMethod("print");
method.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
插件中定义的Test.java
package yc.com.myapplication;
import android.util.Log;
/**
* Created by suns on 2021/09/07 11:33 .
*/
public class Test {
public static void print(){
Log.e("log", "我是插件的普通方法 " );
}
}
至此一个简单的插件化开发便实现了。但是这并不是结束而是开始,因为Activity组件的加载并不像这样简单,它需要借助ams handler等通过hook技术来实现。。。。