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;}@Overridepublic 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;@Overrideprotected 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() {@Overridepublic void onClick(View arg0) {new CalcNum(getApplicationContext());}});btnfix.setOnClickListener(new OnClickListener() {@Overridepublic 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和系统的dexElementsObject 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项目标签 --> <projectname="MultiDex"default="release" ><!-- 项目编译环境配置 --><propertyname="sdk-folder"value="D:\Android\SDK" /><propertyname="platform-folder"value="${sdk-folder}\platforms\android-20" /><propertyname="platform-tools-folder"value="${sdk-folder}\build-tools\20.0.0" /><propertyname="jdk-folder"value="C:\Program Files\Java\jdk1.8.0_77" /><propertyname="android-jar"value="${platform-folder}\android.jar" /><propertyname="tools.aapt"value="${platform-tools-folder}/aapt.exe" /><propertyname="tools.javac"value="${jdk-folder}\bin\javac.exe" /><propertyname="tools.dx"value="${platform-tools-folder}\dx.bat" /><propertyname="tools.apkbuilder"value="${sdk-folder}\tools\apkbuilder.bat" /><propertyname="tools.jarsigner"value="${jdk-folder}\bin\jarsigner.exe" /><!-- 项目输入目录配置 --><propertyname="project-dir"value="." /><propertyname="assets"value="${project-dir}\assets" /><propertyname="res"value="${project-dir}\res" /><propertyname="src"value="${project-dir}\src" /><propertyname="libs"value="${project-dir}\libs" /><!-- 项目输出目录配置 --><propertyname="bin"value="${project-dir}\bin" /><propertyname="gen"value="${project-dir}\gen" /><propertyname="manifest"value="${project-dir}\AndroidManifest.xml" /><!-- 生成文件放置地方 --><propertyname="java-file-gen"value="${gen}\com\example\andfix\*.java" /><propertyname="java-file-src"value="${src}\com\example\andfix\*.java" /><propertyname="main-dex-name"value="${bin}\classes.dex" /><propertyname="sub-dex-name"value="${bin}\classes2.dex" /><propertyname="package-temp-name"value="${bin}\${ant.project.name}.arsc" /><!-- 未签名包 --><propertyname="unsigned-apk-name"value="${ant.project.name}_unsigned.apk" /><propertyname="unsigned-apk-path"value="${bin}\${unsigned-apk-name}" /><!-- 签名包 --><propertyname="signed-apk-name"value="${ant.project.name}.apk" /><propertyname="signed-apk-path"value="${bin}\${signed-apk-name}" /><!-- 密钥 --><propertyname="keystore-name"value="${project-dir}\rearviewkey.keystore" /><propertyname="keystore-alias"value="rearview" /><propertyname="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类文件 --><targetname="gen-R"depends="init" ><echo message="Generating R.java from the resources." /><execexecutable="${tools.aapt}"failοnerrοr="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文件 --><targetname="compile"depends="gen-R" ><echo message="compile..." /><javacbootclasspath="${android-jar}"destdir="${bin}"compiler="javac1.8"encoding="utf-8"includeantruntime="false"listfiles="true"target="1.6"><src path="${project-dir}" /><classpath><!-- 引入第三方jar包所需要引用,用于辅助编译,并没有将jar打包进去。 --><filesetdir="${libs}"includes="*.jar" /></classpath></javac></target><!-- 构建多分包dex文件 --><targetname="multi-dex"depends="compile" ><echo message="Generate multi-dex..." /><execexecutable="${tools.dx}"failοnerrοr="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) --><targetname="package"depends="multi-dex" ><echo message="package-res-and-assets..." /><execexecutable="${tools.aapt}"failοnerrοr="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><!-- 对临时目录进行打包 --><targetname="build-unsigned-apk"depends="package" ><echo message="Build-unsigned-apk" /><javaclassname="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项目的根目录下 --><targetname="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文件 --><targetname="add-subdex-toapk"depends="copy_dex" ><echo message="Add subdex to apk..." /><foreachparam="dir.name"target="aapt-add-dex" ><path><filesetdir="${bin}"includes="classes*.dex" /></path></foreach></target><!-- 使用aapt命令添加dex文件 --><target name="aapt-add-dex" ><echo message="${dir.name}" /><echo message="执行了app" /><!-- 使用正则表达式获取classes的文件名 --><propertyregexcasesensitive="false"input="${dir.name}"property="dexfile"regexp="classes(.*).dex"select="\0" /><if><equalsarg1="${dexfile}"arg2="classes.dex" /><then><echo>${dexfile} is not handle</echo></then><else><echo>${dexfile} is handle</echo><execexecutable="${tools.aapt}"failοnerrοr="true" ><arg value="add" /><arg value="${unsigned-apk-path}" /><arg value="${dexfile}" /></exec></else></if><delete file="${project-dir}\${dexfile}" /></target><!-- 生成签名的apk --><targetname="sign-apk"depends="add-subdex-toapk" ><echo message="Sign apk..." /><execexecutable="${tools.jarsigner}"failοnerrοr="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><!-- 签名发布 --><targetname="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/DroidPluginppt介绍:https://github.com/Qihoo360/DroidPlugin/tree/master/DOCDemo:https://github.com/SpikeKing/wcl-plugin-test-app详解:http://blog.csdn.net/yzzst/article/details/48093567 http://v2ex.com/t/2164942.AndFix 用途:热修复GitHub地址:https://github.com/alibaba/AndFix讲解:http://blog.csdn.net/yzzst/article/details/48465031http://blog.csdn.net/qxs965266509/article/details/49816007http://blog.csdn.net/yaya_soft/article/details/504601023.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/14edcb444c514.Small 用途:动态加载GitHub地址:https://github.com/wequick/SmallDemo:https://github.com/cayden/MySmall5. DynamicAPK 用途:动态加载、热修复案例:携程GitHub地址:https://github.com/CtripMobile/DynamicAPK详解:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading6.ClassPatch 用途:热修复GitHub地址:https://github.com/Jarlene/ClassPatch详解:http://blog.csdn.net/xwl198937/article/details/498019757.ACDD 用途:动态加载GitHub地址:https://github.com/bunnyblue/ACDD8.HotFix 用途:热修复GitHub地址:https://github.com/dodola/HotFix该项目是基于QQ空间终端开发团队的技术文章实现的9.Nuwa 用途:热修复GitHub地址:https://github.com/jasonross/Nuwa详解:http://www.jianshu.com/p/72c17fb76f21/comments/128004610.DroidFix 用途:热修复GitHub地址:https://github.com/bunnyblue/DroidFix详解:http://bunnyblue.github.io/DroidFix/11.AndroidDynamicLoader 用途:动态加载GitHub地址:https://github.com/mmin18/AndroidDynamicLoaderDemo:https://github.com/mmin18/AndroidDynamicLoader/raw/master/host.apk
android热加载随记相关推荐
- 【Flutter】Flutter 混合开发 ( 混合开发中 Flutter 的 热重启 / 热加载 )
文章目录 前言 一.混合开发中启用 Flutter 的 热重启 / 热加载 二.混合开发中 Flutter 的 热重启 / 热加载 命令测试 三.指定混合应用连接的设备 四.相关资源 前言 上一篇博客 ...
- Android动态加载进阶 代理Activity模式
基本信息 作者:kaedea 项目:android-dynamical-loading 技术背景 简单模式中,使用ClassLoader加载外部的Dex或Apk文件,可以加载一些本地APP不存在的类, ...
- Android动态加载技术
基本信息 Author:kaedea GitHub:android-dynamical-loading 我们很早开始就在Android项目中采用了动态加载技术,主要目的是为了达到让用户不用重新安装AP ...
- Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/53939176 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭 ...
- Flutter开发(二十九):Flutter热重启、热加载、调试与发布应用
1.Flutter热重启.热加载 2.混合开发调试 dart 代码 3.发布应用 1.Flutter热重启.热加载 Flutter自带热重启/热加载的功能,但是到了 Android 项目中集成 Flu ...
- ANDROID动态加载 使用SO库时要注意的一些问题
转载自:http://blog.csdn.net/qq_23331691/article/details/51699888 基本信息 作者:kaedea 项目:android-dynamical-lo ...
- Flutter热更新与热加载
之前偶然从Flutter官方文档上看到了支持热更新,当然这是从2019年才开始的 Dynamic updates The Dart Platform, on which Flutter is buil ...
- 热加载和热部署,没听过?看看 Tomcat 是怎么实现的
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者 | 不学无数的程序员 来源 | https://urlify ...
- 前端使用fis3开启本地服务器,并实现热加载功能
为了摆脱调试页面的时候需要不断切换刷新的痛苦,热加载就是因此而生,自动刷新是如此快乐 使用fis3便可以帮我们轻松实现,如果不知道什么是fis3,你可以猛点这里:fis3 接下来就要开始了: 安装fi ...
- android edittext html 图片,Android EditText加载HTML内容(内容包含网络图片) -电脑资料...
android中的Html.fromHtml可以用来加载HTML的内容,fromHtml有三个参数需要设置,第一个是要显示的html内容,第二个就是要说的重点,ImageGetter,用来处理图片加载 ...
最新文章
- 机器视觉检测中的图像预处理方法
- 寻找XenApp的下载
- 构建JSE 开发环境(图文并茂)
- 网页简单配置捉取网购信息
- 我是如何从一个新闻狗转行成为程序猿的?
- [Qt教程] 第35篇 网络(五)获取本机网络信息
- shell 非_Shell基本操作(一)
- 前端_网页编程 HTTP协议(进阶)
- 生成ltx文件命令_利用二次开发工具批量生成PCDMIS程序
- iOS的消息机制和消息转发
- python和java学哪个好-Python和Java学哪个好?大家是怎么选的
- 程序员面试金典——4.7最近公共祖先
- Atitit 图片 验证码生成attilax总结
- VTK(五)---内窥镜漫游(基于VMTK血管中心线提取)
- 手机麦克风结构原理图_麦克风的分类和工作原理
- html5 canvas 涂鸦画板
- 程序员、架构师、技术总监、CTO
- 计算机日常英语句子,计算机英语句子
- matplotlib.pyplot超详细入门总结
- ASM介绍及简易教程
热门文章
- selenium 如何处理table
- MacDev.GarbageCollectionIsDeprecated-WhenXcodeCompileMacAppProject
- MVC3----配置连接数据库
- 奇葩错误SLF4J: Failed to load class org.slf4j。。的修复
- 一个高性能的key/value存储服务器 - Tarantool/Box
- Oracle数据泵对已经存在的表加载索引
- Android WiFi Dhcp 获取到 IP 和配置的过程
- CentOS 6系统FreeSwitch和RTMP服务 安装及演示(四)
- linux的审计功能(audit)
- 注册(三)之设置Contact地址的过期参数