zoukankan      html  css  js  c++  java
  • 热修复设计之CLASS_ISPREVERIFIED(二)

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

    一、前言

    本文将解决两个问题。

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

    二、建立测试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.class

    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,让用户再立刻更新一次显然很不友好,此时热补丁修复技术就有用了。

    三、制作补丁

    在加载dex的代码之前,我们先来制作补丁。
    1. 首先我们将Cat类修复,汪汪汪改成喵喵喵,然后重新编译项目。(Rebuild一下就行了)
    2. 去保存项目的地方,将Cat.class文件拷贝出来,在这里

     
    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.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;
        }
    
    }
    

    五、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

  • 相关阅读:
    linux系统编程之(一) 信号量
    linux 工具(1)------终端提示符配置
    网络那点事之socket队列
    磨刀砍柴
    线程的分离链接属性
    error C1010: 在查找预编译头时遇到意外的文件结尾。是否忘记了向源中添加“#include "stdafx.h"”?
    mkdir和_mkdir的区别
    错误 1 error C4996: 'getcwd': The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _getcwd. See online help for details.
    error C4996: 'getcwd': The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _getcwd. See online help for details. c:users12968desktop estapp estapp estapp.c
    .net与C#
  • 原文地址:https://www.cnblogs.com/Android-Alvin/p/11963055.html
Copyright © 2011-2022 走看看