Android中的类加载-查找和在hotpatch上的问题

最近Android社区里关于hotpatch的好文章非常多,看了其中的几篇,发现它们都提到了一个问题,就是[现有的hotpatch方案在ART虚拟机上会出现内存移位],随着Android5.0以上机器的不断普及,这个问题也会被越来越多的遇到,所以这篇文章就带大家看看和这个问题相关的Android中的几个知识,希望大家看完会有收获~

前言

说到上面说的那个问题,其实可以细分到Android hotpatch中的class文件patch这一块,而市面上现有的hotpatch方案中,使用[动态插入Element方式]的框架都会遇到这个问题,所以我们就从这种方式开始谈起。

熟悉hotpatch的同学都知道,通过这种方式完成需求的大志步骤就是调用DexPathList的makeDexElements方法将patch的dex动态的插入到DexPathList的成员变量dexElements,也就是Element数组的最前面,从而达到在类加载的时候[先加载patch的dex中的class,如果没有,再加载app的dex中的class]的效果。那么要搞清楚上面抛出的那个问题,我们就要从这里入手。

参考文章

Android运行时ART加载OAT文件的过程分析

Android运行时ART加载类和方法的过程分析

Android运行时ART执行类方法的过程分析

Android ART虚拟机中的文件格式

首先为什么要讲这个呢,因为我们在接下去的源码分析中会用到。我们知道在dalvik虚拟机时代,存在一个odex文件,就是优化过的dex文件,虚拟机会自动的帮我们优化每一个dex文件以便提升效率,所以在dalvik虚拟机上存在一个[优化]的过程,从而就导出了hotpatch中最“著名”的一个问题——类校验,这个具体的我在这里不细讲了,因为这不是这篇文章的重点,有兴趣的同学可以自行google。但是呢dalvik虚拟机还是不足以确保Android系统的流畅,因为整个Android的应用都是基于虚拟机的,正因如此,所以就算Android将java文件优化成了dex文件,进而又优化成了odex并辅助以JIT,还是避免不了[解释]这一层面,[解释]通俗的讲就是将java代码解释成机器指令的过程。所以出现了ART,Android RunTime。在ART的时代,每一个Android应用在安装的时候都会存在一个aot操作(Ahead of time),用于生成oat文件,这个oat文件呢,既包含转化前的dex文件,又包含机器指令,所以我们的应用在运行的时候可以免去[解释]这一层而直接加载机器指令。最后说一点,其实ART中还是需要解释器的,因为我们可以手动开启ART的解释模式。

Android中的oat文件加载

前面说过,我们是通过DexPathList的makeDexElements去创建一个patch的Element的,所以我们先看这个方法。

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
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/

for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
/*
* 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: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
/*
* 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). Safe to just ignore
* the exception here, and let dex == null.
*/

}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}

这个方法比较长,但是逻辑很好理解,就是区分我们传进来的参数是一个dex文件还是一个apk或者zip这样的压缩文件,在hotpatch的场景中我们一般传的都是后者,于是就通过loadDexFile方法去生成了一个DexFile,并且生成一个新的Element添加到成员变量elements中。下面让我们看loadDexFile方法。

1
2
3
4
5
6
7
8
9
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);
}
}

如果我们传的optimizedDirectory不为空,就获取一个optimizedPath并且调用DexFile.loadDex方法,否则直接new出一个DexFile,而DexFile.loadDex方法其实在内部也是直接new出一个DexFile的。所以我们直接看DexFile的构造函数。

可以看到其中调用了openDexFile,并且返回了一个cookie值,这个值大家可以看做索引,之后我们查找一个类的时候就需要用到它,所以,我们接下去要看的就是openDexFile这个方法,而这个方法是一个native方法。

友情提示,接下去的代码全都是C++的,请大家喝口水,缓解下焦躁的心情,慢慢看。

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
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 {
// The vector should be empty after a failed loading attempt.
DCHECK_EQ(0U, dex_files->size());

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 0;
}
}

其中最重要的逻辑就是创建了一个dex_files的vector,并且调用class_linker的OpenDexFilesFromOat去从oat文件真正的打开一个Dex文件。从本文的第一小节我们可以知道,Android应用在ART虚拟机上,当它安装的时候都会生成一个oat文件,所以这里就是从这个oat文件中寻找对应的dex文件。下面让我们来看看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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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.
uint32_t dex_location_checksum;
uint32_t* dex_location_checksum_pointer = &dex_location_checksum;
bool have_checksum = true;
std::string checksum_error_msg;
if (!DexFile::GetChecksum(dex_location, dex_location_checksum_pointer, &checksum_error_msg)) {
// This happens for pre-opted files since the corresponding dex files are no longer on disk.
dex_location_checksum_pointer = nullptr;
have_checksum = false;
}

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) {
// Can only do this if we have a checksum, else error.
if (!have_checksum) {
error_msgs->push_back(checksum_error_msg);
return false;
}

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) {
const OatFile* oat_file = open_oat_file.release(); // Avoid deleting it.
if (needs_registering) {
// We opened the oat file, so we must register it.
RegisterOatFile(oat_file);
}
// If the file isn't executable we failed patchoat but did manage to get the dex files.
return oat_file->IsExecutable();
} 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.

// Need a checksum, fail else.
if (!have_checksum) {
error_msgs->push_back(checksum_error_msg);
return false;
}

// 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) {
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;
}

// Try to load again, but stronger checks.
success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,
dex_location_checksum_pointer,true, error_msgs, dex_files);
if (success) {
RegisterOatFile(open_oat_file.release());
return true;
} else {
return false;
}
}

代码非常的长,但是我们惊喜的发现,贴心的google工程师已经帮我写好了step,所以我们就按照他们给的顺序进行源码分析。

step1:从已经在内存中的oat文件中查询时候有我们需要的dex文件。在这一步骤中,会生成一个dex文件的checksum值,这个值大家可以看做一个校验值,用来校验dex文件的。在FindOpenedOatDexFile方法中,会根据我们传入的oat文件路径(oat_location,也就是之前的optimizedPath)去查找在内存中是否已经有相同的oat文件,之后通过OatFile的GetOatDexFile去根据dex文件的路径(dex_location)获取一个OatDexFile,这个类可以获取对应的OatFile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const OatFile::OatDexFile* ClassLinker::FindOpenedOatDexFile(const char* oat_location,const char* dex_location,const uint32_t* dex_location_checksum) {
ReaderMutexLock mu(Thread::Current(), dex_lock_);
for (const OatFile* oat_file : oat_files_) {
DCHECK(oat_file != nullptr);
if (oat_location != nullptr) {
if (oat_file->GetLocation() != oat_location) {
continue;
}
}

const OatFile::OatDexFile* oat_dex_file = oat_file->GetOatDexFile(dex_location,dex_location_checksum,false);
if (oat_dex_file != nullptr) {
return oat_dex_file;
}
}
return nullptr;
}

GetOatDexFile这个函数我们就不跟进去看了,具体的操作就是从oat_dexfiles这个vector中找到对应的OatDexFile。每次生成一个oat文件之后,都会将对应的OatDexFile加入到oat_dex_files这个vector中。

我们这里的oat文件路径和dex文件路径都是patch的,所以是找不到的,于是就进入了step2。

step2:从磁盘中查找。时间原因,具体的代码我们就不分析了,这里当然也是找不到的。如果step1,step2命中,就会进入step3,调用LoadMultiDexFilesFromOatFile方法。

step3:调用LoadMultiDexFilesFromOatFile方法加载我们所需要的dex文件。由于我们step1和step2都没有命中,所以这一步也可以不看。

step4:最后一步,注释写的十分明确,[regenerate and load]重新生成一个oat文件并且加载到内存中。

在这一个步骤中,我们只需要了解,class_linker会调用CreateOatFileForDexLocation方法,借助dex2oat工具去生成一个oat文件,值得一提的是,如果我们没有传oat_location,也就是说之前的optimizedPath为空,那么系统就用自己的路径,/data/dalvik-cache,所以我们app的oat文件就在这个路径下。在CreateOatFileForDexLocation方法中,等生成了对应的OatFile之后会调用它的open函数,在这个函数中,会去判断oat文件是dl类型的还是elf类型的,不过最后都会调用setup函数,在这个函数中,OatFile生成了一个OatDexFile并将其加入到oat_dexfiles这个vector中,这也证实了我之前说的[每次生成一个oat文件之后,都会将对应的OatDexFile加入到oat_dex_files这个vector中]。之后,class_linker会重新调用LoadMultiDexFilesFromOatFile方法,最后调用RegisterOatFile方法将oat文件加入到oat_files这个vector中。

LoadMultiDexFilesFromOatFile方法,看名字就知道和MultiDex有关。我们知道,MultiDex在Android5.0以上的版本是不需要手动操作的,因为ART虚拟机内部就支持了,原因就在这儿。至于为什么不是4.4就开始。。可能因为4.4的ART虚拟机bug比较多吧~

这个函数在最后会调用OpenFromZip函数,而在这个函数内部会去做一个while循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (i < 100) {
std::string name = StringPrintf("classes%zu.dex", i);
std::string fake_location = location + kMultiDexSeparator + name;
std::unique_ptr<const DexFile> next_dex_file(Open(zip_archive, name.c_str(), fake_location,error_msg, &error_code));
if (next_dex_file.get() == nullptr) {
if (error_code != ZipOpenErrorCode::kEntryNotFound) {
LOG(WARNING) << error_msg;
}
break;
} else {
dex_files->push_back(next_dex_file.release());
}
i++;
}

将所有的classesx.dex(classes1,classes2…..)加入到dex_files这个vector中,最后和对应的oat文件关联,也就是说,ART虚拟机支持多个dex文件和一个oat文件关联。BTW,从这个循环就可以看出,最多分成100个dex,不过。。。也没有那个app会分这么多dex吧~

好了,至此,我们就讲完了关于Android中的class加载的部分,让我们来做个总结。

首先,DexPathList会调用makeDexElements去生成一个对应dex文件的Element。在这个函数中,会主动去生成一个新的DexFile。在DexFile的构造函数中,调用openDexFile函数去在oat文件中获取对应的dex文件并且返回一个cookie值给DexFile。openDexFile是一个native函数,其中调用了class_linker的OpenDexFilesFromOat函数。这个函数的逻辑分4部,上面都已经讲了,最后获取或者生成了一个oat文件,其中的LoadMultiDexFilesFromOatFile方法说明了ART内部支持MultiDex。

结合我们的hopatch,我们知道,在patch生效后,文件系统和内存中会多出一个oat文件,其中包含patch的dex和对应的机器指令。

Android中的类查找

我们都知道,在Android中,一个类是通过ClassLoader去加载和查找的,所以我们就先看ClassLoader的BaseDexClassLoader的findClass方法。

1
2
3
4
5
6
7
8
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}

这个方法很简单,直接调用了DexPathList的findClass方法。

1
2
3
4
5
6
7
8
9
10
11
12
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}

这个方法会去遍历成员变量dexElements(这也是hotpatch能起作用的原因),并且调用Elememt中的DexFile对象的loadClassBinaryName方法。

在DexFile的loadClassBinaryName方法中,会去获取对应的cookie,并且调用一个native方法defineClass去加载类。

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
static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,
jlong cookie)
{

std::vector<const DexFile*>* dex_files = toDexFiles(cookie, env);
if (dex_files == NULL) {
VLOG(class_linker) << "Failed to find dex_file";
return NULL;
}
ScopedUtfChars class_name(env, javaName);
if (class_name.c_str() == NULL) {
VLOG(class_linker) << "Failed to find class_name";
return NULL;
}
const std::string descriptor(DotToDescriptor(class_name.c_str()));
const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
for (const DexFile* dex_file : *dex_files) {
const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str(), hash);
if (dex_class_def != nullptr) {
ScopedObjectAccess soa(env);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
class_linker->RegisterDexFile(*dex_file);
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
mirror::Class* result = class_linker->DefineClass(soa.Self(), descriptor.c_str(), hash,
class_loader, *dex_file, *dex_class_def);
if (result != nullptr) {
VLOG(class_linker) << "DexFile_defineClassNative returning " << result;
return soa.AddLocalReference<jclass>(result);
}
}
}
VLOG(class_linker) << "Failed to find dex_class_def";
return nullptr;
}

首先,这个方法调用了toDexFile,将cookie传进去,通过这个索引获取对应的dexFile。然后,调用class_linker的defineClass去获取一个类。值得一提的是,在defineClass中,这里存在一个缓存机制,会有一个DexCache用来缓存已经加载过的Dex信息,作用是提高效率和懒加载。

最后,class_linker会调用loadClass方法。

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
void ClassLinker::LoadClass(const DexFile& dex_file,const DexFile::ClassDef& dex_class_def,Handle<mirror::Class> klass,mirror::ClassLoader* class_loader) {
CHECK(klass.Get() != nullptr);
CHECK(klass->GetDexCache() != nullptr);
CHECK_EQ(mirror::Class::kStatusNotReady, klass->GetStatus());
const char* descriptor = dex_file.GetClassDescriptor(dex_class_def);
CHECK(descriptor != nullptr);

klass->SetClass(GetClassRoot(kJavaLangClass));
if (kUseBakerOrBrooksReadBarrier) {
klass->AssertReadBarrierPointer();
}
uint32_t access_flags = dex_class_def.GetJavaAccessFlags();
CHECK_EQ(access_flags & ~kAccJavaFlagsMask, 0U);
klass->SetAccessFlags(access_flags);
klass->SetClassLoader(class_loader);
DCHECK_EQ(klass->GetPrimitiveType(), Primitive::kPrimNot);
klass->SetStatus(mirror::Class::kStatusIdx, nullptr);

klass->SetDexClassDefIndex(dex_file.GetIndexForClassDef(dex_class_def));
klass->SetDexTypeIndex(dex_class_def.class_idx_);
CHECK(klass->GetDexCacheStrings() != nullptr);

const byte* class_data = dex_file.GetClassData(dex_class_def);
if (class_data == nullptr) {
return; // no fields or methods - for example a marker interface
}

OatFile::OatClass oat_class;
if (Runtime::Current()->IsStarted()
&& !Runtime::Current()->UseCompileTimeClassPath()
&& FindOatClass(dex_file, klass->GetDexClassDefIndex(), &oat_class)) {
LoadClassMembers(dex_file, class_data, klass, class_loader, &oat_class);
} else {
LoadClassMembers(dex_file, class_data, klass, class_loader, nullptr);
}
}

这个方法中的klass对象就代表着我们需要的类,其中主要做的操作就是给其设置一些属性,其中比较重要的就是SetDexClassDefIndex这个方法,设置这个class在dex中的索引值,之后我们需要通过这个值去获取oat文件中的OatClass。具体就是调用FindOatClass去获取对应的OatClass。FindOatClass最终调用了FindOpenedOatDexFile方法。这个方法在之前已经讲过,具体的逻辑就是这个函数就很简单了,遍历oatfiles中的每一个oat文件,找到路径匹配的然后通过GetOatDexFile获取一个OatDexFile。

回到我们的LoadClass的最后,调用LoadClassMembers去加载这个类的相关信息。在这个方法中,会先去加载该类的field,然后去加载该类的method。我们以method为例,其中会调用linkCode方法,在这个方法中,我们会传入一个method_index作为方法的索引值,通过这个索引,我们从OatClass中获取了对应的OatMethod。既然如此,让我们看看OatMethod的构造函数。

1
2
3
4
OatFile::OatMethod::OatMethod(const byte* base,const uint32_t code_offset)
: begin_(base),
code_offset_(code_offset) {
}

其中begin_表示内存的启示值,code_offset表示偏移量,通过这两个值,我们就可以找到对应方法的机器指令在内存中的位置了。

通过上面的源码分析,我们知道,在ART虚拟机的环境下,类和方法遵循[dex class]->(class index)[oat class]->(method index)[oat method]->(begin,codeoffset)[native code]这样的逻辑。其中,dex文件和oat文件的class顺序必须要一致,因为是通过index去关联的。

hotpatch中存在的问题

下面这些都是我个人的思考,如果存在不对的地方,请大家指正,谢谢!(但是这种hotpatch方式在ART虚拟机上存在问题是不争的事实)。

一开始,我单纯的以为由于ART虚拟机上的aot操作,将内存地址在一开始就全部写死,这样动态的添加一个oat文件就会造成内存移位,但是后来在实际业务场景中发现,这个crash并不是必现的,甚至可以说出现的概率比较低,所以应该不可能是这种情况造成的,后来在查阅资料后也发现,只有系统的oat文件是固定地址的,在image文件后面,这也侧面证实了我的说法。

之后,我又开始到底怎么样才会出现这个crash,网上有很多同学说只要在patch类中调用非patch类的方法就会复现,原因是非patch类的方法找不到具体地址,oat文件上打印的地址有问题,于是我反汇编了一个patch的oat文件。

首先,假设我现在有两个类,MainActivity和Test2,我在MainActivity中调用了Test2的test方法,让我们先看看MainActivity在patch中而Test2不在patch中的oat文件:

反汇编1

我们可以看到,在图片中的最后一行,直接写的是#70,也就是Test2.test的method index。

再看看MainActivity和Test2都在patch中的情况:

反汇编2

由于MainActivity和Test2都在patch中,所以没有出现上面的那种现象。

所以有人会说,是这个问题,由于我对汇编代码实在一窍不通,所以也不好肯定的说这是错误的,但是如果你写过这样的demo,你会发现其实交叉引用[patch引用非patch]也是不一定能复现出crash的,而且如果你看过oat文件的全文,你就会发现其实像#70这样的引用是很常见的,所以,我觉得这就是一种类似[符号引用]的东西,不会存在什么问题。

最后,我又翻了翻class_linker的代码,发现在它的FindClass方法中,会先调用LookupClass方法,在这个方法内部,有一个逻辑叫LookupClassFromImage,这个逻辑里,它会先从dex_cache中查找,如果找不到,才会调用defineClass。

对于dex_cache,这里我多说两句,class_linker中,一个dex对应一个dex_cache,而在dex_cache中有一个数组resolved_methods,用来存储所有dex文件中的方法,而在初始化dex_cache的时候,resolved_methods数组里的方法会初始化成trampoline,之后,在每一个方法第一次调用的时候会被填充到resolved_methods数组中,起到缓存的作用。

Android N混合编译与对热补丁影响解析一文中,文章的作者详细的说明了在AndroidN上patch失败的原因,其中原理部分和我上面提到的第三种情况比较相似,只是我在Android5.1的源码中没有看到base.art和AppImage,只有一个boot.art,用来缓存系统的类,但是,在我看来,这种情况导致patch失败的可能性是最大的。

以上就是这篇文章的所有内容了,对于最后一点hotpatch失败的猜想,欢迎大家一起讨论,当然如果有大神明确的知道其中的原理的话,能告诉我那就最好了~