热修复设计之CLASS_ISPREVERIFIED(二)

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
本篇文章将继续从CLASS_ISPREVERIFIED实战来介绍热修复设计:java

1、前言

本文将解决两个问题。android

  1. 怎么将修复后的Bug类打包成dex
  2. 怎么将外部的dex插入到ClassLoader中

2、创建测试Demo

2.1 目录结构

 
19956127-ee34540995528b64.png
 

2.2 源码

activity_main.xml数组

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="click"
        android:text="小喵叫一声"/>
</RelativeLayout>

MainActivity.class架构

package com.aitsuki.bugfix;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.aitsuki.bugfix.animal.Cat;

public class MainActivity extends AppCompatActivity {

    private Cat mCat;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCat = new Cat();
    }

    public void click(View view) {
        Toast.makeText(this, mCat.say(),Toast.LENGTH_SHORT).show();
    }
}

Cat.classapp

package com.aitsuki.bugfix.animal;

/**
 * Created by AItsuki on 2016/3/14.
 */
public class Cat {
    public String say() {
        return "汪汪汪!";
    }
}

2.3 运行结果

 
19956127-ffa85b60de3a1a37.png
 

假设这是咱们公司的开发项目,刚刚上线就发现了严重bug,猫会狗叫。
想修复bug,让用户再马上更新一次显然很不友好,此时热补丁修复技术就有用了。框架

3、制做补丁

在加载dex的代码以前,咱们先来制做补丁。
1. 首先咱们将Cat类修复,汪汪汪改为喵喵喵,而后从新编译项目。(Rebuild一下就好了)
2. 去保存项目的地方,将Cat.class文件拷贝出来,在这里ide

 
19956127-0989390f7bb0258f.png
 

 

3. 新建文件夹,要和该Cat.class文件的包名一致,而后将Cat.class复制到这里,如图函数

 
19956127-da91cd0696e84f4b.png
 

 

4. 命令行进入到图中的test目录,运行一下命令,打包补丁。如图:工具

 
19956127-ef3c14557d440be2.png
 

 

而后test目录是这样的学习

 

 
19956127-3c35a15d3e59a306.png
 

 

patch_dex.jar就是咱们打包好的补丁了,咱们将它放到sdCard中,待会从这里加载补丁。

关于什么用这么复杂的方法打包补丁的说明:
你也能够直接将java文件拷出来,经过javac -d带包编译再转成jar。
但我这么麻烦是有缘由的,由于用这种方法你可能会遇到ParseException,缘由是jar包版本和dx工具版本不一致。
而从项目中直接将编译好的class直接转成jar就没问题,由于java会向下兼容,打出来的jar包和class版本是一致的。
总而言之,dx版本要和class编译版本对应。

4、加载补丁

4.1 思路

经过上一篇博文,咱们知道dex保存在这个位置
BaseDexClassLoader–>pathList–>dexElements

apk的classes.dex能够从应用自己的DexClassLoader中获取。
path_dex的dex须要new一个DexClassLoader加载后再获取。
分别经过反射取出dex文件,从新合并成一个数组,而后赋值给盈通自己的ClassLoader的dexElements

4.2 代码实现

加载外部dex,咱们能够在Application中操做。
首先新建一个HotPatchApplication,而后在清单文件中配置,顺便加上读取sdcard的权限,由于补丁就保存在那里。

HotPatchApplication代码以下:

package com.aitsuki.hotpatchdemo;

import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;

/**
 * Created by hp on 2016/4/6.
 */
public class HotPatchApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // 获取补丁,若是存在就执行注入操做
        String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
        File file = new File(dexPath);
        if (file.exists()) {
            inject(dexPath);
        } else {
            Log.e("BugFixApplication", dexPath + "不存在");
        }
    }

    /**
     * 要注入的dex的路径
     *
     * @param path
     */
    private void inject(String path) {
        try {
            // 获取classes的dexElements
            Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
            Object pathList = getField(cl, "pathList", getClassLoader());
            Object baseElements = getField(pathList.getClass(), "dexElements", pathList);

            // 获取patch_dex的dexElements(须要先加载dex)
            String dexopt = getDir("dexopt", 0).getAbsolutePath();
            DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
            Object obj = getField(cl, "pathList", dexClassLoader);
            Object dexElements = getField(obj.getClass(), "dexElements", obj);

            // 合并两个Elements
            Object combineElements = combineArray(dexElements, baseElements);

            // 将合并后的Element数组从新赋值给app的classLoader
            setField(pathList.getClass(), "dexElements", pathList, combineElements);

            //======== 如下是测试是否成功注入 =================
            Object object = getField(pathList.getClass(), "dexElements", pathList);
            int length = Array.getLength(object);
            Log.e("BugFixApplication", "length = " + length);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 经过反射获取对象的属性值
     */
    private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }

    /**
     * 经过反射设置对象的属性值
     */
    private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }

    /**
     * 经过反射合并两个数组
     */
    private Object combineArray(Object firstArr, Object secondArr) {
        int firstLength = Array.getLength(firstArr);
        int secondLength = Array.getLength(secondArr);
        int length = firstLength + secondLength;

        Class<?> componentType = firstArr.getClass().getComponentType();
        Object newArr = Array.newInstance(componentType, length);
        for (int i = 0; i < length; i++) {
            if (i < firstLength) {
                Array.set(newArr, i, Array.get(firstArr, i));
            } else {
                Array.set(newArr, i, Array.get(secondArr, i - firstLength));
            }
        }
        return newArr;
    }

}

5、CLASS_ISPREVERIFIED

运行一下Demo,报如下错误。(AndroidStudio 2.0可能不会报错,须要打包的时候才会出现错误,这是Instant run致使的)

 

 
19956127-06454a85485d449f.png
 

 

dexElements的length = 2,看来咱们的patch_dex已经成功添加进去了。
可是从黄色框框和黄色框上面那一段log提示中能够看出,MainActivity引用了Cat,可是发现他们在不一样的Dex中。

看到这里可能就会问:
为何以前那么多项目都采用分包方案,可是却不会出现这个错误呢?
我在这里总结了一个过程,想知道详细分析过程的请看QQ空间开发团队的原文。

在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程当中会对全部class一个校验。
校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。若是A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
被打上这个标记的类不能引用其余dex中的类,不然就会报图中的错误
在咱们的Demo中,MainActivity和Cat自己是在同一个dex中的,因此MainActivity被打上了CLASS_ISPREVERIFIED。而咱们修复bug的时候却引用了另一个dex的Cat.class,因此这里就报错了
而普通分包方案则不会出现这个错误,由于引用和被引用的两个类一开始就不在同一个dex中,因此校验的时候并不会被打上CLASS_ISPREVERIFIED
补充一下第二条:A类若是还引用了一个C类,而C类在其余dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其余dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。

5.1 解决方案

根据上面的第六条,咱们只要让全部类都引用其余dex中的某个类就能够了。

下面是QQ控件给出的解决方案

 

 
19956127-33d70cfd35de67d2.png
 

 

在全部类的构造函数中插入这行代码 System.out.println(AntilazyLoad.class);
这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类均可以进行打补丁操做。
hack.dex在应用启动的时候就要先加载出来,否则AntilazyLoad类会被标记为不存在,即便后面再加载hack.dex,AntilazyLoad类仍是会提示不存在。该类只要一次找不到,那么就会永远被标上找不到的标记了。
咱们通常在Application中执行dex的注入操做,因此在Application的构造中不能加上System.out.println(AntilazyLoad.class);这行代码,由于此时hack.dex尚未加载进来,AntilazyLoad并不存在。
之因此选择构造函数是由于他不增长方法数,一个类即便没有显式的构造函数,也会有一个隐式的默认构造函数。

5.2 插入代码的难点

1.首先在源码中手动插入不太可行,hack.dex此时并无加载进来,AntilazyLoad.class并不存在,编译不经过。
2.因此咱们须要在源码编译成字节码以后,在字节码中进行插入操做。对字节码进行操做的框架有不少,可是比较经常使用的则是ASM和javaassist
3.但AndroidStudio是使用Gradle构建项目,编译-打包都是自动化的,咱们怎么操做呢。

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
原文连接:https://blog.csdn.net/u010386612/article/details/51077291

相关文章
相关标签/搜索