Android热修复手动实现

Posted lxn_李小牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android热修复手动实现相关的知识,希望对你有一定的参考价值。

前言

热修复,简单的说就是在不重新下载安装app的情况下,自动修复现有app的问题,今天来做一个简单的实现。


效果图

点击TEST我们执行下面的方法

 public void test(View view) 
        TestCaculate testCaculate = new TestCaculate();
        testCaculate.caculate(this);
    
public class TestCaculate 
   public int a = 10;
   public int b = 0;
   
   public void caculate(Context context) 
       Toast.makeText(context, "结果" + a / b, Toast.LENGTH_SHORT).show();
   

很明显,这里会产生一个除数为0的异常,导致app退出。接下来我们就来修复。

热修复的前提是apk中有多个dex,所以我们要先进行multidex的支持,

implementation 'com.android.support:multidex:1.0.3' // 引入multidex库
defaultConfig 
        applicationId "practice.lxn.cn.weather"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
        multiDexKeepFile file('maindexlist.txt') // maindexlist.txt文件指定哪些类在主dex中
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    

    dexOptions 
        javaMaxHeapSize "4g"
        preDexLibraries = false
        additionalParameters = ['--multi-dex', '--main-dex-list='+ project.rootDir.absolutePath + '/maindexlist.txt', '--minimal-main-dex',
                                '--set-max-idx-number=1000']
    

内容如下

android/support/multidex/MultiDex.class
android/support/multidex/MultiDexApplication.class
android/support/multidex/MultiDexExtractor.class
android/support/multidex/MultiDexExtractor$1.class
android/support/multidex/MultiDex$4.class
android/support/multidex/MultiDex$14.class
android/support/multidex/MultiDex$19.class
android/support/multidex/ZipUtil.class
android/support/multidex/ZipUtil$CentralDirectory.class

practice/lxn/cn/weather/MainActivity.class
practice/lxn/cn/weather/CustomApplication.class

应用的目录如下



这里我们把有问题的类和修复工具打到从dex里,编译打包,我们可以看到两个dex文件,通过dex2jar和JD-GUI可以查看,确实打进去了。


然后我们把问题修复,重新打包

public class TestCaculate 
   public int a = 10;
   public int b = 1;
   
   public void caculate(Context context) 
       Toast.makeText(context, "结果" + a / b, Toast.LENGTH_SHORT).show();
   

接下来把打好的修复包classes2.dex放到外部存储目录,修复的时候我们手动把它拷贝到应用自己的目录下,进行修复,整个过程代码如下

public class MainActivity extends AppCompatActivity 
    public static final String DEX_PATH = Environment.getExternalStorageDirectory().getAbsolutePath();

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    

    public void test(View view) 
        TestCaculate testCaculate = new TestCaculate();
        testCaculate.caculate(this);
    

    public void fix(View view) 
        try 
            // 把dex文件从外部目录复制到应用程序所在目录,方便类加载器加载
            String fileName = "classes2.dex";
            File dir = new File(DEX_PATH + File.separator);
            File file = new File(dir + File.separator + fileName);
            FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(getDir("dex",MODE_PRIVATE) + fileName);
            int len;
            byte[] bytes = new byte[1024];
            while ((len = fis.read(bytes)) != -1) 
                fos.write(bytes,0,len);
            
            fis.close();
            fos.close();
            FixUtil.patch(this,file.getAbsolutePath(),"practice.lxn.cn.weather.test.TestCaculate");
            Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();
         catch (Exception e) 
            Toast.makeText(this, "修复失败" + e.getMessage(), Toast.LENGTH_SHORT).show();
            e.printStackTrace();
        
    

public class FixUtil 
    private static final String TAG = "FixUtil";
    /**
     * 修复指定的类
     * @param context        上下文对象
     * @param patchDexFile   dex文件
     * @param patchClassName 被修复类名
     */
    public static void patch(Context context, String patchDexFile, String patchClassName) 
        if (patchDexFile != null && new File(patchDexFile).exists()) 
            try 
                if (hasLexClassLoader()) 
                    injectInAliyunOs(context, patchDexFile, patchClassName);
                 else if (hasDexClassLoader()) 
                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
                 else 
                    injectBelowApiLevel14(context, patchDexFile, patchClassName);
                
             catch (Throwable th) 
                Log.d(TAG, "patch: " + th.getMessage());
            
        
    

    private static boolean hasLexClassLoader() 
        try 
            Class.forName("dalvik.system.LexClassLoader");
            return true;
         catch (ClassNotFoundException e) 
            return false;
        
    

    private static boolean hasDexClassLoader() 
        try 
            Class.forName("dalvik.system.BaseDexClassLoader");
            return true;
         catch (ClassNotFoundException e) 
            return false;
        
    

    private static void injectInAliyunOs(Context context, String patchDexFile, String patchClassName)
            throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            InstantiationException, NoSuchFieldException 
        PathClassLoader obj = (PathClassLoader) context.getClassLoader();
        String replaceAll = new File(patchDexFile).getName().replaceAll("\\\\.[a-zA-Z0-9]+", ".lex");
        Class<?> cls = Class.forName("dalvik.system.LexClassLoader");
        Object newInstance =
                cls.getConstructor(String.class, String.class, String.class, ClassLoader.class).newInstance(
                        context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
                        context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj);
        cls.getMethod("loadClass", String.class).invoke(newInstance, patchClassName);
        setField(obj, PathClassLoader.class, "mPaths",
                appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
        setField(obj, PathClassLoader.class, "mFiles",
                combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
        setField(obj, PathClassLoader.class, "mZips",
                combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
        setField(obj, PathClassLoader.class, "mLexs",
                combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
    

    @TargetApi(14)
    private static void injectBelowApiLevel14(Context context, String str, String str2)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException 
        PathClassLoader obj = (PathClassLoader) context.getClassLoader();
        DexClassLoader dexClassLoader =
                new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());
        dexClassLoader.loadClass(str2);
        setField(obj, PathClassLoader.class, "mPaths",
                appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class,
                        "mRawDexPath")
                ));
        setField(obj, PathClassLoader.class, "mFiles",
                combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class,
                        "mFiles")
                ));
        setField(obj, PathClassLoader.class, "mZips",
                combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class,
                        "mZips")));
        setField(obj, PathClassLoader.class, "mDexs",
                combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class,
                        "mDexs")));
        obj.loadClass(str2);
    

    private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException 
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
                getDexElements(getPathList(
                        new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
        // 获取pathList对象
        Object a2 = getPathList(pathClassLoader);
        //新的dexElements对象重新设置回去
        setField(a2, a2.getClass(), "dexElements", a);
        pathClassLoader.loadClass(str2);
    

    /**
     * 通过反射先获取到pathList对象
     *
     * @param obj
     * @return
     * @throws ClassNotFoundException e
     * @throws NoSuchFieldException e
     * @throws IllegalAccessException e
     */
    private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
            IllegalAccessException 
        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    

    /**
     * 从上面获取到的PathList对象中,进一步反射获得dexElements对象
     *
     * @param obj
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException 
        return getField(obj, obj.getClass(), "dexElements");
    

    private static Object getField(Object obj, Class cls, String str)
            throws NoSuchFieldException, IllegalAccessException 
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);//设置为可访问
        return declaredField.get(obj);
    

    private static void setField(Object obj, Class cls, String str, Object obj2)
            throws NoSuchFieldException, IllegalAccessException 
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);//设置为可访问
        declaredField.set(obj, obj2);
    

    //合拼dexElements
    private static Object combineArray(Object obj, Object obj2) 
        Class componentType = obj2.getClass().getComponentType();
        int length = Array.getLength(obj2);
        int length2 = Array.getLength(obj) + length;
        Object newInstance = Array.newInstance(componentType, length2);
        for (int i = 0; i < length2; i++) 
            if (i < length) 
                Array.set(newInstance, i, Array.get(obj2, i));
             else 
                Array.set(newInstance, i, Array.get(obj, i - length));
            
        
        return newInstance;
    

    private static Object appendArray(Object obj, Object obj2) 
        Class componentType = obj.getClass().getComponentType();
        int length = Array.getLength(obj);
        Object newInstance = Array.newInstance(componentType, length + 1);
        Array.set(newInstance, 0, obj2);
        for (int i = 1; i < length + 1; i++) 
            Array.set(newInstance, i, Array.get(obj, i - 1));
        
        return newInstance;
    

最后给出源码的下载路径

Android热修复实现

欢迎大家在下方评论和留言,有问题一起讨论



以上是关于Android热修复手动实现的主要内容,如果未能解决你的问题,请参考以下文章

一步步手动实现热修复-类的加载机制简要介绍

一步步手动实现热修复Class文件的替换

Android热修复(HotFix)实战

一步步手动实现热修复-dex文件的生成与加载

Android Gradle 插件热修复实现 ① ( Android 热修复系统组成 | 热修复工作流程 | 热修复使用到的技术 | 热修复框架选择注意事项 )

Android Gradle 插件热修复实现 ① ( Android 热修复系统组成 | 热修复工作流程 | 热修复使用到的技术 | 热修复框架选择注意事项 )