clover hotpatch 热更新怎么更新

以下是引用他人文章内容:
为什么需要 WaxPatch
什么是 WaxPatch
《魔兽世界》内的Lua编辑器
由于 Lua 编程简单,运行速度快,在 iOS 上, 一个支持使用 Lua 语言编写 iOS 应用的项目 Wax 诞生了。Wax 项目允许用户使用 Lua 语言调动苹果 iOS SDK 的功能,进行应用程序的开发。
Wax Patch 项目是由 Wax 项目衍生而来,Wax patch 不仅仅允许用户使用 Lua 调用 iOS SDK 和应用程序内部的 API, 而且使用了 Objective-C runtime 的 class_replaceMethod 调用替换应用程序内部由 Objective-C 编写的类方法,从而达到功能微调或者缺陷修复的目的。
Wax/WaxPatch 主要特点:
● 所有 Objective-C 调用接口构建在 Objective-C runtime 之上,所以其调用 Objective-C 的 API 方式非常方便,不像调用 C/C++ 哪样,必须先为 Lua 编写调用接口(有些技术可以帮助 Lua 调用 C/C++ 编写的动态库而不必事先编写调用接口, 但是在 iOS 上不能调用应用程序自己编写的 API)。
● 对于 Objective-C 和 Lua 之间的数据类型转换进行了封装,使得开发者不必关心 Lua 和 Objective-C 的数据类型转换,方便开发。WaxPatch 工作原理Objectvie-C 语言的特性和实现机制决定了任何其他脚本语言对其进行调用都很方便。主要原因在于 Objective-C runtime 提供了对于 类/对象 等 OC 类型的反射和自省机制。相关的 API 如下:
typedefstruct objc_class *Cstructobjc_object "【"Class isa OBJC_ISA_AVAILABILITY;"】";typedefstruct objc_object *typedefstruct objc_selector *SEL;typedefid (*IMP)(id, SEL, ...);SELsel_getUid(const char *str);constchar *object_getClassName(id obj);Classobjc_getClass(const char *name);Methodclass_getInstanceMethod(Class cls, SEL name);Methodclass_getClassMethod(Class cls, SEL name);IMPclass_getMethodImplementation(Class cls, SEL name);IMPclass_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
藉由以上 API,则可以通过字符串来动态调用 Objective-C 的类和对象的方法。
Wax/WaxPatch 调用 Objective-C 并不是简单得对 Objective-C runtime 的 API 进行 1 对 1 得封装,而是把所有的 Objective-C 的对象\类\函数\等抽象成一个 WaxInstance,对 WaxInstance 进行操作。而 WaxPatch 则对 WaxInstance 的元表的 __newindex 属性进行改写,调用 class_replaceMethod 方法改写父类的函数实现,使用 forwardInvocation 机制把针对父类的调用方法转发到 Lua 改写的类方法里面。动态补丁流程我们在一些项目中使用 WaxPatch 来实现热更新机制,主要的流程如下所示。
1. 补丁包发布流程
2 客户端请求补丁流程
3 客户端使用补丁流程
整个流程遵循以下原则:
● 简单有效,避免复杂逻辑。整个补丁包是所有补丁的全量包而不是增量包。
● 安全性要有保证。发布版的补丁包是需要经过加密的,避免被恶意篡改。
● 正确性。在打补丁的过程中,一旦出现错误,则立刻退出补丁流程,避免对原应用程序的流程产生致命的损坏。
4 全量补丁包格式├── │ ├──LoadPatchViewController.lua│ └──init.lua├── │ ├──LTMoviePlayerViewController.lua│ ├──LTPlayControlCenterView.lua│ └──init.lua├── LuaPatchManager.lua└── letv_hotpatch_mapping.lua
① letvPatchManager.lua :补丁管理和执行文件,通过对客户端版本号和 letv_hotpatch_mapping.ua 文件判断补丁是否应该用于客户端。② letv_hotpatch_mapping.lua:补丁索引文件,记录补丁适用于那个客户端版本。③ :补丁包分包,为了方便补丁包管理,对单个补丁文件进行分组。④ /init.lua:补丁分组执行文件。⑤ /xxxx.lua:功能性的补丁。整个方案主要由以下部分组成:
● 补丁包管理器:集成在客户端内部,使用 Objective-C 编写,负责从服务器下载补丁包,并解压到客户端的沙盒中。
● 补丁包:见上文。
● 补丁打包器:负责把所有明文补丁进行加密,并打包成 zip 格式供客户端下载。改进WaxPatchblock 调用和实现WaxPatch 对于 Objective-C 的 block 支持不够完备,虽然有一些 WaxPatch 的衍生版本增加了一些 block 的支持,但是并不能令我们满意,尤其是 block 的参数支持上。对此。我们自己增加了 WaxPatch 对 block 的支持,做到了对 block 的可变参数的支持。
安全性解决方案由于 WaxPatch 可以做到对所有 Objective-C 的 API 修改(包括系统的和客户端内部的)。所以理论上来说,一个非法的 lua 补丁可以修改客户端所有的流程和功能,以致于对最终用户的信息造成伤害。
为了保证客户端功能和流程的安全性,在发布的补丁包中,所有的 Lua 补丁源码都由秘钥进行加密。在客户端方面,我们针对 Lua 的解释器进行修改,使用公钥进行解密和验证码,避免非法的 Lua 补丁文件被执行。其他解决方案与结论与 WaxPatch 方案相同的另外一个方案是 JSPatch。JSPatch 使用系统内置的 Javascript 解释器来动态 Javascript 代码,达到与 WaxPatch 同样的目的。其原理与 WaxPatch 基本相同。
经过调查,JSPatch 与 WaxPatch 相比,有一些固有的缺点无法解决:
① 在 iOS6.0 系统上,Apple 并没有开放 JavascriptCore 引擎,需要自己内置 JS 引擎,增加客户端的体积。
② 由于 JSPatch 使用系统的 JavascriptCore 引擎,我们无法去验证 Javascript 补丁的合法性和有效性,对客户端带来了很大的安全隐患。
以上原因决定使用 WaxPatch 的方案而不是 JSPatch 的方案。
软件开发的正途应该是:以加强代码质量和严格的测试来控制客户端质量。虽然 WaxPatch 可以做到对线上应用程序的缺陷进行快速修复,但是这种手段永远只能作为最后的一个防线,而不能过于依赖它。
附上开源项目
/piaojin/iOS-WaxPatch
阅读(...) 评论()利用系统Hotpatch加载驱动的一种比较取巧的方法_百度文库
两大类热门资源免费畅读
续费一年阅读会员,立省24元!
利用系统Hotpatch加载驱动的一种比较取巧的方法
&&利用系统Hotpatch加载驱动的一种比较取巧的方法
阅读已结束,下载文档到电脑
想免费下载更多文档?
定制HR最喜欢的简历
下载文档到电脑,方便使用
还剩1页未读,继续阅读
定制HR最喜欢的简历
你可能喜欢1680人阅读
android(33)
此文参考何明桂的文章:http://blog.csdn.net/hmg25/article/details/8100896,不过原文中有些步骤太过简略,一句带过,相信很多同学是没那么容易走完全程的,花了一下午研究了下,特此记录,同时希望能帮到需要帮助的同学。
增量升级的原理
& & & & 首先将应用的旧版本Apk与新版本Apk做差分,得到更新的部分的补丁,例如旧版本的APK有5M,新版的有8M,更新的部分则可能只有3M左右(这里需要说明的是,得到的差分包大小并不是简单的相减,因为其实需要包含一些上下文相关的东西),使用差分升级的好处显而易见,那么你不需要下载完整的8M文件,只需要下载更新部分就可以,而更新部分可能只有3、4M,可以很大程度上减少流量的损失。
& & & & &在用户下载了差分包之后,需要在手机端将他们组合起来。可以参考的做法是先将手机端的旧版本软件(多半在/data/下),复制到SD卡或者cache中,将它们和之前的差分patch进行组合,得到一个新版本的apk应用,如果不出意外的话,这个生成的apk和你之前做差分的apk是一致的。
增量升级的操作
& & & &在了解基本的原理之后,我们来逐步解决其中的各个难点。首先是差分包patch的生成。
android中提供我们用来制作差分增量升级包的工具,&bsdiff&,这是一个很牛X开源的二进制差分工具.相关的介绍
相关的代码&或者在android的代码目录下 \external\bsdiff
& & & &bsdiff是二进制差分工具,其对应的bspatch是相应的补丁合成工具
& & & &需要注意的是增量升级的补丁包,是需要在服务器端,即PC端完成,大致流程如,制作补丁时调用bsdiff函数,根据两个不同版本的二进制文件,生成补丁文件。&
& & & &将生成的补丁包 xx.patch放置在升级服务器上,供用户下载升级,对应多版本需要对不同的版本进行差分,对于版本跨度较大的,建议整包升级。
& & & & 用户在下载了 xx.patch补丁包后,需要用到补丁所对应的apk,即原来系统安装的旧版本apk和补丁合成的bspatch工具。系统旧版本的apk可以通过copy系统data/app目录下的apk文件获取,而补丁合成的bspatch可以通过将bspatch源码稍作修改,封装成一个so库,供手机端调用。
& & & 和差分时的参数一样。合成新的apk便可以用于安装。
& & 以上只是简单的操作原理,增量升级还涉及很多其他方面,例如,升级补丁校验等问题,可以参考android源码中bootable\recovery\applypatch的相关操作,本文只是浅析,在此不表。
& & & &多说无益,实践才是王道。下面就来简单实践一下,检测之前理论的正确性。&(),解压后文件如下
& & & &以附带的iReader来做测试,在shell进入test\bsdiff4.3-win32文件夹,并下运行命令:
& & & &原来的apk(2.94M),新版本的(3.24M),得到的patch文件为1.77M,用户需要下载的就只是1.77M,流量节省了很多。
& & &下面先在电脑端将他们合并。
=============================================原文===================================
下面我们在手机端合成看看,将根目录下的bspatch(此为手机端运行的)、iReader1.6.2.0(v35).apk 和ireader.patch ,通过adb push到你有权限操作的目录,最好是在/sdcard/下,然后设置bspatch的执行权限,重复操作上述命令,可以合成新版本的apk,稍后安装查看验证版本即可,不详述。
=========================================================================================
上述原文中有几点不是很清楚,或者错误的地方。
首先,上述三个文件不要拷到/sdcard下,sdcard下的文件的执行权限是会被安卓系统拒绝的,哪怕设置了文件的执行权限,所以我是复制到data目录下的。
然后,接下来的问题是怎么在手机端调用执行这个命令。很自然的我们想到了adb shell这个终端工具。
在shell终端执行上述命令时,大家肯定会得到这个结果sh : not found ,解决办法是原命令需要小修改:/data/bspatch iReader1.6.2.0v35.apk &new.apk & ireader.patch 区别就是bspatch需要指定全目录名(ps,注意iReader1.6.2.0v35.apk
文件名区别,v35的小括号去掉了,shell命令中小括号会报错,所以改了文件名)
OK,这样子在手机端合成新版本也实现了,然后最后的一步就是封装成lib包供代码调用。大部分人包括原文章主人可能都会封装成.so文件然后通过jni调用来实现吧,这种方式应该是OK的,不过通过上面的步骤我觉得不用JNI,直接在安卓代码中执行上述的adb shell命令不知道是否可行,未完待续,有时间试一下。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:27641次
排名:千里之外
原创:29篇
转载:17篇
评论:14条
(2)(1)(1)(1)(3)(4)(1)(1)(1)(1)(1)(6)(2)(1)(2)(1)(2)(2)(2)(2)(9)(4)
(window.slotbydup = window.slotbydup || []).push({
id: '4740881',
container: s,
size: '200,200',
display: 'inlay-fix'Android中的类加载-查找和在hotpatch上的问题 - 推酷
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虚拟机中的文件格式
首先为什么要讲这个呢,因为我们在接下去的源码分析中会用到。我们知道在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的,所以我们先看这个方法。
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).
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)) {
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
System.logE(&Unable to open zip file: & + file, ex);
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.
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方法。
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
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++的,请大家喝口水,缓解下焦躁的心情,慢慢看。
static jlong DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == NULL) {
NullableScopedUtfChars outputName(env, javaOutputName);
if (env-&ExceptionCheck()) {
ClassLinker* linker = Runtime::Current()-&GetClassLinker();
std::unique_ptr&std::vector&const DexFile*&& dex_files(new std::vector&const DexFile*&());
std::vector&std::string& error_
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()));
// 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 != itE ++it) {
ThrowWrappedIOException(&%s&, it-&c_str());
其中最重要的逻辑就是创建了一个dex_files的vector,并且调用class_linker的OpenDexFilesFromOat去从oat文件真正的打开一个Dex文件。从本文的第一小节我们可以知道,Android应用在ART虚拟机上,当它安装的时候都会生成一个oat文件,所以这里就是从这个oat文件中寻找对应的dex文件。下面让我们来看看OpenDexFilesFromOat函数。
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_
uint32_t* dex_location_checksum_pointer = &dex_location_
bool have_checksum = true;
std::string checksum_error_
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 =
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_
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_
// 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_
error_msgs-&push_back(compound_msg);
// TODO: What to lock here?
bool obsolete_file_cleanup_
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();
if (needs_registering) {
// We opened it, delete it.
open_oat_file.reset();
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_
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_
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_
// 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;
return false;
代码非常的长,但是我们惊喜的发现,贴心的google工程师已经帮我写好了step,所以我们就按照他们给的顺序进行源码分析。
step1:从已经在内存中的oat文件中查询时候有我们需要的dex文件。在这一步骤中,会生成一个dex文件的checksum值,这个值大家可以看做一个校验值,用来校验dex文件的。在FindOpenedOatDexFile方法中,会根据我们传入的oat文件路径(oat_location,也就是之前的optimizedPath)去查找在内存中是否已经有相同的oat文件,之后通过OatFile的GetOatDexFile去根据dex文件的路径(dex_location)获取一个OatDexFile,这个类可以获取对应的OatFile。
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) {
const OatFile::OatDexFile* oat_dex_file = oat_file-&GetOatDexFile(dex_location,dex_location_checksum,false);
if (oat_dex_file != nullptr) {
return oat_dex_
GetOatDexFile这个函数我们就不跟进去看了,具体的操作就是从oat_dex files 这个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_dex files 这个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循环:
while (i & 100) { std::string name = StringPrintf(&classes%zu.dex&, i);
std::string fake_location = location + kMultiDexSeparator +
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_
dex_files-&push_back(next_dex_file.release());
将所有的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方法。
@Overrideprotected Class&?& findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
这个方法很简单,直接调用了DexPathList的findClass方法。
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexF
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return null;}
这个方法会去遍历成员变量dexElements(这也是hotpatch能起作用的原因),并且调用Elememt中的DexFile对象的loadClassBinaryName方法。
在DexFile的loadClassBinaryName方法中,会去获取对应的cookie,并且调用一个native方法defineClass去加载类。
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 & &&
return soa.AddLocalReference&jclass&(result);
VLOG(class_linker) && &Failed to find dex_class_def&;
首先,这个方法调用了toDexFile,将cookie传进去,通过这个索引获取对应的dexFile。然后,调用class_linker的defineClass去获取一个类。值得一提的是,在defineClass中,这里存在一个缓存机制,会有一个DexCache用来缓存已经加载过的Dex信息,作用是提高效率和懒加载。
最后,class_linker会调用loadClass方法。
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) {
// no fields or methods - for example a marker interface
OatFile::OatClass oat_
if (Runtime::Current()-&IsStarted()
&& !Runtime::Current()-&UseCompileTimeClassPath()
&& FindOatClass(dex_file, klass-&GetDexClassDefIndex(), &oat_class)) {
LoadClassMembers(dex_file, class_data, klass, class_loader, &oat_class);
LoadClassMembers(dex_file, class_data, klass, class_loader, nullptr);
这个方法中的klass对象就代表着我们需要的类,其中主要做的操作就是给其设置一些属性,其中比较重要的就是SetDexClassDefIndex这个方法,设置这个class在dex中的索引值,之后我们需要通过这个值去获取oat文件中的OatClass。具体就是调用FindOatClass去获取对应的OatClass。FindOatClass最终调用了FindOpenedOatDexFile方法。这个方法在之前已经讲过,具体的逻辑就是这个函数就很简单了,遍历oat files 中的每一个oat文件,找到路径匹配的然后通过GetOatDexFile获取一个OatDexFile。
回到我们的LoadClass的最后,调用LoadClassMembers去加载这个类的相关信息。在这个方法中,会先去加载该类的field,然后去加载该类的method。我们以method为例,其中会调用linkCode方法,在这个方法中,我们会传入一个method_index作为方法的索引值,通过这个索引,我们从OatClass中获取了对应的OatMethod。既然如此,让我们看看OatMethod的构造函数。
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,code offset )[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文件:
我们可以看到,在图片中的最后一行,直接写的是#70,也就是Test2.test的method index。
再看看MainActivity和Test2都在patch中的情况:
由于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数组中,起到缓存的作用。
一文中,文章的作者详细的说明了在AndroidN上patch失败的原因,其中原理部分和我上面提到的第三种情况比较相似,只是我在Android5.1的源码中没有看到base.art和AppImage,只有一个boot.art,用来缓存系统的类,但是,在我看来,这种情况导致patch失败的可能性是最大的。
以上就是这篇文章的所有内容了,对于最后一点hotpatch失败的猜想,欢迎大家一起讨论,当然如果有大神明确的知道其中的原理的话,能告诉我那就最好了~
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致

我要回帖

更多关于 hotpatch是什么 的文章

 

随机推荐