Android熱修復(fù)原理探索與實踐

最近一直在閱讀微信Tinker的源碼,為了加深理解和記憶,便自己動手實現(xiàn)了一下熱修復(fù),做了一個比較簡單的demo,并寫了這篇博客與大家分析,理解有誤的地方還請大家指出~
閱讀本文大概需要5分鐘時間。

類加載器

說到熱修復(fù),那么肯定要先說一下Android中的類加載器,這里我們簡單介紹一下。
在Android中,我們應(yīng)用自己的Class是交由PathClassLoader來加載的,PathClassLoader的代碼很簡單,只有兩個構(gòu)造函數(shù)

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

我們看到他的父類其實是BaseDexClassLoader,那么奧妙一定是在這里面,我們看一下BaseDexClassLoader的源碼

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    ...

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

    ...
}

這里我們可以看到findClass查找Class的方法,BaseDexClassLoader重寫了ClassLoaderfindClass方法,在方法內(nèi)部調(diào)用了DexPathListfindClass方法,我們進去看看這個方法做了什么

final class DexPathList {
    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;
    ...  
    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

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

可以看到,DexPathList中有一個Element數(shù)組的成員變量,根據(jù)注釋可以知道這個數(shù)組是用于存放dex的路徑節(jié)點的,通過它的findClass方法可以知道,通過遍歷數(shù)組然后找到需要的類并返回,我們的切入點便是這里,將我們需要進行熱修復(fù)的“補丁”插入到這個數(shù)組的前端,那么在尋找Class的時候便會先找到我們已經(jīng)修復(fù)的類,從而實現(xiàn)熱修復(fù)。

為了控制本文篇幅,這里就不對Android的類加載器進行太多講解,對這一塊還不是特別清楚的童鞋可以先看看鴻洋大神前陣子推送的jjlanbupt寫的《Android插件化框架系列之類加載器》。

動手實踐

Demo

這里比較簡單,就是點擊SHOW按鈕彈出個Toast,我們現(xiàn)在要做的就是通過熱修復(fù)把Toast中的文本進行替換。SHOW按鈕的點擊事件如下

  @Override
    public void onClick(View v) {
        int viewId = v.getId();
        ...
        if (viewId == R.id.btn_show) {
            Toast.makeText(this, Test.test(), Toast.LENGTH_SHORT).show();
        }
    }

這里我們看一下Test類,也是比較簡單

public class Test {
    public static String test() {
        return "hello world";
    }
}

ok,目標(biāo)明確,我們要通過熱修復(fù)更改test方法中返回的字符串,開始擼代碼

public class Test {
    public static String test() {
        return "I am change!!!";
    }
}

首先,我們把字符串改了之后把它編譯為class,再打包成jar,如果忘了如何把java編譯后打包成jar的方法的同學(xué),可以網(wǎng)上搜一下,這里就不再啰嗦了。
打包了jar后,別忘記要使用sdk中的dx工具將jar包轉(zhuǎn)換成dx格式的jar包,工具目錄在sdk目錄下的**...\build-tools\ **中


在控制臺使用該命令即可,現(xiàn)在我們已經(jīng)完成了補丁包的制作,我們先來看一下執(zhí)行熱修復(fù)的代碼

private static void patch(Context base) {
        try {
            //獲取PathClassLoader加載器
            ClassLoader classLoader = MainActivity.class.getClassLoader();
            //反射獲得BaseDexClassLoader中的pathList成員變量
            Field dexPathListField = classLoader.getClass().getSuperclass().getDeclaredField("pathList");
            //設(shè)為可訪問
            dexPathListField.setAccessible(true);
            //獲得PathClassLoader中的pathList對象
            Object pathList = dexPathListField.get(classLoader);
            //反射獲得DexPathList中的dexElements成員變量
            Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
            //設(shè)為可訪問
            dexElementsField.setAccessible(true);
            //獲得pathList中的dexElements數(shù)組對象
            Object dexElements[] = (Object[]) dexElementsField.get(pathList);
            //反射獲得pathList中的makeDexElements方法
            Method method= pathList.getClass().getDeclaredMethod("makeDexElements", ArrayList.class, File.class,
                    ArrayList.class);
            //設(shè)為可訪問
            method.setAccessible(true);
            List<File> files = new ArrayList<>();
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            //獲得我們的patch補丁包
            File patchFile = new File(copyAssetsFile("patch.jar", base));
            files.add(patchFile);
            //指定解壓目錄
            File optimizedDirectory = new File(base.getFilesDir().getAbsolutePath() + File.separator + "patch");
            if (!optimizedDirectory.exists()) {
                optimizedDirectory.mkdirs();
            }
            //執(zhí)行makeDexElements方法,解析我們的補丁包獲得dexElements數(shù)組
            Object dexElementsResult[] = (Object[]) method.invoke(pathList, files, optimizedDirectory, suppressedExceptions);
            //創(chuàng)建一個新的數(shù)組
            Object finalResult[] = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + dexElementsResult.length);
            //先把我們的補丁包的dexElements數(shù)組放入剛創(chuàng)建的數(shù)組
            System.arraycopy(dexElementsResult, 0, finalResult, 0, dexElementsResult.length);
            //再把原來的dexElements數(shù)組放入
            System.arraycopy(dexElements, 0, finalResult, dexElementsResult.length, dexElements.length);
            //將新的數(shù)組設(shè)置回去
            dexElementsField.set(pathList, finalResult);
            for (Object o : finalResult) {
                Log.d(MainActivity.class.getSimpleName(), o.toString());
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

這里主要就是使用反射獲取到dexElements數(shù)組,然后將我們自己的補丁包插入。因為我這里偷了個懶把補丁包直接放在assets目錄下,copyAssetsFile方法是將assets目錄下的文件復(fù)制到我們的app目錄下,因為PathClassLoader只能讀取app目錄下的文件,代碼也很簡單,這里也貼一下

private static String copyAssetsFile(String assetsFileName,Context context) {
        String src = context.getFilesDir().getAbsolutePath() + File.separator + assetsFileName;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = context.getAssets().open(assetsFileName);
            outputStream = new BufferedOutputStream(new FileOutputStream(src));
            byte[] temp = new byte[1024];
            int len;
            while ((len = (inputStream.read(temp))) != -1) {
                outputStream.write(temp, 0, len);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.flush();
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return src;
    }

到這里我們代碼就搞定了,但是有兩個地方還是需要注意一下:

  1. 如果是在Activity中進行熱修復(fù),并且開啟了instant run,那么會導(dǎo)致看不到熱修復(fù)的效果,因為Android Studio的instant run會將ClassLoader替換成DelegateClassLoader,這里建議大家打包成apk然后提取出來安裝運行就可以。
  2. 為了避免掉進CLASS_ISPREVERIFIED的坑中,這里我們使用Art的機型來運行(Android 5.0以上),具體原因大家可以參考安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹,微信Tinker由于采用了是全量替換dex的方法,所以也可以說是從另一個角度解決了問題。

最后我們看一下運行效果,重啟app,先點擊patch按鈕,再點擊show按鈕

可以看到,patch成功。

總結(jié)

最近看Tinker的源碼,確實對各種異常情況處理的很到位,收獲頗豐。各位有興趣的同學(xué)也可以去閱讀一番,自己動手實踐實踐,確實能加深自己的理解。若本文中哪里錯誤的地方大家也可以在評論中指出,謝謝大家~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,939評論 25 709
  • 一、簡述 熱修復(fù)無疑是這2年較火的新技術(shù),是作為安卓工程師必學(xué)的技能之一。在熱修復(fù)出現(xiàn)之前,一個已經(jīng)上線的app中...
    GitLqr閱讀 23,128評論 32 153
  • 從去年下半年開始,熱修復(fù)技術(shù)在 Android 技術(shù)社區(qū)熱了一陣子,這種不用發(fā)布新版本就可以修復(fù)線上 bug 的技...
    小小亭長閱讀 6,477評論 6 19
  • 前言 好幾個月之前關(guān)于AndroidApp熱補丁修復(fù)火了一把,源于QQ空間團隊的一篇文章安卓App熱補丁動態(tài)修復(fù)技...
    lgzaaron閱讀 831評論 1 3
  • 最近才意識到自己的人生其實糟透了,以前也知道它并不是那么的好,但卻一直在努力去改變,但現(xiàn)在發(fā)覺其實還在原地打轉(zhuǎn)?;?..
    N皮臉閱讀 937評論 0 0

友情鏈接更多精彩內(nèi)容