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热修复实现

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



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值