Android热修复技术,你会怎么选?

1,202 阅读15分钟

前言

目前Android业内,热修复技术百花齐放,各大厂都推出了自己的热修复方案,使用的技术方案也各有所异,当然各个方案也都存在各自的局限性。在面对众多的方案,希望通过梳理这些热修复方案的对比及实现原理,掌握热修复技术的本质,同时也对项目接入做好准备。

什么是热修复技术?

关于热修复这个名词,并不陌生。相信大家都有过更新window补丁的经历,通过补丁可以动态修复系统的漏洞,只不过这个过程对用户而言是可选及自行操作。

那么关于Android平台的热修复技术,简单来说,就是通过下发补丁包,让已安装的客户端动态更新,让用户可以不用重新安装APP,就能够修复软件缺陷的一种技术。

随着热修复技术的发展,不仅可以修复代码,同时可以修复资源文件及SO库。

为什么要使用热修复技术?

在回答这个问题之前,我觉得应该先思考如下几个问题。

  1. 开发上线的版本能保证不存在Bug么?
  2. 修复后的版本能保证用户都及时更新么?
  3. 如何最大化减少线上Bug对业务的影响?

从这些角度来说,相信大家应该都能有所体会,热修复技术带来的优势不言而喻。

  1. 可快速修复,避免线上Bug带来的业务损失,把损失降到最低。
  2. 保证客户端的更新率,无须用户进行版本升级安装
  3. 良好的用户体验,无感知修复异常。节省用户下载安装成本。

怎么选择热修复技术方案?

国内主流的技术方案

1、阿里系

名称 说明
AndFix 开源,实时生效
HotFix 阿里百川,未开源,免费、实时生效
Sophix 未开源,商业收费,实时生效/冷启动修复

HotFix是AndFix的优化版本,Sophix是HotFix的优化版本。目前阿里系主推是Sophix。

2、腾讯系

名称 说明
Qzone超级补丁 QQ空间,未开源,冷启动修复
QFix 手Q团队,开源,冷启动修复
Tinker 微信团队,开源,冷启动修复。提供分发管理,基础版免费

3、其他

名称 说明
Robust 美团, 开源,实时修复
Nuwa 大众点评,开源,冷启动修复
Amigo 饿了么,开源,冷启动修复

方案对比

方案对比 Sophix Tinker nuwa AndFix Robust Amigo
类替换 yes yes yes no no yes
So替换 yes yes no no no yes
资源替换 yes yes yes no no yes
全平台支持 yes yes yes no yes yes
即时生效 同时支持 no no yes yes no
性能损耗 较少 较小 较大 较小 较小 较小
补丁包大小 较小 较大 一般 一般 较大
开发透明 yes yes yes no no yes
复杂度 傻瓜式接入 复杂 较低 复杂 复杂 较低
Rom体积 较小 Dalvik较大 较小 较小 较小
成功率 较高 较高 一般 最高 较高
热度
开源 no yes yes yes yes yes
收费 收费(设有免费阈值) 收费(基础版免费,但有限制) 免费 免费 免费 免费
监控 提供分发控制及监控 提供分发控制及监控 no no no no

参考Tinker及Sophix官方对比

为什么使用 Tinker?

Sophix产品优势?

怎么选?

怎么选?这个只能说一切看需求。如果公司综合实力强,完全考虑自研都没问题,但需要综合考虑成本及维护。下面给出2点建议,如下:

1、项目需求

  • 只需要简单的方法级别Bug修复?
  • 需要资源及so库的修复?
  • 对平台兼容性要求及成功率要求?
  • 有需求对分发进行控制,对监控数据进行统计,补丁包进行管理?
  • 公司资源是否支持商业付费?

2、学习及使用成本

  • 集成难度
  • 代码侵入性
  • 调试维护

3、选择大厂

  • 技术性能有保障
  • 有专人维护
  • 热度高,开源社区活跃

如果考虑付费,推荐选择阿里的Sophix,Sophix是综合优化的产物,功能完善、开发简单透明、提供分发及监控管理。

如果不考虑付费,只需支持方法级别的Bug修复,不支持资源及so,推荐使用Robust。

如果考虑需要同时支持资源及so,推荐使用Tinker。

最后如果公司综合实力强,可考虑自研,灵活性及可控制最强。

从Github上的热度及提交记录上看,nuwa、AndFix、Amigo等的提交都是2 years ago。

内业主要热修复技术方案原理?

技术分类

NativeHook 原理

原理及实现

NativeHook的原理是直接在native层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能。 下面以AndFix的一段jni代码来进行说明,如下:

void replace_6_0(JNIEnv* env, jobject src, jobject dest) {

    // 通过Method对象得到底层Java函数对应ArtMethod的真实地址
	art::mirror::ArtMethod* smeth =
			(art::mirror::ArtMethod*) env->FromReflectedMethod(src);

	art::mirror::ArtMethod* dmeth =
			(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ =
    reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloader
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
    reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_-1;
    //for reflection invoke
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
    //把旧函数的所有成员变量都替换为新函数的
    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_);
}

void setFieldFlag_6_0(JNIEnv* env, jobject field) {
	art::mirror::ArtField* artField =
			(art::mirror::ArtField*) env->FromReflectedField(field);
	artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;
	LOGD("setFieldFlag_6_0: %d ", artField->access_flags_);
}

每一个Java方法在art中都对应一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括访问权限及代码执行地址等。通过env->FromReflectedMethod得到方法对应的ArtMethod的真正开始地址,然后强转为ArtMethod指针,从而对其所有成员进行修改。

这样以后调用这个方法时就会直接走到新方法的实现中,达到热修复的效果。

优缺点

优点

  • 即时生效
  • 没有性能开销,不需要任何编辑器的插桩或代码改写

缺点

  • 存在稳定及兼容性问题。ArtMethod的结构基本参考Google开源的代码,各大厂商的ROM都可能有所改动,可能导致结构不一致,修复失败。
  • 无法增加变量及类,只能修复方法级别的Bug,无法做到新功能的发布

javaHook 原理

原理及实现

以美团的Robust为例,Robust 的原理可以简单描述为:

1、打基础包时插桩,在每个方法前插入一段类型为 ChangeQuickRedirect 静态变量的逻辑,插入过程对业务开发是完全透明

2、加载补丁时,从补丁包中读取要替换的类及具体替换的方法实现,新建ClassLoader加载补丁dex。当changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的

Robust 官方介绍示例图

下面通过Robust的源码来进行分析。 首先看一下打基础包是插入的代码逻辑,如下:

public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
        //为每个方法自动插入修复逻辑代码,如果ChangeQuickRedirect为空则不执行
        if (u != null) {
            if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
                PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
                return;
            }
        }
        super.onCreate(bundle);
        ...
    }

Robust的核心修复源码如下:

public class PatchExecutor extends Thread {
    @Override
    public void run() {
        ...
        applyPatchList(patches);
        ...
    }
    /**
     * 应用补丁列表
     */
    protected void applyPatchList(List<Patch> patches) {
        ...
        for (Patch p : patches) {
            ...
            currentPatchResult = patch(context, p);
            ...
            }
    }
     /**
     * 核心修复源码
     */
    protected boolean patch(Context context, Patch patch) {
        ...
        //新建ClassLoader
        DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
                null, PatchExecutor.class.getClassLoader());
        patch.delete(patch.getTempPath());
        ...
        try {
            patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
            patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
            } catch (Throwable t) {
             ...
        }
        ...
        //通过遍历其中的类信息进而反射修改其中 ChangeQuickRedirect 对象的值
        for (PatchedClassInfo patchedClassInfo : patchedClasses) {
            ...
            try {
                oldClass = classLoader.loadClass(patchedClassName.trim());
                Field[] fields = oldClass.getDeclaredFields();
                for (Field field : fields) {
                    if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
                        changeQuickRedirectField = field;
                        break;
                    }
                }
                ...
                try {
                    patchClass = classLoader.loadClass(patchClassName);
                    Object patchObject = patchClass.newInstance();
                    changeQuickRedirectField.setAccessible(true);
                    changeQuickRedirectField.set(null, patchObject);
                    } catch (Throwable t) {
                    ...
                }
            } catch (Throwable t) {
                 ...
            }
        }
        return true;
    }
}

优缺点

优点

  • 高兼容性(Robust只是在正常的使用DexClassLoader)、高稳定性,修复成功率高达99.9%
  • 补丁实时生效,不需要重新启动
  • 支持方法级别的修复,包括静态方法
  • 支持增加方法和类
  • 支持ProGuard的混淆、内联、优化等操作

缺点

  • 代码是侵入式的,会在原有的类中加入相关代码
  • so和资源的替换暂时不支持
  • 会增大apk的体积,平均一个函数会比原来增加17.47个字节,10万个函数会增加1.67M

java mulitdex 原理

原理及实现

Android内部使用的是BaseDexClassLoader、PathClassLoader、DexClassLoader三个类加载器实现从DEX文件中读取类数据,其中PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader实现。dex文件转换成dexFile对象,存入Element[]数组,findclass顺序遍历Element数组获取DexFile,然后执行DexFile的findclass。源码如下:

// 加载名字为name的class对象
public Class findClass(String name, List<Throwable> suppressed) {
    // 遍历从dexPath查询到的dex和资源Element
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        // 如果当前的Element是dex文件元素
        if (dex != null) {
            // 使用DexFile.loadClassBinaryName加载类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

所以此方案的原理是Hook了ClassLoader.pathList.dexElements[],将补丁的dex插入到数组的最前端。因为ClassLoader的findClass是通过遍历dexElements[]中的dex来寻找类的。所以会优先查找到修复的类。从而达到修复的效果。

图片引用自QQ空间热修复介绍

下面使用Nuwa的关键实现源码进行说明如下:

public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        //新建一个ClassLoader加载补丁Dex
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
        //反射获取旧DexElements数组
        Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
        //反射获取补丁DexElements数组
        Object newDexElements = getDexElements(getPathList(dexClassLoader));
        //合并,将新数组的Element插入到最前面
        Object allDexElements = combineArray(newDexElements, baseDexElements);
        Object pathList = getPathList(getPathClassLoader());
        //更新旧ClassLoader中的Element数组
        ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
    }

    private static PathClassLoader getPathClassLoader() {
        PathClassLoader pathClassLoader = (PathClassLoader) DexUtils.class.getClassLoader();
        return pathClassLoader;
    }

    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return ReflectionUtils.getField(paramObject, paramObject.getClass(), "dexElements");
    }

    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        return ReflectionUtils.getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    private static Object combineArray(Object firstArray, Object secondArray) {
        Class<?> localClass = firstArray.getClass().getComponentType();
        int firstArrayLength = Array.getLength(firstArray);
        int allLength = firstArrayLength + Array.getLength(secondArray);
        Object result = Array.newInstance(localClass, allLength);
        for (int k = 0; k < allLength; ++k) {
            if (k < firstArrayLength) {
                Array.set(result, k, Array.get(firstArray, k));
            } else {
                Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
            }
        }
        return result;
    }

优缺点

优点

  • 不需要考虑对dalvik虚拟机和art虚拟机做适配
  • 代码是非侵入式的,对apk体积影响不大

缺点

  • 需要下次启动才修复
  • 性能损耗大,为了避免类被加上CLASS_ISPREVERIFIED,使用插桩,单独放一个帮助类在独立的dex中让其他类调用。

dex替换

原理及实现

为了避免dex插桩带来的性能损耗,dex替换采取另外的方式。原理是提供dex差量包,整体替换dex的方案。差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并成一个完整的dex,完整dex加载得到dexFile对象作为参数构建一个Element对象然后整体替换掉旧的dex-Elements数组。

图片引用自TInker介绍

这也是微信Tinker采用的方案,并且Tinker自研了DexDiff/DexMerge算法。Tinker还支持资源和So包的更新,So补丁包使用BsDiff来生成,资源补丁包直接使用文件md5对比来生成,针对资源比较大的(默认大于100KB属于大文件)会使用BsDiff来对文件生成差量补丁。

下面我们关键看看Tinker的实现源码,当然具体的实现算法很复杂,我们只看关键的实现,最后的修复在UpgradePatch中的tryPatch方法,如下:

  @Override
    public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
        //省略一堆校验
        ... ....

        //下面是关键的diff算法及合并实现,实现相对复杂,感兴趣可以再仔细阅读源码
        //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
        if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
            return false;
        }

        if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
            return false;
        }

        if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
            return false;
        }

        // check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpreted
        if (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");
            return false;
        }

        if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
            manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
            return false;
        }

        TinkerLog.w(TAG, "UpgradePatch tryPatch: done, it is ok");
        return true;
    }

优缺点

优点

  • 兼容性高
  • 补丁小
  • 开发透明,代码非侵入式

缺点

  • 冷启动修复,下次启动修复
  • Dex合并内存消耗在vm head上,容易OOM,最后导致合并失败

资源修复原理

Instant Run

1、构建一个新的AssetManager,并通过反射调用addAssertPath,把这个完整的新资源包加入到AssetManager中。这样就得到一个含有所有新资源的AssetManager

2、找到所有值钱引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager

 public static void monkeyPatchExistingResources(Context context,
                                                    String externalResourceFile, Collection activities) {
        if (externalResourceFile == null) {
            return;
        }
        try {
            //反射一个新的   AssetManager
            AssetManager newAssetManager = (AssetManager) AssetManager.class
                    .getConstructor(new Class[0]).newInstance(new Object[0]);
           //反射 addAssetPath 添加新的资源包
            Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class});
            mAddAssetPath.setAccessible(true);
            if (((Integer) mAddAssetPath.invoke(newAssetManager,
                    new Object[]{externalResourceFile})).intValue() == 0) {
                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]);
            //反射得到Activity中AssetManager的引用处,全部换成刚新构建的AssetManager对象
            if (activities != null) {
                for (Activity activity : activities) {
                    Resources resources = activity.getResources();
                    try {
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    Resources.Theme theme = activity.getTheme();
                    try {
                        try {
                            Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            ma.set(theme, newAssetManager);
                        } catch (NoSuchFieldException ignore) {
                            Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
                            themeField.setAccessible(true);
                            Object impl = themeField.get(theme);
                            Field ma = impl.getClass().getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            ma.set(impl, newAssetManager);
                        }
                        Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
                        mt.setAccessible(true);
                        mt.set(activity, null);
                        Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);
                        mtm.setAccessible(true);
                        mtm.invoke(activity, new Object[0]);
                        Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);
                        mCreateTheme.setAccessible(true);
                        Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);
                        Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
                        mTheme.setAccessible(true);
                        mTheme.set(theme, internalTheme);
                    } catch (Throwable e) {
                        Log.e("InstantRun",
                                "Failed to update existing theme for activity "
                                        + activity, e);
                    }
                    pruneResourceCaches(resources);
                }
            }
            Collection 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  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  map = (HashMap) fMActiveResources.get(thread);
                references = map.values();
            }
            for (WeakReference 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) {
                        Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }
 

so修复原理

接口调用替换

sdk提供接口替换System默认加载so库的接口

SOPatchManger.loadLibrary(String libName)
替换
System.loadLibrary(String libName)

SOPatchManger.loadLibrary接口加载so库的时候优先尝试去加载sdk指定目录下补丁的so。若不存在,则再去加载安装apk目录下的so库

优点:不需要对不同sdk版本进行兼容,所以sdk版本都是System.loadLibrary这个接口

缺点:需要侵入业务代码,替换掉System默认加载so库的接口

反射注入

采取类似类修复反射注入方式,只要把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库而不是原来so库的目录,从而达到修复。

public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);

        for (NativeLibraryElement element : nativeLibraryPathElements) {
            String path = element.findNativeLibrary(fileName);

            if (path != null) {
                return path;
            }
        }

        return null;
    }

优点:不需侵入用户接口调用

缺点:需要做版本兼容控制,兼容性较差

使用热修复技术有哪些需要注意的问题?

版本管理

使用热修复技术后由于发布流程的变化,肯定也需求采用相应的分支管理进行控制。

通常移动开发的分支管理采用特性分支,如下:

分支 描述
master 主分支(只能merge,不能commit,设置权限),用于管理线上版本,及时设置对应Tag
dev 开发分支,每个新版本的研发根据版本号基于主分支创建,测试通过验证后,上线合入master分支
function X 功能分支,按需求设定。基于开发分支创建,完成功能开发后合入dev开发分支

接入热修复后,推荐可参考如下分支策略:

分支 描述
master 主分支(只能merge,不能commit,设置权限),用于管理线上版本,及时设置对应Tag(一般3位版本号)
hot_fix 热修复分支。基于master分支创建,修复紧急问题后,测试推送后,将hot_fix再合并到master分支。再次为master分支打tag。(一般4位版本号)
dev 开发分支,每个新版本的研发根据版本号基于主分支创建,测试通过验证后,上线合入master分支
function X 功能分支,按需求设定。基于开发分支创建,完成功能开发后合入dev开发分支

注意热修复分支的测试及发布流程应用正常版本流程一致,保证质量。

分发监控

目前主流的热修复方案,像Tinker及Sophix都会提供补丁的分发及监控。这也是我们选择热修复技术方案需要考虑的关键因素之一。毕竟为了保证线上版本的质量,分发控制及实时监测必不可少。

总结

Android热修复技术发展至今已经是百花齐放,各大厂也都推出了自己的技术框架。也有像阿里推出的《深入探索Android热修复技术原理》对热修复技术的深入解读。本文大部分总结也都参考这本经典。鉴于热修复技术的多种多样,所以才决定进行梳理,提供选择时的一些注意事项及参考建议,也加深自己对热修复技术的理解。总的来说,还是收获满满。

参考资料

AndFix源码

Nuwa源码

Tinker官方介绍

Android热补丁之Robust原理解析(一)

Android热修复技术原理详解(最新最全版本)

《深入探索Android热修复技术原理》

关于

欢迎关注我的个人公众号

微信搜索:一码一浮生,或者搜索公众号ID:life2code