主頁 > 移動端開發 > 進階高工必備技能:Android熱修復技術全決議!(附視頻+電子書分享)

進階高工必備技能:Android熱修復技術全決議!(附視頻+電子書分享)

2020-12-09 14:38:17 移動端開發

前言

熱修復技術是當下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,設定權限),用于管理線上版本,及時設定對應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都會提供補丁的分發及監控,這也是我們選擇熱修復技術方案需要考慮的關鍵因素之一,畢竟為了保證線上版本的質量,分發控制及實時監測必不可少,

最后

想要深入了解熱修復,需要了解類加載機制,Instant Run,multidex以及java底層實作細節,JNI,AAPT和虛擬機的知識,需要龐大的知識貯備才能進行深入理解,當然Android Framwork的實作細節是非常重要的,熟悉熱修復的原理有助于我們提供自己的編程水平,提升自己解決問題的能力,最后熱修復不是簡單的客戶端SDK,它還包含了安全機制和服務端的控制邏輯,整條鏈路也不是短時間可以快速完成的,

所以為了方便朋友們更直觀快速的學習掌握Android熱修復技術,我這里收集整理一套視頻+電子書的熱修復系列學習資料,視頻教程為愛奇藝高級工程師Lance老師主講,以Qzone熱修復實戰專案為例,深入淺出的全方位講解熱修復技術,電子書是來源于阿里的《深入探索Android熱修復技術原理》對熱修復技術有很深入解讀,

由于篇幅原因,這里只做一些截圖展示,需要完整資料的朋友,可以在點贊+評論后,點擊此處免費獲取!

熱修復學習視頻
熱修復視頻內容

深入探索Android熱修復技術原理電子書
熱修復技術原理電子書內容

需要完整資料的朋友,可以在點贊+評論后,點擊此處免費獲取!

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/232126.html

標籤:其他

上一篇:Android Studio安裝教程(超詳細教程)

下一篇:使用ffmpeg進行錄屏出現的問題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more