在咱們平常的開發過程當中,程序不免會出現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