hotpatch完结篇之ART虚拟机crash之谜

最近几个月博客里的文章除了两篇和相关的,其他都或多或少涉及到了hotpatch,写了很多字了,这也算是最后一篇和hotpatch相关的博客,让我们解决上一篇文章中提出的问题吧,hotpatch在ART虚拟机上crash的问题。

前言

在看这篇文章之前,如果你对hotpatch的了解不是很多,可以先看我之前写的几篇文章:

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

热修复中的资源修复

当你准备开发一个热修复框架的时候,你需要了解的一切

记一次苦逼的资源逆向分析

从Instant-Run出发,谈谈Android上的热修复

让我们来聊一聊插件化吧

OAT文件解析

我们知道,在ART虚拟机上,当应用首次安装,或者动态的通过ClassLoader加载一个dex的时候,都会去做一个AOT(Ahead Of Time)操作,在这个操作中,Android系统会使用dex2oat去生成一个oat文件,如果没有指定路径的话(基本都是没有指定的,除非手动加载dex的情况),那么这个oat文件存放的路径是/data/dalvik-cache 目录下,如果你的手机是ARM的话,就应该在/data/dalvik-cache/arm下,会有一个data@app@[package name]-1@base.apk@classes.dex的文件,虽然这个文件的后缀是.dex,这个它确实就是我们要找的oat文件。我们可以使用dump命令去把它dump出来,具体的命令如下:

oatdump --file-name=data@app@[package name]-1@base.apk@classes.dex --output=dump.txt

然后就可以愉快的使用adb pull把对应的dump.txt拉取到你想到存放的路径下了。

下面让我们分块来解析oat文件。

oat_dump

首先让我们看看oat文件的头部。

MAGIC表示魔数,和java文件的魔术是一个道理。

CHECKSUM表示校验和,这个也很好理解。

INSTRUCTION SET表示指令集,一个有四个选项,在arm机器中就是Thumb2。

INSTRUCTION SET FEATURES表示指令集功能,和我们今天的文章没什么关联。

DEX FILE COUNT表示oat文件中的dex文件个数。我们知道oat文件包含着两部分——oat data和oat exec。其中oat data里就有被转换的dex文件,而oat exec中有所转换好的native code。

EXECUTABLE OFFSET表示oat exec段的偏移量,也就是oat data和oat exec分隔的地方。

后面的几个都表示ARM虚拟机下的trampoline机制,这一点我们留到后面再讲。

最后我们看一下KEY VALUE STORE的值。

其中–zip-location表示对应apk文件的路径。–oat-location表示生成的oat文件的路径。image-location表示image文件的路径,具体路径是/data/dalvik-cache/arm/system@framework@boot.art,大家可以把它看成一个特殊的oat文件,里面包含着android系统的dex和对应的native code,所以取名boot,它的作用基本就是用来缓存,预先编译好framework的代码。

在这之后,就是dex文件的内容和对应的native code了,我们这里以一个为例。

dex_oat_code

dex_method_idx表示这个方法在对应的dex文件中的方法索引。

DEX CODE下的代码就是dex文件对应的代码。

CODE下的代码就是对应的native code。

ART机制出初探

大致了解了一下oat文件的格式,下面让我们看看ART虚拟机是如何操作它的。

首先在ART虚拟机中,会通过OatFile::Open函数去加载一个oat文件。

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
OatFile* OatFile::Open(const std::string& filename,const std::string& location,
byte* requested_base,uint8_t* oat_file_begin,bool executable,std::string* error_msg) {
CHECK(!filename.empty()) << location;
CheckLocation(location);
std::unique_ptr<OatFile> ret;
if (kUsePortableCompiler && executable) {
// If we are using PORTABLE, use dlopen to deal with relocations.
// We use our own ELF loader for Quick to deal with legacy apps that
// open a generated dex file by name, remove the file, then open
// another generated dex file with the same name. http://b/10614658
ret.reset(OpenDlopen(filename, location, requested_base, error_msg));
} else {
// If we aren't trying to execute, we just use our own ElfFile loader for a couple reasons:
// On target, dlopen may fail when compiling due to selinux restrictions on installd.
// On host, dlopen is expected to fail when cross compiling, so fall back to OpenElfFile.
// This won't work for portable runtime execution because it doesn't process relocations.
std::unique_ptr<File> file(OS::OpenFileForReading(filename.c_str()));
if (file.get() == NULL) {
*error_msg = StringPrintf("Failed to open oat filename for reading: %s", strerror(errno));
return nullptr;
}
ret.reset(OpenElfFile(file.get(), location, requested_base, oat_file_begin, false, executable,
error_msg));

// It would be nice to unlink here. But we might have opened the file created by the
// ScopedLock, which we better not delete to avoid races. TODO: Investigate how to fix the API
// to allow removal when we know the ELF must be borked.
}
return ret.release();
}

其中通过kUsePortableCompiler && executable的的值去判断执行OpenDlopen还是OpenElfFile。这个逻辑不用具体去了解,这两个函数最终都会执行到Setup()方法。

而在Setup方法中,最重要的是去获取包含在oatdata段的DEX文件描述信息。

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
for (size_t i = 0; i < dex_file_count; i++) {
uint32_t dex_file_location_size = *reinterpret_cast<const uint32_t*>(oat);
if (UNLIKELY(dex_file_location_size == 0U)) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd with empty location name",
GetLocation().c_str(), i);
return false;
}
oat += sizeof(dex_file_location_size);
if (UNLIKELY(oat > End())) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd truncated after dex file "
"location size", GetLocation().c_str(), i);
return false;
}

const char* dex_file_location_data = reinterpret_cast<const char*>(oat);
oat += dex_file_location_size;
if (UNLIKELY(oat > End())) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd with truncated dex file "
"location", GetLocation().c_str(), i);
return false;
}

std::string dex_file_location(dex_file_location_data, dex_file_location_size);

uint32_t dex_file_checksum = *reinterpret_cast<const uint32_t*>(oat);
oat += sizeof(dex_file_checksum);
if (UNLIKELY(oat > End())) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd for '%s' truncated after "
"dex file checksum", GetLocation().c_str(), i,
dex_file_location.c_str());
return false;
}

uint32_t dex_file_offset = *reinterpret_cast<const uint32_t*>(oat);
if (UNLIKELY(dex_file_offset == 0U)) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd for '%s' with zero dex "
"file offset", GetLocation().c_str(), i, dex_file_location.c_str());
return false;
}
if (UNLIKELY(dex_file_offset > Size())) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd for '%s' with dex file "
"offset %ud > %zd", GetLocation().c_str(), i,
dex_file_location.c_str(), dex_file_offset, Size());
return false;
}
oat += sizeof(dex_file_offset);
if (UNLIKELY(oat > End())) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd for '%s' truncated "
" after dex file offsets", GetLocation().c_str(), i,
dex_file_location.c_str());
return false;
}

const uint8_t* dex_file_pointer = Begin() + dex_file_offset;
if (UNLIKELY(!DexFile::IsMagicValid(dex_file_pointer))) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd for '%s' with invalid "
" dex file magic '%s'", GetLocation().c_str(), i,
dex_file_location.c_str(), dex_file_pointer);
return false;
}
if (UNLIKELY(!DexFile::IsVersionValid(dex_file_pointer))) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd for '%s' with invalid "
" dex file version '%s'", GetLocation().c_str(), i,
dex_file_location.c_str(), dex_file_pointer);
return false;
}
const DexFile::Header* header = reinterpret_cast<const DexFile::Header*>(dex_file_pointer);
const uint32_t* methods_offsets_pointer = reinterpret_cast<const uint32_t*>(oat);

oat += (sizeof(*methods_offsets_pointer) * header->class_defs_size_);
if (UNLIKELY(oat > End())) {
*error_msg = StringPrintf("In oat file '%s' found OatDexFile #%zd for '%s' with truncated "
" method offsets", GetLocation().c_str(), i,
dex_file_location.c_str());
return false;
}

std::string canonical_location = DexFile::GetDexCanonicalLocation(dex_file_location.c_str());

// Create the OatDexFile and add it to the owning container.
OatDexFile* oat_dex_file = new OatDexFile(this,
dex_file_location,
canonical_location,
dex_file_checksum,
dex_file_pointer,
methods_offsets_pointer);
}

这段函数比较长,但是主要逻辑就是解析出dex文件的一些值,最终生成一个OatDexFile文件。其中最重要的是下面两个值:

dex_file_offset和methods_offsets_pointer。

前者让我们可以定位到在oatdata段中的dex文件,后者则是一个数组,包含了该dex文件中的所有方法。我们可以通过class_index去索引这个数组得到对应class的偏移量。然后通过前面提到的method_index去索引得到对应的method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const OatMethodOffsets* OatFile::OatClass::GetOatMethodOffsets(uint32_t method_index) const {
// NOTE: We don't keep the number of methods and cannot do a bounds check for method_index.
if (methods_pointer_ == nullptr) {
CHECK_EQ(kOatClassNoneCompiled, type_);
return nullptr;
}
size_t methods_pointer_index;
if (bitmap_ == nullptr) {
CHECK_EQ(kOatClassAllCompiled, type_);
methods_pointer_index = method_index;
} else {
CHECK_EQ(kOatClassSomeCompiled, type_);
if (!BitVector::IsBitSet(bitmap_, method_index)) {
return nullptr;
}
size_t num_set_bits = BitVector::NumSetBits(bitmap_, method_index);
methods_pointer_index = num_set_bits;
}
const OatMethodOffsets& oat_method_offsets = methods_pointer_[methods_pointer_index];
return &oat_method_offsets;
}

现在我们可以知道,在ART虚拟机中,有一个class_index和一个mehod_index,它会通过这两个值去索引对应的数组,最终找到想要执行的方法。

得到了想要执行的方法,我们可以看看这个方法是如何执行的。我们可以看一下ArtMethod的invoke方法。

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
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,const char* shorty) {

........

// Call the invoke stub, passing everything as arguments.
if (UNLIKELY(!runtime->IsStarted())) {
if (IsStatic()) {
art::interpreter::EnterInterpreterFromInvoke(self, this, nullptr, args, result);
} else {
Object* receiver = reinterpret_cast<StackReference<Object>*>(&args[0])- >AsMirrorPtr();
art::interpreter::EnterInterpreterFromInvoke(self, this, receiver, args + 1, result);
}
} else {
const bool kLogInvocationStartAndReturn = false;
bool have_quick_code = GetEntryPointFromQuickCompiledCode() != nullptr;
#if defined(ART_USE_PORTABLE_COMPILER)
bool have_portable_code = GetEntryPointFromPortableCompiledCode() != nullptr;
#else
bool have_portable_code = false;
#endif
if (LIKELY(have_quick_code || have_portable_code)) {
if (!IsStatic()) {
.............
(*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
} else {
(*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty);
}
#else
(*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
#endif
} else {
(*art_portable_invoke_stub)(this, args, args_size, self, result, shorty[0]);
}
}

// Pop transition.
self->PopManagedStackFragment(fragment);
}

在QUICK类型下(PORTABLE忽略),其中会根据是否是静态方法调用art_quick_invoke_stub和art_quick_invoke_static_stub,我们来看art_quick_invoke_stub这是一段ARM汇编代码。我们重点关注下面三句:

1
2
3
str    ip, [sp]                        @ store NULL for method* at bottom of frame  
ldr ip, [r0, #METHOD_CODE_OFFSET] @ get pointer to the code
blx ip @ call the method

当找到了对应的方法之后,会偏移到METHOD_CODE_OFFSET这儿,然后执行。

1
2
// Offset of field Method::entry_point_from_compiled_code_  
#define METHOD_CODE_OFFSET 40

这里看到,METHOD_CODE_OFFSET这个值在5.0以上是44,在4.4上是40。而这个位置的地址指向的是entry_point_from_compiledcode ,这个就是我们下面要讲的ART方法调用机制。

方法调用机制

在ART中,会调用ClassLinker的LinkCode方法关联方法。

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
if (oat_class != nullptr) {
// Every kind of method should at least get an invoke stub from the oat_method.
// non-abstract methods also get their code pointers.
const OatFile::OatMethod oat_method = oat_class->GetOatMethod(method_index);
oat_method.LinkMethod(method.Get());
}

// Install entry point from interpreter.
bool enter_interpreter = NeedsInterpreter(method.Get(),
method->GetEntryPointFromQuickCompiledCode(),
if (enter_interpreter && !method->IsNative()) {
method->SetEntryPointFromInterpreter(interpreter::artInterpreterToInterpreterBridge);
method->SetEntryPointFromInterpreter(artInterpreterToCompiledCodeBridge);
}

if (method->IsStatic() && !method->IsConstructor()) {
// For static methods excluding the class initializer, install the trampoline.
// It will be replaced by the proper entry point by ClassLinker::FixupStaticTrampolines
// after initializing class (see ClassLinker::InitializeClass method).
method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionTrampoline());
} else if (enter_interpreter) {
if (!method->IsNative()) {
// Set entry point from compiled code if there's no code or in interpreter only mode.
method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
} else {
method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniTrampoline());
}
}

光看代码可能比较难懂,但是其实逻辑是非常清晰的。

首先,在ART虚拟机中,一个方法会有两个入口,分别是EntryPointFromInterpreter和EntryPointFromCompiledCode,通过对应的setter去设置,设置的值就是上面提到的entry_point_from_compiled_code和entry_point_from_interpretercode这是什么意思呢,虽然ART虚拟机会有AOT操作,但是我们还是可以手动开启解释器模式的,另外一些代码可能没有native code,在这样的情况下,ART就需要使用原来dalvik的那部分逻辑,使用解释模式运行,所以现在就存在四种情况:

解释器->native code,解释器->解释器,native code->解释器,native code->native code。

所以每个方法就需要两个入口,保证解释器和native code都可以调用成功。

我们这里只关注EntryPointFromCompiledCode,在LinkCode中,我们先会调用oat_method.LinkMethod(method.Get())方法,在这个方法中,我们会将entry_point_from_compiled_code设置成这个方法的code_offset。然后去判断这个方法是否是静态的非构造函数,如果是则将entry_point_from_compiled_code设置成GetQuickResolutionTrampoline。

这里就涉及到了Trampoline机制。

Trampoline的中文意思是蹦床,在ART虚拟机中,我们可以通过Trampoline从native code跳回到ART虚拟机内。具体的实现逻辑大家可以参考老罗的Android运行时ART执行类方法的过程分析一文。我们上面截图的那一段oat文件其实也有用到:

trampoline

可以看到pAllocObject就是一个Trampoline,它可以在跳转表中找到对应的方法,通过跳转表native code就可以通过跳转项调用ART虚拟机内部构造一个对象的逻辑了。下面是跳转表的代码:

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
void InitEntryPoints(InterpreterEntryPoints* ipoints, JniEntryPoints* jpoints,
PortableEntryPoints* ppoints, QuickEntryPoints* qpoints)
{

// Interpreter
ipoints->pInterpreterToInterpreterBridge = artInterpreterToInterpreterBridge;
ipoints->pInterpreterToCompiledCodeBridge = artInterpreterToCompiledCodeBridge;

// JNI
jpoints->pDlsymLookup = art_jni_dlsym_lookup_stub;

// Portable
ppoints->pPortableResolutionTrampoline = art_portable_resolution_trampoline;
ppoints->pPortableToInterpreterBridge = art_portable_to_interpreter_bridge;

// Alloc
qpoints->pAllocArray = art_quick_alloc_array;
qpoints->pAllocArrayWithAccessCheck = art_quick_alloc_array_with_access_check;
qpoints->pAllocObject = art_quick_alloc_object;
qpoints->pAllocObjectWithAccessCheck = art_quick_alloc_object_with_access_check;
qpoints->pCheckAndAllocArray = art_quick_check_and_alloc_array;
qpoints->pCheckAndAllocArrayWithAccessCheck = art_quick_check_and_alloc_array_with_access_check;

// Cast
qpoints->pInstanceofNonTrivial = artIsAssignableFromCode;
qpoints->pCanPutArrayElement = art_quick_can_put_array_element;
qpoints->pCheckCast = art_quick_check_cast;

// DexCache
qpoints->pInitializeStaticStorage = art_quick_initialize_static_storage;
qpoints->pInitializeTypeAndVerifyAccess = art_quick_initialize_type_and_verify_access;
qpoints->pInitializeType = art_quick_initialize_type;
qpoints->pResolveString = art_quick_resolve_string;

// Field
qpoints->pSet32Instance = art_quick_set32_instance;
qpoints->pSet32Static = art_quick_set32_static;
qpoints->pSet64Instance = art_quick_set64_instance;
qpoints->pSet64Static = art_quick_set64_static;
qpoints->pSetObjInstance = art_quick_set_obj_instance;
qpoints->pSetObjStatic = art_quick_set_obj_static;
qpoints->pGet32Instance = art_quick_get32_instance;
qpoints->pGet64Instance = art_quick_get64_instance;
qpoints->pGetObjInstance = art_quick_get_obj_instance;
qpoints->pGet32Static = art_quick_get32_static;
qpoints->pGet64Static = art_quick_get64_static;
qpoints->pGetObjStatic = art_quick_get_obj_static;

// FillArray
qpoints->pHandleFillArrayData = art_quick_handle_fill_data;

// JNI
qpoints->pJniMethodStart = JniMethodStart;
qpoints->pJniMethodStartSynchronized = JniMethodStartSynchronized;
qpoints->pJniMethodEnd = JniMethodEnd;
qpoints->pJniMethodEndSynchronized = JniMethodEndSynchronized;
qpoints->pJniMethodEndWithReference = JniMethodEndWithReference;
qpoints->pJniMethodEndWithReferenceSynchronized = JniMethodEndWithReferenceSynchronized;

// Locks
qpoints->pLockObject = art_quick_lock_object;
qpoints->pUnlockObject = art_quick_unlock_object;

// Math
qpoints->pCmpgDouble = CmpgDouble;
qpoints->pCmpgFloat = CmpgFloat;
qpoints->pCmplDouble = CmplDouble;
qpoints->pCmplFloat = CmplFloat;
qpoints->pFmod = fmod;
qpoints->pSqrt = sqrt;
qpoints->pL2d = __aeabi_l2d;
qpoints->pFmodf = fmodf;
qpoints->pL2f = __aeabi_l2f;
qpoints->pD2iz = __aeabi_d2iz;
qpoints->pF2iz = __aeabi_f2iz;
qpoints->pIdivmod = __aeabi_idivmod;
qpoints->pD2l = art_d2l;
qpoints->pF2l = art_f2l;
qpoints->pLdiv = __aeabi_ldivmod;
qpoints->pLdivmod = __aeabi_ldivmod; // result returned in r2:r3
qpoints->pLmul = art_quick_mul_long;
qpoints->pShlLong = art_quick_shl_long;
qpoints->pShrLong = art_quick_shr_long;
qpoints->pUshrLong = art_quick_ushr_long;

// Intrinsics
qpoints->pIndexOf = art_quick_indexof;
qpoints->pMemcmp16 = __memcmp16;
qpoints->pStringCompareTo = art_quick_string_compareto;
qpoints->pMemcpy = memcpy;

// Invocation
qpoints->pQuickResolutionTrampoline = art_quick_resolution_trampoline;
qpoints->pQuickToInterpreterBridge = art_quick_to_interpreter_bridge;
qpoints->pInvokeDirectTrampolineWithAccessCheck = art_quick_invoke_direct_trampoline_with_access_check;
qpoints->pInvokeInterfaceTrampoline = art_quick_invoke_interface_trampoline;
qpoints->pInvokeInterfaceTrampolineWithAccessCheck = art_quick_invoke_interface_trampoline_with_access_check;
qpoints->pInvokeStaticTrampolineWithAccessCheck = art_quick_invoke_static_trampoline_with_access_check;
qpoints->pInvokeSuperTrampolineWithAccessCheck = art_quick_invoke_super_trampoline_with_access_check;
qpoints->pInvokeVirtualTrampolineWithAccessCheck = art_quick_invoke_virtual_trampoline_with_access_check;

// Thread
qpoints->pCheckSuspend = CheckSuspendFromCode;
qpoints->pTestSuspend = art_quick_test_suspend;

// Throws
qpoints->pDeliverException = art_quick_deliver_exception;
qpoints->pThrowArrayBounds = art_quick_throw_array_bounds;
qpoints->pThrowDivZero = art_quick_throw_div_zero;
qpoints->pThrowNoSuchMethod = art_quick_throw_no_such_method;
qpoints->pThrowNullPointer = art_quick_throw_null_pointer_exception;
qpoints->pThrowStackOverflow = art_quick_throw_stack_overflow;
};

hotpatch crash之谜

通过上面的学习,我们知道了ART虚拟机调用方法的逻辑其实就是通过预先将dex code转换成native code,然后配以特定的函数调用机制去调用。

在这之中,Trampoline起到了非常重要的作用,在跨dex的方法调用中(例如patch的方法调用app的方法)时,ART就会使用Trampoline机制去跳回到虚拟机内部执行。我们可以简单的看一下例子:

static_trampoline

当调用的method不在当前的dex中时,会使用具体的trampoline函数,这里使用的是pInvokeStaticTrampolineWithAccessCheck。

从这一点,我们可以证明其实简单的跨dex调用在ART环境中是不存在问题的。

但是在ART虚拟机做AOT操作的时候,它已经将具体的地址全部写死了,包括class_index和method_index,这样就会有问题了。如果我们的patch地址发生了改变,无论是什么地址,或者说method_index发生了改变(patch中的类方法数目可能有删减),那么在app中的方法调用的时候,通过index索引的机制去完成就会出现错位的问题,这里Trampoline也无能为力了,因为patch中的方法在app中确确实实是存在的,所以ART无法通过Trampoline去执行。

解决方案

既然出现了问题,那就得想办法解决,这里提供几个思路:

1.既然交叉调用会有问题,那么我们可以将patch的类以及调用了它们方法的类全部打进patch里。但是这种方案存在两个问题,首先,这样会使patch包的大小急剧增加,并且最后基本就是将整个app的类打进了patch,此外,Android_N混合编译与对热补丁影响解析一文中指出,在Android_N上新增了一个base.art,类似于boot.art,只不过它是将一些非android framework的代码进行缓存,这样就导致了patch的不可控。

2.我们可以参考instant run的方法,新增一个ClassLoader作为代替系统的PathClassLoader,绕开前面的缓存机制。

3.通过diff算法生成diff文件,在客户端合成全量的。

4.不采用ClassLoader的实现,自己去hook native层对应的entry_point,将其指向自己的函数。但是这种方案实现难度和风险都是最大的,AndFix这样的框架在patch成功率上应该是不如ClassLoader的框架的。

结尾

这篇文章应该是博客里关于hotpatch的最后一篇文章了,其实研究这样的技术是我比较中意的。之前在android社区里看到了很多类似[杀不死的进程]之类的文章,其实这种技术我是非常非常反感的,它的出现只会污染android的生态环境,最终开发者将自食其果。有这点时间,不如多研究研究Material Design,研究研究插件化和hotpatch,实在不行多去和产品撕撕逼也不错啊~