一种在ART上快速加载dex的方法

您所在的位置:网站首页 dex2oat优化 一种在ART上快速加载dex的方法

一种在ART上快速加载dex的方法

2023-12-14 21:21| 来源: 网络整理| 查看: 265

在国内的大环境下,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中,这个函数的实现是这样的:

12345678910111213141516171819202122232425static 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 dex_files(new std::vector()); std::vector 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(reinterpret_cast(dex_files.release())); } else { // 加载失败的情况,省略 }}

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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117// 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* error_msgs, std::vector* 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 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) 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,无关,省略}

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

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

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

12345if (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。这个函数的实现如下:

1234567bool 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_flock和scoped_flock.HasFile(),让我们看看是否可以让这两个条件不成立。has_flock的赋值:

123456789bool 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可能在哪里被初始化:

12345678910111213ScopedFlock 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方法:

12345678910bool 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:

123456public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); }}

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

12345678910public 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都调用了同一个构造函数:

123456789public 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中

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

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

12345678// 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:

1234567if (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.0,在Android 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。

1234567891011121314151617181920212223const OatFile* ClassLinker::CreateOatFileForDexLocation(const char* dex_location, int fd, const char* oat_location, std::vector* error_msgs) { // Generate the output oat file for the dex file VLOG(class_linker)


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3