Android-0. ClassLoader工作机制相关简介(热修复)

ClassLoader简介

程序运行在虚拟机上时,虚拟机通过ClassLoader把需要的Class加载进来才能创建实例对象并工作。

AndroidDalvik/ART虚拟机如同标准的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此,我们可以利用这一点,在程序运行时手动加载Class,从而达到代码动态加载可执行文件的目的。

AndroidDalvik/ART虚拟机虽然与标准的JVM虚拟机不一样,ClassLoader具体的加载细节不一样,但是工作机制是类似的。

ClassLoader 源码

Android中,ClassLoader是一个抽象类,实际开发过程中,我们一般是使用其具体的子类DexClassLoaderPathClassLoader这些类加载器来加载类的,它们的不同之处是:

  1. DexClassLoader可以加载任意目录下的dex、jar、zip、apk文件,可以从SD卡中加载未安装的apk,比PathClassLoader更加灵活。
  2. PathClassLoader只能加载系统中已经安装过的apk(data/aap目录),是Android系统默认的类加载器。

PathClassLoader源码

点击查看AOSP源码
该类继承了BaseDexClassLoader类,并且在仅有的两个构造方法中也调用到了父类的构造方法中。

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
    public PathClassLoader(
            String dexPath, String librarySearchPath, ClassLoader parent,
            ClassLoader[] sharedLibraryLoaders) {
        super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
    }

DexClassLoader源码

点击查看AOSP源码
该类也是继承了BaseDexClassLoader了,并且在仅有的一个构造方法中调用到了父类的构造方法。

    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }

optimizedDirectory:dex文件的输出目录,因为在加载zip、apk、jar格式的程序文件的时候会解压出其中的dex文件,该目录
就是专门用于存放这些被解压出来的dex文件,但是从api26开始就失效了,即使传入了具体的值也不会被使用

BaseDexClassLoader源码

点击查看AOSP源码

public BaseDexClassLoader(String dexPath,String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
            boolean isTrusted) {
super(parent); //  对父加载器进行初始化
        ...
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted); // 初始化成员变量pathList
       ...
    }

–>继续看DexPathList中的构造方法

/**
* definingContext:当前的类加载器
* dexPath:要加载的dex、jar、apk或者zip文件string路径列表,每一个dex路径用:分隔开
* librarySearchPath:加载程序文件的库文件
* optimizedDirectory:dex文件的解压目录,但是在api26以后就不在使用了
**/
DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
    ..........//判断数据的合法性
    
    this.definingContext = definingContext;
    //初始化IO异常列表
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    //将dex文件构造为Elements对象
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions, definingContext, isTrusted);
     .....
}

–> splitDexPath,它分隔string路径dex列表到一个list中,以:分隔

/**
*  searchPath:要加载的dex、jar、apk或者zip文件string路径列表,每一个dex路径用:分隔开
**/
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
    List<File> result = new ArrayList<>();
    if (searchPath != null) {
        for (String path : searchPath.split(File.pathSeparator)) {
            if (directoriesOnly) {
                    StructStat sb = Libcore.os.stat(path);
                  ....
            }
            //将string列表中单个dex、jar、apk或者zip文件路径存放到list中
            result.add(new File(path));
        }
    }
    return result;
}

–>makeDexElements,它把分隔的dex路径列表解析成Element列表

/**
* files:dex、jar、zip或者apk文件路径列表
* optimizedDirectory: dex解压路径,在api26以后为null
* suppressedExceptions:IO异常列表
**/
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
        List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
  Element[] elements = new Element[files.size()];
  int elementsPos = 0;
  for (File file : files) {
      //文件是一个目录,则直接添加到elements列表中,后续解析的时候直接从目录中找到dex文件
      if (file.isDirectory()) {
          elements[elementsPos++] = new Element(file);
      } else if (file.isFile()) {
          String name = file.getName();
          DexFile dex = null;
          //如果该文件是.dex结尾的文件则将该文件包装为DexFile对象
          if (name.endsWith(DEX_SUFFIX)) {
              try {
                  dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  if (dex != null) {
                      elements[elementsPos++] = new Element(dex, null);
                  }
              } catch (IOException suppressed) {
                  System.logE("Unable to load dex file: " + file, suppressed);
                  suppressedExceptions.add(suppressed);
              }
          } else {
              //如果该文件是jar、apk或者zip文件,则从这些文件中提取出dex文件并包装成DexFile对象,具体的提取是在DexFile
              //中通过native方法进行提取
              try {
                  dex = loadDexFile(file, optimizedDirectory, loader, elements);
              } catch (IOException suppressed) {
                  suppressedExceptions.add(suppressed);
              }
              if (dex == null) {
                  elements[elementsPos++] = new Element(file);
              } else {
                  elements[elementsPos++] = new Element(dex, file);
              }
          }
          if (dex != null && isTrusted) {
            dex.setTrusted();
          }
      } else {
          System.logW("ClassLoader referenced unknown path: " + file);
      }
  }
  //如果实际的长度和理论的长度不等,则将elements的长度变更为实际长度
  //实际长度<=理论长度
  if (elementsPos != elements.length) {
      elements = Arrays.copyOf(elements, elementsPos);
  }
  return elements;
}

PathClassLoaderDexClassLoader的构造方法开始,最后会在BaseClassLoader中将包含dex的文件或者文件夹构造成一个个的Element对象。

ClassLoader讲解

点击查看AOSP源码

通过上述的过程将各个dex文件包装成了Element对象,那实际的类加载是在什么地方加载的呢,看源码发现是在ClassLoader中的loadClass方法中进行加载的。

从上面的讲解中我们知道了PathClassLoaderDexClassLoader以及BaseDexClassLoader,除此之外还有BootClassLoader用于加载Android Framework层的class文件,接下来对BootClassLoader的初始化以及loadClass的源码进行讲解。

BootClassLoader初始化

BootClassLoader的初始化是在ZygoteInit类中初始化的, 简化代码如下:

 public static void main(String argv[]) {
        ZygoteServer zygoteServer = new ZygoteServer();
        ....
        preload(bootTimingsTraceLog);
       ...
}
-->
 static void preload(TimingsTraceLog bootTimingsTraceLog) {
 		...
 		preloadClasses();
 		...
 }
 -->
 private static void preloadClasses() {
 		...
 		Class.forName(line, true, null);
 }

最后会调用到Class的forName函数里面:

public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)throws ClassNotFoundException {
    //如果classLoader为null那么创建BootClassLoader
    if (loader == null) {
        loader = BootClassLoader.getInstance();
    }
    Class<?> result;
    try {
        //通过class的包名从classLoader中查找我们需要的class文件并返回
        result = classForName(name, initialize, loader);
    } catch (ClassNotFoundException e) {
        Throwable cause = e.getCause();
        if (cause instanceof LinkageError) {
            throw (LinkageError) cause;
        }
        throw e;
    }
    return result;
}

看到这里发现这不是我们在使用反射的时候获取class的方法吗: result = classForName(name, initialize, loader);

PathClassLoader的初始化

PathClassLoader是Andorid应用默认的类加载器,只能用于加载已经安装到系统中的apk中的class文件,那么为什么它会是默认的类加载器类,请看下面的分析。

ClassLoader的默认构造函数中,有如下方法:

    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

getSystemClassLoader看名字都知道是获取系统的类加载器。

public static ClassLoader getSystemClassLoader() {
    return SystemClassLoader.loader;
}
-->loadr来源
static private class SystemClassLoader {
    public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
-->createSystemClassLoader来源
 private static ClassLoader createSystemClassLoader() {
    String classPath = System.getProperty("java.class.path", ".");
    String librarySearchPath = System.getProperty("java.library.path", "");
    return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

这里可以看出SystemClassLoader创建的类加载器是PathClassLoader对象,并且它的父类加载器是BootClassLoader

说它是默认的类加载器,是因为在ActivityThreadperformLaunchActivity方法中,通过SystemClassLoader方法或者直接生成PathClassLoader对象的方式获取到了PathClassLoader,并且用于构造新的Activity,部分源码如下所示:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ........
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    ........
    
}
-->调用到ContentImpl类中的getClassLoader,如果mPackageInfo为null则通过ClassLoader中的SystemClassLoader直接返回PathClassLoader
public ClassLoader getClassLoader() {
    return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
}
-->调用到LoadedAPK中的getClassLoader函数
public ClassLoader getClassLoader() {
    synchronized (this) {
        if (mClassLoader == null) {
            createOrUpdateClassLoaderLocked(null /*addedPaths*/);
        }
        return mClassLoader;
    }
}
private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
    // 如果是系统
     if (mPackageName.equals("android")) {
        if (mClassLoader != null) {
            return;
        }
        if (mBaseClassLoader != null) {
            mClassLoader = mBaseClassLoader;
        } else {
            mClassLoader = ClassLoader.getSystemClassLoader();
        }
        return;
    }
    ........
     mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
                mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
                libraryPermittedPath, mBaseClassLoader);
    ........
}
-->最后会到ApplicationLoaders中通过工厂模式生成PathClassLoader并返回
private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
                                   String librarySearchPath, String libraryPermittedPath,
                                   ClassLoader parent, String cacheKey) {
    .......
     PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(
                                                  zip,
                                                  librarySearchPath,
                                                  libraryPermittedPath,
                                                  parent,
                                                  targetSdkVersion,
                                                  isBundled);
    .......
}

loadClass方法

所有的class都会被ClassLoader中的loaderClass函数加载,下面对该函数进行讲解。

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    -->
   protected Class<?> loadClass(String name, boolean resolve)  throws ClassNotFoundException
    {
            // 1. 从已加载的类的缓存中查找。
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        // 2.如果父加载器不为空,则委托父加载器加载。
                        c = parent.loadClass(name, false);
                    } else {
                       // 3.如果父加载器为空,则委托 bootstrap 加载器加载。
                        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.
                    //4.最后调用 `findClass(name)` 加载。
                    c = findClass(name);
                }
            }
            return c;
    }

从源码中我们也可以看出,loadClass方法在加载一个类的实例的时候

  1. findLoadedClass(name) 从已加载的类的缓存中查找。
  2. parent.loadClass(name, false) 如果父加载器不为空,则委托父加载器加载,可以看出这其实是一个向上递归的过程
  3. findBootstrapClassOrNull(name) 如果父加载器为空,则委托 bootstrap 加载器加载。
  4. findClass(name) 最后调用 findClass(name) 加载。

findClass方法

查看ClassLoaderfindClass源码发现并没有任何实现,说明该方法是在子类中实现的,继续看DexClassLoader中的findClass方法, 也即是BaseDexClassLoader中的findClass方法,内部调到了DexPathListfindClass方法。源码如下:

  protected Class<?> findClass(String name) throws ClassNotFoundException {
       .......
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        .....
        return c;
    }
    
--->DexPathList的源码

/**
* name: 需要寻找的class名
* suppressed: 异常列表
**/
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;
}

遍历dexElements列表,找到与传入的className相对应的第一个class并返回;正因为这个特性成为了热修复的突破点,我们只需要 将需要修复的bug类编译成dex文件然后放到dexElements列表的第一个元素位置,当系统在查找类的时候就会只加载我们插入的dex 文件

最终会调用到ElementfindClass方法, ElementDexPathList内部的一个静态类:

static class Element {
		.....
        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }
}

最终会调用到DexFile中的loadClassBinaryName方法:
点击查看AOSP源码

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                 DexFile dexFile, List<Throwable> suppressed) { 
    Class result = null;
    try {
        //调用到native层从dex文件中查找到与name相对应的clas文件
        result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}

双亲委托模型

在分析完了Android中类加载的大致过程之后,发现类加载的过程使用到了双亲委托模型,也就是某一个特定的类加载器在接到了加载类的请求的时候,会先将该请求委托给父类进行加载(递归向上查询),如果父类加载成功了则直接返回,如果没有加载成功则由自己来进行加载。而所有的类加载器则形成了一个链状结构。

ClassLoader中的双亲委托模式:ClassLoader按级别分为了三个级别:

1.最上级bootStrap ClassLoader(根类加载器):负责加载虚拟机的核心类库,如java.lang.*等,根类类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层的操作系统,属于虚拟机实现的一部分。

2.中间级别extension ClassLoader(扩展类加载器):父类加载器是根加载器,它从java.ext.dirs系统属性所指定的目录中加载类库,用于加载Andorid Framework中的class文件。该加载器是纯java实现,也就是BootClassLoader类加载器。System类的加载器就是BootClassLoader

3.最低级别app ClassLoader(应用类加载器):它的父加载器是extension ClassLoader,它从环境变量或者系统属性java.class.path所指定的目录中加载类。也就是Andorid中的PathClassLoader类加载器。

测试代码:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testClassLoaderNum();
    }

    void testClassLoaderNum() {
        int i = 0;
        ClassLoader classLoader = getClassLoader();
        if (classLoader != null) {
            Log.e("hgy413", "[ClassLoader]:" + i + ":" + classLoader.toString());
            i++;
            while (classLoader.getParent() != null) {
                classLoader = classLoader.getParent();
                Log.e("hgy413", "[ClassLoader]:" + i + ":" + classLoader.toString());
                i++;
            }
        }
    }

输出结果为

hgy413: [ClassLoader]:0:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.hgy413.classloader-1/base.apk"],nativeLibraryDirectories=[/data/app/com.hgy413.classloader-1/lib/arm64, /vendor/lib64, /system/lib64]]]
/hgy413: [ClassLoader]:1:java.lang.BootClassLoader@e6bc431

可以看到有2个Classloader实例,一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader(应用启动时创建的,用于加载"/data/app/com.hgy413.classloader-1/base.apk”里面的类)。由此也可以看出,一个运行的Android应用至少有2个ClassLoader。

双亲委托模型的好处

按上面的机制,可以看出作用是防止内存中出现多份同样的字节码

比如两个类A和类B都要加载System

类A会递归的向父类查找,也就是首选用BootClassLoader尝试加载,如果找不到再向下。这里的System就能在BootClassLoader中找到然后加载。

如果此时类B也要加载System,也从BootClassLoader开始,此时BootClassLoader发现已经加载过了System,那么直接返回内存中的System即可, 而不需要重新加载,这样内存中就只有一份System的字节码了。

同时也避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。这也好理解,一些系统层级的类会在系统初始化的时候被加载,比如java.lang.String,如果在一个应用里面能够简单地用自定义的String类把这个系统的String类给替换掉,那将会有严重的安全问题。

热修复实现

同一种类型的类定义

如果你希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader会一直优先使用旧类。

如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的ClassLoader没有树的继承关系的另一个ClassLoader来加载新类,因为ClassLoader只会检查其Parent有没有加载过当前要加载的类,如果两个ClassLoader没有继承关系,那么旧类和新类都能被加载。

不过这样一来又有另一个问题了,在Java中,只有当两个实例的类名、包名以及加载其的ClassLoader都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。

同一个Class = 相同的 ClassName + PackageName + ClassLoader

以上问题在采用动态加载功能的开发中容易出现,请注意。

热修复实现步骤

文章最开始就讲到Android中存在两个类加载器PathClassLoaderDexClassLoader,它们虽然都是为了将一个个dex文件构造成Element对象,并从dex文件中加载出对应的class文件,但是它们的使用方式却不相同。

PathClassLoader是Android默认的dex文件加载器,DexClassLoader则是为了能够加载没有被初始化在apk中的代码,它可以加载Android中任意目录下包含dex的jar、apk、zip等文件,而这也成为了我们实现热修复的突破点。根据这种思路实现热修复大致步骤如下:

  1. 将需要加入到apk的java文件编译为dex文件格式
  2. 获取到默认的PathClassLoader实例对象
  3. 获取指定目录下面所有包含dex文件的apk、jar、zip等文件
  4. 根据获取到的文件构造出DexClassLoader
  5. 获取到DexClassLoader中的dexElements列表,并存储到集合中
  6. 获取PathClassLoader中的dexElements列表
  7. 将获取到的dexElements列表集合按先后顺序存储到PathClassLoaderdexElements列表中的头部

当app重新启动之后就会加载最新的dex文件,这样就会将Bug修复了,不过老的dex文件依旧存在于dexElements列表中,只是没有机会被加载到了而已。

热修复详细步骤

参看 热修复——深入浅出原理与实现

参考:
Android动态加载基础 ClassLoader工作机制
Android中类加载机制
热修复——深入浅出原理与实现

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值