| 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的处理速度提升帮助是很大的。
由此,我们实现了完整的dex合成。但仍然有个问题,这个问题所有完整dex替换方案都会遇到,那就是对Application的处理。
总所周知,Application是整个App的入口,因此,在进入到替换的完整dex之前,一定会通过Application的代码,然而Application必然是加载在原来的dex里面的。只有在补丁加载后使用的类,会在新的完整dex里面找到。
因此,在加载补丁后,如果Application类使用其他新dex里的类,由于在不同的一个dex里,如果Application被打上了pre-verified标识,这时就会抛出异常。
对此,我们解决办法很简单,既然被打上了pre-verified标识,那么,清除它就是了。
类的标识位于 ClassObject
的 accessFlags
成员中,而 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处理方案既没有侵入编译过程,也不需要进行反射替换,所有的兼容操作都在运行期自动做好。接入过程及其顺滑。
然而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()
的最前面,一般都没有问题。
而第二种方法稍加繁琐,是在代码架构层面进行重新设计,不过可以一劳永逸的解决问题。
================================================================================
冷启动完整修复方案,本质就是替换掉整个原有的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里面最好。
在进行初始化的时候,经常容易错误地提早引入其他类。
下面这段代码是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();
}
}
}
这段简单的代码里面,包含了许多开发者都会出现的错误,这里一一指出每个问题:
-
CreashReport.initCrashReport(this)
在Sophix热修复初始化之前提早引入了,必然是不行的 -
虽然初始化确实是在attachBaseContext里面,但是包装了一个SophixWrApper类,这会导致初始化之前提前引入类,因此Sophix的初始化不可以包装在其他类中。
-
在
setAppVersion
的时候使用了BuildConfig类,这个BuildConfig类是Android编译期间动态生成的,也属于非系统类,如果在这里使用就会有提前引入的问题,这里建议用PackageManager来获取版本号。 -
LocalStorageUtil直接在声明处赋值了它的示例,这个赋值其实是隐式发生在对象构造函数中的,这个时候甚至是更早与attachBaseContext的,因此也是不行的,需要在初始化之后才能进行赋值
-
在回调用中使用了MyLogger,在回调状态的时候引入很可能热修复还未初始化完毕,因此这里需要换位系统类android.utils.log
-
MultiDex.install(this)调用放在了热修复初始化后,这样做虽然没有引入类的问题,但是可能会导致后面热修复框架初始化的时候找不到其他不在主dex中的热修复框架内部类,因此需要把它提前到热修复初始化之前。而提早引入MultiDex类不会带来问题,因为在热修复初始化之后,再也没有调用到这个MultiDex类的地方。
-
最后,经常会有人一楼了 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;
}
private void initSophix(Application context) {
String AppVersion = “0”;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/8a12d3d18468636784d28369b28ab729.jpeg)
总结
笔者之前工作是在金融公司可能并不是特别追求技术,而笔者又是喜欢追求技术的人,所以格格不入,只能把目标放在互联网大厂了。也希望大家都去敢于尝试和追逐自己的梦想!
BATJ大厂Android高频面试题
觉得有收获的记得点赞,关注+收藏哦!你们的点赞就是我的动力!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
oid)**
![](https://i-blog.csdnimg.cn/blog_migrate/8a12d3d18468636784d28369b28ab729.jpeg)
总结
笔者之前工作是在金融公司可能并不是特别追求技术,而笔者又是喜欢追求技术的人,所以格格不入,只能把目标放在互联网大厂了。也希望大家都去敢于尝试和追逐自己的梦想!
BATJ大厂Android高频面试题
[外链图片转存中…(img-7dz03qNk-1713683968913)]
[外链图片转存中…(img-eJNqeOmg-1713683968914)]
[外链图片转存中…(img-bA8Ta1qV-1713683968915)]
[外链图片转存中…(img-5EcZgnfz-1713683968916)]
觉得有收获的记得点赞,关注+收藏哦!你们的点赞就是我的动力!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!