手动实现最简单的Android热修复(最新最全详细小白教程)

版权声明:转载请附上原地址。 https://blog.csdn.net/hq942845204/article/details/81044158

前言

最近了解到了热修复相关的东西,于是很好奇原理,便一番搜索资料,同时为了加深对热修复的理解,便自己照着网上的例子去实现一个热修复,因为基础相对比较差,而且网上很多例子都是过时的,而且很多细节不注意到的话,就是一个坑,而且还五花八门的,于是我觉得将自己的这个实现热修复的例子记录下来事很有必要的,主要是参考并综合了网上很多热修复的例子,自己实现并完成整个从0到1的过程,好了,我们开始吧!

需要知道的基本概念和原理

首先是热修复的基本概念,我不太喜欢那种专业术语的描述方式,因为那样很容易让人觉得晦涩难懂,而且我觉得唯一的效果就是营造出一种初学者觉得高大上的装X效果而已,所以我就说下我的理解:假定一个场景,你的APP上线了,现在发现了一个小Bug,这个Bug很简单,可能是一行代码的事,但是由于你才上线,要是再重新打包你的APP再上线,这个过程就很麻烦了,于是我们期望有一种方式,不需要用户来重新安装新的APP即可运行我们修复了Bug的APP,这种方式就叫热修复。

是不是觉得很神奇。我也是,在没接触之前,我也觉得很神奇,但是明白之后,其实真的没什么,很简单。

再说下热修复的基本原理,这里很多网上的讲解非常的专业,我看了之后也理解了好久,但是自己再梳理一下,其实没有那么难理解,我还是以通俗的方式来说:

假设有一个数组,这个数组,里面有很多个dex文件(不了解的只需要知道里面就是存放了类的二进制数据,用来给安卓虚拟机加载),然后安卓虚拟机在加载类文件的时候,会有个顺序,我们暂且不用管是什么顺序,或者是怎么加载的,我们只需要知道,它会有顺序,我们假定它从数组下标为0的地方开始循环找,一旦找到了对应的文件,那么后面即便仍然还有该类的dex文件,也不去加载了,相当与前面加载的会覆盖后面的,就是这样一个原理。

那么实际中,可以怎么实现呢,我们可以将相应的dex文件放在服务器上,然后在用户不知道的情况下(可以在欢迎界面扫描服务器山的文件,如果有则下载进行热修复,否则不管),将这个dex文件从服务器上下载下来,并移动到指定位置即可。

我们也不需知道具体移动到哪里了,只知道移动的地方需要满足的条件是:在对应的类的dex文件加载顺序之前。这样就可以实现覆盖效果,让新的类文件比旧的类文件先加载,旧的就不会生效,达到我们想要的效果。

动手实现

在动手实现前,需要知道的事:

上面我们说了一种方案是从服务器上下载对应的dex文件,这里因为只是模拟一下效果,便采用手动复制对应的dex文件到指定目录下,来达到同样的效果。

开始吧:

首先我们新建工程,随便写一个Bug,比如我这里除数为0的Bug

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();

    }

}


当我们调用caculate方法时肯定会提示异常导致退出,现在我们以热修复的方式来修复Bug。

首先我们需要生成的文件就是我们修复好Bug的程序的dex文件,看清楚了,是修复好Bug的,代表什么意思呢,也就是在进行下一步之前,TestCaculate代码是这样的

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();

    }

}


然后我们在布局文件中添加二个按钮,一个按钮点击调用caculate方法,触发Bug,另一个按钮点击修复Bug,需要注意的是,千万不要忘记了权限的申请,因为整个过程涉及到文件的读取和写入,而6.0以上需要动态获取权限,所以要在清单文件中加入下列两行代码。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

MainActivity代码如下

public class MainActivity extends AppCompatActivity {

    private Button btn, btn_fix;

    public static final int REQUEST_CODE = 1;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        btn = findViewById(R.id.btn);

        btn_fix = findViewById(R.id.btn_fix);

        btn.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {

                TestCaculate testCaculate = new TestCaculate();

                testCaculate.caculate(MainActivity.this);

            }

        });

        btn_fix.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {

                fix();

            }

        });

        ActivityCompat.requestPermissions(MainActivity.this,

                new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);

    }

    private void fix() {

        try {

            String dexPath = Environment.getExternalStorageDirectory() + "/classes2.dex";

            HotFix.patch(this, dexPath, "com.aiiage.testhotfix.TestCaculate");

            Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();

        } catch (Exception e) {

            Toast.makeText(this, "修复失败" + e.getMessage(), Toast.LENGTH_SHORT).show();

            e.printStackTrace();

        }

    }

    @Override

    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

        switch (requestCode) {

            case REQUEST_CODE: {

                if (grantResults.length > 0) {

                    // permission was granted

                } else {

                    // permission denied

                }

                return;

            }

        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    }

}


然后就是我们热修复的工具类,怎么使用,在MainActivity中已经有使用的代码了,工具类中用到了反射的知识,但是不是本文的重点,有需要的小伙伴自行学习相关知识,这个工具类中,最终要的两个类就是DexClassLoader和PathClassLoader,看名字就知道这是两个类加载器,用来加载类的,想知道具体实现的,下面就是源码

public final class HotFix {

    /**

    * 修复指定的类

    *

    * @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) {

            }

        }

    }

    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(new Class[]{String.class, String.class, String.class, ClassLoader.class}).newInstance(

                        new Object[]{context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,

                                context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj});

        cls.getMethod("loadClass", new Class[]{String.class}).invoke(newInstance, new Object[]{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()))));

        Object a2 = getPathList(pathClassLoader);

        //新的dexElements对象重新设置回去

        setField(a2, a2.getClass(), "dexElements", a);

        pathClassLoader.loadClass(str2);

    }

    /**

    * 通过反射先获取到pathList对象

    *

    * @param obj

    * @return

    * @throws ClassNotFoundException

    * @throws NoSuchFieldException

    * @throws IllegalAccessException

    */

    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;

    }

}


布局文件就两个按钮,就不贴了,占空间,好了,代码准备完毕,接着下一步吧。

接下来,我们生成项目对应的dex文件,网上资料有点少,,而且有的还是错的,各种莫名其妙的操作,哎说多了都是泪,但是也还是有正确的,我这里采用了一种相对简单的方式,首先在app的module下的build.gradle文件中加入代码,不要加入到某个节点下。最终代码如下

apply plugin: 'com.android.application'

android {

    compileSdkVersion 28

    defaultConfig {

        applicationId "com.aiiage.testhotfix"

        minSdkVersion 26

        targetSdkVersion 28

        versionCode 1

        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    buildTypes {

        release {

            minifyEnabled false

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }

    }

}

dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'

    implementation 'com.android.support.constraint:constraint-layout:1.1.2'

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test:runner:1.0.2'

    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

}

//加入的代码从这里开始

task clearJar(type: Delete) {

    delete('libs/log.jar')

}

task makeJar(type: org.gradle.api.tasks.bundling.Jar) {

    //指定生成的jar名称

    baseName 'log'

    //从哪里打包class文件

    from('build/intermediates/classes/debug/com/aiiage/testhotfix/')

    //打包到jar后的目录结构

    into('com/aiiage/testhotfix/')

    //去掉不需要打包的目录和文件

    exclude('text/', 'BuildConfig.class', 'R.class', 'BuildConfig.class')

    exclude {

        it.name.startsWith('R$');

    }

}

makeJar.dependsOn(clearJar, build)


加入的代码代表什么意思注释已经很清楚了,这个过程最终会生成一个jar包,然后打开AndroidStudio底下的命令行,如图

在命令行中,我们输入gradlew makeJar 注意,不要输错了,等待约2分钟左右,看到如下的字样,代表生成jar成功

生成的jar包存放的地方在配置文件中配置了,比如我这里就在这个目录下,如图

我这里的,名字叫log,所以最终得到的是一个名为log.jar的文件,现在我们用这个jar来得到dex文件,需要用到的工具是dx,这个工具在哪里呢,就是SDK目录下的build-tools,然后随便选择一个版本进去就可以看到名为dx.bat的文件,这个就是我们需要使用的。

我们将log.jar文件复制到这个目录下,按住shift右击鼠标在该目录下打开命令行,输入命令

dx --dex --output=D:/test log.jar

其中D:/test为保存生产的dex文件的目录,同时注意空格。回车若没有错误说明生产成功,我们来到指定的D:/test目录,发现我们的最终目标正静静的躺在里面等着我们,嘿嘿!

好了,我们现在将这个乖巧的classes.dex文件复制到我们的手机目录下,这里为了演示效果,我就采用的模拟器,如下,我这里将它重命名为classes2.jar,不重命名也没关系,名字无所谓,复制的目录为

这个过程在实际当中就是用户下载服务器上的对应文件,然后用代码将其放到指定目录下,只不过这里我们是手动模拟的这个操作。

然后我们就可以运行我们的程序了,但是运行程序之前还有两件事:

一:没猜错的话,你现在的代码是修复Bug后的代码,所以我们要将代码改会错误的版本,也就是下面这个

这样我们才能有Bug来修复嘛,不然我们Bug都没有,修复啥呢,对不

二:打开AndroidStudio的设置,取消掉instant run这里的勾勾

这样做是干嘛,取消掉这个勾勾之后,AndroidStudio在给我们安装新应用时,就不会只安装修改的部分,而是全部代码都重新编译并安装。

好了,我们准备工作做完了。接下来运行看效果吧。

首先我们点击修复按钮进行模拟热修复,看到修复成功的字样,说明修复成功,然后我们再点击HELLO按钮,这里按照预期会导致除数为0的异常,但是你会惊讶的发现,程序没有崩掉,而是Toast提示 结果10。说明程序已经被热修复,因为我们生成的dex文件中,将除数b改为了1,而这个正确的版本被安卓虚拟机预先加载了,所以不会执行我们程序中错误版本的代码。

结语

至此,一个完整的最简单的小白热修复程序已经完成!!有兴趣的可以深入研究哦!!

有问题欢迎留言,我会及时回复的。

---------------------

作者:黄庆庆

来源:CSDN

原文:https://blog.csdn.net/hq942845204/article/details/81044158

版权声明:本文为博主原创文章,转载请附上博文链接!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容