Shadow对PackageManager的处理方法

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

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.git

从官方文档上能肯定PackageManager通常都是经过Context的getPackageManager方法得到的,实际上咱们日常开发中也只有这个途径。github

显然,若是插件框架什么都不作,插件没有安装到系统中,PackageManager是不可能能够查询到插件的任何信息的。因此,插件框架要处理这个问题。bash

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

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

好比插件中写这样的代码:框架

context.getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA)
复制代码

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

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

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子类中覆盖这个方法就好了。ui

public Drawable getUserBadgeForDensity(UserHandle user, int density){
        return null;
}
复制代码

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

第二个问题就是,不光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.im/post/5d1357… 。因此,在多插件的场景下,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,不管是插件仍是系统都是拿不到的。

相关文章
相关标签/搜索