Shadow对PackageManager的处理方法

7,681 阅读7分钟

在Android开发中免不了使用PackageManager获取当前应用的一些信息。

Class for retrieving various kinds of information related to the application packages that are currently installed on the device. You can find this class through Context#getPackageManager.

从官方文档上能确定PackageManager一般都是通过Context的getPackageManager方法获得的,实际上我们平常开发中也只有这个途径。

显然,如果插件框架什么都不做,插件没有安装到系统中,PackageManager是不可能可以查询到插件的任何信息的。所以,插件框架要处理这个问题。

常见的旧方案和其存在的问题

想要让插件代码对插件框架无感知,不必在插件代码中写形如Shadow.getPluginContext().getPluginPackageManager()之类的代码,常见的方案就是Override插件的Context的getPackageManager方法,返回一个PackageManager的子类。然后在子类中Override各种方法,返回插件的信息。

比如插件中写这样的代码:

context.getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA)

在这样的代码中getPackageName()要么是一个没有在系统中安装的PackageName,要么就是宿主的PackageName。所以PackageManager的子类在OverridegetApplicationInfo方法时一般要判断PackageName,然后返回对应的插件的ApplicationInfo。

这种实现看起来没有任何问题,但是实际上线后会发现各种各样的Crash。原因在于Android官方系统和OEM系统都会向PackageManager这个抽象类增加抽象的hide方法。比如:

public abstract class PackageManager {
 /**
     * 省略注释
     * @hide
     */
    public abstract Drawable getUserBadgeForDensity(UserHandle user, int density);
}

getUserBadgeForDensity方法就是可以在Android官方系统源码中看到的hide方法。这样的hide的abstract方法,在继承子类时是不需要覆盖就能编译通过的。但是在运行时系统也会拿到插件的Context,get出我们的PackageManager子类,然后调用这个hide方法。当系统调用时,就会出现AbstractMethodError而Crash。解决方法也非常简单,我们只需要在PackageManager子类中覆盖这个方法就行了。

public Drawable getUserBadgeForDensity(UserHandle user, int density){
        return null;
}

也不用写@Override注解。但是这个hide方法的实现就不能保证是正确的了,这是这种方案的第一个问题。

第二个问题就是,不光Android官方系统有这些hide方法,OEM系统也有。比如Oppo手机上会有isClosedSuperFirewall()方法。这样的话,就需要插件框架不停地兼容各种OEM系统

上面两个问题只是表面的,最关键的问题是这种实现方法实际上违背了不使用任何非公开API的原则。我们需要去兼容非公开API实际上也是在使用非公开API。

Shadow的方案

首先我们分析了一下,插件代码有可能使用这些hide方法吗?不可能,业务代码就不应该使用非公开API,所以这些hide方法插件自己不会用,只有系统会去使用。系统会需要从这些hide方法中获取到任何插件的私有信息吗?也不可能,插件的私有信息就没有安装到系统,让系统知道了既没用也没有好处。

那么要保持hide方法的实现不变,我们就不能返回PackageManager的子类,不能继承PackageManager。

但是我们还需要改变PackageManager的一些公开方法的实现,比如getApplicationInfo方法。那么继承不能用了,还有什么能修改一个类的方法实现呢?自然是字节码编辑技术。但是我们能不能修改系统的PackageManager子类实现呢?肯定不能,系统的PackageManager子类是一个私有类,叫什么名字我们都不能确定,就算知道叫什么了也没用,系统类是不会打包在插件中的,是存在于系统的BootClassLoader中的。我们改不到人家的字节码。

但是我们还有一个办法,就是修改插件代码中对PackageManager的调用代码,这些调用代码不是系统代码而是插件自己的代码。比如:

public void test() {
    PackageManager pm = context.getPackageManager();
    ApplicationInfo info = pm.getApplicationInfo("packageName", GET_META_DATA);
}

这里面对pm对象调用getApplicationInfo方法的字节码就是属于插件代码的。

所以我们有机会将这行调用代码进行修改,如果我们修改成这样:

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) {
    ...
    ...
}

我们是不是就可以在staticMethod方法的实现中任意处理这次调用的所有参数,然后决定返回一个什么值了?由于我们只修改我们关心的调用,比如getApplicationInfo方法。所以也就不用像继承PackageManager一样,要对每一个抽象方法都要实现一遍。

非静态调用改为静态调用的字节码编辑

上面说的方法想要实现,需要在字节码编辑上能够做到非静态调用改为静态调用。本来在Javassist中是没有这种高级API的,但是我研究了一下JVM字节码的规则,发现了一点有趣的知识。

类A的非静态方法add和类S的静态方法add在被调用时,各有5个指令的字节码。注意,类S的add方法比类A的add方法多了一个类型是A的参数,但是它们被调用是的字节码只有一点点区别。区别就在于第4条指令,invoke指令的类型和参数。

其余的4条指令,前3条是先将被调用方法的参数压栈,第一条指令对于非静态方法的调用来说,就是被调用对象本身。而对于静态方法调用来说,从第一条指令开始就是调用参数了。所以,对非静态方法的调用指令改成静态调用后,原本被调用对象的压栈正好就成了静态方法的第一个参数。最后一条指令是return返回值指令。

实际字节码编辑只需要修改2个字节,见Shadow的源码:com.tencent.shadow.core.transform_kit.CodeConverterExtension#redirectMethodCallToStaticMethodCall

所以这是一个非常非常通用的AOP手段,可以将修改任意一个非静态调用的行为。因为静态方法可以拿到原本的被调用对象本身和原本调用的全部参数。所以如此通用的方法,我们在开发Shadow时也贡献回了Javassist:github.com/jboss-javas… 。目前Javassist的最新版本已经包含了这个方法,大家可以直接使用了。

多插件的支持

前面staticMethod方法在Shadow的实际代码位于com.tencent.shadow.core.transform.specific.PackageManagerTransform#setupPackageManagerTransform。可以看到实际上这个方法对于getApplicationInfo来说,生成的static方法叫getApplicationInfo_shadow

在Shadow里插件的PackageName都是和宿主一样的,原因见 juejin.cn/post/684490… 。所以,在多插件的场景下,static方法收到都是一样的PackageName,那static方法的实现就不能区分要返回哪个插件的ApplicationInfo了。所以,我们选择在Loader加载插件时,将插件的ClassLoader作为key,建立一个ClassLoader反查插件partKey的Map。这样实现static方法时就可以实现成:

public static ApplictionInfo getApplicationInfo_shadow(PackageManager pm, String packageName, int flags) {
    Classloader classloader = this.getClass().getClassLoader();
    return PackageManagerInvokeRedirect.getApplicationInfo(classloader, packageName, flags);
}

将这个static调用再次委托给Runtime层的类PackageManagerInvokeRedirect,这样这个static调用的实现可以用源码比较方便的撰写。当前调用getApplicationInfo方法的类所在的CLassLoader作为参数传给它,它就可以知道这是哪个插件了。同时注意到,我们就不需要原本被调用的PackageManager了,但是我们不能在第一时间放弃这个参数,因为前面讲的字节码编辑的细节,我们通过字节码编辑转调的静态方法的第一个参数必须是原来被调用的对象。

一些细节

Shadow中不是真的没有继承PackageManager实现子类,是有一个PluginPackageManager的,这个类中完成了返回插件信息,同时持有了宿主的PackageManager,可以在查询非插件信息时用宿主的PackageManager返回。

所以在PluginPackageManager的逻辑中,凡是PackageName等于宿主的,我们假设这个代码想要的就是插件的信息。对于插件真的想要查询宿主的信息的场景,只能让插件代码拿到宿主的Context后直接拿宿主的PackageManager获取。在Shadow中,插件的Context的baseContext都是宿主的Context,所以可以通过baseContext获得到宿主的Context。

写到这里,也相当于重新Review了一下这块的实现,发现PluginPackageManager确实也不需要真的继承PackageManager。因为这个PackageManager,无论是插件还是系统都是拿不到的。