热修复小试牛刀

前言

热修复出现这么久,一直没有机会在项目中接触到,现在趁还不是很忙的时候来一探究竟。众所周知,热修复的就是在不发版的情况还能替换运行的代码,实现不发版还能解决一些小bug的场景,发布一次的代价是巨大的,如此一来好处真是多了去,但是在详细了解背后,隐患还是有的,不说别的,至少Google Play上线app就不允许,不过带着对技术的好奇还是来一探究竟。如果真有需要应用的场景,当下腾讯的Tinker和阿里的Sophix都是比较成熟的框架,根据开发文档集成入项目即可,前者免费后者收费。

大纲

1.实现原理

2.简易实现

3.混淆问题解决和gradle脚本

4.类加载校验问题解决

5.Android N混编问题解决

6.总结

1.实现原理

具体实现原理不同框架各不相同,详情可参照其他博客详细解说,在此仅用简单一句话概括

1.Tinker,需重启应用,使用dexdif差分出补丁dex和基准apk差异,生成出查分包在用户端生成新dex,替换旧dex加载实现;应用最广泛

2.AndFix,不需重启应用,底层修改相关结构体成员指向修改后成员;已停止维护

3.Robust,不需重启应用,所有类插入静态变量,所有方法插入控制逻辑,当不需要替换时静态变量为null走正常逻辑,若出现问题使静态变量不为null,从而走控制逻辑;增加代码量但最有效

4.QZone,需重启应用,生成改动后的dex,在用户端加载初期通过反射操作classLoader,在其dexElements头部插入补丁dex,达到替换目标类

本文主要讲述仿照Qzone方案如何实现

2.简易实现

首先要理解classLoader加载机制,先检验自己缓存是否加载,然后双亲委托机制查看自己父类是否加载,若都没有则自己加载。

//Android9.0    ClassLoader.java
 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

然后是遍历从dexElement中加载到dex也就是要加载类,dexElements是个数组,因此我们只需将我们的补丁dex插入到该数组头部即可完成替换。

//Android 9.0    DexPathList.java
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;
    }

这里值得注意的是,要反射操作系统类一定要做好版本适配,目前看来5.0以下、5.0 - 7.0、7.0以上这个三个区间相关函数都稍许不同,应该根据不同api获取正确函数;在此仅根据9.0适配。

屡一下需要反射操作的类和成员以及方法

//Android 9.0

//BaseDexClassLoader.java
private final DexPathList pathList;


//DexPathList.java
//最终需要加入补丁的数组
private Element[] dexElements;

//补丁file生成Element[]的函数
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {...}


明确操作步骤了就可以写我们的代码了

public static void init(Application application, String dexPath) {
        //获取PathClassLoader
        ClassLoader classLoader = application.getClassLoader();
        try {
            //获取PathClassLoader父类BaseDexClassLoader的pathList
            Object pathList = ReflectionUtils.getClassLoaderPathList(classLoader.getClass(), classLoader);
            //获取DexPathList的dexElements
            Field dexElementsF = ReflectionUtils.getFiledObj(pathList.getClass(), "dexElements", pathList);
            Object[] dexElements = (Object[]) dexElementsF.get(pathList);
            //获取DexPathList的makeDexElements方法
            Method makeDexElementsM = ReflectionUtils.getMethod(pathList.getClass(), "makeDexElements", List.class, File.class, List.class, ClassLoader.class);
            //makeDexElements第一个参数,dexFile的list
            List<File> files = new ArrayList<>();
            files.add(new File(dexPath));
            //makeDexElements第二个参数,输出路径
            File dexOutDir = application.getCacheDir();
            //makeDexElements第三个参数为IOException,传空list即可;第四个为当前classLoader实例
            //创建出我们自己的Element[]
            Object[] patchDexElements = (Object[]) makeDexElementsM.invoke(dexElements, files, dexOutDir, new ArrayList<>(), classLoader);
            //创建一个新的Element[]
            Object[] newDexElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + patchDexElements.length);
            //将自己的Element[]和旧Element[]合并为新Element[]
            //顺序不能错,[0]:patchDexElements,[1]:dexElements
            System.arraycopy(patchDexElements, 0, newDexElements, 0, patchDexElements.length);
            System.arraycopy(dexElements, 0, newDexElements, patchDexElements.length, dexElements.length);
            //使用新Element[]替换旧的Element[]
            dexElementsF.set(pathList, newDexElements);
        } catch (IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException | NullPointerException e) {
            e.printStackTrace();
        }
    }
    /**
     * 从当前class获取filed,若找不到就从父类找,主要适配classLoader
     *
     * @param className
     * @return
     */
    public static Object getClassLoaderPathList(@NonNull Class<?> className, ClassLoader classLoader) throws IllegalAccessException {
        Log.e("className1:", className.getSimpleName());
        Object object = null;
        try {
            Field field = className.getDeclaredField(NAME_PATH_LIST);
            field.setAccessible(true);
            object = field.get(classLoader);
        } catch (NoSuchFieldException e) {
            //pathList是BaseDexClassLoader的属性,classLoader是PathClassLoader,因此则需从其父类BaseDexClassLoader寻找
            if (className.getSuperclass() != null) {
                object = getClassLoaderPathList(className.getSuperclass(), classLoader);
            } else {
                e.printStackTrace();
            }
        }
        return object;
    }

    public static Field getFiledObj(@NonNull Class<?> className, String filedName, Object obj) throws NoSuchFieldException {
        Log.e("className2:", className.getSimpleName());
        Field field = null;
        field = className.getDeclaredField(filedName);
        field.setAccessible(true);
        return field;
    }

    public static Method getMethod(@NonNull Class<?> className, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
        Log.e("className3:", className.getSimpleName());
        Method method = null;
        method = className.getDeclaredMethod(methodName, parameterTypes);
        method.setAccessible(true);
        return method;
    }

最后手动配置下,MainActivity是bug代码

public class MainActivity extends AppCompatActivity {

    private TextView tv_main;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv_main = findViewById(R.id.tv_main);
        throw new NullPointerException("bug");
        //dx --dex --output=patch.dex dex
//        tv_main.setText("Hello Hot fix 2!");
    }
}

App里配置热修复逻辑

public class App extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        File patchFile = new File(getCacheDir(), "patch.dex");
        Log.e("patch dir:", patchFile.getAbsolutePath() + ", " + patchFile.exists());
        HotFixUtils.init(this, patchFile.getAbsolutePath());
    }
}

运行就会crash抛出异常

修改后的MainActivity

public class MainActivity extends AppCompatActivity {

    private TextView tv_main;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv_main = findViewById(R.id.tv_main);
//        throw new NullPointerException("bug");
        //dx --dex --output=patch.dex dex
        tv_main.setText("Hello Hot fix 2!");
    }
}

rebuild一下去app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\包名\下找到MainActivity.class,这里需注意不同gradle版本路径有所不同,只要找到编译后class文件即可。我这里使用的5.1.1。

找到后在当前目录下建立一个文件目录,将.class放入,如dex/com/zidian/hellohotfix/MainActivity.class

使用sdk\build-tools\版本号\dx工具将.class文件生成.dex文件,在包含上述dex文件夹路径下运行terminal指令(需配置build-tools环境变量,没有配置则需在dx工具所在路径运行,路径名需自行修改)

这样就在dex文件夹同级目录下生成了patch.dex

将这个文件push到我们App里设置的路径中,我的cache路径是/data/user/0/com.zidian.hellohotfix/cache/patch.dex,所以设备中应该是这样

7这样就设置完毕了,再来运行下我们app,发现就不闪退了,到此完成热修复的简易实现~

7.未完待续

之后的步骤需要涉及到gradle相关较深知识,还有ASM字节码插桩等;本想一笔带过以热修复实现为主,后面一想如果只是单纯照搬实现,不了解其中原理对学习起不到积极作用,所以实现暂且到此为止,我先去补习其他知识去~

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值