Android基础——类加载器和动态加载

类加载器和动态加载

一、类加载器:

双亲委派模式

·(以递归的形式逐级向上)(一般java有三层类加载器)
·三层:
1)系统类加载器(应用程序类加载器):AppClassLoader
ClassLoader.getSystemClassLoader()
2)系统类加载器的父类加载器(扩展类加载器):Extension ClassLoader
ClassLoader.getSystemClassLoader.getParent()
3)扩展类加载器的父类加载器(启动类加载器)(C++编写):Bootstrap ClassLoader
在这里插入图片描述
·双亲委派模式:在加载.class文件时,以递归的形式逐级向上委托给父加载器,如果加载过就不用再加载一次了,直接返回,如果未加载过,继续向上委托给父加载器,直到链路顶级,如果顶级加载器未加载过,则尝试加载,加载失败(在他的搜索范围中没有找到所需的类)则逐级向下交还给子加载器调用自己的findClass()方法进行加载。

·findClass和defineClass
用来将byte字节解析成虚拟机能够识别的Class对象。defineClass()方法通常与findClass()方法一起使用。在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法获取要加载类的字节码,然后调用defineClass()方法生成Class对象。

二、Android中类加载机制

·类加载机制:.java不可执行,需要先编译成.class文件
·虚拟机:java虚拟机运行JVM,运行的是.class文件;Android虚拟机是dalvik/art,运行的是dex文件(class文件的集合)
·Android运行流程
1)Android程序编译的时候,会将.java文件编译成.class文件
2)生成apk时,将.class文件打包为.dex文件(不是简单的压缩,而是完全对class文件内部的各种函数表、变量表等进行优化,所以java和Android中的classloader也不一样)
3)Android程序运行时,虚拟机加载dex文件,然后加载其中的.class文件到内存中来使用
·类加载流程:类被加载到虚拟机内存:加载(插件化可以实现)、连接(验证、准备、解析)、初始化(执行实际程序中的代码)
·类加载时机
1)显式加载:使用LoadClass()、forName()加载
2)隐式加载:创建类的实例(new一个对象)、初始化类的子类(先初始化子类的父类)、访问某个类或接口的静态变量或对该静态变量赋值、调用类的静态方法、反射Class.forName(“android.app.ActivityThread”)
·Android类加载器
在这里插入图片描述
·findLoadedClass[–>ClassLoader.java]
源码链接

    protected final Class<?> findLoadedClass(String name) {
    
    
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, name);
    }

·findClass[–>BaseDexClassLoader.java]
源码链接

    protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
        // First, check whether the class is present in our shared libraries.
        if (sharedLibraryLoaders != null) {
    
    
            for (ClassLoader loader : sharedLibraryLoaders) {
    
    
                try {
    
    
                    return loader.loadClass(name);
                } catch (ClassNotFoundException ignored) {
    
    
                }
            }
        }
        // Check whether the class in question is present in the dexPath that
        // this classloader operates on.
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c != null) {
    
    
            return c;
        }
        // Now, check whether the class is present in the "after" shared libraries.
        if (sharedLibraryLoadersAfter != null) {
    
    
            for (ClassLoader loader : sharedLibraryLoadersAfter) {
    
    
                try {
    
    
                    return loader.loadClass(name);
                } catch (ClassNotFoundException ignored) {
    
    
                }
            }
        }
        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;
    }

·DexPathList.findClass:

·Android中的ClassLoader

1.BootClassLoader

·启动类加载器(最顶级的那个),加载Zygote进程已经预加载(Class.forName())的基本类,只需从缓存中加载;内部类,应用程序无权直接访问
(基本类:object、class、classloader、string等)
·没有父类加载器,调用自己的findClass()方法—>调用Class.classForName()方法
·预加载:ZygoteInit.preloadClasses()中调用Class.forName(),实际是指定BootClassLoader为类加载器,且只需要在预加载的时候进行类初始化,只需要一次。
·Class.classForName()方法和Class.forName()方法仅可以直接加载基本类,且两种方法都是动态加载class文件。
在这里插入图片描述
·无论是系统类加载器(PathClassLoader)还是自定义的类加载器(DexClassLoader),最顶层的根类加载器默认是 BootClassLoader

2.PathClassLoader

·可以加载系统类和应用程序的类,通常用来加载已安装的apk的dex文件
·在Zygote进程启动的时候初始化,在预加载基本类之后执行(在APP进程中),所以每个APP进程从Zygote进程中fork出来之后都自动携带一个PathClassLoader。
·创建PathClassLoader的过程就是加载dex文件的过程

3.DexClassLoader

·可以加载dex文件以及包含dex文件的压缩文件(比如apk、jar、zip等),也就是从包含classes.dex的文件中加载类,能够加载系统未安装的apk或者jar文件,一般为自定义类加载器
·可以实现动态加载,因为它提供了optimizedDirectory,它是用来存放dex文件的地方。

4.BaseDexClassLoader

·实际应用层类文件的加载,真正的加载委托给pathList完成(记录dex文件的路径信息)

public class BaseDexClassLoader extends ClassLoader {
    
    
    private final DexPathList pathList;  //记录dex文件路径信息

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
    
    
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
}

其中:dexPath:包含目标类或资源的apk、jar列表,当有多个路径时采用分割
optimizedDirectory:优化后的dex文件存在的目录,可为空
libraryPath:native库所在的路径列表,当有多个路径时采用分割
ClassLoader:父类的类加载器
·DexClassLoader与PathClassLoader都继承于BaseDexClassLoader,这两个类只是提供了自己的构造函数,没有额外的实现。
·初始化dexPathList(用来查找dex、so库的路径的类,以数组形式呈现),用来收集dex文件(根据多路径的“;”分隔符,将dexPath转换成File列表,记录所有的dexFile)和Native文件动态库(包括app目录和系统目录的native库)
· dex的加载过程:
DexPathList.makePathElements

private static Element[] makePathElements(List<File> files, File optimizedDirectory,
        List<IOException> suppressedExceptions) {
    
    
    return makeDexElements(files, optimizedDirectory, suppressedExceptions, null);
}

makeDexElements:获取一个包含dex文件的元素集合,创建Elements数组

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
        List<IOException> suppressedExceptions, ClassLoader loader) {
    
    
    return makeDexElements(files, optimizedDirectory, suppressedExceptions, loader, false);
}

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) {
    
    
      if (file.isDirectory()) {
    
    
          elements[elementsPos++] = new Element(file);
      } else if (file.isFile()) {
    
    
          String name = file.getName();
          DexFile dex = null;
          //匹配以.dex为后缀的文件
          if (name.endsWith(DEX_SUFFIX)) {
    
    
              dex = loadDexFile(file, optimizedDirectory, loader, elements);
              if (dex != null) {
    
    
                  elements[elementsPos++] = new Element(dex, null);
              }
          } else {
    
    
              dex = loadDexFile(file, optimizedDirectory, loader, elements);             
              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);
      }
  }
  if (elementsPos != elements.length) {
    
    
      elements = Arrays.copyOf(elements, elementsPos);
  }

  return elements;
}

DexPathList.loadDexFile:加载dexFile文件,把优化后的dex文件缓存到对应目录(optimizedDirectory)

private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                   Element[] elements)
        throws IOException {
    
    
    if (optimizedDirectory == null) {
    
    
        return new DexFile(file, loader, elements);  //创建DexFile对象
    } else {
    
    
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
    }
}

DexFile:描述dex文件,调用该类的native方法完成dex的加载和class的查找

DexFile(File file, ClassLoader loader, DexPathList.Element[] elements)
        throws IOException {
    
    
    this(file.getPath(), loader, elements);
}

DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
    
    
    mCookie = openDexFile(fileName, null, 0, loader, elements);
    mInternalCookie = mCookie;
    mFileName = fileName;
}

openDexFile

private static Object openDexFile(String sourceName, String outputName, int flags,
        ClassLoader loader, DexPathList.Element[] elements) throws IOException {
    
    
    return openDexFileNative(new File(sourceName).getAbsolutePath(),
                             (outputName == null) ? null : new File(outputName).getAbsolutePath(),
                             flags,
                             loader,
                             elements);
}

其中:sourceName是PathClassLoader构造函数传递的dexPath中以分隔符划分之后的文件名
elements是makeDexElements()过程中生成的Element数组
openDexFileNative:
·loadClass加载(双亲委派模式)
一个Classloader可以包含多个dex文件,每个dex文件被封装到一个Element对象,这些Element对象排列成有序的数组 dexElements。当查找某个类时,会遍历所有的dex文件,如果找到则直接返回,不再继续遍历dexElements。

public abstract class ClassLoader {
    
    

    public Class<?> loadClass(String className) throws ClassNotFoundException {
    
    
        return loadClass(className, false);
    }

    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    
    
        //判断当前类加载器是否已经加载过指定类,若已加载则直接返回
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
    
    
            //如果没有加载过,则调用parent的类加载递归加载该类,若已加载则直接返回
            clazz = parent.loadClass(className, false);

            if (clazz == null) {
    
    
                //还没加载,则调用当前类加载器来加载
                clazz = findClass(className);
            }
        }
        return clazz;
    }
}

在这里插入图片描述
·native层的ClassLinker
1)从已加载类的class_table中查询,如果找到,直接返回;若找不到则说明类是第一次加载,则执行加载流程(maybe穿插加载依赖的类),加载完成后缓存到class_table中。
2)维护两种class_table(基本类和其他的类),class_table是缓存已经加载过的类,所以在加载类之前,先从class_table中查询
·热修复
当两个类在不同的dex中出现时,会优先处理排在前面的dex文件,将需要修复的类所打包的dex文件插入到dexElements前面。
·初始化和加载流程总结
在这里插入图片描述
三、动态加载
·java中的动态加载:java的可执行文件是jar文件,运行在JVM虚拟机上,JVM虚拟机通过 ClassLoader加载 jar文件,并且执行里边的代码逻辑。
·Android中的动态加载:在程序运行时候首先把对应的类加载到内存中
(Android的可执行文件是 dex文件,运行在 Dalvik/ART虚拟机上,在apk文件中会有一个或多个 dex文件,而我们写的所有的代码都会被编译到这些 dex文件中,Android运行的时候就是通过执行这些 dex文件来完成应用功能的。如果 apk文件构建出来是不能够修改里边的 dex文件,但是可以通过加载 外部SD卡的或者网络的 dex文件达到动态加载。)
·核心思想:动态调用外部的dex文件
·动态加载so库:NDK就是用动态加载,动态加载 .so库并通过 JNI调用其 .so库封装好的方法,.so库一般是由 C/C++编译的,只能被反编译成汇编代码,很难被破解, 一般情况是把 .so库一并打包到APK内部, .so库也可以动态的从 SD卡或者网络中加载进来;
·动态加载dex、jar、apk文件:是基于 ClassLoader动态加载 dex/jar/apk文件,而且一般都是通过 DexClassLoader (在Android项目中,所有的 Java代码都会被编译成 .dex文件,在app运行时候,是通过执行dex文件里边的代码进行工作,采用动态加载技术可以在app运行时候加载外部dex文件,通过网络下载新的dex文件替换原有的dex文件,就可以达到不用安装Apk,就可以升级应用。)
·原理:在程序运行时加载一些外部的可执行文件,然后调用这些文件的某个方法执行业务逻辑。对于这些外部的可执行文件,在Android应用调用前都要先把它们拷贝到data/packagename/内部存储文件路径,使用类加载器加载相应的文件通过反射获取内部资源,以供宿主APP的使用。
·过程:把可执行文件(dex、jar、apk等)拷贝到应用APP内部存储(app/data/cache目录下)—>加载可执行文件—>调用具体的方法执行业务
·实现方式
1)简单动态加载模式
通过JDK的编译命令java.c把java代码编译成.class文件,再使用jar命令把.class文件封装成.jar文件。最后使用Android SDK的工具把.jar文件优化成.dex文件。
通过DexClassLoader加载后使用反射或者接口方式调用里面的方法。

    private void getOutData(){
    
    
        File optimizadDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"test_dexloafer.jar");
        // 无法直接从外部路径加载.dex文件,需要指定APP内部路径作为缓存目录(.dex文件会被解压到此目录)
        File dexOutputDir = this.getDir("dex",0);
        //构造类加载器
        DexClassLoader dexClassLoader = new DexClassLoader(optimizadDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(),null,ClassLoader.getSystemClassLoader());
    } 

2)使用反射的方式
使用DexClassLoader加载进来的类无法直接调用,可以通过反射的方式调用。

 private void getOutData(){
    
    
        File optimizadDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"test_dexloafer.jar");
        File dexOutputDir = this.getDir("dex",0);
        DexClassLoader dexClassLoader = new DexClassLoader(optimizadDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(),null,ClassLoader.getSystemClassLoader());
        Class libProviderClazz = null;
        try {
    
    
            libProviderClazz = dexClassLoader.loadClass("包名.类名");
            //遍历所有的方法
            Method[] methods = libProviderClazz.getDeclaredMethods();
            for (int i = 0;i<methods.length;i++){
    
    
                Log.e("test",methods[i].toString());
            }
            //通过方法名获取func方法
            Method func= libProviderClazz.getDeclaredMethod("func");
            //外部可以调用
            func.setAccessible(true);
            //调用该方法获得值
            String string = (String) func.invoke(libProviderClazz.newInstance());
            Toast.makeText(this, string, Toast.LENGTH_SHORT).show();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }

3)使用接口的方式
把.dex文件的方法抽象成公共接口,把这些接口复制到主项目中,通过接口的调用实现动态加载

pulic interface IFunc{
    
    
    public String func();
}
 
// 调用
IFunc ifunc = (IFunc)libProviderClazz;
String string = ifunc.func();
Toast.makeText(this, string, Toast.LENGTH_LONG).show();

4)使用代理Activity模式
通过动态加载,让现在的Android应用启动一些“新”的Activity,甚至不用安装就启动一个“新”的APK。宿主APK需要先注册一个空壳的Activity用于代理执行插件APK的Activity的生命周期。
使用插件apk里的Activity需要解决两个问题:
1.如何使插件apk里面的Activity具有生命周期
主项目apk注册一个空壳的ProxyActivity,通过在ProxyActivity的生命周期里面同步调用插件中的Activity的生命周期方法,从而实现执行插件apk的业务逻辑。
2.如何使插件apk里的Activity具有上下文环境
插件里需要用到的新资源都是通过java代码的方式创建(xml布局,动画,点九图)。
可以通过AsserManager实例创建Resource实例

  public int getAppNameResId(String apkPath){
    
    
        PackageManager packageManager = this.getPackageManager();
        PackageInfo info = packageManager.getPackageArchiveInfo(apkPath,0);
        return info.applicationInfo.labelRes;
    }
    private Resources getExtResource(Context context,String apkPath){
    
    
        int mDexPath = getAppNameResId(apkPath);
        Resources res = context.getResources();
        try {
    
    
            Class<?> assetClass = Class.forName("android.content.res.AssetManager");
            Object assetManager = assetClass.getConstructor(null).newInstance(null);
            assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
            addAssetPath.invoke(assetManager,mDexPath);
 
            Class<?> resClass = Class.forName("android.content.res.Resources");
            res = (Resources) resClass.getConstructor(assetClass,res.getDisplayMetrics().getClass(),res.getConfiguration().getClass())
                    .newInstance(assetManager,res.getDisplayMetrics(),res.getConfiguration());
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
        return res;
    }

宿主APK可以启动未安装的插件APK;
插件APK也可以作为一个普通APK安装并且启动;
插件APK可以调用宿主APK里的一些功能;
宿主APK和插件APK都要接入一套指定的接口框架才能实现以上功能。
·创建插件工程
插件工程就是提供一个类,生成一个jar包供主工程加载调用
step1.双击gradle中assemble任务
step2.在./myplugin/build/intermediates/aar_main_jar/debug/classes.jar路径下找到jar文件
step3.打开dx工具执行以下命令对origin.jar做优化

dx --dex --output=target.jar origin.jar

step4.将buildTypes的debug中的minifyEnabled设置false
在这里插入图片描述
step5.使用adb push XXX.jar /sdcard/ 将dex/jar放在sdcard。

猜你喜欢

转载自blog.csdn.net/weixin_44901971/article/details/127524347