我报名参与金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动概况
前语
热修正
到现在2022年现已不是一个新名词,但是作为Android开发中心技能栈的一部分,我这儿还得来一次冷饭热炒。
随着移动端业务复杂程度的添加,传统的版别更新流程明显无法满意业务和开发者的需求, 热修正技能的推出在很大程度上改进了这一局面。国内大部分老练的干流 App都具有自己的热更新技能,像手淘、支付宝、微信、QQ、饿了么、美团等。
能够说,一个好的热修正技能,将为你的 App助力百倍。关于每一个想在 Android 开发领域有所造诣的开发者,掌握热修正技能更是必备的素质。
热修正
是 Android 大厂面试中高频面试知识点,也是咱们必需求掌握的知识点。热修正技能,能够看作 Android平台开展老练至必定阶段的必定产品。
Android热修正了解吗?修正哪些东西?
常见热修正结构比照以及各原理剖析?
1.什么是热修正
热修正说白了便是不再运用传统的运用商店更新或许自更新办法,运用补丁包推送的办法在用户无感知的情况下,修正运用bug或许推送新的需求
传统更新
和热更新
进程比照如下:
热修正优缺陷
:
-
长处:
- 1.只需求打补丁包,不需求从头发版别。
- 2.用户无感知,不需求从头下载最新运用
- 3.修正成功率高
-
缺陷:
- 补丁包滥用,简略导致运用版别不可控,需求开发一套完整的补丁包更新机制,会添加必定的成本
2.热修正计划
首要咱们得知道热修正修正哪些东西?
- 1.代码修正
- 2.资源修正
- 3.动态库修正
2.1:代码修正计划
从技能角度来说,咱们的意图是非常清晰的:把过错的代码替换成正确的代码。 注意这儿的替换,并不是直接擦写dx文件,而是供给一份新的正确代码,让运用运转时绕过过错代码,履行新的正确代码。
主意简略直接,但完成起来并不简略。现在主要有三类技能计划:
2.1.1.类加载计划
之前剖析类加载机制有说过: 加载流程先是遵循双亲派遣准则,假如派遣准则没有找到此前加载过此类, 则会调用CLassLoader的findClass办法,再去BaseDexClassLoader下面的dexElements数组中查找,假如没有找到,终究调用defineClassNative办法加载
代码修正便是根据这点: 将新的做了修正的dex文件,经过反射注入到BaseDexClassLoader的dexElements数组的第一个方位上dexElements[0],下次从头启动运用加载类的时分,会优先加载做了修正的dex文件,这样就到达了修正代码的意图。原理很简略
代码如下:
public class Hotfix {
public static void patch(Context context, String patchDexFile, String patchClassName)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//获取体系PathClassLoader的"dexElements"特点值
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object origDexElements = getDexElements(pathClassLoader);
//新建DexClassLoader并获取“dexElements”特点值
String otpDir = context.getDir("dex", 0).getAbsolutePath();
Log.i("hotfix", "otpdir=" + otpDir);
DexClassLoader nDexClassLoader = new DexClassLoader(patchDexFile, otpDir, patchDexFile, context.getClassLoader());
Object patchDexElements = getDexElements(nDexClassLoader);
//将patchDexElements刺进原origDexElements前面
Object allDexElements = combineArray(origDexElements, patchDexElements);
//将新的allDexElements从头设置回pathClassLoader
setDexElements(pathClassLoader, allDexElements);
//从头加载类
pathClassLoader.loadClass(patchClassName);
}
private static Object getDexElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//首要获取ClassLoader的“pathList”实例
Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
pathListField.setAccessible(true);//设置为可拜访
Object pathList = pathListField.get(classLoader);
//然后获取“pathList”实例的“dexElements”特点
Field dexElementField = pathList.getClass().getDeclaredField("dexElements");
dexElementField.setAccessible(true);
//读取"dexElements"的值
Object elements = dexElementField.get(pathList);
return elements;
}
//合拼dexElements
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
//读取obj长度
int length = Array.getLength(obj);
//读取obj2长度
int length2 = Array.getLength(obj2);
Log.i("hotfix", "length=" + length + ",length2=" + length2);
//创立一个新Array实例,长度为ojb和obj2之和
Object newInstance = Array.newInstance(componentType, length + length2);
for (int i = 0; i < length + length2; i++) {
//把obj2元素刺进前面
if (i < length2) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
//把obj元素顺次放在后边
Array.set(newInstance, i, Array.get(obj, i - length2));
}
}
//回来新的Array实例
return newInstance;
}
private static void setDexElements(ClassLoader classLoader, Object dexElements) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//首要获取ClassLoader的“pathList”实例
Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
pathListField.setAccessible(true);//设置为可拜访
Object pathList = pathListField.get(classLoader);
//然后获取“pathList”实例的“dexElements”特点
Field declaredField = pathList.getClass().getDeclaredField("dexElements");
declaredField.setAccessible(true);
//设置"dexElements"的值
declaredField.set(pathList, dexElements);
}
}
类加载进程如下:
微信Tinker
,QQ 空间的超级补丁
、手 QQ 的QFix
、饿了 么的 Amigo
和 Nuwa
等都是运用这个办法
缺陷:由于类加载后无法卸载,所以类加载计划有必要重启App,让bug类从头加载后才干收效。
2.1.2:底层替换计划
底层替换计划不会再次加载新类,而是直接在 Native 层 修正原有类,
这儿咱们需求说到Art虚拟机中ArtMethod
:
每一个Java办法在Art虚拟机中都对应着一个 ArtMethod
,ArtMethod记录了这个Java办法的一切信息,包括所属类、拜访权限、代码履行地址等。
结构如下:
// art/runtime/art_method.h
class ArtMethod FINAL {
...
protected:
GcRoot<mirror::Class> declaring_class_;
GcRoot<mirror::PointerArray> dex_cache_resolved_methods_;
GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_;
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
struct PACKED(4) PtrSizedFields {
void* entry_point_from_interpreter_; // 1
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_; //2
} ptr_sized_fields_;
...
}
在 ArtMethod结构体中,最重要的便是 注释1和注释2标示的内容,从名字能够看出来,他们便是办法的履行进口。 咱们知道,Java代码在Android中会被编译为 Dex Code。
Art虚拟机中能够选用解说形式或许 AOT机器码模式履行 Dex Code
- 解说形式: 便是去除Dex Code,逐条解说履行。 假如办法的调用者是以解说形式运转的,在调用这个办法时,就会获取这个办法的 entry_point_from_interpreter_,然后跳转履行。
- AOT形式: 就会预先编译好 Dex Code对应的机器码,然后在运转期直接履行机器码,不需求逐条解说履行Dex Code。 假如办法的调用者是以AOT机器码办法履行的,在调用这个办法时,便是跳转到 entry_point_from_quick_compiled_code_中履行。
那是不是只需求替换这个几个 entry_point_* 进口地址就能够完成办法替换了呢? 并没有那么简略,由于不论是解说形式仍是AOT形式,在运转期间还会需求调用ArtMethod中的其他成员字段
AndFix选用的是改动指针指向:
// AndFix/jni/art/art_method_replace_6_0.cpp
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src); // 1
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest); // 2
...
// 3
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
LOGD("replace_6_0: %d , %d",
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}
缺陷:存在一些兼容性问题,由于ArtMethod结构体是Android开源的一部分,所以每个手机厂商都或许会去更改这部分的内容,这就或许导致ArtMethod替换计划在某些机型上面呈现不知道过错。
Sophix为了规避上面的AndFix的危险,选用直接替换整个结构体
。这样不论手机厂商如何更改体系,咱们都能够正确定位到办法地址
2.4.3:install run
计划
Instant Run 计划的中心思想是——插桩,在编译时经过插桩在每一个办法中刺进代码,修正代码逻辑,在需求时绕过过错办法,调用patch类的正确办法。
首要,在编译时Instant Run为每个类刺进IncrementalChange变量
IncrementalChange $change;
为每一个办法添加类似如下代码:
public void onCreate(Bundle savedInstanceState) {
IncrementalChange var2 = $change;
//$change不为null,表明该类有修正,需求重定向
if(var2 != null) {
//经过access$dispatch办法跳转到patch类的正确办法
var2.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState});
} else {
super.onCreate(savedInstanceState);
this.setContentView(2130968601);
this.tv = (TextView)this.findViewById(2131492944);
}
}
如上代码,当一个类被修正后,Instant Run会为这个类新建一个类,命名为xxx&override,且完成IncrementalChange接口,并且赋值给原类的$change变量。
public class MainActivity$override implements IncrementalChange {
}
此时,在运转时原类中每个办法的var2 != null,经过accessdispatch(参数是办法名和原参数)定位到patch类MainActivitydispatch(参数是办法名和原参数)定位到patch类MainActivityoverride中修正后的办法。
Instant Run是google在AS2.0时用来完成“热布置”的,一起也为“热修正”供给了一个绝佳的思路。美团的Robust便是根据此。
2.2:资源修正计划
这儿咱们来看看install run的原理即可,市面上的常见修正计划大部分都是根据此办法。
public static void monkeyPatchExistingResources(Context context,
String externalResourceFile, Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}
try {
// 创立一个新的AssetManager
AssetManager newAssetManager = (AssetManager) AssetManager.class
.getConstructor(new Class[0]).newInstance(new Object[0]); // ... 1
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath", new Class[] { String.class }); // ... 2
mAddAssetPath.setAccessible(true);
// 经过反射调用addAssetPath办法加载外部的资源(SD卡资源)
if (((Integer) mAddAssetPath.invoke(newAssetManager,
new Object[] { externalResourceFile })).intValue() == 0) { // ... 3
throw new IllegalStateException(
"Could not create new AssetManager");
}
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(
"ensureStringBlocks", new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources(); // ... 4
try {
// 反射得到Resources的AssetManager类型的mAssets字段
Field mAssets = Resources.class
.getDeclaredField("mAssets"); // ... 5
mAssets.setAccessible(true);
// 将mAssets字段的引证替换为新创立的newAssetManager
mAssets.set(resources, newAssetManager); // ... 6
} catch (Throwable ignore) {
...
}
// 得到Activity的Resources.Theme
Resources.Theme theme = activity.getTheme();
try {
try {
// 反射得到Resources.Theme的mAssets字段
Field ma = Resources.Theme.class
.getDeclaredField("mAssets");
ma.setAccessible(true);
// 将Resources.Theme的mAssets字段的引证替换为新创立的newAssetManager
ma.set(theme, newAssetManager); // ... 7
} catch (NoSuchFieldException ignore) {
...
}
...
} catch (Throwable e) {
Log.e("InstantRun",
"Failed to update existing theme for activity "
+ activity, e);
}
pruneResourceCaches(resources);
}
}
/**
* 根据SDK版别的不同,用不同的办法得到Resources 的弱引证调集
*/
Collection<WeakReference<Resources>> references;
if (Build.VERSION.SDK_INT >= 19) {
Class<?> resourcesManagerClass = Class
.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod(
"getInstance", new Class[0]);
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null,
new Object[0]);
try {
Field fMActiveResources = resourcesManagerClass
.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
ArrayMap<?, WeakReference<Resources>> arrayMap = (ArrayMap) fMActiveResources
.get(resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass
.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
references = (Collection) mResourceReferences
.get(resourcesManager);
}
} else {
Class<?> activityThread = Class
.forName("android.app.ActivityThread");
Field fMActiveResources = activityThread
.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
Object thread = getActivityThread(context, activityThread);
HashMap<?, WeakReference<Resources>> map = (HashMap) fMActiveResources
.get(thread);
references = map.values();
}
//遍历并得到弱引证调集中的 Resources ,将 Resources mAssets 字段引证替换成新的 AssetManager
for (WeakReference<Resources> wr : references) {
Resources resources = (Resources) wr.get();
if (resources != null) {
try {
Field mAssets = Resources.class
.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
...
}
resources.updateConfiguration(resources.getConfiguration(),
resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
- 在注释1处创立一个新的 AssetManager ,
- 在注释2 和注释3 处经过反射调用 addAssetPath 办法加载外部( SD 卡)的资源。
- 在注释4 处遍历 Activity 列表,得到每个 Activity 的 Resources ,
- 在注释5 处经过反射得到 Resources 的 AssetManager 类型的 rnAssets 字段 ,
- 注释6处改写 mAssets 字段的引证为新的 AssetManager 。
选用相同的办法,
- 在注释7处将 Resources. Theme 的 m Assets 字段 的引证替换为新创立的 AssetManager 。
- 紧接着 根据 SDK 版别的不同,用不同的办法得到 Resources 的弱引证调集,
- 再遍历这个弱引证调集, 将弱引证调集中的 Resources 的 mAssets 字段引证都替换成新创立的 AssetManager 。
资源修正原理
:
- 1.创立新的AssetManager,经过反射调用addAssetPath办法,加载外部资源,这样新创立的AssetManager就含有了外部资源
- 2.将AssetManager类型的mAsset字段全部用新创立的AssetManager目标替换。这样下次加载资源文件的时分就能够找到包含外部资源文件的AssetManager。
2.3:动态链接库so的修正
1.接口调用替换计划:
sdk供给接口替换System默认加载so库接口
SOPatchManager.loadLibrary(String libName) -> System.loadLibrary(String libName)
SOPatchManager.loadLibrary接口加载 so库的时分优先测验去加载sdk 指定目录下的补丁so,
加载策略
如下:
假如存在则加载补丁 so库而不会去加载装置apk装置目录下的so库 假如不存在补丁so,那么调用System.loadLibrary去加载装置apk目录下的 so库。
咱们能够很清楚的看到这个计划的优缺陷: 长处:不需求对不同 sdk 版别进行兼容,由于一切的 sdk 版别都有 System.loadLibrary 这个接口。 缺陷:调用方需求替换掉 System 默认加载 so 库接口为 sdk供给的接口, 假如是现已编译混杂好的三方库的so 库需求 patch,那么是很难做到接口的替换
尽管这种计划完成简略,一起不需求对不同 sdk版别区别处理,但是有必定的局限性没法修正三方包的so库一起需求强制侵入接入方接口调用,接着咱们来看下反射注入计划。
2、反射注入计划
前面介绍过 System. loadLibrary ( “native-lib”); 加载 so库的原理,其实native-lib 这个 so 库终究传给 native 办法履行的参数是 so库在磁盘中的完整途径,比方:/data/app-lib/com.taobao.jni-2/libnative-lib.so, so库会在 DexPathList.nativeLibraryDirectories/nativeLibraryPathElements 变量所表明的目录下去遍历查找
sdk<23 DexPathList.findLibrary 完成如下
能够发现会遍历 nativeLibraryDirectories数组,假如找到了 loUtils.canOpenReadOnly (path)回来为 true, 那么就直接回来该 path, loUtils.canOpenReadOnly (path)回来为 true 的前提肯定是需求 path 表明的 so文件存 在的。那么咱们能够采纳类似类修正反射注入办法,只要把咱们的补丁so库的途径刺进到nativeLibraryDirectories数组的最前面就能够到达加载so库的时分是补丁 库而不是本来so库的目录,从而到达修正的意图。
sdk>=23 DexPathList.findLibrary 完成如下
sdk23 以上 findLibrary 完成现已发生了变化,如上所示,那么咱们只需求把补丁so库的完整途径作为参数构建一个Element目标,然后再刺进到nativeLibraryPathElements 数组的最前面就好了。
- 长处:能够修正三方库的so库。一起接入方不需求像计划1 —样强制侵入用 户接口调用
- 缺陷:需求不断的对 sdk 进行适配,如上 sdk23 为分界线,findLibrary接口完成现已发生了变化。
关于 so库的修正计划现在更多采纳的是接口调用替换办法,需求强制侵入用户 接口调用。 现在咱们的so文件修正计划采纳的是反射注入的计划,重启收效。具有更好的普遍性。 假如有so文件修正实时收效的需求,也是能够做到的,只是有些约束情况。
常见热修正结构?
特性 | Dexposed | AndFix | Tinker/Amigo | QQ Zone | Robust/Aceso | Sophix |
---|---|---|---|---|---|---|
技能原理 | native底层替换 | native底层替换 | 类加载 | 类加载 | Instant Run | 混合 |
所属 | 阿里 | 阿里 | 微信/饿了么 | QQ空间 | 美团/蘑菇街 | 阿里 |
即时收效 | YES | YES | NO | NO | YES | 混合 |
办法替换 | YES | YES | YES | YES | YES | YES |
类替换 | NO | NO | YES | YES | YES | YES |
类结构修正 | NO | NO | YES | NO | NO | YES |
资源替换 | NO | NO | YES | YES | NO | YES |
so替换 | NO | NO | YES | NO | NO | YES |
支撑gradle | NO | NO | YES | YES | YES | YES |
支撑ART | NO | YES | YES | YES | YES | YES |
能够看出,阿里系多选用native底层计划,腾讯系多选用类加载机制。其中,Sophix是商业化计划;Tinker/Amigo支撑特性较多,一起也更复杂,假如需求修正资源和so,能够挑选;假如仅需求办法替换,且需求即时收效,Robust是不错的挑选。
总结:
尽管热修正(或热更新)相关于迭代更新有诸多优势,市面上也有很多开源计划可供挑选,但现在热修正仍然无法代替迭代更新形式。有如下原因: 热修正结构多多少少会添加功用开销,或添加APK大小 热修正技能本身存在局限,比方有些计划无法替换so或资源文件 热修正计划的兼容性,有些计划无法一起兼顾Dalvik和ART,有些深度定制体系也无法正常工作 监管危险,比方苹果体系严格约束热修正
所以,关于功用迭代和常规bug修正,版别迭代更新仍然是干流。一般的代码修正,运用Robust能够处理,假如还需求修正资源或so库,能够考虑Tinker。
参阅文章
-
Tinker-接入攻略
-
热修正原理学习(2)底层替换原理和打破底层差异的办法
-
深化理解Android热修正技能原理之so库热修正技能