从入门到精通,一文全解Android热修复技术

从入门到精通,一文全解Android热修复技术

前言

热修复技术是当下Android开发中比较高级和热门的知识点,是中级开发人员通向高级开发中必须掌握的技能。同时目前Android业内,热修复技术也是百花齐放,各大厂都推出了自己的热修复方案,使用的技术方案也各有所异,当然各个方案也都存在各自的局限性。希望通过本文的梳理阐述,了解这些热修复方案的对比及实现原理,掌握热修复技术的本质,同时也能应用实践到实际项目中去,帮助大家学以致用**(文末有学习笔记分享)。**

什么是热修复

简单来讲,为了修复线上问题而提出的修补方案,程序修补过程无需重新发版!

在这里插入图片描述

正常版本开发与热修复开发流程对比

为什么要学习热修复

**在正常软件开发流程中,线下开发->上线->发现bug->紧急修复上线。**不过对于这种方式代价太大,而且永远避免不了面临如下几个问题:

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

**而相对比之下,热修复的开发流程就显得更加灵活,无需重新发版,实时高效热修复,无需下载新的应用,代价小,最重要的是及时的修复了bug。**而且随着热修复技术的发展,现在不仅可以修复代码,同时还可以修复资源文件及SO库。

在这里插入图片描述

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

文章开篇就说了现在各大厂都推出了自己的热修复方案,那么我们到底该如何去选择一套适合自己的热修复技术去学习呢?接下来我将从现在主流热修复的方案对比来给予你答案。

国内主流热修复技术方案

1、阿里系

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

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

2、腾讯系

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

3、其他

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

各热修复方案对比

在这里插入图片描述

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

  1. 项目需求
  • 只需要简单的方法级别Bug修复?
  • 需要资源及so库的修复?
  • 对平台兼容性要求及成功率要求?
  • 有需求对分发进行控制,对监控数据进行统计,补丁包进行管理?
  • 公司资源是否支持商业付费?
  1. 学习及使用成本
  • 集成难度
  • 代码侵入性
  • 调试维护
  1. 选择大厂
  • 技术性能有保障
  • 有专人维护
  • 热度高,开源社区活跃
  1. 如果考虑付费,推荐选择阿里的Sophix,Sophix是综合优化的产物,功能完善、开发简单透明、提供分发及监控管理。如果不考虑付费,只需支持方法级别的Bug修复,不支持资源及so,推荐使用Robust。如果考虑需要同时支持资源及so,推荐使用Tinker。最后如果公司综合实力强,可考虑自研,灵活性及可控制最强。

热修复技术方案原理

技术分类

在这里插入图片描述

image

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的源码来进行分析。
首先看一下打基础包是插入的代码逻辑,如下:

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来寻找类的。所以会优先查找到修复的类。从而达到修复的效果。

在这里插入图片描述

下面使用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自研了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,设置权限),用于管理线上版本,及时设置对应Tagdev开发分支,每个新版本的研发根据版本号基于主分支创建,测试通过验证后,上线合入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都会提供补丁的分发及监控。这也是我们选择热修复技术方案需要考虑的关键因素之一。毕竟为了保证线上版本的质量,分发控制及实时监测必不可少。

最后

想要深入了解热修复,需要了解类加载机制,Instant Run,multidex以及java底层实现细节,JNI,AAPT和虚拟机的知识,需要庞大的知识贮备才能进行深入理解,当然Android Framwork的实现细节是非常重要的。熟悉热修复的原理有助于我们提供自己的编程水平,提升自己解决问题的能力,最后热修复不是简单的客户端SDK,它还包含了安全机制和服务端的控制逻辑,整条链路也不是短时间可以快速完成的。

所以为了方便朋友们更直观快速的学习掌握Android热修复技术,我这里收集整理一套视频+电子书的热修复系列学习资料。视频教程为爱奇艺高级工程师Lance老师主讲,以Qzone热修复实战项目为例,深入浅出的全方位讲解热修复技术。电子书是来源于阿里的《深入探索Android热修复技术原理》对热修复技术有很深入解读。

由于篇幅原因,这里只做一些截图展示,需要完整资料的朋友,可以在点赞+评论后,后台私信我免费获取!

在这里插入图片描述

热修复学习视频

在这里插入图片描述

热修复视频内容

在这里插入图片描述

深入探索Android热修复技术原理电子书

在这里插入图片描述

热修复技术原理电子书内容

需要完整资料的朋友,可以在点赞+评论后,后台私信我免费获取!

----《深入探索Android修复技术原理》高清完整版PDF---- 2017年6月,阿里巴巴手淘技术团队推出了史上首个非侵入式移动更新解决方案——Sophix。在Android修复的三大领域:代码修复、资源修复、SO修复方面,以及方案的安全性和易用性方面,Sophix都做到了业界领先。 《深入探索Android修复技术原理》从阿里Sophix方案开发过程入手权威解读,分享了阿里巴巴手淘技术团队对系统底层的原性发现,是业界首部全方位完整介绍修复原理的书籍。 阿里技术大牛联袂推荐 自 2014 年至今,手淘定义和引领了业界 Android 组件化和修复技术风潮,至于后来者 Instant App 或多或少也受了国内技术风气影响。今天看到团队同学将这块技术认真系统化整理成书,非常欣喜。在这本书里,既能看到对修复技术风潮的发展历史系统深入总结,看到国内程序员在Android系统级技术持续突破上的不懈努力,更看到国内程序员坚持打造世界级优秀专业移动技术产品的雄心壮志! 手机淘宝基础平台部负责人,阿里巴巴资深技术专家 吴天华(天施) 业内少有的讲解 Android 修复的深度书籍,对于原理、代码讲解得非常的清晰和深入,值得 Android 工程师研读。 手机淘资深专家,倪生华(玄黎) 应用修复是一项略带神秘而又颇具争议的技术,但是它的确赋予应用开发者“驾着飞机修引擎”的能力。本书从 Android 应用修复技术的原理及代码实现、多种方案进行比较的角度,系统化地阐述了 Android 平台上的应用修复技术。对 Android 应用修复有好奇心的技术人员,这本专题书不容错过。 计算机技术领域著名作家,阿里巴巴飞猪事业部首席架构师 潘爱民
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值