android熱加載隨記

在咱們平常的開發過程當中,程序不免會出現BUG,通常有集中處理方式,發佈新版本APP讓用戶來升級,或者打補丁來修復bugjava

前者本文在這裏不錯討論,打補丁升級又分爲兩種一種是須要重啓應用,一種是不須要。不須要的也能夠叫他熱加載。android

首先使用熱加載須要瞭解一些基本常識git

一、什麼是dexgithub

Dex是Dalvik VM executes的全稱,和windows上的exe很像,你項目的源碼java文件已被編譯成了.dex.正則表達式

在用ide開發的時候編譯發佈構建工具(ant,gradle)會調用(aapt)將DEX文件,資源文件以及AndroidManifest.xml文件組合成一個應用程序包(APK)windows

二、安裝apk的過程是怎麼樣的數組

複製APK安裝包到data/app目錄下,解壓並掃描安裝包,把dex文件(Dalvik字節碼)保存到dalvik-cache目錄,並data/data目錄下建立對應的應用數據目app

ODEX是安卓上的應用程序apk中提取出來的可運行文件,即將APK中的classes.dex文件經過dex優化過程將其優化生成一個.dex文件單獨存放,原APK中的classes.dex文件會保留框架

這樣作能夠加快軟件的啓動速度,預先提取,減小對RAM的佔用,由於沒有odex的話,系統要從apk包中提取dex再運行eclipse

三、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
相關文章
相關標籤/搜索