In Android development, it is inevitable to use PackageManager to obtain some information about the current application.
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.
From the official documentation can determine the PackageManager is generally obtained through the Context getPackageManager method, in fact, we usually develop only this way.
Obviously, if the plug-in framework does nothing and the plug-in is not installed on the system, PackageManager cannot query any information about the plug-in. So, the plug-in framework takes care of this.
Common old schemes and their problems
To make plug-in code aware of the plug-in framework, there is no need to write plug-in code such as shadow.getPluginContext ().getPluginPackagemanager () code, A common solution is to Override the getPackageManager method of the Context of the Override plugin, which returns a subclass of PackageManager. Then Override the various methods in the subclass to return information about the plug-in.
For example, write code like this in a plug-in:
context.getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA)
Copy the code
In such code getPackageName() is either a PackageName that is not installed on the system, or the PackageName of the host. So the PackageManager subclasses in OverridegetApplicationInfo method to judge the PackageName, then ApplicationInfo returns the corresponding plug-in.
This implementation doesn’t seem to have any problems, but in fact you’ll find various crashes behind the line. The reason is that the Android official system and OEM system will add abstract hide method to the Abstract class PackageManager. Such as:
Public abstract class PackageManager {/** * omitted annotation * @hide */ public abstract Drawable getUserBadgeForDensity(UserHandle) user, int density); }Copy the code
The getUserBadgeForDensity method is the hide method that can be seen in the official Android system source code. The abstract method of hide is compiled without overwriting when subclasses are inherited. But at run time the system will also get the plug-in Context, get our PackageManager subclass, and then call the hide method. When a system call is made, AbstractMethodError will appear and Crash. The solution is very simple, we just need to override the method in the PackageManager subclass.
public Drawable getUserBadgeForDensity(UserHandle user, int density){
return null;
}
Copy the code
You don’t have to write @override. But the implementation of the hide method is not guaranteed to be correct, which is the first problem with this scheme.
The second problem is that these hide methods are not only available in the Official Android system, but also in OEM systems. For example, Oppo phones have the isClosedSuperFirewall() method. In this case, the plug-in framework is constantly compatible with various OEM systems.
The above two problems are superficial, but the most important one is that this implementation actually violates the principle of not using any non-public API. We need to be compatible with non-public apis and actually use non-public apis.
Shadow of the scheme
First of all, is it possible for plug-in code to use these hide methods? No, business code should not use a private API, so these hide plugins are not used by themselves, only by the system. Does the system need to get private information about any plug-ins from these hide methods? No, the plug-in’s private information is not installed on the system, and it is useless and useless to let the system know.
To keep the implementation of the hide method unchanged, we cannot return a PackageManager subclass and cannot inherit PackageManager.
But we also need to change the implementation of some of PackageManager’s exposed methods, such as the getApplicationInfo method. So if inheritance doesn’t work, what else can change a class’s method implementation? Bytecode editing, of course. But can we modify the system’s PackageManager subclass implementation? Certainly not, the system PackageManager subclass is a private class, what name we are not sure, even if know what is useless, system class is not packaged in the plug-in, is in the system BootClassLoader. We can’t change their bytecode.
But we also have a way, is to modify the plug-in code in the PackageManager call code, these call code is not the system code but the plug-in’s own code. Such as:
public void test() {
PackageManager pm = context.getPackageManager();
ApplicationInfo info = pm.getApplicationInfo("packageName", GET_META_DATA);
}
Copy the code
The bytecode that calls the getApplicationInfo method on the PM object here belongs to the plug-in code.
So we have the opportunity to modify this line of calling code if we modify it to look like this:
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) { ... . }Copy the code
Can we just handle all the parameters of the call arbitrarily in the implementation of staticMethod and decide what value to return? Since we only modify the calls we care about, such as the getApplicationInfo method. So there is no need to implement every abstract method like the inherited PackageManager.
Bytecode editing of non-static calls to static calls
To implement the above method, you need to be able to make non-static calls to static calls in the bytecode editing. This high-level API wasn’t originally available in Javassist, but I looked into the JVM bytecode rules and found something interesting.
The non-static method add of class A and the static method add of class S each have five instructions of bytecode when called. Note that the add method of class S has one more parameter of type A than the add method of class A, but they are called with only A slight difference in bytecode. The difference lies in the fourth directive, the type and parameters of the invoke directive.
The other four instructions, the first three are to first push the parameters of the called method, the first instruction for non-static method invocation, is the called object itself. For static method calls, arguments are invoked from the first instruction. Therefore, when the call instruction of a non-static method is changed to a static call, the pushdown of the object being called becomes the first parameter of the static method. The last instruction is the return value instruction.
The actual bytecode editing only requires 2 bytes to be modified, see Shadow’s source code: Com. Tencent. Shadow. Core. Transform_kit. CodeConverterExtension# redirectMethodCallToStaticMethodCall.
So this is a very, very general AOP approach that will modify the behavior of any non-static call. Because static methods can get the original called object itself and all the arguments of the original call. So with this general approach, we also contributed back to Javassist: github.com/jboss-javas… . The latest version of Javassist now includes this method, so you can use it directly.
Multi-plug-in support
StaticMethod method in the Shadow in front of the actual code in com. Tencent. Shadow. Core. The transform. Specific. PackageManagerTransform# setupPackageManagerTran Sform. You can see that this method actually generates a static method called getApplicationInfo_shadow for getApplicationInfo.
In Shadow, the PackageName of the plugin is the same as that of the host. See juejin.cn/post/684490… . So, in a multi-plug-in scenario, if static methods receive the same PackageName, the static method implementation cannot distinguish which plugin’s ApplicationInfo to return. Therefore, we choose to use the ClassLoader of the plug-in as the key when the Loader loads the plug-in, and establish a Map of the ClassLoader to check the plug-in partKey. The static method can then be implemented as:
public static ApplictionInfo getApplicationInfo_shadow(PackageManager pm, String packageName, int flags) {
Classloader classloader = this.getClass().getClassLoader();
return PackageManagerInvokeRedirect.getApplicationInfo(classloader, packageName, flags);
}
Copy the code
The static call entrusted to the Runtime layer class PackageManagerInvokeRedirect again, so that the static call implementation can use the source code is more convenient to write. The current CLassLoader that calls the getApplicationInfo method is passed to it as a parameter, so it knows which plug-in it is. At the same time, note that we don’t need the original PackageManager to be called, but we can’t drop this argument in the first place, because of the bytecode editing details, the first argument of the static method we call through bytecode editing must be the original object to be called.
Some of the details
Shadow does not really inherit the PackageManager implementation subclass, there is a PluginPackageManager, this class is completed to return plug-in information, while holding the host PackageManager, Can be returned with the host PackageManager when querying non-plug-in information.
So in PluginPackageManager logic, where PackageName is equal to the host, we assume that what the code wants is information about the plug-in. For scenarios where the plug-in really wants to query the host’s information, the plug-in code can only get the host’s Context directly from the host’s PackageManager. In Shadow, the baseContext of the plug-in Context is the host Context, so you can use baseContext to get the host Context.
PluginPackageManager does not really need to inherit PackageManager. Because the PackageManager, either plug-in or system are not available.