Android插件化(一)-如何加载插件的类

介绍

插件化技术可以说是Android高级工程师所必须具备的技能之一。学习这项技术是关心背后技术实现的原理,但是在项目中能不用就不用,因为插件化的做法Google本身是不推荐的。

插件化技术最初是源于免安装运行apk的想法,这个免安装的apk我们称之为插件,而支持插件的APP我们称为宿主。所以插件化开发就是将整个APP拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk,最终发版的时候可以只发布宿主apk,插件apk在用户需要相应模块的功能的时候,才去从服务器上获取并且加载。

那么插件化能解决什么问题呢?

  • APP的功能模块越来越多,体积越来越大,通过插件化可以减少主包的大小。
  • 不发布版本上新功能。
  • 模块之间耦合度高,协同开发沟通成本越来越大。
  • 方法数目超过65535,APP占用内存比较大

插件化实现的过程需要思考如下几个问题:

  • 如何加载插件的类?
  • 如何加载插件的资源?
  • 如何调用插件类?

类加载器

Java和Android中的类加载器都是ClassLoader,Android中的ClassLoader的关系如下:

ClassLoader类图.png

我们可以写一个demo打印一下ClassLoader的关系:

    private void printClassLoader(){
        ClassLoader classLoader = getClassLoader();
        while (classLoader != null) {
            Log.i("jawe", "printClassLoader: classLoader="+classLoader);
            classLoader = classLoader.getParent();
        }

        Log.d("jawe", "printClassLoader: classLoader="+ Activity.class.getClassLoader());
    }

打印结果如下:

2019-12-10 13:29:48.498 25138-25138/? I/jawe: printClassLoader: classLoader=dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.studio.busdemo-1_HIoy4YiVYjhXH04u_SeQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.studio.busdemo-1_HIoy4YiVYjhXH04u_SeQ==/lib/arm64, /system/lib64, /vendor/lib64, /product/lib64]]]
2019-12-10 13:29:48.498 25138-25138/? I/jawe: printClassLoader: classLoader=java.lang.BootClassLoader@8a457f1
2019-12-10 13:29:48.498 25138-25138/? D/jawe: printClassLoader: classLoader=java.lang.BootClassLoader@8a457f1

由此可见我们自己对象的ClassLoader是PathClassLoader,PathClassLoader对象的parent是BootClassLoader,系统类Activity.class对象的ClassLoader也是BootClassLoader。

我们加载一个类的实现如下:

DexClassLoader classLoader = new DexClassLoader(appPath, context.getCacheFile().getAbsolutePath,null,comtext.getClassLoader);

classLoader.loadClass("com.jawe.test.Test");

通过这段代码我们看一下ClassLoader加载类的原理,这里使用8.0的源码查看。

/**
 * A class loader that loads classes from {@code .jar} and {@code .apk} files
 * containing a {@code classes.dex} entry. This can be used to execute code not
 * installed as part of an application.
 *
 * 

This class loader requires an application-private, writable directory to * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create * such a directory:

   {@code
 *   File dexOutputDir = context.getCodeCacheDir();
 * }
* *

Do not cache optimized classes on external storage. * External storage does not provide access controls necessary to protect your * application from code injection attacks. */ public class DexClassLoader extends BaseDexClassLoader { /** * Creates a {@code DexClassLoader} that finds interpreted and native * code. Interpreted classes are found in a set of DEX files contained * in Jar or APK files. * *

The path lists are separated using the character specified by the * {@code path.separator} system property, which defaults to {@code :}. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; must not be {@code null} * @param librarySearchPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), librarySearchPath, parent); } }

这里翻译一下类的注释文档:
一个从含有classes.dex实体的.jar或者.apk包中加载class的类加载器,这个类可以用来执行一个没有安装的应用的代码即插件中的代码。


/**
 * Provides a simple {@link ClassLoader} implementation that operates on a list
 * of files and directories in the local file system, but does not attempt to
 * load classes from the network. Android uses this class for its system class
 * loader and for its application class loader(s).
 */
public class PathClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code PathClassLoader} that operates on a given list of files
     * and directories. This method is equivalent to calling
     * {@link #PathClassLoader(String, String, ClassLoader)} with a
     * {@code null} value for the second argument (see description there).
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * 
    *
  • JAR/ZIP/APK files, possibly containing a "classes.dex" file as * well as arbitrary resources. *
  • Raw ".dex" files (not inside a zip file). *
* * The entries of the second list should be directories containing * native library files. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param librarySearchPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); } }

PathClassLoader的注释是:
提供一个简单的ClassLoader来执行系统本地文件的文件列表或者目录,但是不能从网络加载类。
Android使用这个类作为系统的类加载器,并且作为应用的类加载器。

从上边两个类我们可以看出两者区别是:
PathClassLoader是作为应用或者系统使用的类加载器,而DexClassLoader可以用来加载未安装apk的classes.dex.
DexClassLoader在构造方法内创建了一个存储优化dex的目录,而PathClassLoader没有。

我们看一下他们的父类BaseDexClassLoader的构造方法:

public class BaseDexClassLoader extends ClassLoader {
  ......
    /**
     * Constructs an instance.
     * Note that all the *.jar and *.apk files from {@code dexPath} might be
     * first extracted in-memory before the code is loaded. This can be avoided
     * by passing raw dex files (*.dex) in the {@code dexPath}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android.
     * @param optimizedDirectory this parameter is deprecated and has no effect
     * @param librarySearchPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if (reporter != null) {
            reporter.report(this.pathList.getDexPaths());
        }
    }
}

这里注意看参数optimizedDirectory的注释:这个参数已经废弃,并且无效了。
方法体中的new DexPathList的时候第四个参数直接传递null,这个参数也是优化目录optimizedDirectory。
所以在Android8.0中PathClassLoader和DexClassLoader无本质区别,但是使用的时候还是按照官方注释使用吧。加载插件apk的时候使用DexClassLoader,PathClassLoader是系统使用的。

通过源码可以知道加载类流程不在PathClassLoader和DexClassLoader中,在BaseDexClassLoader 也没有找到loadClass方法,根据类的继承关系向上查找父类是CalssLoader,在CalssLoader中查看loadClass的实现如下:

 public Class loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }


 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 {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {//1没有找到类
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

第一步检查这个类是不是已经加载过了,加载过就直接返回,否则调用parent的loadClass,前边的分析我们知道了PathClassLoader的parent是BootClassLoader,我们看一下BootClassLoader的实现:

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

   ......
}

BootClassLoader的findLoadedClass中没有找到clazz,就调用findClass,findClass中调用反射Class.classForName加载类。
在ClassLoader的loadClass我们看到如果parent也没有找到类,就调用子类本身的findClass方法。
以上流程就是我们常说的类加载的双亲委托机制。整个加载类的流程图如下:


双亲委托机制.png

这是大概的流程,那么具体的加载类是怎么实现的呢?

加载类流程

PathClassLoader和DexClassLoader里边只有构造方法,所以真正的findClass是在BaseDexClassLoader中实现的。

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

这里又调用的是 pathList.findClass的方法, 前边的分析得知pathList是在构造函数中创建的。我们继续往下看pathList.findClass的实现

final class DexPathList {
  private Element[] dexElements;
...
   public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {

         //安全校验
        ......      
     
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
        
        ......
        //加载native库
        ......
      }


     public Class findClass(String name, List 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;
      }

      private static Element[] makeDexElements(List files, File optimizedDirectory,
            List suppressedExceptions, ClassLoader loader) {
        Element[] elements = new Element[files.size()];
        int elementsPos = 0;
        /*
         * Open all files and load the (direct or contained) dex files up front.
         */
        for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      DexFile 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 {
                  DexFile dex = null;
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

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

}

DexPathList #findClass 主要是从数组dexElements数组的Element中findClass查找类,dexElements是在构造的时候根据接收到的path调用makeDexElements创建的,makeDexElements根据传进来的path扫描目录下的所有dex文件,由于optimizedDirectory是null,所以DexFile是通过new创建的,然后通过new Element(dex, null)创建Element对象。

至此就是ClassLoader加载类的过程,那么我们实现加载插件类就可以从这里为突破口。实现的过程大致如下:
1.创建插件的DexClassLoader,通过反射获取插件的dexElements值。
2.获取宿主的PathClassLoader,通过反射获取宿主的dexElements值。
3.合并插件的dexElements和宿主的dexElements,生成新的Element[]值。
4.通过反射将新的Element[]设置给宿主dexElements。

实现加载插件的类

1.准备
创建一个插件的app,插件类中有一个类Test如下:

public class Test {
    public static void test(){
        Log.i("jawe", "test: 我是插件中的方法");
    }
}

2.宿主app的module中创建一个工具类LoadUtils实现加载插件目录下的所有dex包,然后实现加载插件类的过程。

public class LoadUtils {
    public static final String pluginApkPath = "/sdcard/plugin-debug.apk";

    public static void loadPlugin(Context context){

        try {
            //1.宿主的elements
            Class baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);//允许访问私有属性
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Object hostPathList = pathListField.get(pathClassLoader);
            Field dexElementsField = hostPathList.getClass().getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object[] hostElements = (Object[]) dexElementsField.get(hostPathList);

            //2.插件的elements
            DexClassLoader dexClassLoader = new DexClassLoader(pluginApkPath, context.getCacheDir().getAbsolutePath(), 
                    null, pathClassLoader);
            Object pluginPathList = pathListField.get(dexClassLoader);
            Object[] pluginElements = (Object[]) dexElementsField.get(pluginPathList);

            //3.合并elements
            Object[] elements = (Object[]) Array.newInstance(hostElements.getClass().getComponentType(), 
                    hostElements.length+pluginElements.length);
            System.arraycopy(hostElements, 0, elements,0, hostElements.length);
            System.arraycopy(pluginElements, 0, elements, hostElements.length, pluginElements.length);

            //4.将新的elements设置给宿主的dexElements
            dexElementsField.set(hostPathList, elements);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注释很详细这里不在详述。
3.宿主加载插件的时机是越早越好,一个app最先调用的是Application的attachBaseContext方法。所以我们要在宿主中自定义一个Application,然后在attachBaseContext中调用LoadUtils.loadPlugin(this);

MainActivity调用插件中的方法如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.loadTv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Class clazz = Class.forName("com.jawe.plugin.Test");
                    Method testMethod = clazz.getMethod("test");
                    testMethod.invoke(null);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

至此就可以加载插件类和调用插件中类的方法了。

总结

通过这一节的学习我们知道了什么是双亲委托机制?类加载器的工作原理,Java的反射使用等等知识点。

你可能感兴趣的:(Android插件化(一)-如何加载插件的类)