热更新技术对比(美团Robust、微信Tinker、阿里AndFix /Sophix)

美团robust框架

  1. 先获取美团包名
adb shell pm list packages | findstr meituan
  1. 查找包名位置
adb shell pm path com.sankuai.meituan
  1. 拉取包名
adb pull 目标.apk  目标保存位置

使用jadx编译工具打开apk文件,随便打开一个类文件如下:

package com.meituan.hotel.android.hplus.diagnoseTool;

import com.meituan.android.paladin.Paladin;
import com.meituan.robust.ChangeQuickRedirect;
import com.meituan.robust.PatchProxy;

/* JADX INFO: loaded from: classes8.dex */
public final class a {
    /* JADX INFO: renamed from: a, reason: collision with root package name */
    public static a f78232a; // 原对象
    public static ChangeQuickRedirect changeQuickRedirect; // 热修复标记

    static {
        Paladin.record(1275720172492966010L);
    }

    public static synchronized a a() {
        Object[] objArr = new Object[0]; // 这里表示的是该方法的入参,因为这个方法没用入参,所以是空数组
        ChangeQuickRedirect changeQuickRedirect2 = changeQuickRedirect;
        if (PatchProxy.isSupport(objArr, null, changeQuickRedirect2, 5484360)) {
            return (a) PatchProxy.accessDispatch(objArr, null, changeQuickRedirect2, 5484360);
        }
        if (f78232a == null) {
            f78232a = new a();
        }
        return f78232a;
    }
}

因为开了混淆,所以代码逻辑比较难看明白,不过不重要,热更新的关键代码已经有了

        if (PatchProxy.isSupport(objArr, null, changeQuickRedirect2, 5484360)) {
            return (a) PatchProxy.accessDispatch(objArr, null, changeQuickRedirect2, 5484360);
        }

可以看到这个同步方法在执行的时候,会通过这个if判断,如果满足条件就会把当前的参数传递给PatchProxy去执行注入的钩子方法,以实现热修复拦截。


执行流程.png

其原始代码可能如下:

// 原始代码(开发者写的)一个简单的单例模式
public class Singleton {
    private static Singleton instance;
    
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

程序员只需要写正常的代码即可,Robust插件会在编译时自动为每个类添加上面例子中的changeQuickRedirect静态变量,并为每个方法的前面插入像例子中那样的if判断逻辑。
当App上线后发现有Bug,可以下发一个补丁包。你的App下载这个补丁包后,框架会通过DexClassLoader加载它,并利用反射技术,把changeQuickRedirect这个静态变量指向补丁包中实现了新逻辑的对象。这样,下次再执行到这个方法时,就会自动“绕道”去执行补丁里的新代码了。
由于不需要重新加载 Class,只需要替换一个静态变量的值。所以可以做到实时更新,不需要重启APP就可以生效。
其实最关键的就是,如何在运行期,修改changeQuickRedirect静态变量,和补丁方法的注入。

dex加载流程.png

具体代码可以看 PatchExecutorPatchManipulateImplPatchManipulateImpl负责加载补丁包, PatchExecutor负责创建DexClassLoader加载这个补丁包,这两个类都没有被混淆,可以放心查看😄。
DexClassLoader 可以加载外部存储的 JAR/APK/DEX 文件,这是 Android 热修复的基础能力。加载后,补丁包中的类就可以被访问了,可以通过反射修改对应类的字段信息。

    //PatchExecutor
    public boolean patchClass(Context context, Patch patch) {
        PatchesInfo patchesInfo;
        Object objNewInstance;
        ClassLoader patchClassLoader = PatchManager.getPatchClassLoader(patch);
        if (patchClassLoader == null) {
            try {
                patchClassLoader = new DexClassLoader(patch.getTempPath(), PatchManager.getCurrentProcessPatchCacheDir(context).getAbsolutePath(), null, PatchExecutor.class.getClassLoader());
            } catch (Throwable th) {
                // do something
            }
        }
        try {
            patch.getPatchesInfoImplClassFullName();
            // 使用DexClassLoader加载补丁修复方法列表
            patchesInfo = (PatchesInfo) patchClassLoader.loadClass(patch.getPatchesInfoImplClassFullName()).newInstance(); 
        } catch (Throwable th2) {
            // do something
        }
        List<PatchedClassInfo> patchedClassesInfo = patchesInfo.getPatchedClassesInfo();
        int i = 0;
        for (PatchedClassInfo patchedClassInfo : patchedClassesInfo) {
            String str = patchedClassInfo.patchedClassName;  // 目标类名
            String str2 = patchedClassInfo.patchClassName;   // 补丁类名
            if (!TextUtils.isEmpty(str) && !TextUtils.isEmpty(str2)) {
                try {
                    try {
                        Class<?> clsLoadClass = patchClassLoader.loadClass(str.trim());
                        Field field = clsLoadClass.getField(Constants.INSERT_FIELD_NAME); // 尝试直接获取名为 "changeQuickRedirect" 的字段
                        if (field == null) {
                            // 这里是为了避免直接获取字段获取不到(可能因混淆等原因导致),再遍历fields 通过ChangeQuickRedirect类型进行匹配。
                            Field[] declaredFields = clsLoadClass.getDeclaredFields();
                            while (true) {
                                Field field2 = declaredFields[i2];
                                if (TextUtils.equals(field2.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field2.getDeclaringClass().getCanonicalName(), clsLoadClass.getCanonicalName())) {
                                    field = field2;
                                    break;
                                }
                            }
                        }
                        if (field == null) {
                            // do something
                        } else {
                            try {
                                objNewInstance = patchClassLoader.loadClass(str2).newInstance();
                                field.setAccessible(true);
                            } catch (Throwable th4) {
                                th = th4;
                            }
                            try {
                                // 关键赋值!将补丁实例赋值给目标类的静态 changeQuickRedirect 字段
                                // 第一个参数 null 表示这是静态字段(不需要传入具体对象实例)
                                field.set(null, objNewInstance);
                            } catch (Throwable th5) {
                            }
                        }
                    } catch (Throwable th6) {
                    }
                } catch (ClassNotFoundException e2) {
                }
            } else {
            }
        }
        return i == patchedClassesInfo.size();
    }

到此,补丁类已经加载完毕,再看下调用执行时时怎么执行的。

    /**
     * 类PatchProxy
     * @param objArr 方法入参
     * @param obj 原对象指针,传入null表示这个是静态方法
     * @param changeQuickRedirect 补丁代理对象
     * @param i 被替换的方法ID,使用方法ID定位具体的方法,避免混淆导致方法名称对应不上的问题
     * @return 是否支持
     */
    public static boolean isSupport(Object[] objArr, Object obj, ChangeQuickRedirect changeQuickRedirect, int i) {
        boolean z;
        if (changeQuickRedirect == null) {
            return false;
        }
        if (obj == null) {
            z = true;
        } else {
            z = false;
        }
        String classMethod = getClassMethod(z, String.valueOf(i));
        if (isEmpty(classMethod)) {
            return false;
        }
        try {
            return changeQuickRedirect.isSupport(classMethod, getObjects(objArr, obj, z));  // 根据入参判断是否有适合的方法执行
        } catch (Throwable unused) {
            return false;
        }
    }

具体执行则是最终调用PatchProxy.accessDispatch -->changeQuickRedirect.accessDispatch

public interface ChangeQuickRedirect {
    Object accessDispatch(String str, Object[] objArr);

    boolean isSupport(String str, Object[] objArr);
}

到这里其实基本就明白了美团Robust框架是如何基于方法插桩实现热修复的,通过插桩插件在编译期向每一个方法插入一段拦截代码。
其实这种方案的优缺点也很明显。

  • 优点是实时生效,无需重启,且基于方法,框架执行的时候自动插桩,兼容性高。
  • 缺点则是增加了包apk的体积(虽然支持根据配置自行选择哪些方法需要插桩,但是写代码的时候一般都没人能敢保证不会有bug吧😁),由于基本每个方法都会插桩,所以导致程序运行时都会执行判断补丁的逻辑,导致运行有一定的性能损耗,且不支持资源文件的替换。

微信Tinker热更新框架

相比上面的robust框架,Tinker 的核心思路是:不直接在原 Dex 上打补丁,而是将基准 Dex 与补丁包合成一个完整的新 Dex,然后替换旧的。


Tinker.png

同样的,先pull下微信的apk文件

adb shell pm list packages | grep tencent
adb shell pm path com.tencent.mm
adb pull /data/app/com.tencent.mm-xxx/base.apk  E:\apks\wechat\

tinker框架的相关核心代码都在com.tencent.tinker.loader目录下。

package com.tencent.mm.app;

import android.content.Context;
import com.tencent.tinker.loader.app.TinkerApplication;

/* JADX INFO: loaded from: classes9.dex */
public class Application extends TinkerApplication {
    private static final String TINKER_LOADER_ENTRY_CLASSNAME = "com.tencent.tinker.loader.TinkerLoader";
    private static final String WECHAT_APPLICATION_LIKE_CLASSNAME = "com.tencent.mm.app.MMApplicationLike";
    private boolean mIsAttachBaseContextDone;
    private final boolean[] mIsDisallowedToCallGetBaseContextInAttachBaseContext;

    public Application() {
        //    7 = 1 (DEX) + 2 (RESOURCE) + 4 (LIBRARY),表示同时支持 Dex、资源和 So 库的热修复。
        //     WECHAT_APPLICATION_LIKE_CLASSNAME 指定了业务代理类 MMApplicationLike,Tinker 会在合适的时机(如 attachBaseContext、onCreate)回调这个类中的对应方法。
        //    指定了 Tinker 的加载器,这是执行补丁加载任务的入口。
        //    false:表示在加载补丁时,不进行签名校验, 第一个true :开启安全模式。如果补丁加载导致应用连续崩溃,安全模式会自动禁用补丁。第二个true:开启补丁加载结果的检查,如果加载失败会记录相关信息,便于排查问题
        super(7, WECHAT_APPLICATION_LIKE_CLASSNAME, TINKER_LOADER_ENTRY_CLASSNAME, false, true, true);
        this.mIsAttachBaseContextDone = false;
        this.mIsDisallowedToCallGetBaseContextInAttachBaseContext = new boolean[]{false};
    }

    public Context _doNotCallThisMethodUnlessYouNeedToGetBaseContextForHacking() {
        return super.getBaseContext();
    }

    @Override // com.tencent.tinker.loader.app.TinkerApplication, android.content.ContextWrapper
    public void attachBaseContext(Context context) {
        // // 调用父类,触发 Tinker 的补丁加载流程
        super.attachBaseContext(context);
        this.mIsAttachBaseContextDone = true;
    }

    @Override // android.content.ContextWrapper, android.content.Context
    public Context getApplicationContext() {
        return this;
    }

    @Override // com.tencent.tinker.loader.app.TinkerApplication, android.content.ContextWrapper
    public Context getBaseContext() {
        if (this.mIsAttachBaseContextDone || !isDisallowedToCallGetBaseContextInAttachBaseContext()) {
            return super.getBaseContext();
        }
        throw new UnsupportedOperationException("please don't call app.getBaseContext(), use app itself directly would be fine in most cases.");
    }

    public boolean isDisallowedToCallGetBaseContextInAttachBaseContext() {
        boolean z16;
        synchronized (this.mIsDisallowedToCallGetBaseContextInAttachBaseContext) {
            z16 = this.mIsDisallowedToCallGetBaseContextInAttachBaseContext[0];
        }
        return z16;
    }

    public void markDisallowToCallGetBaseContextInAttachBaseContext() {
        synchronized (this.mIsDisallowedToCallGetBaseContextInAttachBaseContext) {
            this.mIsDisallowedToCallGetBaseContextInAttachBaseContext[0] = true;
        }
    }
}

这是微信的application入口,他其实做的事很少,完全将热更新的操作交给了父类TinkerApplication

  • TINKER_LOADER_ENTRY_CLASSNAME:这是 Tinker 框架的入口。父类会通过反射创建这个类的实例,来执行具体的补丁加载任务。
  • WECHAT_APPLICATION_LIKE_CLASSNAME :这是微信自己真正的“Application”逻辑所在。因为继承了 TinkerApplication 的类不能被热修复,所以微信将所有业务初始化代码都迁移到了 MMApplicationLike 这个代理类中。当补丁加载完成后,Tinker 框架会通过回调,让 MMApplicationLike 中的新逻辑生效。
public abstract class TinkerApplication extends Application {
    private void loadTinker() {
        try {
            Class<?> cls = Class.forName(this.loaderClassName, false, TinkerApplication.class.getClassLoader());
            this.tinkerResultIntent = (Intent) cls.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class).invoke(cls.getConstructor(new Class[0]).newInstance(new Object[0]), this);
        } catch (Throwable th6) {
            Intent intent = new Intent();
            this.tinkerResultIntent = intent;
            ShareIntentUtil.setIntentReturnCode(intent, -20);
            this.tinkerResultIntent.putExtra("intent_patch_exception", th6);
        }
    }

    @Override // android.content.ContextWrapper
    public void attachBaseContext(Context context) {
        super.attachBaseContext(context); 
        long jElapsedRealtime = SystemClock.elapsedRealtime();
        long jCurrentTimeMillis = System.currentTimeMillis();
        Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));
        onBaseContextAttached(context, jElapsedRealtime, jCurrentTimeMillis); // 继续调用具体的加载方法
    }

    public void onBaseContextAttached(Context context, long j16, long j17) {
        try {
            loadTinker();
            this.mCurrentClassLoader = context.getClassLoader();
            Handler handlerCreateInlineFence = createInlineFence(this, this.tinkerFlags, this.delegateClassName, this.tinkerLoadVerifyFlag, j16, j17, this.tinkerResultIntent);   // 创建WECHAT_APPLICATION_LIKE_CLASSNAME的代理,用于生命周期的各事件回调触发
            this.mInlineFence = handlerCreateInlineFence;
            TinkerInlineFenceAction.callOnBaseContextAttached(handlerCreateInlineFence, context);
            if (this.useSafeMode) {
                ShareTinkerInternals.setSafeModeCount(this, 0);
            }
        } catch (TinkerRuntimeException e16) {
            throw e16;
        } catch (Throwable th6) {
            throw new TinkerRuntimeException(th6.getMessage(), th6);
        }
    }

    private void loadTinker() {
        try {
            // loaderClassName对应TINKER_LOADER_ENTRY_CLASSNAME
            Class<?> cls = Class.forName(this.loaderClassName, false, TinkerApplication.class.getClassLoader());
            // 调用TINKER_LOADER_ENTRY_CLASSNAME定义的Loader类中的tryload方法加载补丁
            this.tinkerResultIntent = (Intent) cls.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class).invoke(cls.getConstructor(new Class[0]).newInstance(new Object[0]), this);
        } catch (Throwable th6) {
            Intent intent = new Intent();
            this.tinkerResultIntent = intent;
            ShareIntentUtil.setIntentReturnCode(intent, -20);
            this.tinkerResultIntent.putExtra("intent_patch_exception", th6);
        }
    }

    private Handler createInlineFence(Application application, int i16, String str, boolean z16, long j16, long j17, Intent intent) {
        try {
            Class<?> cls = Class.forName(str, false, this.mCurrentClassLoader);
            Class<?> cls2 = Long.TYPE;
            Object objNewInstance = cls.getConstructor(Application.class, Integer.TYPE, Boolean.TYPE, cls2, cls2, Intent.class).newInstance(application, Integer.valueOf(i16), Boolean.valueOf(z16), Long.valueOf(j16), Long.valueOf(j17), intent);
            Constructor<?> constructor = Class.forName("com.tencent.tinker.entry.TinkerApplicationInlineFence", false, this.mCurrentClassLoader).getConstructor(Class.forName("com.tencent.tinker.entry.ApplicationLike", false, this.mCurrentClassLoader));
            constructor.setAccessible(true);
            return (Handler) constructor.newInstance(objNewInstance);
        } catch (Throwable th6) {
            throw new TinkerRuntimeException("createInlineFence failed", th6);
        }
    }
    @Override // android.content.ContextWrapper, android.content.Context
    public Resources getResources() {
        Resources resources = super.getResources();
        Handler handler = this.mInlineFence;
        // 防御性判空,返回tinker加载的替换资源
        return handler == null ? resources : TinkerInlineFenceAction.callGetResources(handler, resources);
    }

    ······
}

核心方法就是TINKER_LOADER_ENTRY_CLASSNAME定义的Loader类中的tryload方法,所以加载的核心代码还得继续往下扒。

public class TinkerLoader extends AbstractTinkerLoader {
    @Override // com.tencent.tinker.loader.AbstractTinkerLoader
    public Intent tryLoad(TinkerApplication tinkerApplication) {
        Guard guard;
        ShareTinkerLog.d(TAG, "tryLoad test test", new Object[0]);
        Intent intent = new Intent();
        Guard[] guardArr = new Guard[1];
        long jElapsedRealtime = SystemClock.elapsedRealtime();
        tryLoadPatchFilesInternal(tinkerApplication, intent, guardArr);
        ShareIntentUtil.setIntentPatchCostTime(intent, SystemClock.elapsedRealtime() - jElapsedRealtime);
        if (ShareIntentUtil.getIntentReturnCode(intent) != 0 && (guard = guardArr[0]) != null) {
            guard.close();
        }
        sProcessGuardRef = guardArr[0];
        if (ShareTinkerInternals.isInMainProcess(tinkerApplication)) {
            tryCleanObsoletePatches(tinkerApplication);
        }
        return intent;
    }
       private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
        // 检查是否在 patch 进程(避免递归加载)
        if (ShareTinkerInternals.isInPatchProcess(app)) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
            return;
        }
        // ========== 1. 获取补丁目录 ==========
        File patchDirectory = SharePatchFileUtil.getPatchDirectory(app);
        if (patchDirectory == null || !patchDirectory.exists()) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
            return;
        }
        // ========== 2. 读取 patch.info ==========
        File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory.getAbsolutePath());
        File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory.getAbsolutePath());
        SharePatchInfo patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
        if (patchInfo == null) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
            return;
        }
        String oldVersion = patchInfo.oldVersion;  // 当前已加载的补丁版本
        String newVersion = patchInfo.newVersion;  // 新补丁版本
        boolean isMainProcess = ShareTinkerInternals.isInMainProcess(app);
        boolean versionChanged = !oldVersion.equals(newVersion);
        // ========== 3. 版本决策 ==========
        String versionToLoad;
        if (versionChanged && isMainProcess) {
            versionToLoad = newVersion;  // 主进程加载新版本
        } else {
            versionToLoad = oldVersion;   // 非主进程继续用旧版本
        }
        // ========== 4. 定位补丁文件 ==========
        String patchVersionDir = patchDirectory.getAbsolutePath() + "/" +
                SharePatchFileUtil.getPatchVersionDirectory(versionToLoad);
        File patchVersionFile = new File(patchVersionDir, "patch-" + versionToLoad + ".apk");
        if (!patchVersionFile.exists()) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_NOT_EXIST);
            return;
        }
        // ========== 5. 分发到具体 Loader ==========
        // Dex 补丁校验与加载
        if (ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlags)) {
            boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDir, securityCheck,
                    patchInfo.oatDir, resultIntent);
            if (!dexCheck) {
                ShareTinkerLog.w(TAG, "tryLoadPatchFiles: dex check fail");
                return;
            }
        }
        // So 补丁校验
        if (ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlags)) {
        }
        // 资源补丁校验
        if (ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlags)) {
        }
        // arkHot校验
        if (ShareTinkerInternals.isTinkerEnabledForArkHot(tinkerFlags)) {
        }
        // ========== 6. 安全模式检查 ==========
        if (!checkSafeModeCount(app)) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles: safe mode count check fail");
            if (isMainProcess) {
                // 主进程:清除补丁并自杀
                SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, new SharePatchInfo(), patchInfoLockFile);
                ShareTinkerInternals.killProcessExceptMain(app);
            }
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_SAFE_MODE_COUNT_FAIL);
            return;
        }
        // ========== 7. 最终加载:修改 ClassLoader ==========
        //加载 Dex 补丁(修改 dexElements)
        if (ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlags)) {
            // 核心代码,加载补丁代码
            boolean loadDex = TinkerDexLoader.loadTinkerJars(app, patchVersionDir, patchInfo.oatDir,
                    resultIntent, isSystemOTA, patchInfo.fingerPrint);
            if (!loadDex) {
                ShareTinkerLog.w(TAG, "tryLoadPatchFiles: load dex fail");
                return;
            }
            // 更新 patch.info 中的 oatDir
            patchInfo.oatDir = TinkerDexLoader.getLastOatDir();
            SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
        }
          //  加载资源补丁(重建 AssetManager)
         if (ShareTinkerInternals.isTinkerEnabledForResources(tinkerFlags)) {}
        //  加载arkHot
        if (ShareTinkerInternals.isTinkerEnabledForArkHot(tinkerFlags)) {}
        // ========== 8. 清理过期补丁 ==========
        if (isMainProcess) {
            tryCleanObsoletePatches(app);
        }
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
        ShareTinkerLog.i(TAG, "tryLoadPatchFiles: load end, ok!");
    }
}

这个tryLoadPatchFilesInternal方法过长,可能jadx等反编译工具无法解析,这里可以参考tinker开源项目中的源码,前面一大长代码都是资源校验与版本控制,最核心的代码其实就这行TinkerDexLoader.loadTinkerJars,类似的还有TinkerArkHotLoaderTinkerResourceLoader等。

//TinkerDexLoader
/**
 * Tinker Dex 补丁加载的核心方法(精简版)
 * 只保留关键步骤,去掉日志和细节处理
 */
public static boolean loadTinkerJars(TinkerApplication tinkerApplication, String patchDir, 
                                     String oatDir, Intent intent, boolean isSystemOTA, boolean isInterpretMode) {
    
    // ==================== 第一步:获取系统的 ClassLoader ====================
    ClassLoader classLoader = TinkerDexLoader.class.getClassLoader();
    // ==================== 第二步:收集所有补丁 Dex 文件 ====================
    String dexPath = patchDir + "/dex/";
    ArrayList<File> patchDexFiles = new ArrayList<>();
    // 2.1 收集普通补丁 Dex
    for (ShareDexDiffPatchInfo info : LOAD_DEX_LIST) {
            // do something
            patchDexFiles.add(dexFile);
    }
    // 2.2 收集 classN.dex(ART 环境需要)
    if (isVmArt && !classNDexInfo.isEmpty()) {
    // do something
        patchDexFiles.add(classNDex);
    }
    // ==================== 第三步:确定 OAT 优化目录 ====================
    File optimizeDir;
    if (isSystemOTA) {
        // 系统 OTA 后需要重新优化 Dex
        try {
            String instructionSet = ShareTinkerInternals.getCurrentInstructionSet();
            deleteOutOfDateOATFile(patchDir);
            optimizeDir = new File(patchDir + "/interpet");
            // 并行优化所有 Dex,生成 OAT 文件
            TinkerDexOptimizer.optimizeAll(tinkerApplication, patchDexFiles, optimizeDir, true, instructionSet, ...);
        } catch (Throwable t) {
        }
    } else {
        optimizeDir = new File(patchDir + "/" + oatDir);
    }
    // ==================== 第四步:将补丁 Dex 注入 ClassLoader ====================
    try {
        // 通过反射修改 ClassLoader 的 dexElements 数组
        // 将补丁类与当前的类数组合并,并且保证补丁 Dex 插入到数组最前面,实现优先加载
        SystemClassLoaderAdder.installDexes(tinkerApplication, classLoader, optimizeDir, patchDexFiles,  isInterpretMode, ...);
        return true;
    } catch (Throwable t) {
    }
}

// SystemClassLoaderAdder.installDexes 核心逻辑
public static void installDexes(Application app, ClassLoader loader, File optimizeDir,
                                List<File> dexFiles, boolean isInterpretMode, ...) {
    // 1. 获取 PathClassLoader 的 pathList 字段
    //    PathClassLoader 内部有一个 DexPathList 类型的 pathList 字段
    Object pathList = getPathList(loader);
    // 2. 获取原有的 dexElements 数组
    //    DexPathList 内部有一个 Element[] 类型的 dexElements 数组
    Object[] originalElements = getDexElements(pathList);
    // 3. 将补丁 Dex 文件转换为 Element 对象数组
    //    每个 Dex 文件对应一个 Element 对象
    Object[] patchElements = makeDexElements(dexFiles, optimizeDir);
    // 4. 关键:合并数组,补丁在前,原 Dex 在后
    Object[] newElements = combineArray(patchElements, originalElements);
    // 5. 将新数组设置回 pathList
    setDexElements(pathList, newElements);
}

这个方法核心逻辑其实就是拿到 ClassLoader → 校验补丁 Dex → 准备优化目录 → 反射修改 dexElements 让补丁优先加载。但是这里其实只是告诉系统后续加载顺序,会优先加载补丁类,但是如果这个类已经被加载了,其实后续是不会再去加载的,所以tinker的热更新需要重启app才能完全生效。


调用补丁类.png
  • installDexes 只是修改了查找路径,没有加载任何补丁类
  • 补丁类是在第一次被使用时,由系统 ClassLoader 按新的 dexElements 顺序加载的
  • 如果一个类在 installDexes 之前已经被加载了,那么它不会被重新加载,补丁对它不生效(这是 Tinker 需要重启的原因)
时机 类加载状态 补丁效果
当前进程 很多类已经加载到内存 已加载的类无法被替换
下次冷启动 所有类重新加载,ClassLoader 已修改 ✅ 补丁中的类被优先加载
类加载插队逻辑.png

相比robust的字节码插桩技术,tinker是通过差分合并dex文件,修改classLoader加载顺序保证热更新,优点是功能全面,支持代码、资源、So库的完整修复,可新增类、字段、方法,且支持与原类同名替换,运行时性能损耗影响更小。但是缺点也很明显,新增类会导致补丁包体积较大(补丁包较大,但是原始apk体积更占优),且需要冷启动触发重启类加载才能真正完全生效。

阿里热更新AndFix /Sophix

the same, pull下支付宝的apk

adb shell pm list packages | grep Alipay
adb shell pm path com.eg.android.AlipayGphone
adb pull /data/app/XXX.AlipayGphone-xxx/base.apk  E:\apks\alipay\

通过查看Robust和Tinker源码,可以看到其核心思想分别是插桩拦截问题方法和修改类加载器中的class顺序拦截问题代码类,AndFix的思想则是直接替换native层的虚拟机中的方法。
AndFix开源项目
Sophix式对AndFix的技术升级与整合,本来想反编译支付宝和淘宝apk找源代码看的,结果都被混淆了,搜不到Sophix相关的类。没招了,只能找网上的开源项目和问问大模型学习了。不过支付宝还保留了AndFix的相关代码(在com.alipay.euler.andfix下,不过部分代码被混淆了,建议还是直接看开源源码)。
Sophix与Tinker或Robust的最大不同在于,它采用了一套融合方案:在大多数情况下,它能实现即时生效(热部署)(整合了AndFix),而当遇到无法即时修复的情况时,会自动降级为重启生效(冷部署)(在 App 下次启动时,通过修改 ClassLoader 的查找顺序,让系统优先加载补丁中的类,从而实现对原有 Bug 类的覆盖。和tinker思想基本一致)。
所以这里主要研究下AndFix是如何做到native层的虚拟机中的方法的,在ART虚拟机中,每个Java方法都对应一个底层的ArtMethod结构体(ART一般不被认为是一个标准的JVM实现)。
AndFix通过JNI在Native层获取新旧方法的ArtMethod指针,然后逐个字段地进行替换

// AndFixManager 找到对应的补丁文件
public synchronized void fix(String patchPath) {
    fix(new File(patchPath), mContext.getClassLoader(), null);
}

// 对加载出的类遍历,判断是否需要进行替换
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
    Method[] methods = clazz.getDeclaredMethods();
    MethodReplace methodReplace;
    String clz;
    String meth;
    for (Method method : methods) {
        methodReplace = method.getAnnotation(MethodReplace.class);
        if (methodReplace == null)
            continue;
        clz = methodReplace.clazz();
        meth = methodReplace.method();
        if (!isEmpty(clz) && !isEmpty(meth)) {
            replaceMethod(classLoader, clz, meth, method);
        }
    }
}

// Java层入口:AndFix.java
private static native void replaceMethod(Method src, Method dest);

// Native层:andfix.cpp
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) {
    if (isArt) {
        art_replaceMethod(env, src, dest);  // ART环境
    } else {
        dalvik_replaceMethod(env, src, dest); // Dalvik环境
    }
}

由于不同Android版本的ArtMethod结构不同,AndFix需要针对每个版本编写单独的替换函数
// art_method_replace.cpp
extern void art_replaceMethod(JNIEnv* env, jobject src, jobject dest) {
    if (apilevel > 23) {
        replace_7_0(env, src, dest);      // Android 7.0
    } else if (apilevel > 22) {
        replace_6_0(env, src, dest);      // Android 6.0
    } else if (apilevel > 21) {
        replace_5_1(env, src, dest);      // Android 5.1
    } else if (apilevel > 19) {
        replace_5_0(env, src, dest);      // Android 5.0
    } else {
        replace_4_4(env, src, dest);      // Android 4.4
    }
}

void replace_7_0(JNIEnv* env, jobject src, jobject dest) {
    // 获取ArtMethod指针
    art::mirror::ArtMethod* smeth =  (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
    art::mirror::ArtMethod* dmeth =  (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
    
    // 逐个字段替换(需要知道每个Android版本的结构)
    smeth->declaring_class_ = dmeth->declaring_class_;
    smeth->access_flags_ = dmeth->access_flags_;
    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->hotness_count_ = dmeth->hotness_count_;
    smeth->ptr_sized_fields_.dex_cache_resolved_methods_ = 
        dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
    smeth->ptr_sized_fields_.dex_cache_resolved_types_ = 
        dmeth->ptr_sized_fields_.dex_cache_resolved_types_ ;

    // 替换方法入口(最关键的一步)
    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_;
}

从上面的代码可以看出,AndFix核心思想就是替换方法结构体的每一个参数, 但是每一个安卓版本得ART中的方法实现都不一样,这就需要为每个 Android 版本编写不同的替换逻辑,而且不同厂商的手机可能会自己修改ROM,所以很容易引发兼容性问题。
优化:Sophix取消了参数一个一个替换的逻辑,采用整体memcpy替换 + 动态计算结构体大小的方式。

由于不同厂商、不同版本的ArtMethod结构体大小不同,Sophix采用了一种巧妙的运行时动态计算方式:
// 1. 在Java层构造一个只有两个空方法的类(Sophix源码)
public class NativeStructsModel {
    final public static void f1() {}
    final public static void f2() {}
}
// 2. 在Native层获取这两个方法的地址,计算差值
JNIEXPORT jlong JNICALL Java_com_taobao_sophix_NativeBridge_getArtMethodSize(JNIEnv* env, jclass clazz) {
    jclass modelClass = env->FindClass("com/taobao/sophix/NativeStructsModel");
    // 获取两个空方法的jmethodID
    jmethodID mid1 = env->GetStaticMethodID(modelClass, "f1", "()V");
    jmethodID mid2 = env->GetStaticMethodID(modelClass, "f2", "()V");
    // 将jmethodID转换为ArtMethod指针
    ArtMethod* method1 = (ArtMethod*) mid1;
    ArtMethod* method2 = (ArtMethod*) mid2;
    // 关键:相邻方法的地址差值 = ArtMethod的大小
    size_t methodSize = (size_t)method2 - (size_t)method1;
    return (jlong) methodSize;
}
// Sophix的Native层代码
JNIEXPORT void JNICALL Java_com_taobao_sophix_NativeBridge_replaceMethod(JNIEnv* env, jclass clazz, jobject srcMethod, jobject destMethod, jlong artMethodSize) {
    // 1. 获取ArtMethod指针
    ArtMethod* src = (ArtMethod*) env->FromReflectedMethod(srcMethod);
    ArtMethod* dest = (ArtMethod*) env->FromReflectedMethod(destMethod);
    // 2. 获取ArtMethod的实际大小(运行时传入,不依赖编译时定义)
    size_t methodSize = (size_t) artMethodSize;
    // 3. 保存旧方法的访问标志(用于后续处理)
    uint32_t oldAccessFlags = src->access_flags_;
    // 4.  整体内存替换
    memcpy(src, dest, methodSize);
    // 5. 恢复关键字段(保持类的结构完整性)
    src->declaring_class_ = dest->declaring_class_;
    src->access_flags_ = oldAccessFlags;
    // 6. 处理特殊方法(如构造方法、静态初始化块)
    if (isConstructor(src) || isClassInitializer(src)) {
        // 特殊处理逻辑...
    }
}

通过直接修改虚拟机中的C++方法,达到实时更新的目的,只能说操作很6。

总结

上面浅显地介绍了美团robust、微信tinker、阿里的AndFix和Sophix实现原理,我们总结一下他们的优缺点。
1、 核心原理对比

对比维度 Robust (美团) Tinker (微信) Sophix (阿里)
底层技术 编译期字节码插桩 Dex差分 + ClassLoader替换 C++底层替换 + 类加载混合
核心机制 每个方法入口插入 if (changeQuickRedirect != null) 判断 通过反射修改 dexElements 数组,将补丁 Dex 前置 小改动 Native 层替换 ArtMethod;大改动冷启动类加载
生效方式 即时生效 冷启动生效(需重启 App) 智能选择:小改动即时生效,大改动冷启动生效
修复粒度 方法级 类/DEX 级 方法级 + 类级

2、 优缺点对比

方案 优点 缺点
Robust (美团) ✅ 即时生效:无需重启,用户无感知
✅ 补丁包极小:只包含变更的方法代码
✅ 兼容性极高:纯 Java 实现,不受系统版本影响
✅ 成功率极高:官方宣称 99.99%
✅ 接入简单:Gradle 插件自动插桩
❌ 增加 APK 体积:每个方法插入约 17 字节代码
❌ 功能有限:不支持资源和 So 库修复
❌ 运行时性能损耗:每个方法多一次 if 判断
❌ 不支持类结构变更:无法新增类/字段/方法
❌ 影响 ProGuard 内联优化
Tinker (微信) ✅ 功能最全面:支持代码、资源、So 库修复
✅ 支持类结构变更:可新增类、字段、方法
✅ 兼容性高:基于官方 Dex 方案,全版本适配
✅ 开发透明:无需修改业务代码
✅ 经过大规模验证:微信数亿设备验证
❌ 需要重启生效:用户体验较差
❌ 补丁包较大:类级替换,差量包相对较大
❌ 合成内存开销大:Dex 合并有 OOM 风险
❌ 不支持 AndroidManifest 修改
❌ 接入成本较高:需要配置 Gradle 插件
Sophix (阿里) ✅ 智能生效:小改动即时生效,大改动冷启动
✅ 功能全面:支持代码、资源、So 库修复
✅ 兼容性极高:memcpy 整体替换,无视 ROM 差异
✅ 补丁包小:差量算法优化
✅ 支持类结构变更:可新增类/字段/方法
❌ 商业产品:核心功能需付费(阿里云 EMAS)
❌ 接入依赖阿里云:需要配置云端服务
❌ 闭源:核心 Native 代码未开源
❌ 社区资料较少:主要依赖官方文档
❌ 即时生效场景有限制:复杂改动需重启
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容