一、首先需要了解java的内存管理机制,大致分为3类
- 方法区
- 堆区
- 栈区
三种内存区域定义如下:
方法区:当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栈帧。
三、热修复分类:两大类
- native层:andfix、sophix
- java层:tinker、robsut
热升级:增量升级
两者异同点:都需要旧的apk与新的apk进行比对,生成差分包。实现的方式是一样的。不同点,目的性不一样
热修复是针对bug,热升级是根据版本升级。
四、AndFix
andFix是通过替换方法表中的方法实现热修复的,是native层的实现
五、手写AndFix修复
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结构体的大小不能有变化。但是,目前不得而知。