当我们开发完一个APP,打包成了apk装进了手机,然后启动和使用APP,这一个过程中,必定会使用各种的类和方法,有系统的有自己的,那这些类都是如何加载成功,供我们使用的呢?
例如有一个A类,我们使用的时候,一般是new A()创建个对象,然后使用,到底是怎么new的?其实就是Android的类加载帮我们做的。
apk的组成
懂apk的打包流程或者反编译过apk的都知道,apk里面会有一个或者多个的dex文件,这些都是我们的代码,但是是经过处理的代码,把我们的代码转化成电脑能懂的代码,其实就是字节码。
安装apk
这里需要分一下版本,不同的版本安装机制有点区别。
Android N(7.0)以上的:
安装apk的时候,不进行任何的预编译(提高安装速度);
运行的过程中解析执行,并且对经常使用的方法进行优化,就是即时编译(JIT just in time),经过JIT处理的代码,都会记录在一个profile配置文件里;
最后在手机闲的时候,有一个编译守护进程,会对profile里面的方法进行预先编译(AOT),把这些代码转化为本地机器码。
Android L(5.0)- Android N:
安装时直接使用预先编译(AOT),就是把所有代码一次性转化为本地机器码,当需要使用时就可以直接使用了。但是缺点就是第一次安装的时候十分的慢,因为一次性转化全部代码很耗时。
Android 2.2-4.4:这部分都是使用JIT,就是说都是在运行的时候,需要用什么,就加载什么,好处就是安装贼快,但缺点也明显,每次运行都需要重新编译,浪费资源,例如电量。
关键类:ClassLoader
image.png我们先了解下类加载的所有类关系。
①BootClassLoader:用来加载系统framework层的class文件。
②BaseDexClassLoader:衍生出PathClassLoader和DexClassLoader。
③ PathClassLoader:Android应用的类加载器,也就是我们写的类,都由这个来加载。
④DexClassLoader:这个是加载一些额外的动态类。
类加载
安装成功了,打开APP,每当我们使用类,创建对象的时候,类加载器都会帮我们从代码里面找出我们要的那个类,然后加载给我们用。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//交给父类加载器去加载 递归
c = parent.loadClass(name, false);
} else {
//这里是如果父类加载器是null(也就是bootstrap),那这个方法会去查找name指向的这个类是不是由bootstrap加载了,是的话就返回class对象,不是的话返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
可以看到,加载一个类,会分为2步
1、Class<?> c = findLoadedClass(name);找缓存,如果已经被加载过了,就直接return返回。
2、如果没有缓存,就根据变量parent是否为null来判断逻辑,如果不为null,就直接调用parent的loadClass方法;如果为null,就调用findBootStrapClassOrNull方法。
这里值得注意的是这个parent,其实这里使用了双亲委托机制(先把任务交给父类去处理,直到没有父类或者父类处理不了,才自己去尝试处理),我们的类需要加载的时候,是由pathClassLoader处理的,但是!这里双亲委托说的父类,指的不是BaseDexClassLoader,而是BootClassLoader。
所以这里的逻辑理解应该是:
先判断当前加载器是否有父类,没有就从Bootstrap里面找,如果没有加载过就自己去执行findClass方法去加载。
如果有父类,就根据双亲委托机制,递归加载,如果都没有加载过,最后也是交给自己去执行findClass。
那findClass又做了什么?
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
ClassLoader中的findClass其实是空实现,也就是说实现交给了子类去实现。那再找BaseDexClassLoader中的。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
重点就这一句:Class c = pathList.findClass(name, suppressedExceptions);
从变量pathList中,根据name来findClass,并且把结果return。那pathList是什么?先看定义
private final DexPathList pathList;
再看赋值
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
可以看到,是一个DexPathList的对象,而且是在BaseDexClassLoader的构造函数里赋值的,而构造函数中的形参dexPath,其实就是dex文件的路径。dex文件是什么?就是开头讲的apk里面的我们写的代码。继续看DexPathList的实现:
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
···省略部分代码···
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
···省略部分代码···
}
通过makeDexElements,把dexPath路径上的文件拆分,变成一个Element数组。
private Element[] dexElements;
也就是说,DexPathList类型的pathList对象里面,有一个dexElements数组,存放的就是我们dex文件里面的所有代码的类。那再看回pathList的findClass方法:
//DexPathList类:
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
//Element类
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
遍历Element数组,再根据Element对象内的dexFile和loadClassBinaryName得到的Class,就可以用了,也就是说,我们new的对象,已经成功了。
双亲委托的作用
第一、避免了重复加载类,交由父类先处理,可以知道是否被加载过。
第二、为了安全,因为各种各样的对象都有类加载器来加载,有系统的,有我们自己的,而系统的必须优先度高并且不可改,不然就会有安全性问题。所以父类加载完系统对象,就算我们在代码里自己写一个去修改实现,也是没用。
APP热修复
通过上面的知识点,了解到了一个apk是怎么安装并启动加载到ART虚拟机里面的了。既然类的加载,是一个Element数组的遍历,而Element存放的又是dexFile。
那也就是说:如果APP某个类有bug,我们只需要修复这个类的bug,然后生成一个dex文件,用户下载后,根据逻辑把这个dex文件放在Element数组的第一位,那么根据类加载的逻辑,修复后的类会先加载,而后面有bug的类,由于类名一样,所以就不会再加载了,达到了问题被修复,而不需要重新发包的目的。
简单例子实现
增加入口,确保第一时间把这个新的类被加载器加载,不然由于同名的原因,如果另一个同名的类先加载了,那这个就无法修复了。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//执行热修复。 插入补丁dex
Hotfix.installPatch(this,new File("/sdcard/bugFix.dex"));
}
}
根据版本,分别处理
public static void installPatch(Application application, File patch) {
//1、获得classloader,PathClassLoader
ClassLoader classLoader = application.getClassLoader();
List<File> files = new ArrayList<>();
if (patch.exists()) {
files.add(patch);
}
File dexOptDir = application.getCacheDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
NewClassLoaderInjector.inject(application, classLoader, files);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} else {
try {
//23 6.0及以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
V19.install(classLoader, files, dexOptDir); //4.4以上
} else { // >= 14
V14.install(classLoader, files, dexOptDir);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
处理无非就是利用反射,找到BaseDexClassLoader中的pathList,然后找到pathList中的makePathElements方法,得到补丁创建的 Element[],最后合并2个Element数组并修改 classLoader中 pathList的 dexelements。
private static final class V23 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
IOException {
//找到 pathList
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
// 从 pathList找到 makePathElements 方法并执行
// 得到补丁创建的 Element[]
Object[] patchElements = makePathElements(dexPathList,
new ArrayList<>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions);
//将原本的 dexElements 与 makePathElements生成的数组合并
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", patchElements);
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
/**
* 把dex转化为Element数组
*/
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
//通过阅读android6、7、8、9源码,都存在makePathElements方法
Method makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements",
List.class, File.class,
List.class);
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}
代码太多,不全放了。
然后MainActivity写点bug,为了方便,我新建了一个类来抛出bug。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ExceptionBug.test();
}
}
public class ExceptionBug {
public static void test() {
throw new UnsupportedOperationException("this is a exception");
}
}
这样就能保证出bug了。
修复
public class ExceptionBug {
public static void test() {
//throw new UnsupportedOperationException("this is a exception");
}
}
注释掉异常的抛出。重新编译项目,注意只是编译项目,不是重新运行到手机。找到这个类的class文件。编译成dex文件。位置在app-build-intermediates-javac
image.png
编译
找到工具:/Users/chenjy/Library/Android/sdk/build-tools/29.0.3
image.png
把这个添加到配置环境里面去。然后回到ExceptionBug.class文件所在的目录。
然后输入命令:dx --dex --output=bugFix.dex com/cjy/hotfixdemo/ExceptionBug.class
执行命令后就会生成这个dex文件了,然后把文件放到MyApplication指定的位置那里,也就是/sdcard/bugFix.dex,放到sdcard里。
再重新打开,就会发现,没抛出异常了。
值得注意的问题
1、AndroidQ(10.0)以上,热修复的dex文件,不要放到sdcard中,因为外部存储的访问权限改了,只能看到自己的,所以应该放到私有目录下,或者application中加入android:requestLegacyExternalStorage="true"。
2、修复的代码一定要先于bug代码被加载,所以这里我直接在application中就调用了,如果先启动的是MAinActivity,已经加载过了ExceptionBug这个类,之后跳转到activity2,再去热修复ExceptionBug类就不行了。
3、这个只是个简单的例子,实际的热修复没这么简单,这里只不过是通过热修复的例子,来解释实现类加载的流程。
网友评论