Android 熱修復原理解析

概述

關聯文章

JVM 類加載機制

Android 中的ClassLoader

假如剛發佈的版本出現了bug,我們就需要解決bug,並且重新發布新的版本,這樣會浪費很多的人力物力,有沒有一種可以不重新發布App,不需要用戶覆蓋安裝,就可以解決bug。

熱修復就是爲了解決上方的問題出現的,熱修復主要分爲三種修復,分別是

  • 代碼修復
  • 資源修復
  • 動態鏈接庫的修復(so修復)

我們一次說一下他們的原理

代碼修復

代碼修復主要有三個方案

  • 底層替換方案
  • 類加載方案
  • Instant Run方案

我們今天主要講類加載方案

類加載方案

類加載方案基於dex分包,由於應用的功能越來越複雜,代碼不斷的增大,可能會導致65536限制異常,這說明應用中的方法數超過了65536個,產生這個問題的原因就是DVM Bytecode的限制,DVM指令集方法調用指令invoke-kind索引爲16bits,最多能引用65536個方法

爲了解決65536限制,從而產生了dex分包方案,dex分包方案主要做的是,在打包的時候把代碼分成多個dex,將啓動時必須用到的類直接放到主dex中,其他代碼放到次dex中,當應用啓動時先加載主dex,然後再動態加載次dex,從而緩解了65536限制

在上篇文章Android中的ClassLoader,中講到DexPathListfindClass方法

 public Class<?> findClass(String name, List<Throwable> suppressed) {      //註釋1
        for (Element element : dexElements) {
            //註釋2
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

Element內部封裝了DexFile,DexFile用於加載dex文件,每一個dex文件對應於一個Element,多個Element組成了有序數組dexElements,當我們在查找類時,會在註釋1處遍歷dexElements數組,註釋2處調用ElementfindClass查找類,如果在dex找到了就返回該類,如果沒有找到就在下一個dex查找

根據上方的流程我們把有bug的key.class類進行修改,然後把修改後的Key.class打包成含dex的補丁包patch.jar,放在dexElements數組的第一個元素,這樣會首先找到patch.jar的key.class來替換有bug的key.class

類加載方案需要重啓AppClassLoader重新加載類,所以採用此方案的不能即時生效

資源修復

資源修復並沒有代碼修復這麼複雜,基本上就是對AssetManager進行修改,很多熱修復參考了instant run的原理,我們直接分析一下instant run原理就行

instant run源碼

    public static void monkeyPatchExistingResources(@Nullable Context context,
                                                    @Nullable String externalResourceFile,
                                                    @Nullable Collection<Activity> activities) {
        if (externalResourceFile == null) {
            return;
        }
        try {
            //利用反射創建一個新的AssetManager
            AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
            //利用反射獲取addAssetPath方法
            Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            mAddAssetPath.setAccessible(true);
            //利用反射調用addAssetPath方法加載外部的資源(SD卡)
            if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
                throw new IllegalStateException("Could not create new AssetManager");
            }
            // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
            // in L, so we do it unconditionally.
            Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
            mEnsureStringBlocks.setAccessible(true);
            mEnsureStringBlocks.invoke(newAssetManager);
            if (activities != null) {
                //遍歷activities
                for (Activity activity : activities) {
                    //拿到Activity的Resources
                    Resources resources = activity.getResources();
                    try {
                        //獲取Resources的成員變量mAssets
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        //給成員變量mAssets重新賦值爲自己創建的newAssetManager
                        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);
                    }
                    //獲取activity的theme
                    Resources.Theme theme = activity.getTheme();
                    try {
                        try {
                            //反射得到Resources.Theme的mAssets變量
                            Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            //將Resources.Theme的mAssets替換成newAssetManager
                            ma.set(theme, newAssetManager);
                        } 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");
                        mtm.setAccessible(true);
                        mtm.invoke(activity);
                        Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme");
                        mCreateTheme.setAccessible(true);
                        Object internalTheme = mCreateTheme.invoke(newAssetManager);
                        Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
                        mTheme.setAccessible(true);
                        mTheme.set(theme, internalTheme);
                    } catch (Throwable e) {
                        Log.e(LOG_TAG, "Failed to update existing theme for activity " + activity,
                                e);
                    }
                    pruneResourceCaches(resources);
                }
            }
            // 根據sdk版本的不同,用不同的方式獲取Resources的弱引用集合
            Collection<WeakReference<Resources>> references;
            if (SDK_INT >= KITKAT) {
                // Find the singleton instance of ResourcesManager
                Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
                Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
                mGetInstance.setAccessible(true);
                Object resourcesManager = mGetInstance.invoke(null);
                try {
                    Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
                    fMActiveResources.setAccessible(true);
                    @SuppressWarnings("unchecked")
                    ArrayMap<?, WeakReference<Resources>> arrayMap =
                            (ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
                    references = arrayMap.values();
                } catch (NoSuchFieldException ignore) {
                    Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                    mResourceReferences.setAccessible(true);
                    //noinspection unchecked
                    references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
                }
            } else {
                Class<?> activityThread = Class.forName("android.app.ActivityThread");
                Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                Object thread = getActivityThread(context, activityThread);
                @SuppressWarnings("unchecked")
                HashMap<?, WeakReference<Resources>> map =
                        (HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread);
                references = map.values();
            }
            //將的到的弱引用集合遍歷得到Resources,將Resources中的mAssets字段替換爲newAssetManager
            for (WeakReference<Resources> wr : references) {
                Resources resources = wr.get();
                if (resources != null) {
                    // Set the AssetManager of the Resources instance to our brand new one
                    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);
        }
    }

可以看出instance run熱修復可以簡單的總結爲倆個步驟

  • 創建新的AssetManager,並通過反射調用addAssetPath方法加載外部資源,這樣新建的AssetManager就包含了外部資源
  • AssetManager類型的mAsset字段的引用全部替換爲新創建的AssetManager

動態鏈接庫的修復(so修復)

so修復有倆種方式可以達到目的

  • 加載so方法的替換
  • 反射注入so路徑

加載so方法的替換

Android平臺加載so庫主要用到了2個方法

System.load:可以加載自定義路徑下的so
System.loadLibaray:用來加載已經安裝APK中的so

通過上面倆個方法我們可以想到,如果有補丁so下發,就調用System.load去加載,如果沒有補丁下發就用System.loadLibaray去加載,原理比較簡單

反射注入so路徑

這個需要我們分析一下System.loadLibaray的源碼,他會調用Runtime的loadLibrary0方法

   synchronized void loadLibrary0(ClassLoader loader, String libname) {
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        if (loader != null) {
            //註釋1
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                // It's not necessarily true that the ClassLoader used
                // System.mapLibraryName, but the default setup does, and it's
                // misleading to say we didn't find "libMyLibrary.so" when we
                // actually searched for "liblibMyLibrary.so.so".
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                               System.mapLibraryName(libraryName) + "\"");
            }
            //註釋2
            String error = nativeLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }

        String filename = System.mapLibraryName(libraryName);
        List<String> candidates = new ArrayList<String>();
        String lastError = null;
        //註釋3
        for (String directory : getLibPaths()) {
            //註釋4
            String candidate = directory + filename;
            candidates.add(candidate);

            if (IoUtils.canOpenReadOnly(candidate)) {
                //註釋5
                String error = nativeLoad(candidate, loader);
                if (error == null) {
                    return; // We successfully loaded the library. Job done.
                }
                lastError = error;
            }
        }

        if (lastError != null) {
            throw new UnsatisfiedLinkError(lastError);
        }
        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
    }

這個方法分爲倆部分,當ClassLoader爲null的時候,註釋3 遍歷getLibPaths方法,這個方法會返回java.library.path選項配置的路徑數組,在註釋4拼接出so路徑並傳入註釋5處nativeLoad方法

ClassLoader不爲null的時候,在註釋2處也調用了nativeLoad方法,不過他的參數是通過註釋1處findLibrary方法獲取的,我們看下這個方法

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

        for (NativeLibraryElement element : nativeLibraryPathElements) {
            //註釋1
            String path = element.findNativeLibrary(fileName);

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

        return null;
    }

這個和上面講的findClass方法類似,nativeLibraryPathElements中的每一個NativeLibraryElement元素都對應一個so庫,在註釋1處調用findNativeLibrary,就會返回so的路徑,這個就可以根據類加載方案一樣,插入nativeLibraryPathElements數組前部,讓補丁的so的路徑先返回

參考:《Android 進階解密》

全面解析 Android 熱修復原理

Android 熱補丁技術——資源的熱修復

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章