Android进阶之热修复

1 热修复概念

1.1 什么是热修复

一般的bug修复:都是等下一个版本解决,然后发布新的apk。
热修复:可以直接在客户已经安装的程序当中修复bug。

1.2 热修复原理图

这里写图片描述

2 热修复框架

(1)阿里系:DeXposed、Andfix。从底层C的二进制来入手的。

(2)腾讯系:Tinker、QQ空间补丁技术。Java类加载机制来入手的。

腾讯系的第三方在线更新平台-Bugly

(3)各大平台对比

【技术干货】Android热修复技术选型——三大流派解析

3 热修复例子

3.1 热修复步骤

(0)打包前注意:
Instant run:做热修复的时候记得把Instant run功能关闭,不然会影响热修复实现。
这里写图片描述

(1)找到~Class.class
a.修复代码,并编译运行

public class MyTestClass {
    public  void testFix(Context context){
        int i = 10;
        int a = 1;
        Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
    }
}

b.在AS的项目下找到MyTestClass.class文件
\app\build\intermediates\bin\MyTestClass.class

(2)配置dx.bat的环境变量
win:Android\sdk\build-tools\23.0.3\dx.bat

mac:
a.Mac系统终端命令行不执行命令,总出现command not found解决方法。打开命令行执行如下命令。

export PATH=/usr/bin:/usr/sbin:/bin:/sbin:/usr/X11R6/bin
cd ~/
touch .bash_profile //创建bash_profile,可忽略
open .bash_profile  //打开并编辑bash_profile

b.配置过的path

export PATH=${PATH}:~/Library/Android/sdk/platform-tools  //adb....
export PATH=${PATH}:~/Library/Android/sdk/build-tools/23.0.3 //dx....

这里写图片描述

参考配置链接:Mac系统终端命令行不执行命令 总出现command not found解决方法

(3)执行命令

win:dx --dex --output=D:\Users\ricky\Desktop\dex\classes2.dex D:\Users\ricky\Desktop\dex

mac:dx --dex --output=/Users/chenliguan/Desktop/dex/classes2.dex  /Users/chenliguan/Desktop/dex

命令解释:
–output=D:\Users\ricky\Desktop\dex\classes2.dex:指定输出路径
D:\Users\ricky\Desktop\dex :最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)

3.2 例子源码

(1)FixDexUtils.java

package com.dn.fixutils;

import android.content.Context;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashSet;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

/**
 * 热修复工具类
 */
public class FixDexUtils {
    private static HashSet<File> loadedDex = new HashSet<File>();

    static{
        loadedDex.clear();
    }

    /**
     * 加载修复的Dex
     */
    public static void loadFixedDex(Context context){
        if(context == null){
            return ;
        }
        //遍历所有的修复的dex
        File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
        File[] listFiles = fileDir.listFiles();
        for(File file:listFiles){
            if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){
                loadedDex.add(file);//存入集合
            }
        }
        //dex合并之前的dex
        doDexInject(context,fileDir,loadedDex);
    }

    private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj,value);
    }

    /**
     * 执行热修复
     */
    private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
        String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
        File fopt = new File(optimizeDir);
        if(!fopt.exists()){
            fopt.mkdirs();
        }
        //1.加载应用程序的dex
        try {
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();

            for (File dex : loadedDex) {
                //2.加载指定的修复的dex文件。
                DexClassLoader classLoader = new DexClassLoader(
                        dex.getAbsolutePath(),//String dexPath,
                        fopt.getAbsolutePath(),//String optimizedDirectory,
                        null,//String libraryPath,
                        pathLoader//ClassLoader parent
                );
                //3.合并
                Object dexObj = getPathList(classLoader);
                Object pathObj = getPathList(pathLoader);
                Object mDexElementsList = getDexElements(dexObj);
                Object pathDexElementsList = getDexElements(pathObj);
                //合并完成
                Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
                //重写给PathList里面的lement[] dexElements;赋值
                Object pathList = getPathList(pathLoader);
                setField(pathList,pathList.getClass(),"dexElements",dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    private static Object getPathList(Object baseDexClassLoader) throws Exception {
            return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
    }

    private static Object getDexElements(Object obj) throws Exception {
            return getField(obj,obj.getClass(),"dexElements");
    }

    /**
     * 两个数组合并
     * @param arrayLhs
     * @param arrayRhs
     * @return
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }

//  [9876] [12345]

//  [9876 12345]
}

(2)activity_main.xml

<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="com.dn.main.fix.com.dex.main.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

    <Button
        android:id="@+id/btn_test"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:onClick="test"
        android:text="test" />

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/btn_test"
        android:onClick="fix"
        android:text="fix" />
</RelativeLayout>

(3)MainActivity.java

package com.dex.main;

import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

public class MainActivity extends Activity {

    private static final int WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void test(View v) {
        MyTestClass myTestClass = new MyTestClass();
        myTestClass.testFix(this);
    }

    public void fix(View v) {
        /**
         * 1.检查权限
         */
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            /**
             * 2.申请授权
             * 第二个参数是需要申请的权限的字符串数组
             * 第三个参数为requestCode,主要用于回调的时候检测
             */
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    WRITE_EXTERNAL_STORAGE_REQUEST_CODE);
        } else {
            fixBug();
        }
    }

    /**
     * 处理权限申请回调
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case WRITE_EXTERNAL_STORAGE_REQUEST_CODE:
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                    fixBug();//调用方法
                } else {
                    Log.e("权限出错:", "权限出错");
                }
                return;

            default:
                break;
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    }

   /**
    * 修复bug
    */
    private void fixBug() {
        //目录:/data/data/packageName/odex
        File fileDir = getDir("odex", Context.MODE_PRIVATE);
        //往该目录下面放置我们修复好的dex文件。
        String name = "classes2.dex";
        String filePath = fileDir.getAbsolutePath() + File.separator + name;

        File file = new File(filePath);
        if (file.exists()) {
            file.delete();
        }

        //搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
        InputStream is = null;
        FileOutputStream os = null;
        try {
            String sourceFile = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + name;
            is = new FileInputStream(sourceFile);
            os = new FileOutputStream(filePath);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            File f = new File(filePath);
            if (f.exists()) {
                Toast.makeText(this, "dex重写成功", Toast.LENGTH_SHORT).show();
            }

            //加载修复的Dex
            FixDexUtils.loadFixedDex(this);
        } catch (Exception e) {
            Log.e("e:", e.toString());
            e.printStackTrace();
        }
    }
}

(4)MyTestClass.java

public class MyTestClass {
    public  void testFix(Context context){
        int i = 10;
        int a = 0;
        Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
    }
}

4 热修复原理

4.1 Android的ClassLoader体系

这里写图片描述

图片来源:MultiDex与热修复实现原理

/**
 * 默认加载器,只能加载/data/app中的apk文件。
 * 
 */
public class PathClassLoader extends BaseDexClassLoader {

}

/**
 * 加载任何路径的apk/dex/jar
 */
public class DexClassLoader extends BaseDexClassLoader {

}

4.2 DexClassLoader动态加载的实现

第一步:创建DexClassLoader对象,加载对应的apk/dex/jar文件。
(1)调用方法使用

//加载应用程序的dex
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();

for (File dex : loadedDex) {
    //加载指定的修复的dex文件。
    DexClassLoader classLoader = new DexClassLoader(
                        dex.getAbsolutePath(),//String dexPath,
                        fopt.getAbsolutePath(),//String optimizedDirectory,
                        null,//String libraryPath,
                        pathLoader//ClassLoader parent
                );
}
//合并
Object dexObj = getPathList(classLoader);
Object pathObj = getPathList(pathLoader);
Object mDexElementsList = getDexElements(dexObj);
Object pathDexElementsList = getDexElements(pathObj);
//合并完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重写给PathList里面的lement[] dexElements;赋值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);

(2)DexClassLoader构造函数

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

(3)BaseDexClassLoader构造函数

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;//DexPathList

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);//DexPathList构造函数
    }
}

(4)Element

static class Element {
   static class Element {
    private final File file;  // 它对应的就是需要加载的apk/dex/jar文件
    private final boolean isDirectory; // 第一个参数file是否为一个目录,一般为false,因为我们传入的是要加载的文件
    private final File zip;  // 如果加载的是一个apk或者jar或者zip文件,该对象对应的就是该apk或者jar或者zip文件
    private final DexFile dexFile; // 它是得到的dex文件
    ......
}
    ......
}

(5)DexPathList

final class DexPathList {
    private Element[] dexElements;

/**
 * definingContext对应的就是当前classLoader
 * dexPath对应的就是上面传进来的apk/dex/jar的路径
 * libraryPath就是上面传进来的加载的时候需要用到的lib库的目录,这个一般不用
 * optimizedDirectory就是上面传进来的dex的输出路径
 */
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);//调用makeDexElements()方法
}
}

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                                             List<IOException> suppressedExceptions,
                                             ClassLoader loader) {
        return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
    }

/**
 * files是一个ArrayList<File>列表,它对应的就是apk/dex/jar文件,因为我们可以指定多个文件。
 * optimizedDirectory是前面传入dex的输出路径
 * suppressedExceptions为一个异常列表
 */
private static Element[] makeElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions,boolean ignoreDexFiles,ClassLoader loader) {
        Element[] elements = new Element[files.size()];
        int elementsPos = 0;
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                zip = new File(split[0]);
                dir = new File(split[1]);
            } else if (file.isDirectory()) {
                // We support directories for looking up resources and native libraries.
                // Looking up resources in directories is useful for running libcore tests.
                elements[elementsPos++] = new Element(file, true, null, null);
            } else if (file.isFile()) {
                if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
                    // Raw dex file (not inside a zip/jar).
                    try {
                        dex = loadDexFile(file, optimizedDirectory, loader, elements);//调用loadDexFile()方法
                    } catch (IOException suppressed) {
                        System.logE("Unable to load dex file: " + file, suppressed);
                        suppressedExceptions.add(suppressed);
                    }
                } else {
                    zip = file;

                    if (!ignoreDexFiles) {
                        try {
                            dex = loadDexFile(file, optimizedDirectory, loader, elements);//调用loadDexFile()方法
                        } catch (IOException suppressed) {
                           suppressedExceptions.add(suppressed);
                        }
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements[elementsPos++] = new Element(dir, false, zip, dex);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

// file为需要加载的apk/dex/jar文件
// optimizedDirectorydex的输出dex文件路径
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);//调用了DexFile.loadDex()方法
        }
    }

//如果我们没有指定dex输出目录的话,就直接创建一个DexFile对象,
//如果我们指定了dex输出目录,我们就需要构造dex输出路径。

(6)DexFile.loadDex

static public DexFile loadDex(String sourcePathName, String outputPathName,
    int flags) throws IOException {
    return new DexFile(sourcePathName, outputPathName, flags);
}

(7)总结
a.在DexClassLoader我们指定了加载的apk/dex/jar文件和dex输出路径optimizedDirectory,它最终会被解析得到DexFile文件。
b.将DexFile文件对象放在Element对象里面,它对应的就是Element对象的dexFile成员变量。
c.将这个Element对象放在一个Element[]数组中,然后将这个数组返回给DexPathList的dexElements成员变量。
d.DexPathList是BaseDexClassLoader的一个成员变量。

这里写图片描述
图片来源:MultiDex与热修复实现原理

第二步:调用dexClassLoader的loadClass,得到加载的dex里面的指定的Class.

clazz = dexClassLoader.loadClass("com.example.apkplugin.PluginTest");

(1)ClassLoader.loadClass

public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, false);
}

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) {
                long t0 = System.nanoTime();
                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.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

(2)BaseDexClassLoader.findClass

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

(3)DexPathList.findClass

/**
 * 就是遍历dexElements数组,从每个Element对象中拿到DexFile类型的dex文件,
 * 然后就是从dex去加载所需要的class文件,直到找到为止。
 */
public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

(4)总结
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

5 MultiDex分包

Android进阶之使用multidex(产生多个dex)解决Dex超出方法数65535的限制

6 参考链接

MultiDex与热修复实现原理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值