在国内的大环境下,Android上插件化/热修复等技术百花齐放,而这一切都基于代码的动态加载。Android提供了一个DexClassLoader。用这个API能成功加载dex,但有一个比较严重的问题:Android Q以下,当这个dex被加载时,如果没有已经生成的oat,则会执行一次dex2oat把这个dex编译为oat,导致第一次加载dex会非常非常慢。个人认为这样的设计是非常不合理的,虽然转换成oat之后执行会很快,但完全可以让用户以解释器模式先愉快的用着,dex2oat放另一个线程执行多好。Android 8.0上谷歌还提供了一个InMemoryDexClassLoader,而以前的Android版本,就要开发者自己想办法了……

源码分析

注:为了说明此方法能在较低版本的ART上运行,本文分析的源码是Android 5.0的源码,之后的Android版本里OpenDexFileFromOat方法搬到了OatFileManager里,而调用dex2oat的部分则重构到了OatFileAssistant中,大致逻辑相同,感兴趣的可以自己去看看;至于Android 4.4,简单扫了一下源码似乎是生成oat失败就会直接抛一个IOException拒绝加载,emmm……

我们在Java代码里用new DexClassLoader()的方式加载dex,最后会调用到DexFile.openDexFileNative中,这个函数的实现是这样的:

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
static jlong DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == NULL) {
return 0;
}
NullableScopedUtfChars outputName(env, javaOutputName);
if (env->ExceptionCheck()) {
return 0;
}

ClassLinker* linker = Runtime::Current()->GetClassLinker();
std::unique_ptr<std::vector<const DexFile*>> dex_files(new std::vector<const DexFile*>());
std::vector<std::string> error_msgs;

bool success = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs,
dex_files.get());

if (success || !dex_files->empty()) {
// In the case of non-success, we have not found or could not generate the oat file.
// But we may still have found a dex file that we can use.
return static_cast<jlong>(reinterpret_cast<uintptr_t>(dex_files.release()));
} else {
// 加载失败的情况,省略
}
}

这里的注释很有意思,如果返回false(生成oat失败),但是有被成功加载的dex,那么还是应该当做成功。
可以看出具体实现在ClassLinker中的OpenDexFilesFromOat里,我们点进去看看:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// Multidex files make it possible that some, but not all, dex files can be broken/outdated. This
// complicates the loading process, as we should not use an iterative loading process, because that
// would register the oat file and dex files that come before the broken one. Instead, check all
// multidex ahead of time.
bool ClassLinker::OpenDexFilesFromOat(const char* dex_location, const char* oat_location,
std::vector<std::string>* error_msgs,
std::vector<const DexFile*>* dex_files) {
// 1) Check whether we have an open oat file.
// This requires a dex checksum, use the "primary" one.
bool needs_registering = false;

const OatFile::OatDexFile* oat_dex_file = FindOpenedOatDexFile(oat_location, dex_location,
dex_location_checksum_pointer);
std::unique_ptr<const OatFile> open_oat_file(
oat_dex_file != nullptr ? oat_dex_file->GetOatFile() : nullptr);

// 2) If we do not have an open one, maybe there's one on disk already.

// In case the oat file is not open, we play a locking game here so
// that if two different processes race to load and register or generate
// (or worse, one tries to open a partial generated file) we will be okay.
// This is actually common with apps that use DexClassLoader to work
// around the dex method reference limit and that have a background
// service running in a separate process.
ScopedFlock scoped_flock;

if (open_oat_file.get() == nullptr) {
if (oat_location != nullptr) {
std::string error_msg;

// We are loading or creating one in the future. Time to set up the file lock.
if (!scoped_flock.Init(oat_location, &error_msg)) {
error_msgs->push_back(error_msg);
return false;
}

// TODO Caller specifically asks for this oat_location. We should honor it. Probably?
open_oat_file.reset(FindOatFileInOatLocationForDexFile(dex_location, dex_location_checksum,
oat_location, &error_msg));

if (open_oat_file.get() == nullptr) {
std::string compound_msg = StringPrintf("Failed to find dex file '%s' in oat location '%s': %s",
dex_location, oat_location, error_msg.c_str());
VLOG(class_linker) << compound_msg;
error_msgs->push_back(compound_msg);
}
} else {
// TODO: What to lock here?
bool obsolete_file_cleanup_failed;
open_oat_file.reset(FindOatFileContainingDexFileFromDexLocation(dex_location,
dex_location_checksum_pointer,
kRuntimeISA, error_msgs,
&obsolete_file_cleanup_failed));
// There's no point in going forward and eventually try to regenerate the
// file if we couldn't remove the obsolete one. Mostly likely we will fail
// with the same error when trying to write the new file.
// TODO: should we maybe do this only when we get permission issues? (i.e. EACCESS).
if (obsolete_file_cleanup_failed) {
return false;
}
}
needs_registering = true;
}

// 3) If we have an oat file, check all contained multidex files for our dex_location.
// Note: LoadMultiDexFilesFromOatFile will check for nullptr in the first argument.
bool success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,
dex_location_checksum_pointer,
false, error_msgs, dex_files);
if (success) {
// 我们没有有效的oat文件,所以不会走到这里
} else {
if (needs_registering) {
// We opened it, delete it.
open_oat_file.reset();
} else {
open_oat_file.release(); // Do not delete open oat files.
}
}

// 4) If it's not the case (either no oat file or mismatches), regenerate and load.

// Look in cache location if no oat_location is given.
std::string cache_location;
if (oat_location == nullptr) {
// Use the dalvik cache.
const std::string dalvik_cache(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA)));
cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str());
oat_location = cache_location.c_str();
}

bool has_flock = true;
// Definitely need to lock now.
if (!scoped_flock.HasFile()) {
std::string error_msg;
if (!scoped_flock.Init(oat_location, &error_msg)) {
error_msgs->push_back(error_msg);
has_flock = false;
}
}

if (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {
// Create the oat file.
open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),
oat_location, error_msgs));
}

// Failed, bail.
if (open_oat_file.get() == nullptr) { // 如果无法生成oat,那么直接加载dex
std::string error_msg;
// dex2oat was disabled or crashed. Add the dex file in the list of dex_files to make progress.
DexFile::Open(dex_location, dex_location, &error_msg, dex_files);
error_msgs->push_back(error_msg);
return false;
}
// 再次尝试加载oat,无关,省略
}

这个函数比较长,所以做了一点精简。
我们在这里看到了一点端倪,这个函数做了这些事情:

  1. 检查我们是否已经有一个打开了的oat
  2. 如果没有,那么检查oat缓存目录(创建DexClassLoader时传入的第二个参数)是否已经有了一个oat,并且检查这个oat的有效性
  3. 如果没有或者这个oat是无效的,那么生成一个oat文件

我们首次加载dex时,肯定没有有效的oat,最后会生成一个新的oat:

1
2
3
4
5
if (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {
// Create the oat file.
open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),
oat_location, error_msgs));
}

这里有一个if判断,直接决定是否进行dex2oat,我们看看能不能通过各种手段让这个判断不成立。

禁用dex2oat

第一招:修改Runtime中的变量

这个if判断里,第一个条件就是Runtime::Current()->IsDex2OatEnabled(),如果返回false,那么就不会生成oat。这个函数的实现如下:

1
2
3
4
5
6
7
bool IsDex2OatEnabled() const {
return dex2oat_enabled_ && IsImageDex2OatEnabled();
}

bool IsImageDex2OatEnabled() const {
return image_dex2oat_enabled_;
}

dex2oat_enabled_image_dex2oat_enabled_都是Runtime对象中的成员变量,而Runtime可以通过JavaVM获取,所以我们只需要修改这个值就能禁用dex2oat。已经有其他人实现了这一步,具体可以看看这篇博客
然而事情真的会这么简单吗?
查看源码发现Runtime是一个炒鸡大的结构体,Android里有什么东西都往这扔,你几乎可以从Runtime对象上直接或间接获取到任何东西,然而也正是因为Runtime太大了,使得没有什么好的办法获取里面的值。

让我们看看还有没有其他方法:

第二招:使用PathClassLoader

我们可以看见,在if判断里,还有两个条件:has_flockscoped_flock.HasFile(),让我们看看是否可以让这两个条件不成立。
has_flock的赋值:

1
2
3
4
5
6
7
8
9
bool has_flock = true;
// Definitely need to lock now.
if (!scoped_flock.HasFile()) {
std::string error_msg;
if (!scoped_flock.Init(oat_location, &error_msg)) {
error_msgs->push_back(error_msg);
has_flock = false;
}
}

又是scoped_flock,看看在上面scoped_flock可能在哪里被初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
ScopedFlock scoped_flock;
if (open_oat_file.get() == nullptr) {
if (oat_location != nullptr) {
std::string error_msg;

// We are loading or creating one in the future. Time to set up the file lock.
if (!scoped_flock.Init(oat_location, &error_msg)) {
error_msgs->push_back(error_msg);
return false;
}
// 省略代码
}
}

看看ScopedFlock的Init方法:

1
2
3
4
5
6
7
8
9
10
bool ScopedFlock::Init(const char* filename, std::string* error_msg) {
while (true) {
file_.reset(OS::OpenFileWithFlags(filename, O_CREAT | O_RDWR));
if (file_.get() == NULL) {
*error_msg = StringPrintf("Failed to open file '%s': %s", filename, strerror(errno));
return false;
}
// 省略一大堆代码……
}
}

可以看见,会打开这个文件,flags为O_CREAT | O_RDWR,那我们只需要设置oat_location为不可写的路径,就能让ScopedFlock::Init返回false。不过我们要注意的是,如果oat_location不为null并且无法使用,那在上面的一个判断里就会直接返回false。怎么办?

是时候请出我们的主角PathClassLoader了!

PathClassLoader作为DexClassLoader的兄弟(也可能是姐妹?),受到的待遇与DexClassLoader截然不同:网上讲解动态加载dex的文章几乎都只讲DexClassLoader,而对于PathClassLoader则是一笔带过:“PathClassLoader只能加载系统中已经安装过的apk”。
然而事实真的是这样吗?或许Android 5.0以前是,但Android 5.0时就已经可以加载外部dex了,今天我要为PathClassLoader正名!
让我们来对比一下DexClassLoader和PathClassLoader的源码。
DexClassLoader:

1
2
3
4
5
6
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}

对,你没看错,有效代码就这么点。
让我们再看看PathClassLoader的源码:

1
2
3
4
5
6
7
8
9
10
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}

实际上所以实现代码都在BaseDexClassLoader中,DexClassLoader和PathClassLoader都调用了同一个构造函数:

1
2
3
4
5
6
7
8
9
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
}

注意第二个参数,optimizedDirectory,DexClassLoader传入的是new File(optimizedDirectory),而PathClassLoader传入的是null。记住这一点。
这两种情况最后都会调用到DexFile.openDexFileNative中

1
private static native long openDexFileNative(String sourceName, String outputName, int flags);

如果是PathClassLoader,outputName为null,会进入这个if分支中:

1
2
3
4
5
6
7
8
// Look in cache location if no oat_location is given.
std::string cache_location;
if (oat_location == nullptr) {
// Use the dalvik cache.
const std::string dalvik_cache(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA)));
cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str());
oat_location = cache_location.c_str();
}

这里会把oat_location设置成/data/dalvik-cache/下的路径,接下来因为我们根本没有对dalvik-cache的写入权限,所以无法打开fd,然后就会走到这里直接加载原始dex

1
2
3
4
5
6
7
if (open_oat_file.get() == nullptr) {
std::string error_msg;
// dex2oat was disabled or crashed. Add the dex file in the list of dex_files to make progress.
DexFile::Open(dex_location, dex_location, &error_msg, dex_files);
error_msgs->push_back(error_msg);
return false;
}

至此整个逻辑已经明朗,通过PathClassLoader加载会把oat输出路径设置成/data/dalvik-cache/下,然后因为我们没有对dalvik-cache的写入权限,所以无法打开fd,之后会直接加载原始dex,不会进行dex2oat。
(注:本文分析源码是Android 5.0Android 8.1时源码有改动,就算是DexClassLoader也会把optimizedDirectory设置成null,输出的oat在dex的父目录/oat/下,所以无法通过PathClassLoader快速加载dex,但在8.1时已经有InMemoryDexClassLoader了,直接通过InMemoryDexClassLoader加载就好了。

简单做了个小测试,在我的AVD(Android 7.1.1)上,用DexClassLoader加载75M的qq apk用了近80秒,并生成了一个313M的oat,而PathClassLoader用时稳定在2秒左右,emmm……

看起来我们已经有一个比较好的办法禁用dex2oat了,不过需要修改源码没法直接全局禁用,修改Runtime风险又太大,让我们看看还有没有其他方法。

第三招:hook execv

到了这里,上面那个判断肯定会成立了,似乎进行dex2oat已成定局?我们继续看CreateOatFileForDexLocation。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const OatFile* ClassLinker::CreateOatFileForDexLocation(const char* dex_location,
int fd, const char* oat_location,
std::vector<std::string>* error_msgs) {
// Generate the output oat file for the dex file
VLOG(class_linker) << "Generating oat file " << oat_location << " for " << dex_location;
std::string error_msg;
if (!GenerateOatFile(dex_location, fd, oat_location, &error_msg)) {
CHECK(!error_msg.empty());
error_msgs->push_back(error_msg);
return nullptr;
}
std::unique_ptr<OatFile> oat_file(OatFile::Open(oat_location, oat_location, nullptr,
!Runtime::Current()->IsCompiler(),
&error_msg));
if (oat_file.get() == nullptr) {
std::string compound_msg = StringPrintf("\nFailed to open generated oat file '%s': %s",
oat_location, error_msg.c_str());
error_msgs->push_back(compound_msg);
return nullptr;
}

return oat_file.release();
}

GenerateOatFile是核心逻辑,这个函数大部分都是我们不关心的配置dex2oat参数就不贴出来了,最后会fork出一个新进程,然后在子进程里执行execv()调用dex2oat。
看起来我们必然要执行dex2oat了?别慌,还有办法。虽然没有直接的开关去阻止dex2oat,但我们还有hook大法!生成oat最后是通过execv调用dex2oat进行的,所以我们可以hook掉execv函数,如果是执行dex2oat那么直接让这个进程退出即可!Lody大神的早期作品TurboDex就是这样实现的。不过这个项目其实还可以优化一下:TurboDex是使用的Substrate进行hook,这是一个inline hook库,而execv是来自libc.so的导出符号,其实直接通过GOT Hook就能hook到,没有必要去用inline hook,反而增加crash风险。

总结

本来我只是为了研究DexClassLoader与PathClassLoader的区别的,网上的文章和实验的结果完全不一样,结果意外发现一个快速加载dex的方法,就写出来了 :)
这个故事告诉我们,没事多看源码(手动滑稽)
另外个人建议,快速加载dex之后后台可以开一个线程单独进行dex2oat,具体可以参考ArtDexOptimizer,下次启动的时候如果完成了可以直接用生成好的oat文件,毕竟用oat比直接加载dex快得多,而且更稳定~