DexClassLoader和PathClassLoader区别
- 在targetSdk 26, 是不一样,optimizedDirectory用于声明dex2oat后oat存放的目录。
- 在targetSdk 28, 是完全一样,optimizedDirectory根本没有用到,注释写得很清楚了。
大量的博客文章表示,DexClassLoader能加载jar, aar, 未安装的apk, PathClassLoader只能装已安装的apk。
这么说其实是片面的,因为targetSdkVersion 26和28的源码,是不一样的(google偷偷的修bug?)
https://www.android.net.cn 下搜索DexClassLoader,有两个版本
// sdk 28下构造函数
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
//sdk 26的构造函数
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
PathClassLoader,则是在26和28是一样的代码
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
结论可知, 在targetSdkVersion 28的时候,其实DexClassLoader和PathClassLoader,其实是一模一样的。面试的时候注意不要被面试官坑了。
仅仅就构造函数中,optimizedDirectory这个参数不一样,在api 28是完全一样的
那么optimizedDirectory真的是可有可无么?
我们可以直接把焦点,聚焦到BaseDexClassLoader的构造函数:
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();
}
}
optimizedDirectory这个参数,没有参与任何的逻辑
BaseDexClassLoader的重要参与者,DexPathList
- 用于存放接续Dex文件后的Elements数组以外,
- findClass的职责,也是pathList里面查找。
BaseDexClassLoader
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();
}
}
在英文的注释上面,写得很清楚,翻译的意思是:
dexPath:jar文件和apk文件所在路径, 用"/"当分隔符,默认值为":"
optimizedDirectory:API level 26.不生效了
librarySearchPath:native Library路径,有可能为空
parent:父classLoader
系统默认的CreateSystemClassLoader中,传入的参数为:
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
打印得出值:
classPath = . , 直接为空
librarySearchPath = /system/lib:/system/product/lib, 系统里面一些.so库
BaseDexClassLoader的构造函数中,调用了super(parent)
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
//checkCreateClassLoader什么都没做,直接一个空
private static Void checkCreateClassLoader() {
return null;
}
看来BaseDexClassLoader的构造函数,super没做什么事。
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
在BaseDexClassLoader中,随处可见DexPathList的身影。
我们 移步 DexPathList.java
DexPathList 有三个构造函数,区别在dexFiles数组从外部传入,还是从路径生成。
/** 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;
/** 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;
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;
}
}
其中两个重要的方法,makeDexElements,以及填充systemNativeLibrary和appNativeLibrary,还有可能会抛出的异常。
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();
}
根据传入的目录,递归解析出DexFile,把dex文件加载到内存中解析成Elements,核心代码在DexFile.java中
移步DexFile.java
/**
* Opens a DEX file from a given filename, using a specified file
* to hold the optimized data.
*
* @param sourceName
* Jar or APK file with "classes.dex".
* @param outputName
* File that will hold the optimized form of the DEX data.
* @param flags
* Enable optional features.
* @param loader
* The class loader creating the DEX file object.
* @param elements
* The temporary dex path list elements from DexPathList.makeElements
*/
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
DexPathList.Element[] elements) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
mInternalCookie = mCookie;
mFileName = sourceName;
//System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}
/*
* Open a DEX file. The value returned is a magic VM cookie. On
* failure, an IOException is thrown.
*/
private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
// Use absolute paths to enable the use of relative paths when testing on host.
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}
private static native Object openDexFileNative(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements);
openDexFileNative是jni调用,那我们直接找dalvik_system_DexFile.cc
// TODO(calin): clean up the unused parameters (here and in libcore).
static jobject DexFile_openDexFileNative(JNIEnv* env,
jclass,
jstring javaSourceName,
jstring javaOutputName ATTRIBUTE_UNUSED,
jint flags ATTRIBUTE_UNUSED,
jobject class_loader,
jobjectArray dex_elements) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == nullptr) {
return 0;
}
Runtime* const runtime = Runtime::Current();
ClassLinker* linker = runtime->GetClassLinker();
std::vector<std::unique_ptr<const DexFile>> dex_files;
std::vector<std::string> error_msgs;
const OatFile* oat_file = nullptr;
dex_files = runtime->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
class_loader,
dex_elements,
/*out*/ &oat_file,
/*out*/ &error_msgs);
if (!dex_files.empty()) {
jlongArray array = ConvertDexFilesToJavaArray(env, oat_file, dex_files);
if (array == nullptr) {
ScopedObjectAccess soa(env);
for (auto& dex_file : dex_files) {
if (linker->IsDexFileRegistered(soa.Self(), *dex_file)) {
dex_file.release();
}
}
}
return array;
} else {
ScopedObjectAccess soa(env);
CHECK(!error_msgs.empty());
// The most important message is at the end. So set up nesting by going forward, which will
// wrap the existing exception as a cause for the following one.
auto it = error_msgs.begin();
auto itEnd = error_msgs.end();
for ( ; it != itEnd; ++it) {
ThrowWrappedIOException("%s", it->c_str());
}
return nullptr;
}
}
这里核心方法,在 dex_files = runtime->GetOatFileManager().OpenDexFilesFromOat()
也就是说,dex文件其实是在OatFile里面查找
通过翻阅应用安装流程
art虚拟机下,应用在安装过程,通过dex2oat,转化成一个ELF格式
文件存放起来。BaseDexClassLoader加载dex文件探索到这里。
下面我们分析BaseDexClassLoader的loadClass
由于BaseDexClassLoader没有重载loadClass方法,其父类java.lang.Class中的classLoader,loadClass源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
// 1.根据类名,找到类对象
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//2.用到的方法为 双亲委托法
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;
}
这里非常有意思,如果class不能直接findLoadedClass,居然是优先从parent中寻找??
其实这是一个jvm的安全机制,名叫双亲委托机制
举个简单的例子:
假设刚好你的代码里面,有个java.lang.String这个类,那么系统是优先加载你的String还是jdk的String ?
答案也很明显,那肯定不能让开发者随意用自己的类,替换系统的!!
所以classLoader会优先委托parent递归找class。实在找不到才用自己的findClass逻辑。
ClassLoader作为最顶层基类,没有直接实现findClass的规则,留给继承者实现
/**
* Returns the class with the given <a href="#name">binary name</a> if this
* loader has been recorded by the Java virtual machine as an initiating
* loader of a class with that <a href="#name">binary name</a>. Otherwise
* <tt>null</tt> is returned.
*/
protected final Class<?> findLoadedClass(String name) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, name);
}
@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;
}
也就是说,通过双亲委托模式,递归查找parent的class无效,BaseDexClassLoader把查找class的任务,从pathList里面的dexElements数组里面查找
/**
* 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;
}
Element
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
DexFile
/**
* See {@link #loadClass(String, ClassLoader)}.
*
* This takes a "binary" class name to better match ClassLoader semantics.
*
* @hide
*/
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
DexFile dexFile)
dalvik_system_DexFile.cc
static jclass DexFile_defineClassNative(JNIEnv* env,
jclass,
jstring javaName,
jobject javaLoader,
jobject cookie,
jobject dexFile) {
std::vector<const DexFile*> dex_files;
const OatFile* oat_file;
if (!ConvertJavaArrayToDexFiles(env, cookie, /*out*/ dex_files, /*out*/ oat_file)) {
VLOG(class_linker) << "Failed to find dex_file";
DCHECK(env->ExceptionCheck());
return nullptr;
}
ScopedUtfChars class_name(env, javaName);
if (class_name.c_str() == nullptr) {
VLOG(class_linker) << "Failed to find class_name";
return nullptr;
}
const std::string descriptor(DotToDescriptor(class_name.c_str()));
const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
for (auto& dex_file : dex_files) {
const DexFile::ClassDef* dex_class_def =
OatDexFile::FindClassDef(*dex_file, descriptor.c_str(), hash);
if (dex_class_def != nullptr) {
ScopedObjectAccess soa(env);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader>(javaLoader)));
ObjPtr<mirror::DexCache> dex_cache =
class_linker->RegisterDexFile(*dex_file, class_loader.Get());
if (dex_cache == nullptr) {
// OOME or InternalError (dexFile already registered with a different class loader).
soa.Self()->AssertPendingException();
return nullptr;
}
ObjPtr<mirror::Class> result = class_linker->DefineClass(soa.Self(),
descriptor.c_str(),
hash,
class_loader,
*dex_file,
*dex_class_def);
// Add the used dex file. This only required for the DexFile.loadClass API since normal
// class loaders already keep their dex files live.
class_linker->InsertDexFileInToClassLoader(soa.Decode<mirror::Object>(dexFile),
class_loader.Get());
if (result != nullptr) {
VLOG(class_linker) << "DexFile_defineClassNative returning " << result
<< " for " << class_name.c_str();
return soa.AddLocalReference<jclass>(result);
}
}
}
VLOG(class_linker) << "Failed to find dex_class_def " << class_name.c_str();
return nullptr;
}
遍历dex_files数组里面DexFile, 根据类名获取description以及其hash值,通过OatDexFile::FindClassDef(*dex_file, descriptor.c_str(), hash) 转换成ClassDef,最后再class_linker中得到结果。
最后附上学习成功:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkAndCopyFile("app-release.apk");
loadApk("com.amberllo.livedemo.business.activity.SplashActivity", "app-release.apk");
}
});
}
public void checkAndCopyFile(String apkName) {
try {
File pluginFile = getExternalFilesDir("plugin");
File apkFile = new File(pluginFile + "/" + apkName);
if (apkFile.exists() && apkFile.length()!=0) {
return;
}
apkFile.delete();
if (pluginFile != null && !pluginFile.exists() || pluginFile.listFiles().length == 0) {
pluginFile.mkdirs();
}
InputStream is = null;
FileOutputStream fos = null;
is = getAssets().open(apkName);
fos = new FileOutputStream(apkFile);
byte[] buffer = new byte[1024];
int byteCount;
while ((byteCount = is.read(buffer)) != -1) {
fos.write(buffer, 0, byteCount);
}
fos.flush();
is.close();
fos.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
public void loadApk(String clazzName, String apkName) {
// Step1\. 获取到插件apk,通常都是从网络上下载,这里为了演示,直接将插件apk push到手机
File pluginFile = getExternalFilesDir("plugin");
if (pluginFile != null && !pluginFile.exists() || pluginFile.listFiles().length == 0) {
Toast.makeText(this, "插件文件不存在", Toast.LENGTH_SHORT).show();
return;
}
pluginFile =new File(pluginFile + "/" + apkName);
// Step2\. 创建插件的DexClassLoader
PathClassLoader dexClassLoader = new PathClassLoader(pluginFile.getAbsolutePath(), getClassLoader());
try {
Class clazz = dexClassLoader.loadClass(clazzName);
Toast.makeText(this, clazz.getSimpleName(), Toast.LENGTH_SHORT).show();
} catch (ClassNotFoundException e) {
e.printStackTrace();
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
}