热修复原理学习(5)Dalvik下完整dex方案探索与初始化时机选择(1)

| type_ids | 类似数据索引,记录了每个类型的字符串索引 |

| proto_ids | 原型数据索引,记录了方法声明的字符串,返回类型字符串,参数列表 |

| field_ids | 字段数据索引,记录了所属类,类型以及方法名 |

| method_ids | 类方法索引,记录方法所属类名,方法声明以及方法名等信息 |

| class_defs | 类定义数据索引,记录指定类各类信息,包括接口,超类,类数据偏移量 |

| data | 数据区,保存了各个类的真实数据 |

| link_data | 静态链接文件中使用的数据。 |

这里我们打算去除dex中的类,因此我们最关心的自然是这里的 class_defs属性。

需要注意的是,并不是要把某个类的所有信息都从dex移除,因为如果这么做,可能会导致dex的各个部分都发生变化,从而需要大量调整offset,这样就会变得费时费力了,我们要做的,仅仅是使得在解析这个dex的时候找不到这个类的定义就可以了。

因此,只需要移除定义的入口,对于类的具体内容不进行删除,这样可以最大限度减少offset的修改。

我们来看虚拟机在dexopt的时候是如果找到某个dex中所有的类定义的,它是在 verifyAndOptimizeClasses()中,注意,和之前的 verifyAndOptimizeClass()是两个方法,它也是在安装Apk时调用的:

// dalvik/vm/analysis/DexPrepare.cpp

static void verifyAndOptimizeClasses(DexFile* pDexFile, bool doVerify,

bool doOpt)

{

u4 count = pDexFile->pHeader->classDefsSize;

u4 idx;

for (idx = 0; idx < count; idx++) {

const DexClassDef* pClassDef;

const char* classDescriptor;

ClassObject* clazz;

pClassDef = dexGetClassDef(pDexFile, idx); // 1

classDescriptor = dexStringByTypeIdx(pDexFile, pClassDef->classIdx);

clazz = dvmLookupClass(classDescriptor, NULL, false);

if (clazz != NULL) {

verifyAndOptimizeClass(pDexFile, clazz, pClassDef, doVerify, doOpt); // 2

} else {

ALOGV(“DexOpt: not optimizing unavailable class ‘%s’”,

classDescriptor);

}

}

}

注释1:返回了 pDexFile下第idx个类的定义。

注释2:调用我们熟知的 verifyAndOptimizeClass来对这个类进行类校验和类优化。

我们来看下注释1的 dexGetClassDef()方法:

// dalvik/libdex/DexFile.h

DEX_INLINE const DexClassDef* dexGetClassDef(const DexFile* pDexFile, u4 idx) {

assert(idx < pDexFile->pHeader->classDefsSize);

return &pDexFile->pClassDefs[idx]; //返回 pClassDefs的第idx元素

}

而这里的 pClassDefs是这么来的呢?下面是dex的赋值:

// dalvik/libdex/DexFile.cpp

void dexFileSetupBasicPointers(DexFile* pDexFile, const u1* data) {

DexHeader pHeader = (DexHeader) data;

pDexFile->pClassDefs = (const DexClassDef*) (data + pHeader->classDefsOff);

}

由此可以看出,一个类的所DexClassDef,也就是类定义,是从 pHeader->classDefsOff偏移处开始的,依次呈线性排列的,一个dex里面一共有 pHeader->classDefsSize个类定义。

因此,我们就可以直接找到pHead->classDefsOff偏移处,遍历所有的DexClassDef,如果发现这个 DexClassDef的类名包含在补丁中,就把它移除,实现下图所示的效果:

在这里插入图片描述

接下来,只要修改 pHeader->classDefsSize,把dex中类的数目改为去除补丁中的类之后的数目即可。

我们只是去除了类的定义,而对于类的方法实体以及其他dex信息不做移除,虽然这样会把这个被移除类的无用信息残留在dex文件中,但这些信息并不占用太多空间。移除类操作的方法对dex的处理速度提升帮助是很大的。

1.3 对于Application的处理


由此,我们实现了完整的dex合成。但仍然有个问题,这个问题所有完整dex替换方案都会遇到,那就是对Application的处理。

总所周知,Application是整个App的入口,因此,在进入到替换的完整dex之前,一定会通过Application的代码,然而Application必然是加载在原来的dex里面的。只有在补丁加载后使用的类,会在新的完整dex里面找到。

因此,在加载补丁后,如果Application类使用其他新dex里的类,由于在不同的一个dex里,如果Application被打上了pre-verified标识,这时就会抛出异常。

对此,我们解决办法很简单,既然被打上了pre-verified标识,那么,清除它就是了。

类的标识位于 ClassObjectaccessFlags成员中,而 pre-verifiyed标识的定义是 CLASS_ISPREVERIFIED = (1 << 16),因此,我们只需要在JNI层清除掉它即可:

classObj->accessFlags &= ~CLASS_ISPREVERIFIED;

这样,在 dvmResolveClass()中找到新的dex里的类后,由于 CLASS_ISPREVERIFIED标识被清空,就不会判断所在dex是否相同,从而成功避免抛出异常。

接下来,我们来比对目前市场上其他完整dex方案是怎么做的。

(1)Tinker

Tinker的方案是在 AndroidManifest.xml声明中就要求开发者将自己的Application直接替换成 TinkerApplication。而对于真正App的Application,要在初始化TinkerApplication时作为参数传入。这样 TinkerApplication会接管这个传入的Application,在生命周期回调时通过反射的方式调用实际 Application的相关回调逻辑。这么做确实很好地将入口Application和用户代码隔离开,不过需要改造原有的Application,如果对Application有更多扩展,接入成本也是比较高的。

(2)Amigo

Amigo的方案是在编译过程中,用Amigo自定义的gradle插件将App的Application替换成了 Amigo自己的另一个Application,并且将原来的Application的name保存了起来,该修复的问题都修复完后再调用之前保存的Application的 attach(context)。将它回调到loadedApk()中,最后调用它的onCreate(),执行原有Application的逻辑,这种方式只是在代码层面开发者无感知,但其实在编译期间偷偷帮用户做了替换,有点掩耳盗铃的意思,并且这种对系统做反射替换本身也是由一定的风险的。

相比之下,Sophix的Application处理方案既没有侵入编译过程,也不需要进行反射替换,所有的兼容操作都在运行期自动做好。接入过程及其顺滑。

1.4 dvmOptReslveClass问题与对策


然而Sophix这种清除标识的方案并非一帆风顺,在开发过程中发现,如果这个入口Application是没有pre-verified,反而有更大的问题。

这个问题是,DVM如果发现某个类没有 pre-verified,就会在初始化这个类的时候做verify操作,将会扫描这个类的所有代码,在扫描过程中对这个类代码使用到的类都要进行 dvmOptResolveClass()操作。

这个 dvmOptResolveClass()正是罪魁祸首,它会在解析的时候对使用到的类进行初始化,而这个逻辑是发生在Application类初始化的时候。此时补丁还没有进行加载,所以就会提前加载到原始dex中的类。接下来当补丁类加载完毕后,当这些已经加载的类用到新dex中的类,并且又是 pre-verified时就会报错。

这里最大的问题是在于我们无法把补丁加载提前到 dvmOptResolveClass之前,因为在一个App的生命周期里,没有可能到达比入口Application初始化更早的时期了。

而这个问题常见于多dex情形,当存在多dex时,无法保证Application用到的类和它处于同个dex中。如果只有一个dex,一般就不会有这个问题。

多dex情况下要想解决这个问题,有两种办法:

  • 让Application用到的所有非系统的类和Application位于同一个dex中,这就可以保证pre-verified标识被打上,避免进入 dvmOptResolveClass,而在补丁加载完之后,我们再清楚pre-verified标识,使得接下来使用其他类也不会报错

  • 把Applicaiton里面除了热修复框架代码以外的其他代码都剥离开,单独提出放到一个其他类里面,这样使得Application不会直接用到过多的非系统类,这样,保证这个单独拿出来的类和Application处于同一个dex的概率还是比较大的。如果想要更保险,Application可以采用反射方式方式访问这个单独得类,这样就彻底把Application和其他类隔绝开了。

第一种方法实现较为简单,因为Android官方multi-dex机制会自动将Application用到的类都打包到主dex中,所以只要把热修复初始化放在 attachBaseContext()的最前面,一般都没有问题。

而第二种方法稍加繁琐,是在代码架构层面进行重新设计,不过可以一劳永逸的解决问题。

2. 入口类与初始化时机选择

================================================================================

2.1 初始化时机


冷启动完整修复方案,本质就是替换掉整个原有的dex文件。然而“完整替换”只是一种理想化的设想,实际上无法做到“完整的”。原因是热修复的初始化本身也是一段代码。必须调用到这段代码,热修复才能执行完成,因此调用到热修复的类,肯定是使用者自己的类,这个类是无法被热修复影响到的,并且它只存在于原始安装包的 classes.dex中。如果要使热修复类之前使用的其他类最少,只能放在Application类入口中。

那么,放在Activity类里面是不是也可以呢?当然,如果你的App里面没有Application,放到Activity里面似乎也没有太大的问题,并且简单测试好像也能正常工作。

但是,如果你的AndroidManifest中注册了ContentProvider,事情就没有那么顺利了。ContentProvider的onCreate方法优先调用于Activity的onCreate方法。这就使得我们可能还没有完成热修复替换,就先执行到了 ContentProvider中的业务逻辑代码,导致某些类被提前引入。提前引入其他类的危害我们在之前的章节已经说明,这不仅会导致这些类无法修复,更可能引起 pre-verify异常,因此,只有把初始化放在Application类中,才能保证不会错误地提早引入类。

如果放在Application中,又有两种选择:放在onCreate()中或者放在 attachBaseContext()中。

放在 attachBaseContext()中自然是没有问题的,因为他是Application中最早被执行的代码,但需要注意的是,在attachBaseContext里面有很多限制,此时App申请的权限还没有授予完成,所以会遇到无法访问网络之类的问题,因此在attachBaseContext里面可以执行初始化,但是不可以进行网络请求下载新补丁。

那放在Application的onCreate中可以吗?简单测试似乎没有什么问题。然而,它和之前的Activity的onCreate方法一样,执行时间会晚于ContentProvider的onCreate方法。

当然,如果你的AndroidManifest里面没有注册过ContentProvider,并且能够保证引入的第三方库的AndoridManifest里面也没有注册,放在onCreate里面就没有什么问题。不过保险起见,为了避免以后某天项目在无意中引入,还是放在attachBaseContext里面最好。

2.2 防不胜防的细节错误


在进行初始化的时候,经常容易错误地提早引入其他类。

下面这段代码是Sophix的热修复初始化代码,SophixManager需要在设置各种属性后调用 initialize()方法进行初始化,就以这段代码为例:

public class SampleApplication extends Application {

LocalStorageUtil localStorageUtil = new LocalStorageUtil();

@Override

protected void attachBaseContext(Context base) {

CrashReport.initCrashReport(this);

SophixWrApper.init(this);

MultiDex.install(this);

localStorageUtil.init(this);

}

@Override

public void onCreate() {

super.onCreate();

SophixWrApper.query();

}

public LocalStorageUtil getLocalStorageUtil() {

return localStorageUtil;

}

static private class SophixWrApper {

static void init(Application context) {

final SophixManager instance = SophixManager.getInstance();

instance.setContext(context)

.setAppVersion(BuildConfig.VERSION_NAME)

.setPatchLoadStatusStub(new PatchLoadStatusListener() {

@Override

public void onLoad(final int mode,

final int code,

final String info,

final int handlePatchVersion) {

if (code == PatchStatus.CODE_LOAD_SUCCESS) {

MyLogger.d(“”, “Sophix load patch success”);

} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {

MyLogger.d(“”, “Sophix preload patch success”);

}

}

});

instance.initialze();

}

static void query() {

SophixManager.getInstance().queryAndLoadNewPatch();

}

}

}

这段简单的代码里面,包含了许多开发者都会出现的错误,这里一一指出每个问题:

  1. CreashReport.initCrashReport(this)在Sophix热修复初始化之前提早引入了,必然是不行的

  2. 虽然初始化确实是在attachBaseContext里面,但是包装了一个SophixWrApper类,这会导致初始化之前提前引入类,因此Sophix的初始化不可以包装在其他类中。

  3. setAppVersion的时候使用了BuildConfig类,这个BuildConfig类是Android编译期间动态生成的,也属于非系统类,如果在这里使用就会有提前引入的问题,这里建议用PackageManager来获取版本号。

  4. LocalStorageUtil直接在声明处赋值了它的示例,这个赋值其实是隐式发生在对象构造函数中的,这个时候甚至是更早与attachBaseContext的,因此也是不行的,需要在初始化之后才能进行赋值

  5. 在回调用中使用了MyLogger,在回调状态的时候引入很可能热修复还未初始化完毕,因此这里需要换位系统类android.utils.log

  6. MultiDex.install(this)调用放在了热修复初始化后,这样做虽然没有引入类的问题,但是可能会导致后面热修复框架初始化的时候找不到其他不在主dex中的热修复框架内部类,因此需要把它提前到热修复初始化之前。而提早引入MultiDex类不会带来问题,因为在热修复初始化之后,再也没有调用到这个MultiDex类的地方。

  7. 最后,经常会有人一楼了 syper.attachBaseContext(base),如果缺少它,后面都无法正常运行。

现在来看一下修改后的代码:

public class SampleApplication extends Application {

LocalStorageUtil localStorageUtil;

@Override

protected void attachBaseContext(Context base) {

super.attachBaseContext(base);

MultiDex.install(this);

initSophix(this);

CrashReport.initCrashReport(this);

initlocalStorageUtil();

}

@Override

public void onCreate() {

super.onCreate();

SophixManager.getInstance().queryAndLoadNewPatch();

}

private void initlocalStorageUtil(){

localStorageUtil = new LocalStorageUtil();

localStorageUtil.init(this);

}

public LocalStorageUtil getLocalStorageUtil() {

return localStorageUtil;

}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

如何成为Android高级架构师!

架构师必须具备抽象思维和分析的能力,这是你进行系统分析和系统分解的基本素质。只有具备这样的能力,架构师才能看清系统的整体,掌控全局,这也是架构师大局观的形成基础。 你如何具备这种能力呢?一是来自于经验,二是来自于学习。

架构师不仅要具备在问题领域上的经验,也需要具备在软件工程领域内的经验。也就是说,架构师必须能够准确得理解需求,然后用软件工程的思想,把需求转化和分解成可用计算机语言实现的程度。经验的积累是需要一个时间过程的,这个过程谁也帮不了你,是需要你去经历的。

但是,如果你有意识地去培养,不断吸取前人的经验的话,还是可以缩短这个周期的。这也是我整理架构师进阶此系列的始动力之一。


成为Android架构师必备知识技能

对应导图的学习笔记(由阿里P8大牛手写,我负责整理成PDF笔记)

部分内容展示

《设计思想解读开源框架》

  • 目录
  • 热修复设计
  • 插件化框架设计

    《360°全方面性能优化》
  • 设计思想与代码质量优化
  • 程序性能优化

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

链图片转存中…(img-yxmK4QF9-1713683932310)]

部分内容展示

《设计思想解读开源框架》

  • 目录
    [外链图片转存中…(img-yzSk7gMR-1713683932311)]
  • 热修复设计
    [外链图片转存中…(img-7PPa4bZJ-1713683932312)]
  • 插件化框架设计
    [外链图片转存中…(img-HUhM89rd-1713683932313)]
    《360°全方面性能优化》
    [外链图片转存中…(img-vO30QXUV-1713683932314)]
  • 设计思想与代码质量优化
    [外链图片转存中…(img-FSZ9naNB-1713683932315)]
  • 程序性能优化
    [外链图片转存中…(img-UK0MbKN3-1713683932316)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 18
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值