1、前言

所谓插件化,是完结动态化的一种详细的技能手段。

关于移动端的App而言,无论是Android仍是iOS,都存在一个共同的问题,那便是更新的周期较长。

当咱们希望快速为App更新功用时,有必要经过开发、测验、发布、审阅、上线等一系列的流程。之后,还需求用户主动升级app才能够生效。

漫长的周期也使得发布新版别时的危险变得更大。而经过动态化,就能够在必定程度上来处理这个问题。

动态化是一个相对庞大的出题,落实到完结计划,其实有非常多的办法,各自适用的运用场景也各不相同。以下罗列了常见的几种计划:

  • 布局动态化。经过下发配置,再由客户端映射为详细的原生布局,完结动态化。这种计划的功用还不错,但只适合布局动态化,更新事务逻辑则较为困难。
  • H5容器。其实webview便是一个天然可完结动态化的计划。这种计划的稳定性和动态化能力都不错,首要缺陷是功用较差,究竟js是解说型言语,毕竟比不过原生。
  • 虚拟运转环境。如Flutter。Flutter所运用的Dart言语既是解说型言语,又是编译型言语,处理了上面说到的功用问题。但这种计划往往需求在apk中依靠一个sdk,添加了包大小。
  • 插件化。插件化经过动态下发部分代码,来完结动态的功用更新。前几年,插件化是较火的计划,近些年受制于体系的约束,变得越来越难以完结。

动态化的范围非常广,本文将聚焦插件化的计划,并以Shadow为例,介绍插件化的原理及Shadow的详细完结。

2、从插件化理论基础说起

插件化的本质,是经过后加载代码的办法来完结的。这个代码,能够是内置于apk里的一个独立产品,但更多的时分,是经过后下发的办法获取的。

动态加载代码这件事听起来很奥秘,但其实并没有什么特别的当地。咱们所熟知的C++的动态库,便是典型的,能够经过动态加载办法运转起来。

回到插件化上。插件化也是一样的思路。这个计划之所以能够完结,又和Java言语的特性分不开。

咱们先回顾一下Java言语的编译流程。Java言语从编写到运转,能够简略分为2步:

  1. 经过Java编译器(如javac)将Java源代码编译为.class文件,.class文件中包含了Java的字节码信息。
  2. 经过Java虚拟机(在Android上,首要指Art虚拟机与Dalvik虚拟机),将字节码再转换为对应的机器码进行执行。现在大部分的java虚拟机,都一同支持解说器和编译器。解说器使得程序能够快速发动,而编译器则担任把热门代码编译为机器码,提高程序的运转效率。

Java言语的这个特性,决定了它是一门完全的动态链接的言语。

所谓链接,指的是程序在编译和装载中心的一个阶段。链接能够分为静态链接和动态链接两种。

  • 动态链接,将对符号的重定位推迟到程序运转时才进行。以Java为例,类A依靠了类B的某个办法,在class文件中保存的其实是类B的称号和办法签名,直至实在需求调用这个办法的时分,才会去查找类B。
  • 静态链接则与之相对,在装载之前就会完结一切符号的引用。静态链接的长处是程序发布时无需带库可独立运转,而缺陷是糟蹋内存,且修正恣意一处需求编译一切当地。

除了少部分优化为Native的类,Java的类都是在运转时动态加载的,这其间也包含了咱们所熟知的Activity(但在非Debug方式下,Activity或许被优化为native)。

事实上,体系也是这么做的。只需求界说好基类Activity的接口,就能够New一个App中指定的Activity,向上转型为基类Activity来运用。

这儿的向上转型,指的是关于体系而言,只关心new了一个activity的目标,而且只关心这个目标上归于activity的那些办法。至于这些办法是否被子类重写,体系是不关心的。

我在这儿举例了activity的例子,是由于activity是咱们最常用的四大组件之一。但关于其他的组件,也是相似的办法。

那么到这儿,插件的基本原理也就比较清晰了。

一切的插件无外乎便是经过一个新的ClassLoader,去加载后下发的插件中的代码进行运用,然后完结动态化。经过classLoader加载代码,只需求一行代码罢了,这并没有什么技能上的难度。

插件化结构首先要处理的问题,并不是怎么动态加载Activity,而是加载后的Activity没有在AndroidManifest中注册,该怎么绕过体系约束发动的问题。当然,也包含其他的细节。

在本文的下半部分,我会为大家介绍一下,插件化技能在面临原生体系约束时遇到的一些问题,以及Shadow在这些问题上,是怎么考虑和选择的。

3、概念名词介绍

下文会以Shadow的官方demo为例,来介绍Shadow在插件化方面的规划。在介绍之前,咱们需求先简略一致一下各个名词的概念,防止歧义。

名词 概念
主进程和插件进程 多进程并不是插件化有必要的完结计划,但大部分状况下,咱们会用多进程的办法来完结。咱们用主进程表明app发动时的默认的进程,用插件进程表明加载并运转插件代码的那个进程。一般来说,当你的插件有许多activity流转时,这些activity便是在插件进程中被创建和展现的。
宿主工程和插件工程 在官方的demo中,宿主工程和插件工程是在一同的。可是这几个module事实上并无依靠关系,是各自独立编译的。宿主工程指编译可独立运转的apk的工程。而插件工程则指编译插件apk的工程,包含了pluginManager.apk和plugin.zip两个部分。需求注意,宿主工程中的代码并非只在主进程中运转。相同,插件工程中的代码也并非只运转在插件进程。

4、Shadow的工程结构

Android插件化框架-Shadow原理解析

从官方的Github上下载最新节点,能够看到Shadow的工程目录如上图所示。

其间,sample-host便是上文所指的宿主工程,sample-manager和sample-plugin下的一切module,统称为插件工程。

插件工程的编译产品有2个,分别是pluginmanager.apk和plugin.zip,而plugin.zip中又包含了4个apk。他们的关系如下图所示:

module称号 module编译产品 终究产品方式 是否动态加载 代码运转所在进程 首要职责
sample-host 可独立运转的apk 可独立运转的apk 主进程和插件进程均有 是对外发布的app
sample-manager pluginmanager.apk pluginmanager.apk 主进程 装置、办理及加载插件
sample-plugin/sample-app app-plugin.apk plugin.zip 插件进程 事务逻辑
sample-plugin/sample-base base-plugin.apk plugin.zip 插件进程 事务逻辑,被app以compileOnly的办法依靠
sample-plugin/sample-loader loader.apk plugin.zip 插件进程 插件的加载
sample-plugin/sample-runtime runtime.apk plugin.zip 插件进程 插件运转时的署理组件,如container activity(见下文)

咱们能够看到,上述的各个module都会编译出各自的独立apk,这也便是说,他们是相对独立的。经过运转时加载代码、动态链接的办法,终究构成一个完结的app。

5、Hack Activity的计划

上文现已说到,关于插件化而言,首要的挑战并不是怎么动态加载代码,而是插件的activity并没有实在在Manifest中注册,怎么绕过体系约束的问题。

那么咱们无妨先考虑一下,假如咱们自己完结一个插件化的结构,怎么处理这个问题。

比较直接的思路,是了解体系查看Manifest的原理,想办法Hack掉其间的关节进程,然后绕过查看。

明显,这种办法对体系的运转环境有必定的要求。当体系源码发生改动,或者国内厂商魔改了源码之后,都会存在必定兼容性的问题,需求不断适配。

在这个问题上,不管是360的Replugin仍是tencent的Shadow,都采用了相似的计划。那便是设法发动一个实在存在的activity,也便是实在在体系的Manifest中注册过的Activity。

咱们把在插件中,事务方想要发动的activity称之为PluginActivity。而实在注册在体系中的,没有详细事务逻辑的署理Activity,称之为ContainerActivity,也是一个几乎为空壳的壳Activity。

上述两个插件化的计划,都是在咱们测验经过Context#startActivity时分,经过一些办法修正intent。将本来测验发动PluginActivity的intent,移花接木为发动ContainerActivity的activity。

由于ContainerActivity是实在注册过的,那么权限查看这块就不存在问题。

再接下来的进程,两个插件的完结思路就不同了:

5.1 Replugin的思路:

Hack宿主的ClassLoader,使得体系收到加载ContainerActivity的恳求时,回来的是PluginActivity类。

由于PluginActivity本质上也是一个承继了android.app.Activity的类,经过向上转型为activity去运用,理论上不会存在什么问题。

Replugin的这个计划的问题之一,是需求在宿主apk中,为每一个插件的事务Activity注册一个对应的坑位Activity、。关于这点,咱们先看下ClassLoader load办法的签名:

public abstract class ClassLoader {
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        ......
    }
}

能够看到,ClassLoader在loadClass的时分,收到的参数只要一个类名。这就导致,关于每个事务插件中的Activity,都需求一个ContainerActivity与之对应。在宿主apk中,咱们需求注册许多的坑位Activity。

别的,Replugin hack了加载class的进程,后边也不得不持续用Hack手段处理体系看到了未装置的Activity的问题。比如体系为插件Activity初始化的Context是以宿主的apk初始化的,插件结构就不得不再去Hack修复。

5.2 Shadow的思路

Shadow则运用了另一种思路。已然对体系而言,ContainerActivity是一个实在注册过的存在的activity,那么就让这个activity发动起来。

一同,让ContainerActivity持有PluginActivity的实例。ContainerActivity将自己的各类办法,顺次转发给PluginActivity去完结,如onCreate等生命周期的办法。

Shadow在这儿所采用的计划,本质上是一种署理的思路。在这种思路中,事实上,PluginActivity并不需求实在承继Activity,它只需求承继一个与Activity有着相似的办法的接口就能够了。

Shadow的这个思路,一个ContainerActivity能够对应多个PluginActivity,咱们只需求在宿主中注册有限个有必要的activity即可。

而且,后续插件假如想要新增一个activity,也不是有必要要修正宿主工程。只要事务上答应,完全能够复用已有的ContainerActivity。

5.2.1 移花接木,替换intent

上文现已说到,Shadow在运转的时分,从体系角度看不到PluginActivity的存在。因而,PluginActivity是否承继了android.app.Activity就显得无关紧要。

事实上,Shadow也是这么干的。详细的技能完结,则是运用AOP的思路,运用官方的在构建进程中的Transform API来完结的。相似的技能,其完结已有许多开源结构用到了。

经过AOP,咱们的事务PluginActivity终究会被替换为承继com.tencent.shadow.core.runtime.ShadowActivity。而ShadowActivity,又承继自ShadowContext。

在ShadowContext中,咱们能够找到这样一段代码:

public class ShadowContext extends SubDirContextThemeWrapper {
        @Override
    public void startActivity(Intent intent, Bundle options) {
        final Intent pluginIntent = new Intent(intent);
        pluginIntent.setExtrasClassLoader(mPluginClassLoader);
        final boolean success = mPluginComponentLauncher.startActivity(this, pluginIntent, options);
        if (!success) {
            super.startActivity(intent, options);
        }
    }
}    

其间,第6行:

mPluginComponentLauncher.startActivity(this, pluginIntent, options)

便是将企图发动PluginActivity修正为发动ContainerActivity的详细完结了。Shadow会解析插件apk中的Manifest,一切在插件apk中注册过的Activity,都会被优先发动。

假如发动失利,还会测验运用super.startActivity,这个时分,便是发动宿主工程中的activity了。

分析这段代码,能够得出2个定论:

  1. 在插件中,会优先发动插件apk中的activity。
  2. 假如插件的activity没有在插件的Manifest中注册,那么还会测验发动宿主apk中的Activity。

5.2.2 runtime与classLoader

到了上面一步,一个插件的页面现已能够被发动了。可是距离实在能够运用,或者说让事务方无感知地运用,还有不少问题要处理。

在介绍Shadow的工程结构的时分,咱们有说到sample-plugin/sample-runtime这样的一个module。而这个module便是寄存上文说到的ContainerActivity的当地。

这是什么意思呢,便是说,咱们所说的ContainerActivity,也便是壳Activity,确实是在宿主apk中实在注册了的。可是它的代码,却是在一个后加载的插件中。不是打包在宿主apk中的,而是动态的。

Shadow这样的规划,是的宿主apk更加轻量化,动态化的程度也更高。可是却面临这一个问题:体系怎么能找到这个后下载的ContainerActivity?

5.2.2.1 什么是ClassLoader

为了答复上面的这个问题,咱们需求先复习一下ClassLoader相关的概念。

ClassLoader,顾名思义,是用来加载Java类的。有时分咱们会遇到一些ClassNotFoundException,“元凶巨恶”便是由于这个ClassLoader。

Android体系上有三个常见的ClassLoader,分别是BootClassLoader、PathClassLoader和DexClassLoader。他们的区别和联络是:

  • PathClassLoader和DexClassLoader都承继自BaseDexClassLoader
  • BootClassLoader用于加载Android Framework层的class文件,比如 Activity、Fragment。8.0以前,PathClassLoader 只能加载咱们装置过的 apk,DexClassLoader 能够加载sd卡上的apk。8.0以后,这两者没有什么区别

这儿要注意的是,由不同的ClassLoader加载的类,其实是不同的类。

在插件化的工程中,咱们常常能够遇到明明是同一个类,可是一个变量一瞬间是A一瞬间是B的场景。遇到这种case,往往便是由于ClassLoader不同导致的。

当一个类测验调用另一个类中的办法的时分,就会向加载了自己的那个ClassLoader去查找另一个类。假如没有加载过,就会首先经过这个ClassLoader进行加载,之后再持续运转。

而ClassLoader加载类的时分,又遵从了双亲派遣的方式,即优先派遣给父加载器进行加载。假如父加载器现已加载过,就不需求再加载了。这儿的双亲二字有必定的迷惑性,其实ClassLoader没有双亲,只要单亲。

在Android中,一般app运用的类都是由PathClassLoader加载的,而PathClassLoader的父加载器则是BootClassLoader。

为什么Java要规划这样的双亲派遣方式呢?大部分场景下,这样的规划都能符合实际的事务场景。例如,不同的事务都需求用到String目标,那么双亲派遣方式就能够确保这个目标都是经过同一个ClassLoader加载出来的。

5.2.2.2 Hack ClassLoader

先说定论:Shadow结构经过反射修正了PathClassLoader的父加载器。

本来的ClassLoader结构为BootClassLoader <- PathClassLoader,刺进后的结构变为BootClassLoader <- RuntimeClassLoader <- PathClassLoader。

这个新刺进的RuntimeClassloader,便是用来加载插件的Runtime的。ContainerActivity即由这个ClassLoader加载。

这个结构的修正,能够使得体系在向PathClassLoader查找ContainerActivity时能够正确找到完结,由于双亲派遣方式的规划,会让PathClassLoader会将加载ContainerActivity的恳求委托给RuntimeClassLoader。

咱们看一下Shadow中的源代码:

public class DynamicRuntime {
        private static void hackParentToRuntime(InstalledApk installedRuntimeApk, ClassLoader contextClassLoader) throws Exception {
        RuntimeClassLoader runtimeClassLoader = new RuntimeClassLoader(installedRuntimeApk.apkFilePath, installedRuntimeApk.oDexPath,
                installedRuntimeApk.libraryPath, contextClassLoader.getParent());
        hackParentClassLoader(contextClassLoader, runtimeClassLoader);
    }
    /**
     * 修正ClassLoader的parent
     *
     * @param classLoader          需求修正的ClassLoader
     * @param newParentClassLoader classLoader的新的parent
     * @throws Exception 失利时抛出
     */
    static void hackParentClassLoader(ClassLoader classLoader,
                                      ClassLoader newParentClassLoader) throws Exception {
        Field field = getParentField();
        if (field == null) {
            throw new RuntimeException("在ClassLoader.class中没找到类型为ClassLoader的parent域");
        }
        field.setAccessible(true);
        field.set(classLoader, newParentClassLoader);
    }
}

这个hackParentClassLoader的办法,便是替换classLoader的parent目标的办法了。

Shadow的这个规划,也是我觉得很有意思的一个规划。

6 插件Resource

至此,咱们现已顺利发动了一个Activity,还想办法把ContainerActivity也做成了动态化的一部份。唯一的小缺憾,或许是ContainerActivity需求在宿主中注册,这个现在没有什么好的技能手段能够去规避了。

6.1 资源 ID 抵触问题

那么下一个问题,便是插件中必定也会有对资源的拜访。一般状况下,资源拜访会是相似下面的这样的方式:

textView.setText(R.string.main_activity_info);

咱们对资源的拜访经过一个int值,而这个值是在apk的打包期间,由脚本生成的。这个值与详细的资源之间存在一一对应的关系。

由于插件和宿主工程是独立编译的,假如不修正分区,两者的资源或许存在抵触,这个时分就不知道应该去哪里加载资源了。

为了处理这个问题,Shadow修正了插件资源的id的分区。修正资源id并不杂乱,只需求一行代码就能够处理:

additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id"

反编译打包完结的apk,也很容易就能够发现,同一个资源的分区是不同的。宿主工程的是7f开头,而插件则是7e。

  • 宿主工程:

Android插件化框架-Shadow原理解析

  • 插件工程:

Android插件化框架-Shadow原理解析

6.2 怎么拜访插件资源

处理了 id 抵触的问题,还有一个问题需求考虑,那便是对体系而言,是看不到插件的存在的。那么,怎么让事务方能够获取插件的资源呢?

其实,Android中对资源是有着和同享库相似的加载机制的。咱们能够经过ApplicationInfo中的一个sharedLibraryFiles变量,拓宽对资源的拜访。虽然这个姓名听起来很像是同享动态库相关的目录,但实际上它确实是资源同享库。

咱们只需求把插件的途径添加到这个 sharedLibraryFiles 中,就能够了。咱们看下中心代码的完结:

object CreateResourceBloc {
    private fun fillApplicationInfoForNewerApi(
        applicationInfo: ApplicationInfo,
        hostApplicationInfo: ApplicationInfo,
        pluginApkPath: String
    ) {
        applicationInfo.publicSourceDir = hostApplicationInfo.publicSourceDir
        applicationInfo.sourceDir = hostApplicationInfo.sourceDir
        // hostSharedLibraryFiles中或许有webview经过私有api注入的webview.apk
        val hostSharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
        val otherApksAddToResources =
            if (hostSharedLibraryFiles == null)
                arrayOf(pluginApkPath)
            else
                arrayOf(
                    *hostSharedLibraryFiles,
                    pluginApkPath
                )
        applicationInfo.sharedLibraryFiles = otherApksAddToResources
    }
}

上述代码的中心在第 18 行。咱们能够看到,Shadow 把 pluginApkPath 添加到了applicationInfo中。后边还会用这个 applicationInfo 来结构 resource 目标。这样,就使得插件进程能够拜访插件的资源。

上面这个办法的称号叫做fillApplicationInfoForNewerApi。自然还有一个办法叫做fillApplicationInfoForLowerApi。

object CreateResourceBloc {
   /**
    * 在API 25及以下代替设置sharedLibraryFiles后经过getResourcesForApplication创建资源的计划。
    * 因调用addAssetPath办法也无法满意CreateResourceTest涉及的场景。
    */
   private fun fillApplicationInfoForLowerApi(
        applicationInfo: ApplicationInfo,
        hostApplicationInfo: ApplicationInfo,
        pluginApkPath: String
    ) {
        applicationInfo.publicSourceDir = pluginApkPath
        applicationInfo.sourceDir = pluginApkPath
        applicationInfo.sharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
    }
}

作者在注释中现已说明了为什么低版别和高版别采用不同的逻辑。在这些低版别中,会结构一个新的MixResources。这个计划依靠的是Resources的一个现已被废弃的结构器。

这个计划和高版别不同的当地在于,高版别是把插件目录添加到 sharedLibraryFiles 中。而低版别,则是结构一个只能加载插件目录的Resource目标。在需求加载资源时,优先交给pluginResource 加载,加载失利的时分再交给 hostResource 加载。下面的代码中的 tryMainThenShared,便是上述逻辑的表现。

@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
@TargetApi(CreateResourceBloc.MAX_API_FOR_MIX_RESOURCES)
private class MixResources(
    private val mainResources: Resources,
    private val sharedResources: Resources
) : Resources(mainResources.assets, mainResources.displayMetrics, mainResources.configuration) {
    private fun <R> tryMainThenShared(function: (res: Resources) -> R) = try {
        function(mainResources)
    } catch (e: NotFoundException) {
        function(sharedResources)
    }
    override fun getText(id: Int) = tryMainThenShared { it.getText(id) }
}

完好创建一个能够加载插件resource的代码如下:

object CreateResourceBloc {
        fun create(archiveFilePath: String, hostAppContext: Context): Resources {
        triggerWebViewHookResources(hostAppContext)
        val packageManager = hostAppContext.packageManager
        val applicationInfo = ApplicationInfo()
        val hostApplicationInfo = hostAppContext.applicationInfo
        applicationInfo.packageName = hostApplicationInfo.packageName
        applicationInfo.uid = hostApplicationInfo.uid
        if (Build.VERSION.SDK_INT > MAX_API_FOR_MIX_RESOURCES) {
            fillApplicationInfoForNewerApi(applicationInfo, hostApplicationInfo, archiveFilePath)
        } else {
            fillApplicationInfoForLowerApi(applicationInfo, hostApplicationInfo, archiveFilePath)
        }
        try {
            val pluginResource = packageManager.getResourcesForApplication(applicationInfo)
            return if (Build.VERSION.SDK_INT > MAX_API_FOR_MIX_RESOURCES) {
                pluginResource
            } else {
                val hostResources = hostAppContext.resources
                MixResources(pluginResource, hostResources)
            }
        } catch (e: PackageManager.NameNotFoundException) {
            throw RuntimeException(e)
        }
    }
}

6.3 未能处理的case

6.3.1 Case 1

咱们对插件资源的拜访,依靠于一个被处理过的resource目标。上文说到,Shadow现现已过trasform替换了PluginActivity所承继的父类为ShadowActivity,由于,咱们在拜访资源的时分,自然而然拿到的是一个PluginResource目标,这没有什么问题。

可是,在一些特殊的状况下,仍是会存在问题。

例如,假如资源的加载是体系完结的。运用把资源id交给体系,然后体系直接向宿主apk索取资源。在这个完结的途径上,Shadow的代码没有任何办法能够hook这个调用,自然也无法拜访到插件中的资源。

Activity的进场动画便是这个case,咱们看下Shadow的完结:

public class ShadowActivity extends PluginActivity {
    @Override
    public void overridePendingTransition(int enterAnim, int exitAnim) {
        //假如运用的资源不是体系资源,咱们无法支持这个特性。
        if ((enterAnim & 0xFF000000) != 0x01000000) {
            enterAnim = 0;
        }
        if ((exitAnim & 0xFF000000) != 0x01000000) {
            exitAnim = 0;
        }
        hostActivityDelegator.overridePendingTransition(enterAnim, exitAnim);
    }
}

shadow直接屏蔽了除了体系资源外的其他资源。更多信息能够参阅这个issue:传送门。

6.3.2 case 2

在 xml 中运用自界说 Drawable 的死后,xml 方式的 Drawable 是经过 Resource 中的 DrawableInflater 来解析和加载的。

可是插件中的DrawableInflater 运用的 ClassLoader 是宿主的 ClassLoader。当自界说的 Drawable中运用到了 R 文件后,加载的 R 文件也是宿主的,这就会导致找不到资源而溃散。

这儿溃散一方面是代码混杂导致的 R 文件加载不到,另一方面是在高版别上,运用sharedLibraryFiles也会出现无法加载插件资源的状况。经过测验,运用 MixResource 就不存在这个问题。后者估计是体系的 bug。

7、Shadow中的Trasform:PackageManager

上文现已说到,Shadow中的Activity,其实并不承继android.app.Activity,是承继com.tencent.shadow.core.runtime.ShadowActivity。而这一进程的完结,是经过Transform的API完结的。

经过AOP的思想去完结一些规划的优点是对用户无感知,可是坏事也是太无感知了。假如不熟悉规划,出现问题后就很难清查。

而Shadow中AOP的运用不只要这一处。还有对PackageManager的Hack。

在Android开发中免不了运用PackageManager获取当时运用的一些信息。而插件本身,对体系而言是看不到的。因而,结构需求处理这方面的问题。

一个直接的思路是直接覆写Context的getPackageManager办法,回来一个PackageManager的子类(ShadowPackageManager)。可是这种做法,在各个OEM上都会出现一些问题,原因是OEM或许会向PackageManager中添加各类Hide的办法,这些办法不需求覆写就能够编译经过,可是运转时就会出现AbstractMethodError的过错。

因而,Shadow在这个问题上的处理计划是,经过Transform的API,修正了事务中拜访PackageManger的当地。这些拜访的当地都是在事务的代码中,是完全能够修正的。

详细的代码能够参阅PackageManagerTransform。代码较多,这儿就不贴细节了。

这段代码的效果,是将插件中对体系的PackageManger的拜访,修正为对PackageManagerInvokeRedirect的拜访。相似于这样的逻辑:

public void test() {
    PackageManager pm = context.getPackageManager();
    ApplicationInfo info = staticMethod.getApplicationInfo(pm, "packageName", GET_META_DATA);
}
private static ApplicationInfo staticMethod(PackageManager pm, String packageName, int flags) {
    ...
    ...
}

但并非一切对PackageManager的办法的拜访都被修正了。详细Hack的接口,能够参阅PackageManagerInvokeRedirect的完结。

public class PackageManagerInvokeRedirect {
    public static PluginPackageManager getPluginPackageManager(ClassLoader classLoaderOfInvokeCode) {
        return PluginPartInfoManager.getPluginInfo(classLoaderOfInvokeCode).packageManager;
    }
    public static ApplicationInfo getApplicationInfo(ClassLoader classLoaderOfInvokeCode, String packageName, int flags) throws PackageManager.NameNotFoundException {
        return getPluginPackageManager(classLoaderOfInvokeCode).getApplicationInfo(packageName, flags);
    }
    public static ActivityInfo getActivityInfo(ClassLoader classLoaderOfInvokeCode, ComponentName component, int flags) throws PackageManager.NameNotFoundException {
        return getPluginPackageManager(classLoaderOfInvokeCode).getActivityInfo(component, flags);
    }
    public static ServiceInfo getServiceInfo(ClassLoader classLoaderOfInvokeCode, ComponentName component, int flags) throws PackageManager.NameNotFoundException {
        return getPluginPackageManager(classLoaderOfInvokeCode).getServiceInfo(component, flags);
    }
    public static ProviderInfo getProviderInfo(ClassLoader classLoaderOfInvokeCode, ComponentName component, int flags) throws PackageManager.NameNotFoundException {
        return getPluginPackageManager(classLoaderOfInvokeCode).getProviderInfo(component, flags);
    }
    public static PackageInfo getPackageInfo(ClassLoader classLoaderOfInvokeCode, String packageName, int flags) throws PackageManager.NameNotFoundException {
        return getPluginPackageManager(classLoaderOfInvokeCode).getPackageInfo(packageName, flags);
    }
    @TargetApi(Build.VERSION_CODES.O)
    public static PackageInfo getPackageInfo(ClassLoader classLoaderOfInvokeCode, VersionedPackage versionedPackage,
                                             int flags) throws PackageManager.NameNotFoundException {
        return getPluginPackageManager(classLoaderOfInvokeCode).getPackageInfo(versionedPackage.getPackageName(), flags);
    }
    public static ProviderInfo resolveContentProvider(ClassLoader classLoaderOfInvokeCode, String name, int flags) {
        return getPluginPackageManager(classLoaderOfInvokeCode).resolveContentProvider(name, flags);
    }
    public static List<ProviderInfo> queryContentProviders(ClassLoader classLoaderOfInvokeCode, String processName, int uid, int flags) {
        return getPluginPackageManager(classLoaderOfInvokeCode).queryContentProviders(processName, uid, flags);
    }
    public static ResolveInfo resolveActivity(ClassLoader classLoaderOfInvokeCode, Intent intent, int flags) {
        return getPluginPackageManager(classLoaderOfInvokeCode).resolveActivity(intent, flags);
    }
    public static ResolveInfo resolveService(ClassLoader classLoaderOfInvokeCode, Intent intent, int flags) {
        return getPluginPackageManager(classLoaderOfInvokeCode).resolveService(intent, flags);
    }
}

8、PluginManager与Plugin

在Shadow,插件编译后的产品有2个,分别是pluginmanager.apk与plugin.zip,这两个都是动态加载的插件代码部分。

8.1 PluginManager

Shadow将PluginManager部分独立出来,用于担任插件的装置、加载等流程。

PluginManager是在主进程被加载的,与事务的交互,也只要一个接口:

public interface PluginManager {
    /**
     * @param context  context
     * @param fromId   标识本次恳求的来源位置,用于区分入口
     * @param bundle   参数列表
     * @param callback 用于从PluginManager完结中回来View
     */
    void enter(Context context, long fromId, Bundle bundle, EnterCallback callback);
}

Shadow有一个该接口的完结类,DynamicPluginManager。DynamicPluginManager的Enter办法再次调用了updateManagerImpl办法,而这个办法创建的implLoader,才是咱们在pluginmanager.apk中完结的详细的加载类。demo中为SamplePluginManager。

public final class DynamicPluginManager implements PluginManager {
        @Override
    public void enter(Context context, long fromId, Bundle bundle, EnterCallback callback) {
        if (mLogger.isInfoEnabled()) {
            mLogger.info("enter fromId:" + fromId + " callback:" + callback);
        }
        updateManagerImpl(context);
        mManagerImpl.enter(context, fromId, bundle, callback);
        mUpdater.update();
    }
    private void updateManagerImpl(Context context) {
        File latestManagerImplApk = mUpdater.getLatest();
        String md5 = md5File(latestManagerImplApk);
        if (mLogger.isInfoEnabled()) {
            mLogger.info("TextUtils.equals(mCurrentImplMd5, md5) : " + (TextUtils.equals(mCurrentImplMd5, md5)));
        }
        if (!TextUtils.equals(mCurrentImplMd5, md5)) {
            ManagerImplLoader implLoader = new ManagerImplLoader(context, latestManagerImplApk);
            PluginManagerImpl newImpl = implLoader.load();
            Bundle state;
            if (mManagerImpl != null) {
                state = new Bundle();
                mManagerImpl.onSaveInstanceState(state);
                mManagerImpl.onDestroy();
            } else {
                state = null;
            }
            newImpl.onCreate(state);
            mManagerImpl = newImpl;
            mCurrentImplMd5 = md5;
        }
    }
}

能够看到,updateManagerImpl会在每次进入前判别前次加载的pluginmanager.apk的MD5是否发生变化。当MD5不一致的时分,就会重新load一个新的完结了PluginManager的实例。

主进程与插件的交互,都从这个enter接口开始。他们之间的依靠,也只要这个enter接口。

demo中的SamplePluginManager,才是实在担任插件的装置与加载的当地,首要包含了

  • 解压plugin.zip
  • 保存插件信息(存储数据库中)
  • 经过startService发动插件进程,建立通讯(经过shadow重写的binder,而不是AIDL)
  • 在插件进程中加载有必要的代码,包含:runtime、loader、base、app这4个apk
  • 手动调用application的onBaseContextAttached和onCreate办法(是在插件的manifest中注册的application,而不是宿主工程中的application。宿主工程的application的生命周期的执行,是由体系回调的,而且它的classLoader也是PathClassLoader)。

下图简略地说明了插件发动的整个进程。

Android插件化框架-Shadow原理解析

8.2 Plugin

Plugin的产品是一个zip包,除了4个apk外,还有一个叫config.json的json文件。

8.2.1 ConfigJson

ConfigJson是在打包进程中由脚本主动主动生成的。该文件中包含了插件的版别信息和插件apk的描述。

跟版别有关的信息如下:

  • version:标识插件的版别信息
  • compact_version:插件向下兼容的版别。在当时其实并没有运用的当地。
  • UUID:能够了解为是插件的id。UUID相同的同一组插件才能够在一同工作。当插件包的内容发生变化后,UUID也会相应改动。
  • UUID_NickName:对实际事务并没有什么效果,可是能够方便咱们办理插件。能够了解为是插件的一个通俗易懂的姓名。

一份完好的configjson的格式如下所示。

{
    "pluginLoader":{
        "apkName":"sample-loader-release.apk",
        "hash":"B26313DE458E7571F214CBD27F2E4DC1"
    },
    "plugins":[
        {
            "partKey":"sample-plugin-app",
            "apkName":"sample-app-plugin-releaseTest.apk",
            "dependsOn":[
                "sample-base"
            ],
            "businessName":"sample-plugin-app",
            "hash":"AF32CEA73F41A93E05DBA8B8C46F23AB"
        },
        {
            "partKey":"sample-base",
            "apkName":"sample-base-plugin-release.apk",
            "businessName":"sample-plugin-app",
            "hostWhiteList":[
                "com.xxx.a.b.c"
            ],
            "hash":"193A7AA41BFC1FCCDC8F8C316A95EB0E"
        }
    ],
    "runtime":{
        "apkName":"sample-runtime-release.apk",
        "hash":"1A1B36A5197D72E5AD128F07C4F4C302"
    },
    "UUID":"6EC46EC1-2358-4CF8-9B08-6BF5F0FB183D",
    "version":1,
    "UUID_NickName":"1.0.6"
}

8.2.2 businessName

businessName是比较容易了解的一个特点。它指的是该插件的事务名,能够为空。

当businessName为空的时分,插件与宿主就会运用相同的data目录。此刻,能够认为插件与宿主其实是同一个事务。

当businessName不为空的时分,宿主的data目录中就会有一个以businessName为称号的子目录。此刻,插件与宿主的数据(如SharedPreference、MMKV等)便是阻隔的。此刻,虽然MMKV本身具有支持多进程的能力,可是由于文件阻隔,导致插件也会无法拜访宿主的MMKV中的数据。

8.2.3 dependsOn与hostWhiteList

这两个特点与classLoader的双亲派遣方式是相关的。其间,dependsOn的优先级要比hostWhiteList高。

咱们先看一下PluginClassLoader的完结。

class PluginClassLoader(
    dexPath: String,
    optimizedDirectory: File?,
    librarySearchPath: String?,
    parent: ClassLoader,
    private val specialClassLoader: ClassLoader?, hostWhiteList: Array<String>?
) : BaseDexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) {
    @Throws(ClassNotFoundException::class)
    override fun loadClass(className: String, resolve: Boolean): Class<*> {
        var clazz: Class<*>? = findLoadedClass(className)
        if (clazz == null) {
            //specialClassLoader 为null 表明该classLoader依靠了其他的插件classLoader,需求遵从双亲派遣
            if (specialClassLoader == null) {
                return super.loadClass(className, resolve)
            }
            //插件依靠跟loader一同打包的runtime类,如ShadowActivity,从loader的ClassLoader加载
            if (className.subStringBeforeDot() == "com.tencent.shadow.core.runtime") {
                return loaderClassLoader.loadClass(className)
            }
            //包名在白名单中的类按双亲派遣逻辑,从宿主中加载
            if (className.inPackage(allHostWhiteTrie)) {
                return super.loadClass(className, resolve)
            }
            var suppressed: ClassNotFoundException? = null
            try {
                //正常的ClassLoader这儿是parent.loadClass,插件用specialClassLoader以跳过parent
                clazz = specialClassLoader.loadClass(className)!!
            } catch (e: ClassNotFoundException) {
                suppressed = e
            }
            if (clazz == null) {
                try {
                    clazz = findClass(className)!!
                } catch (e: ClassNotFoundException) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                        e.addSuppressed(suppressed)
                    }
                    throw e
                }
            }
        }
        return clazz
    }
}

第15行的specialClassLoader == null,对应的便是dependsOn不为空的时分。此刻,认为着该插件的apk依靠了其他插件。此刻,它的ClassLoader需求遵从规范的双亲派遣方式。这个时分,它的hostWhiteList的声明是无效的,需求界说在它所依靠的事务中才能够。

而第25行的className.inPackage(allHostWhiteTrie)对应的便是hostWhiteList特点了。有一些类,例如说,retrofit,能够考虑从宿主apk中加载代码。这样能够减少插件包的大小。

那么在这种状况下,能够设置hostWihiteList特点,答应插件拜访宿主中的类。

在release打包的时分,记得考虑混杂的影响。由于插件和宿主是独立编译的,混杂之后两边的类名会不一样,hostWhiteList特点就或许失效。

Shadow这样的规划,确保了大部分代码都是经过插件的ClassLoader加载的,又答应插件拜访宿主的部分代码。

9、总结

虽然跟着时代的发展,插件化现已没有过去几年那么火爆了。现存的还在保护的插件化结构也没有以前那么多了。

可是研讨插件化仍然是一件非常有意思的事。大部分的开源三方库都是在体系的答应规矩内去帮咱们去做一些事,如网络恳求、图片加载。而插件化反其道而行之,想办法绕过体系的约束,去做原生开发不让咱们做的事。这其间也涉及到了java和android的各方面的知识点。

在学习Shadow的进程中,我也遇到了许多坑,可是也学到了许多。

愿此文能协助正在学习的你们。