三种内存区域定义如下:
方法区:当JVM使用类加载器定位class文件,并将其输入到内存中时,会提取class文件的类型信息,并将这些信息存储到方法区中。同时放入方法区中的还有该类型中的类静态变量。
堆内存:java程序在运行时创建的所有类型对象和数组都存储在堆中。JVM会根据new指令在堆中开辟一个确定类型的对象内存空间。但是堆中开辟对象的控件并没有任何人工指令可以回收,而是通过JVM的垃圾回收期负责回收。
Java栈:每启动一个线程(main),JVM都会为他分配一个java栈,用于存放方法中的局部变量,操作数以及异常数据等,当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入java栈中,方法执行完毕时,JVM会弹出该栈帧并释放掉。所以,我们app应用被压入的第一个栈帧就是ActiviytThread类的main()方法。当main被释放掉时,我们的app就退出了。
本地方法栈:native堆
类在没加载之前会在方法区声明一个int类型符号变量,它指向即将要加载内存的class类信息。当前类的字节码文件中的类信息,包括方法,成员变量。虚拟机加载的时候不会原封不动的把字节码信息加载到内存,而是会做一些调整,其中一点就会创建一个方法表(方法表实际上在native层是ArtMethod数组,方法表中的每一个方法都是ArtMethod结构体),这个方法表包含了这个类的所有方法(字节码转内存过程)。
类是从本地apk文件加载的,当我们安装一个apk时,会拷贝一份apk文件到系统data/app/xxxde1目录下,启动app应用就会通过dexfile读取第一个文件ActivityThread字节码文件,找到可执行的方法main,然后从类的方法表中将main方法压栈,形成一个栈帧。在ActivityThread类中会声明一个app加载的第二个类是application类。执行流程一样,会将onCreate方法压入栈,形成栈帧。所以方法的执行时通过压栈的方式执行的,方法的执行也是需要内存的(方法的执行最终是转为ArtMethod结构体去执行的)。伪代码如下:
class ActivityThread{ Application application; public static void main(string[] args){ application = new Application(); application.onCreate(...); } }在ActivityThread类中声明了Application 类,此时类是没有被加载到内存的,只有在主动引用的时候才会被加载到内存(new操作符、反射class.forName、jni.findClass等)。执行到这里只会在方法区创建一个int类型符号变量。当执行到new的时候,会从我们的apk文件中找到字节码文件,在方法区为它开辟一个存储空间(创建方法表),并且当前的符号变量指向它。此时,class类还没有加载完成,同时在堆区开辟另外一块内存,存放对象的变量信息,这个对象会指向符号变量(实际是通过klass指向它,klass是类的载体,是唯一的)。这就是通过对象能够找到类的原因,伪代码如下:
Student stu = new Student(); Class clazz =stu.getClass();//通过对象找到类对象内存分配完毕,此时执行到对象的onCreate方法,然后对象会送一个事件通过符号变量找到方法表中的该方法,并将它压栈,形成一个onCreate栈帧。
热升级:增量升级
两者异同点:都需要旧的apk与新的apk进行比对,生成差分包。实现的方式是一样的。不同点,目的性不一样
热修复是针对bug,热升级是根据版本升级。
andFix是通过替换方法表中的方法实现热修复的,是native层的实现
1、模拟一个bug类,其中有一个方法抛异常,代码如下:
package com.xinyartech.andfix.bug; /** * <pre> * desc : bug类 * </pre> */ public class AndFixTool { public int testMethod(){ throw new RuntimeException("报异常了,需要修复"); } }2、定义注解类,标明需要修复的类及方法,代码如下:
package com.xinyartech.andfix; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * <pre> * desc : 注解类 获取需要修复的类及方法 * </pre> */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface FixAnnotation { String clazz();//修复哪一个类 String method();//替换哪一个方法 }
3、模拟一个修复类,修复该方法,在修复方法上加入修复类的全路径,及方法名,代码如下:
package com.xinyartech.andfix.fix; import android.util.Log; import com.xinyartech.andfix.FixAnnotation; /** * <pre> * desc : 修复类 * </pre> */ public class AndFixTool { @FixAnnotation(clazz = "com.xinyartech.andfix.bug.AndFixTool",method = "testMethod") public int testMethod(){ Log.e("AndFixTool","已经修复了"); return 10; } }4、借助android studio编译,build一下项目,生成字节码文件。将编译的字节码文件夹拷贝到桌面,备后续使用,注意:红色的是包含包名的路径
5、删除和修复包无用的字节码文件,如下:
6、使用dx.bat命令将当前修复包打成dex文件,命令如下,需要在环境变量中配置dx命令
7、执行dx生成一个dex文件
这个fix.dex文件就是我们的修复文件,一般需要我们将这个文件放到服务器,供用户下载,目前我们手动把修复包放到手机sd卡
8、在Activity中模拟修复方法,fixMethod(),代码如下:
public void fixMethod(View view) { File file = new File(Environment.getExternalStorageDirectory(), "fix.dex"); DexManager dexManager = new DexManager(this); dexManager.load(file); }DexManager代码如下:
public class DexManager { private Context context; public DexManager(Context context) { this.context = context; } public void load(File file) { try { //通过dexFile下载dex文件,这个是fix.dex DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE); //获取当前的dex里面的class 类名集合 Enumeration<String> entry=dexFile.entries(); while (entry.hasMoreElements()) { String clazzName= entry.nextElement(); Class realClazz= dexFile.loadClass(clazzName, context.getClassLoader()); if (realClazz != null) { fixClazz(realClazz); } } } catch (IOException e) { e.printStackTrace(); } } private void fixClazz(Class realClazz) { Method[] methods=realClazz.getMethods(); for (Method rightMethod : methods) { Replace replace = rightMethod.getAnnotation(Replace.class); if (replace == null) { continue; } //获取要修复的类名和方法 String clazzName=replace.clazz(); String methodName=replace.method(); try { //获取有bug的class Class wrongClazz= Class.forName(clazzName); //获取有bug的方法 Method wrongMethod = wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes()); //native替换方法 replace(wrongMethod, rightMethod); } catch (Exception e) { e.printStackTrace(); } } } //native层方法 public native static void replace(Method wrongMethod, Method rightMethod) ; }9、native层实现
(1)需要拷贝系统的和ArtMethod相关的两个文件,目录如下:E:\BaiduNetdiskDownload\Android系统源码\android-5.0.0_r7\android-5.0.0_r7\art\runtime\mirror。。文件名为object.h和art_method.h拷贝到cpp目录下。然后在natice-lib.cpp中添加如下代码:
#include <jni.h> #include <string> #include "art_method.h"//引入头文件 extern "C" JNIEXPORT jstring JNICALL JNIEXPORT void JNICALL Java_com_xinyartech_andfix_DexManager_replace(JNIEnv *env, jclass type, jobject wrongMethod, jobject rightMethod) { //ArtMethod Android 系统源码中 art::mirror::ArtMethod *wrong= (art::mirror::ArtMethod *)env->FromReflectedMethod(wrongMethod); art::mirror::ArtMethod *right= (art::mirror::ArtMethod *)env->FromReflectedMethod(rightMethod); // method --->class --->ClassLoader wrong->declaring_class_ = right->declaring_class_; wrong->dex_cache_resolved_methods_ = right->dex_cache_resolved_methods_; wrong->access_flags_ = right->access_flags_; wrong->dex_cache_resolved_types_ = right->dex_cache_resolved_types_; wrong->dex_code_item_offset_ = right->dex_code_item_offset_; // 这里 方法索引的替换 wrong->method_index_ = right->method_index_; wrong->dex_method_index_ = right->dex_method_index_; }注意:这里引入的是5.0系统的.h文件,所以只适用5.0系统的热修复,这也是andfix严重缺陷。如果需要其他版本的也能够兼容,就需要对不同的系统文件进行引入
原因是这样的,每一个系统版本谷歌工程师都会对ArtMethod结构体动刀,导致有些参数的长度有变化,比如在5.0系统是32位的,在6.0可能就会被改成16位,这样改会导致我们的结构体结构会发生变化,发生溢出。但我们实际操控结构体的时候,就会发现有些变量找不到的情况。其实阿里开源的AndFix针对每个系统版本都做了兼容,不过目前只到7.0系统,后面的版本没有继续更新维护了。截图如下:
要解决所有版本的兼容问题,那么阿里推出了sophix热修复,它是在andfix的基础上解决了上面的问题,但是它是收费的,5000台手机免费。其实解决思路应该就是artmethod结构体的大小不能有变化。但是,目前不得而知。