在Android中想要热更新和插件化功能,是绕不开类加载器的。插件包中会有dex包和资源,通过阅读源码可知,DexClassLoader和PathClassLoader可以加载dex,AssetManager加载apk中的资源。这里有个疑问DexClassLoader和PathClassLoader都可以加载dex到底什么区别呢?为什么目前主流插件化方案都是使用PathClassLoader加载宿主dex,使用PathClassLoader加载插件的dex?可以替换使用吗?接下来看下源码
首先看一下DexClassLoader源码
public class DexClassLoader extends BaseDexClassLoader {
/**
* @param dexPath 包含dex的Apk或者jar路径
* @param optimizedDirectory 优化文件目录,不能为空(dalvik中odex文件目录,art中oat文件目录)
* @param librarySearchPath native库目录,可以为null
* @param parent 父加载器
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
以下是PathClassLoader源码
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);
}
}
由DexClassLoader与PathClassLoader可知,都是继承BaseDexClassLoader,PathClassLoader优化文件目录为null,而DexClassLoader必须不能为null,其他功能都相同。在这边有个疑问如果优化目录设置为null,那么生成的优化文件放在哪里,接下来看一下BaseDexClassLoader源码
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
//创建DexPathList对象
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
BaseDexClassLoader构造函数中会创建DexPathList对象,接下来看一下DexPathList构造函数源码
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
//省略 optimizedDirectory判断
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
由以上源码可知,会调用makeDexElements方法
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
try {
//加载dex文件
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
//加载其他包含dex的压缩文件
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
}
}
...
//把DexFile对象放到elements中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
makeDexElements会调用loadDexFile用来加载dex或是包含dex的Apk、Jar、zip压缩文件,返回的DexFile对象放到elements数组中
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
loadDexFile方法中会判断optimizedDirectory是否为null,如果传入的optimizedDirectory为null会直接创建DexFile对象,接下来看一下DexFile构造函数
public DexFile(File file) throws IOException {
//调用一个String参数的构造函数
this(file.getPath());
}
public DexFile(String fileName) throws IOException {
this(fileName, null, null);
}
public DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
mCookie = openDexFile(fileName, null, 0, loader, elements);
...
}
最终会调用openDexFile
private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}
openDexFileNative是个native方法,不同的系统版本会有些差异
Android7.0
最终实现是dalvik_system_DexFile中DexFile_openDexFileNative
static jobject DexFile_openDexFileNative(JNIEnv* env,
jclass,
jstring javaSourceName,
jstring javaOutputName,
jint flags ATTRIBUTE_UNUSED,
jobject class_loader,
jobjectArray dex_elements) {
ScopedUtfChars sourceName(env, javaSourceName);
...
NullableScopedUtfChars outputName(env, javaOutputName);
if (env->ExceptionCheck()) {
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(),outputName.c_str(),class_loader,dex_elements,&oat_file, &error_msgs);
...
}
在OpenDexFilesFromOat中
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat(
const char* dex_location,
const char* oat_location,
jobject class_loader,
jobjectArray dex_elements,
const OatFile** out_oat_file,
std::vector<std::string>* error_msgs) {
OatFileAssistant oat_file_assistant(dex_location,
oat_location,
kRuntimeISA,
/*profile_changed*/false,
!runtime->IsAotCompiler());
std::string error_msg;
//锁住oat_location
if (!oat_file_assistant.Lock(/*out*/&error_msg)) {
}
...
}
接下来看看OatFileAssistant的实现
OatFileAssistant::OatFileAssistant(const char* dex_location,
const char* oat_location,
const InstructionSet isa,
bool profile_changed,
bool load_executable)
: isa_(isa), profile_changed_(profile_changed), load_executable_(load_executable) {
...
if (oat_location != nullptr) {
cached_oat_file_name_ = std::string(oat_location);
cached_oat_file_name_attempted_ = true;
cached_oat_file_name_found_ = true;
}
}
如果oat_location不为空cached_oat_file_name_attempted_设置为true,再看看OatFileName方法
const std::string* OatFileAssistant::OatFileName() {
if (!cached_oat_file_name_attempted_) {
cached_oat_file_name_attempted_ = true;
std::string cache_dir = StringPrintf("%s%s",
DalvikCacheDirectory().c_str(), GetInstructionSetString(isa_));
std::string error_msg;
cached_oat_file_name_found_ = GetDalvikCacheFilename(dex_location_.c_str(),
cache_dir.c_str(), &cached_oat_file_name_, &error_msg);
...
}
return cached_oat_file_name_found_ ? &cached_oat_file_name_ : nullptr;
}
如果cached_oat_file_name_attempted_为ture,会使用传入的路径,否则调用路径如下
DalvikCacheDirectory->GetDalvikCache->GetDalvikCacheImpl
DalvikCacheDirectory 获取的就是 /data/dalvik-cache/ 目录,最终调用到art/runtime/utils.cc方法中
static std::string GetDalvikCacheImpl(const char* subdir,
const bool create_if_absent,
const bool abort_on_error) {
CHECK(subdir != nullptr);
const char* android_data = GetAndroidData();
const std::string dalvik_cache_root(StringPrintf("%s/dalvik-cache/", android_data));
const std::string dalvik_cache = dalvik_cache_root + subdir;
if (!OS::DirectoryExists(dalvik_cache.c_str())) {
if (!create_if_absent) {
// TODO: Check callers. Traditional behavior is to not to abort, even when abort_on_error.
return "";
}
// Don't create the system's /data/dalvik-cache/... because it needs special permissions.
if (strcmp(android_data, "/data") == 0) {
if (abort_on_error) {
LOG(FATAL) << "Failed to find dalvik-cache directory " << dalvik_cache
<< ", cannot create /data dalvik-cache.";
UNREACHABLE();
}
return "";
}
int result = mkdir(dalvik_cache_root.c_str(), 0700);
if (result != 0 && errno != EEXIST) {
if (abort_on_error) {
PLOG(FATAL) << "Failed to create dalvik-cache root directory " << dalvik_cache_root;
UNREACHABLE();
}
return "";
}
result = mkdir(dalvik_cache.c_str(), 0700);
if (result != 0) {
if (abort_on_error) {
PLOG(FATAL) << "Failed to create dalvik-cache directory " << dalvik_cache;
UNREACHABLE();
}
return "";
}
}
return dalvik_cache;
}
如果optimizedDirectory设置为null,则默认会放在/data/dalvik-cache/目录下
android8.0
Android8.0发生了一些变化,主要调用链如下
DexFile_openDexFileNative->
OpenDexFilesFromOat->
OatFileAssistant::OatFileAssistant->
OatFileAssistant::DexLocationToOdexFilename->
DexLocationToOdexNames
DexFile_openDexFileNative
tatic jobject DexFile_openDexFileNative(JNIEnv* env,
jclass,
jstring javaSourceName,
jstring javaOutputName ATTRIBUTE_UNUSED,
jint flags ATTRIBUTE_UNUSED,
jobject class_loader,
jobjectArray dex_elements) {
...
dex_files = runtime->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
class_loader,
dex_elements,
/*out*/ &oat_file,
/*out*/ &error_msgs);
...
}
在这个方法发生了一些变化,javaOutputName并没有使用,都是用默认路径,默认路径拼接在DexLocationToOdexNames方法中实现
static bool DexLocationToOdexNames(const std::string& location,
InstructionSet isa,
std::string* odex_filename,
std::string* oat_dir,
std::string* isa_dir,
std::string* error_msg) {
...
size_t pos = location.rfind('/');
if (pos == std::string::npos) {
*error_msg = "Dex location " + location + " has no directory.";
return false;
}
//dex目录
std::string dir = location.substr(0, pos+1);
//dex目录/oat/
dir += "oat";
if (oat_dir != nullptr) {
*oat_dir = dir;
}
//dex目录/oat/<isa>
dir += "/" + std::string(GetInstructionSetString(isa));
if (isa_dir != nullptr) {
*isa_dir = dir;
}
// Get the base part of the file without the extension.
std::string file = location.substr(pos+1);
pos = file.rfind('.');
if (pos == std::string::npos) {
*error_msg = "Dex location " + location + " has no extension.";
return false;
}
std::string base = file.substr(0, pos);
*odex_filename = dir + "/" + base + ".odex";
return true;
}
以上处理最终路径是dex目录/oat/<isa>,Android9.0、Android10.0最终路径一样
总结
相同点:
- 两者可以实现的一样的功能,支持加载dex或是jar、apk、zip中包含多个dex压缩文件
- 都是BaseDexClassLoader的派生类
不同点:
- PathClassLoader不支持设置优化文件路径,而DexClassLoader必须设置支持路径。如果没设置优化路径,在Android5.0、Android6.0、Android7.0默认优化路径/data/dalvik-cache/,在Android8.0、Android9.0、Android10自己设置的优化目录没有使用,放到统一dex目录/oat/目录下