android热加载随记

在我们日常的开发过程中,程序难免会出现BUG,一般有集中处理方式,发布新版本APP让用户来升级,或者打补丁来修复bug

前者本文在这里不错讨论,打补丁升级又分为两种一种是需要重启应用,一种是不需要。不需要的也可以叫他热加载。

首先使用热加载需要了解一些基本常识

1、什么是dex

Dex是Dalvik VM executes的全称,和windows上的exe很像,你项目的源码java文件已被编译成了.dex.

在用ide开发的时候编译发布构建工具(ant,gradle)会调用(aapt)将DEX文件,资源文件以及AndroidManifest.xml文件组合成一个应用程序包(APK)

2、安装apk的过程是怎么样的

复制APK安装包到data/app目录下,解压并扫描安装包,把dex文件(Dalvik字节码)保存到dalvik-cache目录,并data/data目录下创建对应的应用数据目

ODEX是安卓上的应用程序apk中提取出来的可运行文件,即将APK中的classes.dex文件通过dex优化过程将其优化生成一个.dex文件单独存放,原APK中的classes.dex文件会保留

这样做可以加快软件的启动速度,预先提取,减少对RAM的占用,因为没有odex的话,系统要从apk包中提取dex再运行

3、app怎么运行的

简单的概括一下,就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?

来看看代码

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

理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图

以上就大致清楚了要做到热加载我们该怎么处理了

下面我们处理一个简单逻辑,用Toast 显示一个 除数为零的  模拟bug

接着我们创建一个application

package com.example.andfix;

import android.app.Application;

public class App extends Application{
    private static Application _app;
    public static Application get()
    {
        return _app;
    }
    @Override
    public void onCreate() {
        _app=this;
        super.onCreate();
    }
    
    
}

在建立一个Activity

package com.example.andfix;


import java.io.File;
import java.io.IOException;

import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

import com.example.andfix.tools.CalcNum;

public class MainActivity extends Activity {

    Button btnfix;
    Button btntest;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnfix=(Button)findViewById(R.id.btnfix);
        btntest=(Button)findViewById(R.id.btntest);
        
        btntest.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View arg0) {
                new CalcNum(getApplicationContext());
            }
        });
        btnfix.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View arg0) {
                fix();
            }
        });
    }

    private void fix()
    {
        inject();
    }
    
    
    
    public void inject() {
        String sourceFile = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + "classes2.dex";


        String targetFile = this.getDir("odex", Context.MODE_PRIVATE).getAbsolutePath() + File.separator
                + "classes2.dex";

        try {
     
            FileUtils.copyFile(sourceFile, targetFile);

            FixDexUtils.loadFixDex(this.getApplication());

        } catch (IOException e) {
            e.printStackTrace();
        }



    }
    
    
    
    
    
    

}

 

一个工具类

package com.example.andfix;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class FileUtils {
    public static void copyFile(String sourceFile, String targetFile) throws IOException {

        InputStream is = new FileInputStream(sourceFile);

        File outFile = new File(targetFile);
        
        if(outFile.exists()){
            outFile.delete();
        }
        
        OutputStream os = new FileOutputStream(targetFile);

        int len = 0;

        byte[] buffer = new byte[1024];

        while ((len = is.read(buffer)) != -1) {
            os.write(buffer, 0, len);
        }

        os.close();
        is.close();

    }
}

 一个热修复逻辑

package com.example.andfix;

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;

import android.content.Context;

public class FixDexUtils {
    private static HashSet<File> loadedDex = new HashSet<File>();

    static {
        loadedDex.clear();
    }


    public static void loadFixDex(Context context) {
        // 获取到系统的odex 目录
        File fileDir = context.getDir("odex", Context.MODE_PRIVATE);
        File[] listFiles = fileDir.listFiles();

        for (File file : listFiles) {
            if (file.getName().endsWith(".dex")) {
                // 存储该目录下的.dex文件(补丁)
                loadedDex.add(file);
            }
        }

        doDexInject(context, fileDir);

    }

    private static void doDexInject(Context context, File fileDir) {
        // .dex 的加载需要一个临时目录
        String optimizeDir = fileDir.getAbsolutePath() + File.separator + "opt_dex";
        File fopt = new File(optimizeDir);
        if (!fopt.exists())
            fopt.mkdirs();
        // 根据.dex 文件创建对应的DexClassLoader 类
        for (File file : loadedDex) {
            DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), fopt.getAbsolutePath(), null,
                    context.getClassLoader());
            //注入
            inject(classLoader, context);

        }
    }

    private static void inject(DexClassLoader classLoader, Context context) {

        // 获取到系统的DexClassLoader 类
        PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
        try {
            // 分别获取到补丁的dexElements和系统的dexElements
            Object dexElements = combineArray(getDexElements(getPathList(classLoader)),
                    getDexElements(getPathList(pathLoader)));
            // 获取到系统的pathList 对象
            Object pathList = getPathList(pathLoader);
            // 设置系统的dexElements 的值
            setField(pathList, pathList.getClass(), "dexElements", dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 通过反射设置字段值
     */
    private static void setField(Object obj, Class<?> cl, String field, Object value)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {

        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     * 通过反射获取 BaseDexClassLoader中的PathList对象
     */
    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 通过反射获取指定字段的值
     */
    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);
    }

    /**
     * 通过反射获取DexPathList中dexElements
     */
    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return getField(paramObject, paramObject.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;
    }
}

这样就可以实现热修复了 此过程是在eclipse 上完成的

通过ant构建

<?xml version="1.0" encoding="UTF-8"?>
<!-- project项目标签 -->
<project
    name="MultiDex"
    default="release" >

    <!-- 项目编译环境配置 -->

    <property
        name="sdk-folder"
        value="D:\Android\SDK" />

    
    <property
        name="platform-folder"
        value="${sdk-folder}\platforms\android-20" />

    <property
        name="platform-tools-folder"
        value="${sdk-folder}\build-tools\20.0.0" />

    <property
        name="jdk-folder"
        value="C:\Program Files\Java\jdk1.8.0_77" />

    <property
        name="android-jar"
        value="${platform-folder}\android.jar" />

    <property
        name="tools.aapt"
        value="${platform-tools-folder}/aapt.exe" />

    <property
        name="tools.javac"
        value="${jdk-folder}\bin\javac.exe" />

    <property
        name="tools.dx"
        value="${platform-tools-folder}\dx.bat" />

    <property
        name="tools.apkbuilder"
        value="${sdk-folder}\tools\apkbuilder.bat" />

    <property
        name="tools.jarsigner"
        value="${jdk-folder}\bin\jarsigner.exe" />

    <!-- 项目输入目录配置 -->

    <property
        name="project-dir"
        value="." />

    <property
        name="assets"
        value="${project-dir}\assets" />

    <property
        name="res"
        value="${project-dir}\res" />

    <property
        name="src"
        value="${project-dir}\src" />

    <property
        name="libs"
        value="${project-dir}\libs" />

    <!-- 项目输出目录配置 -->

    <property
        name="bin"
        value="${project-dir}\bin" />

    <property
        name="gen"
        value="${project-dir}\gen" />

    <property
        name="manifest"
        value="${project-dir}\AndroidManifest.xml" />
    <!-- 生成文件放置地方 -->

    <property
        name="java-file-gen"
        value="${gen}\com\example\andfix\*.java" />

    <property
        name="java-file-src"
        value="${src}\com\example\andfix\*.java" />

    <property
        name="main-dex-name"
        value="${bin}\classes.dex" />

    <property
        name="sub-dex-name"
        value="${bin}\classes2.dex" />

    <property
        name="package-temp-name"
        value="${bin}\${ant.project.name}.arsc" />
    <!-- 未签名包 -->

    <property
        name="unsigned-apk-name"
        value="${ant.project.name}_unsigned.apk" />

    <property
        name="unsigned-apk-path"
        value="${bin}\${unsigned-apk-name}" />
    <!-- 签名包 -->

    <property
        name="signed-apk-name"
        value="${ant.project.name}.apk" />

    <property
        name="signed-apk-path"
        value="${bin}\${signed-apk-name}" />
    <!-- 密钥 -->

    <property
        name="keystore-name"
        value="${project-dir}\rearviewkey.keystore" />

    <property
        name="keystore-alias"
        value="rearview" />

    <property
        name="main-dex-rule"
        value="${project-dir}\main-dex-rule.txt" />

    <taskdef resource="net/sf/antcontrib/antlib.xml" >
        <classpath> 
            <pathelement location="I:\ant-contrib.jar"/> 
        </classpath> 
    </taskdef>

    <!-- 初始化target -->

    <target name="init" >

        <echo message="init..." />

        <delete includeemptydirs="true" >

            <fileset dir="${bin}" >

                <include name="**/*" >
                </include>
            </fileset>
        </delete>

        <mkdir dir="${bin}" />
    </target>

    <!-- 生成R.java类文件 -->

    <target
        name="gen-R"
        depends="init" >

        <echo message="Generating R.java from the resources." />

        <exec
            executable="${tools.aapt}"
            failonerror="true" >

            <!-- package表示打包 -->

            <arg value="package" />

            <arg value="-f" />

            <arg value="-m" />

            <arg value="-J" />

            <arg value="${gen}" />

            <arg value="-S" />

            <arg value="${res}" />

            <arg value="-M" />
        
            <arg value="${manifest}" />

            <arg value="-I" />

            <arg value="${android-jar}" />
        </exec>
    </target>

    <!-- 编译源文件生成对应的class文件 -->

    <target
        name="compile"
        depends="gen-R" >

        <echo message="compile..." />

        <javac
            bootclasspath="${android-jar}"
            destdir="${bin}"
            compiler="javac1.8"
            encoding="utf-8"
            includeantruntime="false"
            listfiles="true"
            target="1.6">

            <src path="${project-dir}" />
    
            <classpath>

                <!-- 引入第三方jar包所需要引用,用于辅助编译,并没有将jar打包进去。 -->

                <fileset
                    dir="${libs}"
                    includes="*.jar" />
            </classpath>
        </javac>
    </target>

    <!-- 构建多分包dex文件 -->

    <target
        name="multi-dex"
        depends="compile" >

        <echo message="Generate multi-dex..." />

        <exec
            executable="${tools.dx}"
            failonerror="true" >
            <arg value="--dex" />
            <arg value="--multi-dex" />
            <!-- 多分包命令,每个包最大的方法数为10000 -->
            <arg value="--set-max-idx-number=10000" />
            <arg value="--main-dex-list" />
            <!-- 主包包含class文件列表 -->
            <arg value="${main-dex-rule}" />
            <arg value="--minimal-main-dex" />
            <arg value="--output=${bin}" />
            <!-- 把bin下所有class打包 -->
            <arg value="${bin}" />
            <!-- 把libs下所有jar打包 -->
            <!-- <arg value="${libs}" /> -->
        </exec>
    </target>

    <!-- 打包资源文件(包括res、assets、AndroidManifest.xml) -->

    <target
        name="package"
        depends="multi-dex" >

        <echo message="package-res-and-assets..." />

        <exec
            executable="${tools.aapt}"
            failonerror="true" >

            <arg value="package" />

            <arg value="-f" />

            <arg value="-S" />

            <arg value="${res}" />

            <arg value="-A" />

            <arg value="${assets}" />

            <arg value="-M" />

            <arg value="${manifest}" />

            <arg value="-I" />

            <arg value="${android-jar}" />

            <arg value="-F" />
            <!-- 放到临时目录中 -->

            <arg value="${package-temp-name}" />
        </exec>
    </target>
    <!-- 对临时目录进行打包 -->

    <target
        name="build-unsigned-apk"
        depends="package" >

        <echo message="Build-unsigned-apk" />

        <java
            classname="com.android.sdklib.build.ApkBuilderMain"
            classpath="${sdk-folder}/tools/lib/sdklib.jar" >

            <!-- 输出路径 -->

            <arg value="${unsigned-apk-path}" />

            <arg value="-u" />

            <arg value="-z" />

            <arg value="${package-temp-name}" />

            <arg value="-f" />

            <arg value="${main-dex-name}" />

            <arg value="-rf" />

            <arg value="${src}" />

            <arg value="-rj" />

            <arg value="${libs}" />
        </java>
    </target>

    <!-- 拷贝文件到apk项目的根目录下 -->

    <target
        name="copy_dex"
        depends="build-unsigned-apk" >

        <echo message="copy dex..." />

        <copy todir="${project-dir}" >

            <fileset dir="${bin}" >

                <include name="classes*.dex" />
            </fileset>
        </copy>
    </target>

    <!-- 循环遍历bin目录下的所有dex文件 -->
    <target
        name="add-subdex-toapk"
        depends="copy_dex" >

        <echo message="Add subdex to apk..." />

        <foreach
            param="dir.name"
            target="aapt-add-dex" >

            <path>

                <fileset
                    dir="${bin}"
                    includes="classes*.dex" />
            </path>
        </foreach>
    </target>

    <!-- 使用aapt命令添加dex文件 -->

    <target name="aapt-add-dex" >
        <echo message="${dir.name}" />
        <echo message="执行了app" />
        <!-- 使用正则表达式获取classes的文件名 -->
        <propertyregex
            casesensitive="false"
            input="${dir.name}"
            property="dexfile"
            regexp="classes(.*).dex"
            select="\0" />
        <if>
            <equals
                arg1="${dexfile}"
                arg2="classes.dex" />
            <then>
                <echo>
                   ${dexfile} is not handle
                </echo>
            </then>
            <else>
                <echo>
                    ${dexfile} is handle
                </echo>
                <exec
                    executable="${tools.aapt}"
                    failonerror="true" >
                    <arg value="add" />
                    <arg value="${unsigned-apk-path}" />
                    <arg value="${dexfile}" />
                </exec>
            </else>
        </if>
        <delete file="${project-dir}\${dexfile}" />
    </target>

    <!-- 生成签名的apk -->
    <target
        name="sign-apk"
        depends="add-subdex-toapk" >

        <echo message="Sign apk..." />

        <exec
            executable="${tools.jarsigner}"
            failonerror="true" >
            <!-- keystore -->
            <arg value="-keystore" />
            <arg value="${keystore-name}" />
            <!-- 秘钥 -->
            <arg value="-storepass" />
            <arg value="111111" />
            <!-- 秘钥口令 -->
            <arg value="-keypass" />
            <arg value="111111" />
            <arg value="-signedjar" />
            <!-- 签名的apk -->
            <arg value="${signed-apk-path}" />
            <!-- 未签名的apk -->
            <arg value="${unsigned-apk-path}" />
            <!-- 别名 -->
            <arg value="${keystore-alias}" />
        </exec>
    </target>

    <!-- 签名发布 -->

    <target
        name="release"
        depends="sign-apk" >

        <delete file="${package-temp-name}" />

        <delete file="${unsigned-apk-path}" />

        <echo>
            APK is released.path:${signed-apk-path}
        </echo>
    </target>

</project> 

主dex文件包含的类说明

com/example/andfix/MainActivity.class
com/example/andfix/App.class
com/example/andfix/FileUtils.class
com/example/andfix/FixDexUtils.class

文档结构如下

实现过程中也有很多坑

比如:

com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000)

解决方法就是降低你的编译版本(jdk)

如果你在过程中遇到其他问题,不要怕麻烦一点一点采坑。走过来就是一种收获

当然本文只是描述热加载的过程和原理

ps:现在这样的框架也有很多

1.DroidPlugin
用途:动态加载

使用案例:360手机助手

GitHub地址:https://github.com/Qihoo360/DroidPlugin

ppt介绍:https://github.com/Qihoo360/DroidPlugin/tree/master/DOC

Demo:https://github.com/SpikeKing/wcl-plugin-test-app

详解:

http://blog.csdn.net/yzzst/article/details/48093567 

http://v2ex.com/t/216494



2.AndFix
用途:热修复

GitHub地址:https://github.com/alibaba/AndFix

讲解:

http://blog.csdn.net/yzzst/article/details/48465031

http://blog.csdn.net/qxs965266509/article/details/49816007

http://blog.csdn.net/yaya_soft/article/details/50460102

3.dexposed
用途:热修复

GitHub地址:https://github.com/alibaba/dexposed

讲解:                 

http://blog.csdn.net/yzzst/article/details/47954479     

http://blog.csdn.net/yzzst/article/details/47659987     

http://www.jianshu.com/p/14edcb444c51

4.Small
用途:动态加载

GitHub地址:https://github.com/wequick/Small

Demo:https://github.com/cayden/MySmall

5. DynamicAPK
用途:动态加载、热修复

案例:携程

GitHub地址:https://github.com/CtripMobile/DynamicAPK

详解:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading

6.ClassPatch
用途:热修复

GitHub地址:https://github.com/Jarlene/ClassPatch

详解:http://blog.csdn.net/xwl198937/article/details/49801975

7.ACDD
用途:动态加载

GitHub地址:https://github.com/bunnyblue/ACDD

8.HotFix
用途:热修复

GitHub地址:https://github.com/dodola/HotFix

该项目是基于QQ空间终端开发团队的技术文章实现的

9.Nuwa
用途:热修复

GitHub地址:https://github.com/jasonross/Nuwa

详解:http://www.jianshu.com/p/72c17fb76f21/comments/1280046

10.DroidFix
用途:热修复

GitHub地址:https://github.com/bunnyblue/DroidFix

详解:http://bunnyblue.github.io/DroidFix/

11.AndroidDynamicLoader
用途:动态加载

GitHub地址:https://github.com/mmin18/AndroidDynamicLoader

Demo:https://github.com/mmin18/AndroidDynamicLoader/raw/master/host.apk

 

posted @ 2016-11-03 18:19  keepsilence  阅读(2186)  评论(0编辑  收藏  举报