Android plug-in principle, ClassLoader loading class principle, parent delegation, calling plug-in code by loading APK

In the previous chapter, we talked about how to load the dex file through ClassLoader, call the code in the plug-in, link:

https://blog.csdn.net/qq_31429205/article/details/103952636

Loading principle

stay Previous chapter As mentioned in, ClassLoader loads class files through the loadClass method

Class<?> clz = dexClassLoader.loadClass("com.enjoy.plugin.PluginTest");

Because we need to load the dex file of the plug-in into the host, we will analyze the source code to see whether the DexClassLoader class loader is
How to load an apk's dex file.

Through the search, it is found that there is no loadClass method in the DexClassLoader and PathClassLoader classes. Follow the path of the parent class (basedexclassloader -- > ClassLoader), and finally find the method in the ClassLoader class. The source code is as follows:

// /libcore/ojluni/src/main/java/java/lang/ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            // Check whether this class has been loaded -- > 1
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        // If the parent is not null, the loadClass of the parent is called to load
                        c = parent.loadClass(name, false);
                    } else {
                        // Normally, it will not go here, because BootClassLoader rewrites the loadClass method and ends the recursion
                        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.
                    // If it still cannot be found, call findClass to find -- > 2
                    c = findClass(name);
                }
            }
            return c;
    }

 // -->1 check whether this class has been loaded
 protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        //Finally, we use the native method to realize the search, and we will not go deep into it
        return VMClassLoader.findLoadedClass(loader, name);
    }

// /libcore/libart/src/main/java/java/lang/VMClassLoader.java
//native method, no further study
native static Class findLoadedClass(ClassLoader cl, String name);

// -->2. Generally, the loader will override this method and define its own loading rules
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

 //BaseDexClassLoader.java 
 // /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
 @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;
    }

First, check whether the class has been loaded. If it has been loaded, it will return directly. If it has not been loaded, the parent is not null. First, look in the parent to see whether the parent has loaded the class. That is, call the parent's loadClass method to find, recurse in turn, and return if it has been found or loaded. Otherwise, call your own findClass method to check Look, this is often referred to as the parent delegation mechanism.

In Chapter one, we know that we have the following code:

private void printlassCloader() {
    ClassLoader classLoader = getClassLoader();
    while (classLoader != null) {
        Log.d(TAG, "printlassCloader classloader = " + classLoader);
        classLoader = classLoader.getParent();
    }
}

From the printing result, the parent of the PathClassLoader is BootClassLoader, and the BootClassLoader is the last ClassLoader printed out by the while loop, which means that the parent of the BootClassLoader is null and cannot be recursively traced upward.

Look at source code:

class BootClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }
    @Override
    protected Class<?> loadClass(String className, boolean resolve)throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) {
            clazz = findClass(className);
        }
        return clazz;
    }
}

From the source code, we know that the findClass and loadClass methods mentioned above have been rewritten in BootClassLoader, and in loadClass, there is no way to continue to look for the parent like in ClassLoader.java, indicating that if you trace back to BootClassLoader, it is the final ClassLoader, and you will not look for the parent again; he will call his own loadClass and findClass methods to look for and add Load class, end recursive search.

Next, let's see how DexClassLoader loads classes when recursion in parent fails to load successfully. By looking up the code, DexClassLoader itself does not have any loading logic implementation. Its loading implementation is in BaseDexClassLoader, as follows:

// /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
private final DexPathList pathList;

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //Find from PathList
    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;
}

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
    //Initialize pathList
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

    if (reporter != null) {
        reportClassLoaderChain();
    }
}

pathList is a DexPathList object. To view the findClass method of the DexPathList class:

// /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
/**
 * List of dex/resource (class path) elements.
 * Should be called pathElements, but the Facebook app uses reflection
 * to modify 'dexElements' (http://b/7726934).
 */
private Element[] dexElements;

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 is the internal class of DexPathList, and its findClass implementation is as follows:
public Class<?> findClass(String name, ClassLoader definingContext,
        List<Throwable> suppressed) {
    return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;
}

We find that the Class object is obtained from the Element, and each Element corresponds to a dex file, because the dex file can
There can be more than one, so the array Element [] is used here. Here comes the idea. If you want to load a class, it should be divided into the following steps:

  1. Create the DexClassLoader class loader of the plug-in, and obtain the dexElements value of the plug-in through reflection.
  2. Get the PathClassLoader class loader of the host (because the app is usually loaded through the PathClassLoader, which is shown in Chapter 1 Also mentioned in), and then get the dexElements value of the host through reflection.
  3. Merge the dexElements of the host with the dexElements of the plug-in to generate a new Element [].
  4. Finally, the new Element [] is assigned to the host's dexElements through reflection.

Attach the loading sequence diagram here:

Sequence diagram:

Flow chart can help us better understand and track the code call process.

Then you need to load APK through DexClassLoader, then invoke plug-in code, and how to operate it.

Add the LoadUtils class to the host APK:

package com.enjoy.myplugin;

import android.content.Context;

import java.lang.reflect.Array;
import java.lang.reflect.Field;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;


public class LoadUtils {

    public static void loadClass(Context context){
        try {
            Class<?> baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoader.getDeclaredField("pathList");
            pathListField.setAccessible(true);

            Class<?> dexPathList = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathList.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);

            //The context.getClassLoader() here is actually the PathClassLoader that loads the host APK
            DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugin-debug.apk",
                    context.getCacheDir().getAbsolutePath(),null,context.getClassLoader());
            //Get the value of pathList in the plug-in classloader
            Object pathListObj = pathListField.get(dexClassLoader);
            //Get the value of dexElements in the plug-in classLoader
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pathListObj);

            /*
            * Host
            * */
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            //Get the value of pathList in the host classloader
            Object hostpathList = pathListField.get(pathClassLoader);
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostpathList);

            /**
             * Create a new array with the length of the host dexElements plus the length of the plug-in dexElements
             * */
            Object[] dexElements = (Object[]) Array.newInstance(pluginDexElements.getClass().getComponentType(),
                    pluginDexElements.length+hostDexElements.length);

            System.arraycopy(hostDexElements,0,dexElements,0,hostDexElements.length);
            System.arraycopy(pluginDexElements, 0, dexElements, hostDexElements.length, pluginDexElements.length);

            // Put the created dexElements into the host's dexElements
            // dexElements of host = new dexElements
            dexElementsField.set(hostpathList, dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The code logic is relatively simple, mainly through reflection, according to the four steps mentioned above to obtain the corresponding object, and then reflect the code in the plug-in.

After implementing LoadUtils, we need to add the MyApplication class to the host and implement it as follows:

package com.enjoy.myplugin;

import android.app.Application;

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        LoadUtils.loadClass(this);
    }
}

In the onCreate method of MyApplication, the implementation of LoadUtils is called. When the host APK just starts, it loads the related class of plug-in APP, and the purpose is to follow up our reflection through the call.

Next, implement the following methods in MainActivity and call:

private void invokePluginByLoadAPK(){
    try{
        Class<?> clz = Class.forName("com.enjoy.plugin.PluginTest");
        Method method = clz.getDeclaredMethod("ShowPluginMsg",String.class);
        Object obj = clz.newInstance();
        method.setAccessible(true);
        method.invoke(obj,"invokePluginByLoadAPK ShowPluginMsg");
    }catch(Exception e){
        e.printStackTrace();
    }
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Example of a call to a native method
    TextView tv = findViewById(R.id.sample_text);
    tv.setText(stringFromJNI());

    //printClassloader();
    //invokePluginMethod();
    invokePluginByLoadAPK();

}

After the code is written, we only need to copy or push the compiled plug-in APK to sdcard (/ sdcard / plugin debug. APK), then obtain the host APK to obtain the read-write SD card permission, and execute the host APK code;

The printing results are as follows:

01-13 17:31:59.489 13927-13927/? D/Rayman: PluginTest ShowPluginMsg msg = invokePluginByLoadAPK ShowPluginMsg

It indicates that we have successfully called the code in the plug-in APP.

The following flow chart can help us better understand the implementation process of the above code loading class.

 

Published 5 original articles, praised 0, visited 33
Private letter follow

Tags: Java Android

Posted on Mon, 13 Jan 2020 04:59:54 -0500 by Phirus