Android Basics - Class Loaders and Dynamic Loading

Class loaders and dynamic loading

1. Class loader:

Parental Delegation

· (Level up step by step in the form of recursion) (generally java has three layers of class loaders)
· Three layers:
1) System class loader (application class loader): AppClassLoader ClassLoader.getSystemClassLoader
()
2) System class loader's Parent class loader (extended class loader): Extension ClassLoader
ClassLoader.getSystemClassLoader.getParent()
3) Extended class loader's parent class loader (startup class loader) (written in C++): Bootstrap ClassLoader
insert image description here
Parental delegation mode : in When loading a .class file, it delegates to the parent loader step by step in a recursive manner. If it has been loaded, it does not need to load it again, and returns directly. If it has not been loaded, it continues to delegate to the parent loader until the top of the link. If the top-level loader has not loaded it, try to load it, and if the loading fails (the required class is not found in its search range), it will be returned to the sub-loader to call its own findClass() method to load.

· findClass and defineClass
are used to parse byte bytes into Class objects that can be recognized by the virtual machine. The defineClass() method is usually used together with the findClass() method. When customizing the class loader, it will directly override the findClass() method of ClassLoader to obtain the bytecode of the class to be loaded, and then call the defineClass() method to generate a Class object.

2. Class loading mechanism in Android

Class loading mechanism : .java is not executable and needs to be compiled into a .class file first.
Virtual machine : The java virtual machine runs the JVM, which runs the .class file; the Android virtual machine is dalvik/art, which runs the dex file (class file collection)
Android running process :
1) When compiling the Android program, the .java file will be compiled into a .class file
2) When the apk is generated, the . Optimize the various function tables and variable tables inside the class file, so the classloader in java and Android is different)
3) When the Android program is running, the virtual machine loads the dex file, and then loads the .class file in it into the memory To use
Class loading process: Classes are loaded into virtual machine memory: loading (plug-in can be realized), connection (verification, preparation, analysis), initialization (execution of code in the actual program) Class loading timing:
1 )
Explicit Loading: Use LoadClass(), forName() to load
2) Implicit loading: create an instance of a class (new an object), initialize a subclass of a class (initialize the parent class of the subclass first), access the static of a class or interface Variable or assign a value to the static variable, call the static method of the class, reflect Class.forName(“android.app.ActivityThread”)
Android class loader
insert image description here
findLoadedClass[–>ClassLoader.java]
source code link

    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]
source link

    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:

·ClassLoader in Android

1.BootClassLoader

Start the class loader (the topmost one), load the basic class that the Zygote process has preloaded (Class.forName()), just load it from the cache; internal classes, the application has no right to directly access (basic class:
object , class, classloader, string, etc.)
No parent class loader, call your own findClass() method —> call Class.classForName() method
Preload: Class.forName() is called in ZygoteInit.preloadClasses(), which is actually Specify BootClassLoader as the class loader, and only need to initialize the class during preloading, only once.
·Class.classForName() method and Class.forName() method can only load basic classes directly, and both methods are dynamically loading class files.
insert image description here
Whether it is a system class loader (PathClassLoader) or a custom class loader (DexClassLoader), the topmost root class loader defaults to BootClassLoader

2.PathClassLoader

Can load system classes and application classes, usually used to load the dex file of the installed apk
Initialize when the Zygote process starts, and execute after preloading the basic classes (in the APP process), so each APP process After forking out of the Zygote process, it automatically carries a PathClassLoader.
The process of creating PathClassLoader is the process of loading dex files

3.DexClassLoader

Can load dex files and compressed files containing dex files (such as apk, jar, zip, etc.), that is, load classes from files containing classes.dex, and can load apk or jar files that are not installed in the system, generally custom The class loader
can achieve dynamic loading because it provides optimizedDirectory, which is used to store dex files.

4.BaseDexClassLoader

The loading of the actual application layer class files, the real loading is entrusted to pathList to complete (record the path information of the dex file)

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);
    }
}

Among them: dexPath : contains the apk and jar list of the target class or resource. When there are multiple paths, it is divided.
optimizedDirectory : the directory where the optimized dex file exists, which can be empty.
libraryPath : the path list where the native library is located. When there are multiple paths Splitting
ClassLoader is used for the path : the class loader of the parent class
DexClassLoader and PathClassLoader both inherit from BaseDexClassLoader. These two classes only provide their own constructors without additional implementation.
Initialize dexPathList (a class used to find the paths of dex and so libraries, presented in the form of an array) to collect dex files (according to the ";" separator of multi-path, convert dexPath into a File list and record all dexFiles) And Native file dynamic library (including app directory and native library of system directory)
dex loading process:
DexPathList.makePathElements :

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

makeDexElements : Get a collection of elements containing a dex file and create an array of 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 : load the dexFile file, and cache the optimized dex file to the corresponding directory (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 : Describe the dex file, call the native method of this class to complete the loading of dex and the search of 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);
}

Among them: sourceName is the file name in the dexPath passed by the PathClassLoader constructor divided by separators elements is the Element array
generated during makeDexElements() A dex file is encapsulated into an Element object, and these Element objects are arranged into an ordered array dexElements. When searching for a certain class, it will traverse all dex files, and return directly if found, without continuing to traverse 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;
    }
}

insert image description here
· ClassLinker in the native layer
1) Query from the class_table of the loaded class, if found, return directly; if not found, it means that the class is loaded for the first time, then execute the loading process (maybe interspersed with loading dependent classes), and the loading is complete After that, it is cached in class_table.
2) Maintain two kinds of class_table (basic class and other classes). The class_table is to cache the loaded class, so before loading the class, query from the class_table. Hot fix When two classes appear in different
dex
, The top dex files will be processed first, and the dex files packaged by the classes that need to be repaired will be inserted in front of dexElements.
Summary of initialization and loading process
insert image description here
3. Dynamic loading
Dynamic loading in java : The executable file of java is a jar file, which runs on the JVM virtual machine. The JVM virtual machine loads the jar file through ClassLoader and executes the code logic inside.
·Dynamic loading in Android : When the program is running, first load the corresponding class into the memory
(the Android executable file is a dex file, running on the Dalvik/ART virtual machine, there will be one or more dex files in the apk file files, and all the code we write will be compiled into these dex files. When Android is running, it executes these dex files to complete the application functions. If the apk file is built, you cannot modify the dex files inside, but you can Dynamic loading is achieved by loading external SD card or network dex files.)
Core idea : dynamically call external dex files
Dynamically load so library: NDK uses dynamic loading, dynamically loads the .so library and calls its packaged .so library through JNI. The .so library is generally compiled by C/C++ and can only be decompiled into assembly code, which is difficult to crack In general, the .so library is packaged into the APK, and the .so library can also be dynamically loaded from the SD card or the network; ·Dynamic loading of
dex, jar, and apk files : based on ClassLoader, dynamically loading dex/jar/ apk files, and generally through DexClassLoader (in Android projects, all Java codes will be compiled into .dex files, and when the app is running, it works by executing the code in the dex file. Load external dex files during running, and download new dex files through the network to replace the original dex files, so that the application can be upgraded without installing Apk.) Principle:
load some external executable files when the program is running, and then Call a method of these files to execute the business logic. For these external executable files, they must be copied to the data/packagename/internal storage file path before being called by the Android application, and the corresponding files are loaded using the class loader to obtain internal resources through reflection for use by the host APP.
·Process : Copy executable files (dex, jar, apk, etc.) to the internal storage of the application APP (under the app/data/cache directory)—>load executable files—>call specific methods to perform business ·Implementation method 1
)
Simple Dynamic loading mode
Compile the java code into a .class file through the JDK compilation command java.c, and then use the jar command to package the .class file into a .jar file. Finally, use the tools of the Android SDK to optimize the .jar file into a .dex file.
After loading through DexClassLoader, use reflection or interface to call the methods inside.

    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) Using reflection
The class loaded with DexClassLoader cannot be called directly, but can be called through reflection.

 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) The method of using the interface
Abstract the method of the .dex file into a public interface, copy these interfaces to the main project, and realize dynamic loading by calling the interface

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

4) Use the proxy Activity mode
to allow the current Android application to start some "new" Activities through dynamic loading, and even start a "new" APK without installing it. The host APK needs to register an empty shell Activity to proxy the life cycle of the Activity that executes the plug-in APK.
Using the Activity in the plug-in apk needs to solve two problems:
1. How to make the Activity in the plug-in apk have a life cycle. The
main project apk registers an empty ProxyActivity, and calls the life cycle of the Activity in the plug-in synchronously in the life cycle of ProxyActivity. method, so as to implement the business logic of executing the plug-in apk.
2. How to make the Activity in the plug-in apk have a context The
new resources that need to be used in the plug-in are all created by java code (xml layout, animation, point nine).
A Resource instance can be created through an AsserManager instance

  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;
    }

The host APK can start an uninstalled plug-in APK;
the plug-in APK can also be installed and started as a normal APK; the
plug-in APK can call some functions in the host APK;
both the host APK and the plug-in APK must be connected to a specified interface framework to achieve above functions.
Create a plug-in
project The plug-in project is to provide a class, generate a jar package for the main project to load and call
step1. Double-click the assemble task
step2 in gradle. Find the jar file under the path of ./myplugin/build/intermediates/aar_main_jar/debug/classes.jar
step3. Open the dx tool and execute the following command to optimize origin.jar

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

step4. Set minifyEnabled in debug of buildTypes to false
insert image description here
step5. Use adb push XXX.jar /sdcard/ to put dex/jar on sdcard.

Guess you like

Origin blog.csdn.net/weixin_44901971/article/details/127524347