Android插件化原理(一):插件类的加载

插件化概述

  插件化技术最初源于免安装运行apk的想法,这个免安装的apk可以理解为插件。支持插件化的app可以在运行时加载和运行插件,这样便可以将app中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现app功能的动态扩展。插件化从最开始提出至今已经发展的非常成熟了,也涌现出了非常多的开源框架,从最开始的Dynamic-load-apk到后来比较有名的RePlugin、VirtualApk,还有前段时间腾讯开源的Shadow,开发者们也可以根据自身的需求以及框架的特性,选择最合适的框架。但是无论哪种框架,想要实现插件化,主要需要解决下面三个问题:

  • 插件类的加载
  • 四大组件生命周期的管理
  • 插件资源的加载

  接下来会针对上面几个问题进行分析,我是基于滴滴开源的VirtualApk框架进行分析,其他框架的实现与VirtualApk会有不同,但是插件化的原理大体是相似的,尤其是类加载以及资源加载,所以如果熟悉了一款插件化框架以后再去阅读其他的插件化框架都是比较容易的。
VirtualApk仓库链接

插件类的加载

  要想理解插件类加载的原理,必须要先对Java以及Android的类加载机制有所了解。这里我不打算深入地讲这个问题,毕竟我们的主题不是这个,对Android类加载机制还不了解的同学可以先看看相关的资料,我稍微提一下和插件化相关的一些知识。

Android类加载基础

  Android是通过ClassLoader来完成类加载的,Android中的ClassLoader与Java中的有一定的区别,在Android中主要三种类型的ClassLoader,分别是BootClassLoader、PathClassLoader以及DexClassLoader。其中BootClassLoader用于加载系统类,PathClassLoader和DexClassLoader都是用于加载应用程序类的,且它们都继承自BaseDexClassLoader,它们的类加载实现都在BaseDexClassLoader的findClass()中,这个方法我们后面会提到。

  这里说到了PathClassLoader和DexClassLoader,有必要说一下它们之间的区别。网上很多资料都流传着PathClassLoader只能加载已安装的apk,而DexClassLoader可以加载任意的apk,其实这种说法是错误的。我们以Android7.0为例看一下DexClassLoader和PathClassLoader的源码,如下所示:

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

  可以看到DexClassLoader和PathClassLoader的源码非常简单,只有构造方法,因为类加载相关的代码都在其父类中。它们之间的区别就在于调用父类构造方法的时候传递的第二个参数,DexClassLoader传递的是一个File类型的对象,PathClassLoader固定传递null。这个参数会一直传递到native层进行处理,具体逻辑比较复杂,最终这个路径是用来存放dex2oat的产物.odex文件的,如果传递的是null,就会存放在默认目录下(/data/dalvik-cache)。所以optimizedDirectory的传递并不影响dex的加载,因此DexClassLoader和PathClassLoader都可以加载任意的apk。另外值得一提的一点就是,optimizedDirectory在8.1以上的系统中被废弃了,我们可以看一下Android8.1中DexClassLoader的源码,如下所示:

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

  可以看到在Android8.1的源码中DexClassLoader的optimizedDirectory同样固定传递null,因此可以认为在Android8.1及以上的系统中,DexClassLoader和PathClassLoader是没有区别的。

  Android中默认是使用PathClassLoader来进行类的加载,当需要加载一个类的时候,都会通过一个默认的PathClassLoader进行加载,这个ClassLoader我们可以通过Context.getClassLoader()获取到。和Java一样的是,Android中ClassLoader同样是遵循双亲委派模型的,其中PathClassLoader的父类加载器是BootClassLoader,BootClassLoader没有父类加载器。所以如果我们要加载的类是系统类,最终会由BootClassLoader完成加载,如果要加载的是应用程序类,则会交由PathClassLoader完成加载。那么PathClassLoader是如何完成类加载的呢?
  前面已经说到了PathClassLoader和DexClassLoader都继承自BaseDexClassLoader且它们的类加载实现都在其父类的findClass()中,因此我们看看该方法,如下所示:

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是一个DexPathList类型的成员变量,因此我们再看一下DexPathList的findClass()的实现,如下所示:

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

  DexPathList的内部有一个成员变量dexElements,它是一个Element类型的数组,Element的内部有一个DexFile类型的成员,而DexFile就是用于加载一个dex文件的。调用DexPathList的findClass()时,会遍历dexElements,依次从每个Element中取出DexFile,调用DexFile的loadClassBinaryName(),该方法内部会调用native方法从其对应的dex文件中完成类的加载,更深入的过程我们就不需要关注了。
  看到这里我们对Android的类加载机制有了一定的了解,正常情况下Element数组只会包含宿主的dex信息,而我们的插件存放的位置可以是任意的,系统也并不知道插件的存在,所以正常情况下插件类是无法被加载的,因此我们需要特殊的处理解决插件类加载的问题。

VirtualApk源码实现

  接下来我们就通过VirtualApk源码来看一下VirtualApk是如何实现插件类的加载的。当我们需要加载一个插件的时候,会调用PluginManager的loadPlugin(),该方法如下所示:

public void loadPlugin(File apk) throws Exception {
    if (null == apk) {
        throw new IllegalArgumentException("error : apk is null.");
    }
    if (!apk.exists()) {
        InputStream in = new FileInputStream(apk);
        in.close();
    }
    LoadedPlugin plugin = createLoadedPlugin(apk);
    if (null == plugin) {
        throw new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
    }
    mPlugins.put(plugin.getPackageName(), plugin);
    synchronized (mCallbacks) {
        for (int i = 0; i < mCallbacks.size(); i++) {
            mCallbacks.get(i).onAddedLoadedPlugin(plugin);
        }
    }
}

  该方法首先判断了我们要加载的插件文件是否存在,如果存在则调用createLoadedPlugin()创建一个封装了插件信息的LoadedPlugin对象,并将这个对象根据插件包名添加到mPlugins保存起来,createLoadedPlugin()内部就直接通过构造方法创建了一个LoadedPlugin对象,因此我们看一下LoadedPlugin的构造方法,方法如下:

public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
    ...
    mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);  // 1
    ...
    mResources = createResources(context, getPackageName(), apk);  // 2
    mClassLoader = createClassLoader(context, apk, mNativeLibDir, context.getClassLoader());  // 3
    ...
    invokeApplication();  // 4
}

  LoadedPlugin的构造方法比较长,我们只看一下我们关心的部分。注释1处调用了PackageParserCompat.parsePackage(),这个方法会调用PackageParser的parsePackage()去解析我们插件APK,得到一个包含在插件AndroidManifest文件中声明的Application以及四大组件信息的Package对象,这个Package对象在四大组件插件化的实现上起着非常重要的作用,在四大组件插件化的章节我们会再次提到它。其实我们在安装一个apk时,系统的PackageManagerService也会调用PackageParser的parsePackage()去解析我们的apk,这个过程是一样的。接着在注释2出调用createResources()创建用于加载插件资源的Resource对象,这个过程我们会在资源的插件化章节中进行分析。注释3调用createClassLoader()创建了一个ClassLoader对象,最后在注释4处调用invokeApplication()根据注释1解析到的Application信息实例化一个表示插件的Application对象并调用它的onCreate()方法。那我们这里重点关注一下createClassLoader()的实现,方法如下所示:

protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
    File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
    String dexOutputPath = dexOutputDir.getAbsolutePath();
    DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);  // 1
    if (Constants.COMBINE_CLASSLOADER) {  // 2
        DexUtil.insertDex(loader, parent, libsDir);  // 3
    }
    return loader;
}

  可以看到方法在注释1处创建了一个DexClassLoader,创建DexClassLoader时传递的第一个参数表示dex文件的路径,这里传递了插件apk的绝对路径,第四个参数表示其父类加载器,这里传递的parent是前面通过Context.getClassLoader()获取到的,这个ClassLoader我们知道就是PathClassLoader,这样形成了DexClassLoader -> PathClassLoader -> BootClassLoader的类加载结构,当用插件对应的DexClassLoader进行加载时,根据双亲委派模型,会先交给宿主的PathClassLoader进行加载并继续向上传递。那么是不是宿主类会由PathClassLoader加载而插件类由DexClassLoader加载呢?答案是不一定,我们先继续往下看,注释2处判断Constants.COMBINE_CLASSLOADER,若为真则执行注释3处代码,Constants.COMBINE_CLASSLOADER是个常量为true,因此默认情况下都会执行注释3的代码,除非修改源码重新编译。而我们前面说到的问题正是由注释3代码是否执行决定,因此我们需要知道该行代码究竟做了什么,如下所示:

public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
    Object baseDexElements = getDexElements(getPathList(baseClassLoader));
    Object newDexElements = getDexElements(getPathList(dexClassLoader));
    Object allDexElements = combineArray(baseDexElements, newDexElements);
    Object pathList = getPathList(baseClassLoader);
    Reflector.with(pathList).field("dexElements").set(allDexElements);

    insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}

  我们前面讲到了PathClassLoader内部有一个DexPathList类型的成员变量,DexPathList内部又有一个Element类型的数组,每个Element都对应一个dex文件,类加载最终就是通过这个Element内的DexFile进行加载的。那么如果我们构造一个对应插件dex的Element对象,并把它添加到PathClassLoader的Element数组中,PathClassLoader不就可以加载插件中的类了吗?VirtualApk这里正是用了这种思路,上面代码看到的getPathList()getDexElements()都是通过反射获取DexPathList对象以及Element数组。上面代码首先获取了baseClassLoader的Element数组,这个baseClassLoader就是宿主的PathClassLoader;接着再获取dexClassLoader的Element数组,这个dexClassLoader是为插件创建的DexClassLoader,创建DexClassLoader时传递了插件的路径,DexClassLoader的Element数组内包含了对应我们插件的dex文件的Element对象;接着调用combineArray()将前面获得的两个数组合并生成新的Element数组;最后再通过反射将这个新的Element数组赋值给宿主的PathClassLoader。

  那么接下来分析一下上述代码对类加载流程的影响,首先需要明确的一点是,当需要加载一个类的时候,除非我们显式指定了用哪个类加载器加载(例如我们执行classLoader.loadClass("")),否则都会通过加载当前类的类加载器进行加载,例如我们想要在宿主的Activity加载一个插件类的时候(执行Class.forName("")),就会调用PathClassLoader的loadClass()进行加载。
  我们分以下几种情况分别看看类加载的过程是怎样的:

1. Constants.COMBINE_CLASSLOADER为true

  此时会执行DexUtil.insertDex(),宿主的PathClassLoader既可以加载宿主类也可以加载插件类。

  • 在宿主中加载一个普通的插件类时(非四大组件启动),会通过加载当前宿主类的ClassLoader即PathClassLoader进行加载,由于PathClassLoader可以加载插件类,因此会由PathClassLoader完成加载。
  • 在宿主中启动一个插件的四大组件,这时候就会由插件对应的DexClassLoader进行加载(这个过程在下一节会详细介绍,现在只要知道是调用DexClassLoader的loadClass()进行加载就可以了),这个过程遵循双亲委派模型,最终会先由宿主的PathClassLoader进行加载,显然PathClassLoader是可以完成加载的。
  • 在插件类中加载类(本插件的类或是其他插件的类或是宿主类),因为插件类是由宿主的PathClassLoader加载,因此在加载任意的类时,都会调用PathClassLoader进行加载,由于PathClassLoader包含了宿主以及各个插件的dex,因此都会由PathClassLoader完成加载。

  那么我们总结一下,如果Constants.COMBINE_CLASSLOADER为true,所有的应用类最终都会由PathClassLoader完成加载,DexClassLoader并没有参与到任何类的实际加载过程中。另外在这种场景下,宿主和插件以及插件与插件之间是可以相互依赖的,整体是一个单ClassLoader架构。

2. Constants.COMBINE_CLASSLOADER为false

  此时不会执行DexUtil.insertDex(),因此宿主的PathClassLoader只能加载宿主类,插件类只能由插件对应的DexClassLoader加载。

  • 在宿主中加载一个普通的插件类(非四大组件启动),会通过加载当前宿主类的ClassLoader即PathClassLoader进行加载,此时PathClassLoader无法加载插件类,因此会抛出ClassNotFoundException。
  • 在宿主中启动一个插件的四大组件,这时候就会由插件对应的DexClassLoader进行加载,这时是可以完成类加载的,因此宿主可以正常启动插件的四大组件。
  • 在插件中加载本插件的类,此时会由加载本插件类的ClassLoader即插件对应的DexClassLoader进行加载,这个过程显然是没有问题的。
  • 在插件中加载宿主类,此时会由加载本插件类的ClassLoader即插件对应的DexClassLoader进行加载,但根据双亲委派模型,会先交由宿主的PathClassLoader进行加载,PathClassLoader可以完成宿主类的加载。
  • 在插件中加载其他插件的类(非四大组件启动),会通过加载当前插件类的DexClassLoader进行加载,这个DexClassLoader只能加载本插件,其父加载器PathClassLoader也无法加载插件类,因此无法加载其他插件类,会抛出ClassNotFoundException。
  • 在插件中启动一个其他插件的四大组件,这时候会由要启动的四大组件所在的插件对应的DexClassLoader进行加载(这里还是先放一下,下一节会说到的),因此可以正常进行类加载并启动其他插件的四大组件。

  这里也是总结下,如果Constants.COMBINE_CLASSLOADER为false,宿主与插件以及插件与插件之间的四大组件是可以正常启动的,插件可以调用宿主的类,但是宿主没法加载并调用插件类,插件之间的类也是无法相互加载调用的。可以看到这种场景下是多ClassLoader架构的,宿主有专用的PathClassLoader,每个插件也有对应的DexClassLoader,相比前一种场景宿主与插件之间的隔离性会更好,健壮性也会更好,例如当不同插件依赖了同一类库的不同版本时,它们是可以相互共存的,因为不同类加载器加载出的类不被认为是同一个。

  到这里我们再回顾一下之前留下的问题,是不是宿主类会由PathClassLoader加载而插件类由DexClassLoader加载呢?想必看到这就很清晰了,答案是不一定,取决于Constants.COMBINE_CLASSLOADER的值,如果为true所有的应用程序类都会由宿主的PathClassLoader加载,插件的DexClassLoader没有实际参与到类加载流程中;若为false,宿主的PathClassLoader只加载宿主类,插件类由插件对应的DexClassLoader负责加载。

  通过上面的分析,VirtualApk对插件类加载的处理也都完成了,经过上面的处理后,在需要加载一个类时都会自动地找到对应的类加载器进行加载。其实这里我觉得VirtualApk的实现不是特别完美,因为在Constants.COMBINE_CLASSLOADER为true的情况下,宿主和插件之间可以完全的相互调用,但是宿主和所有的插件都用同一个PathClassLoader加载健壮性会比较差;Constants.COMBINE_CLASSLOADER为false时宿主和插件用单独ClassLoader进行加载健壮性变好了,但相互之间的调用变得困难。那么有没有一种方案既可以实现宿主和插件之间用不同的ClassLoader进行加载,还能够让宿主与插件之间的调用没有限制呢?
  答案是有的,出现宿主与插件之间不能相互调用的原因是加载类所需的ClassLoader并不在当前的类加载结构上,比如宿主想要加载插件的类,会调用PathClassLoader进行加载,PathClassLoader的类加载结构是PathClassLoader -> BootClassLoader,而加载插件所需的DexClassLoader并不在其中,所以无法加载。因此可以通过自定义一个ClassLoader,通过反射形成PathClassLoader -> 自定义ClassLoader -> BootClassLoader类加载结构,这个ClassLoader不负责具体的类加载,只是接管了类加载流程,这样就可以在自定义ClassLoader内挑选合适的ClassLoader进行加载,这样就解决了上面的问题,感兴趣的同学可以思考下如何实现。

  插件类的成功加载也为后边解决四大组件的插件化问题奠定了基础,由于四大组件都不是普通类,创建出实例它们还不能正常工作,它们需要频繁与AMS通信,且有复杂的生命周期需要处理,所以下一节我们将解决四大组件插件化的问题。

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

推荐阅读更多精彩内容