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.
 *
 * <p>This class loader requires an application-private, writable directory to
 * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
 * such a directory: <pre>   {@code
 *   File dexOutputDir = context.getCodeCacheDir();
 * }</pre>
 *
 * <p><strong>Do not cache optimized classes on external storage.</strong>
 * 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.
     *
     * <p>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:
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
     * well as arbitrary resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * 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<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;
    }

这里又调用的是 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<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;
      }

      private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> 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的反射使用等等知识点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容