------This article reproduced from the Android plug-in principle analysis-plug-in loading mechanism
This series of articles is really good to write!
5 Hook ClassLoader
From the above analysis, we know that in the process of acquiring loadedapk, we use a cached data;
This cached data is a map, from the package name to a mapping of the loadedapk. Under normal circumstances, our plug-in will certainly not exist in this object;
But what if we manually add the information from our plugin to the inside? The system will hit the cache directly during the lookup of the cache!
Then use the loadedapk classloader that we added to load this particular activity Class! So we can take over the loading process of our own plugin Class!
This cache object Mpackages exists in the Activitythread class; The old method, we first get this object:
First get to the current Activitythread object class<?> activitythreadclass = Class.forName ("Android.app.ActivityThread"); Method Currentactivitythreadmethod = Activitythreadclass.getdeclaredmethod ("Currentactivitythread"); Currentactivitythreadmethod.setaccessible (true); Object Currentactivitythread = Currentactivitythreadmethod.invoke (null);//gets to mpackages this static member variable, which caches the Dex package information field Mpackagesfield = Activitythreadclass.getdeclaredfield ("Mpackages" ); Mpackagesfield.setaccessible (true); Map mpackages = (map) mpackagesfield.get (currentactivitythread);
What happens next after you get this map? We need to populate this map and plug the plugin information into this map,
So that the system can hit the cache when it looks up. But this fills this map out of the need for a package name,
You also need a loadedapk object; how do I create a loadedapk object?
We can, of course, directly reflect the constructor that calls it to create the desired object directly, but what if there is an omission and the construction parameter is incorrectly filled?
or the different versions of Android use different parameters, resulting in the object we created is inconsistent with the object created by the system, what can I do?
So we need to create the loadedapk object in exactly the same way as the system, and from the above analysis we know that
The system creates the Loadedapk object through Getpackageinfo, so we can call this function to create the LOADEDAPK object;
But this function is private and we can't use it.
Some children's shoes may have doubts, private can not also reflect it? We can actually call this function,
But private indicates that this function is an internal implementation, perhaps that day Google happy, the function of changing the name of our directly GG;
But the public function is different, the public is exported function you cannot guarantee whether someone else calls it, so most of the cases will not be modified;
We'd better call the public function to ensure that you have as few compatibility issues as possible.
(Of course, if the real wood has a way to consider calling the private method, handle the compatibility problem yourself, this we will encounter later)
The public function that indirectly calls Getpackageinfo this private function has the same name as the Getpackageinfo series and Getpackageinfonocheck;
Simple view of source code discovery, Getpackageinfo in addition to obtaining information about the package, but also check some components of the package;
To circumvent these validations, we chose to use Getpackageinfonocheck to obtain loadedapk information.
5.1 Building a plug-in loadedapk object
The purpose of this step is to make it clear that the Getpackageinfonocheck function is used to create the LOADEDAPK object we need for the next use.
The signature of this function is as follows:
Public final loadedapk Getpackageinfonocheck (ApplicationInfo ai, compatibilityinfo compatinfo) {
Therefore, in order to call this function, we need to construct two parameters. One is ApplicationInfo, the other is compatibilityinfo;
The second parameter, as the name implies, represents the compatibility information for this app, such as the TARGETSDK version, and so on, where we just need to extract the information from the app.
So you can use the default compatibility directly; There is a public field in the Compatibilityinfo class
Default_compatibility_info represents the default compatibility information, so our primary goal is to get this applicationinfo information.
5.2 Building Plugin ApplicationInfo information
Let's first look at what ApplicationInfo stands for, and the documentation for this class is very clear:
Information can retrieve about Aparticular application. This corresponds-information collected from Theandroidmanifest.xml ' s <application> tag.
In other words, this class is the androidmanifest.xml inside of the label below the information;
This androidmanifest.xml is undoubtedly a standard XML file, so we can use parse to parse this information ourselves.
So how does the system get this information? In fact, the framework has a such parser, that is, packageparser;
Theoretically, we can also borrow the parser of the system to parse Androidmanifest.xml and get applicationinfo information.
Unfortunately, the compatibility of this class is poor; Google almost every Android version of this kind of knife,
If you persist in using the system's parsing method, you must write a series of compatible line codes!! Droidplugin chose this way.
We decided to use the Packageparser class to extract applicationinfo information, which seems to have the method generateapplication we need;
Indeed, depending on this method we can successfully get to applicationinfo.
Since Packageparser is @hide, we need to call through reflection. We are based on the signature of this generateapplicationinfo method:
public static ApplicationInfo Generateapplicationinfo (package p, Int. flags, packageuserstate State)
You can write the reflection code that calls Generateapplicationinfo:
class<?> Packageparserclass = Class.forName ("Android.content.pm.PackageParser");//First Get our ultimate goal: Generateapplicationinfo method//API!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! public static ApplicationInfo Generateapplicationinfo (package P, Int. flags,// packageuserstate State) {// Other Android versions are not guaranteed to be so .class<?> Packageparser$packageclass = Class.forName ("android.content.pm.packageparser$ Package "); class<?> Packageuserstateclass = Class.forName ("Android.content.pm.PackageUserState"); Method Generateapplicationinfomethod = Packageparserclass.getdeclaredmethod ("Generateapplicationinfo", Packageparser$packageclass,int.class, Packageuserstateclass);
To successfully invoke this method, you need three more parameters, so next we need to build a step-by-step procedure to call this function's parameter information.
5.3 Building Packageparser.package
The first parameter required by the Generateapplicationinfo method is packageparser.package;
From the name of this class to represent the information of an APK package, let's see how the document explains:
Representation of a full package parsed fromapk files on disk. A package consists of a single base APK, and zero or Moresplit APKs.
Sure enough, this class represents the information from an APK package that is parsed from Packageparser and is a representation of the data structure in memory of the APK file on disk;
Therefore, to get this class, you will definitely need to parse the entire APK file. The core method of parsing apk in Packageparser is Parsepackage,
This method returns an instance of the package type, so we call this method and use the reflection code as follows:
First, we have to create a package object for this method call//And this requires that the object can be returned by Android.content.pm.packageparser#parsepackage this method The package object gets a field to get//Create a Packageparser object for use with object packageparser = Packageparserclass.newinstance ();//Call Packageparser.parsepackage parsing apk information Method Parsepackagemethod = Packageparserclass.getdeclaredmethod ("ParsePackage" , File.class, Int.class);//is actually a Android.content.pm.PackageParser.Package object packageobj = Parsepackagemethod.invoke (Packageparser, apkfile, 0);
In this way, we get the first parameter of the Generateapplicationinfo, the second parameter is the flag used by the parsing packet, we directly choose to parse all the information, that is, 0;
5.4 Building Packageuserstate
The third parameter is Packageuserstate, which represents the information for the package in different users. Since Android is a multi-tasking multi-user system,
So different users may have different states for the same package; Here we just need to get the information of the package, so we can use the default.
At this point, generateapplicaioninfo parameters we have all constructed, call this method directly to get the ApplicationInfo object we need;
Before we go back, we need to make a little bit of a change: Using this method of system system to parse the ApplicationInfo object
There is no information about the APK file itself, so we set the path to the parsed apk file (Classloa der relies on the Dex file and the path of the APK):
The third parameter mdefaultpackageuserstate we use the default constructor directly to construct an object defaultpackageuserstate = Packageuserstateclass.newinstance ();//Everything has!!!!!!!!!!!!!! ApplicationInfo ApplicationInfo = (applicationinfo) generateapplicationinfomethod.invoke (PackageParser, Packageobj, 0, defaultpackageuserstate); String Apkpath = Apkfile.getpath (); applicationinfo.sourcedir = Apkpath;applicationinfo.publicsourcedir = ApkPath;
5.5 Replace ClassLoader5.5.1 get loadedapk information
Just to get applicationinfo we took a lot of energy; review our intentions:
Our ultimate goal is to call Getpackageinfonocheck to get loadedapk's information,
and replace the Mclassloader and add it to the Activitythread mpackages cache;
Thus achieving the purpose of using our own ClassLoader to load the classes in the plugin.
And now we've got the Getpackageinfonocheck. The first parameter ApplicationInfo in this method is very important;
The second parameter mentioned above Compatibilityinfo represents the device compatibility information, directly using the default value;
So, two parameters have been constructed and we can call Getpackageinfonocheck to get loadedapk:
android.content.res.compatibilityinfoclass<?> Compatibilityinfoclass = Class.forName (" Android.content.res.CompatibilityInfo "); Method Getpackageinfonocheckmethod = Activitythreadclass.getdeclaredmethod ("Getpackageinfonocheck", Applicationinfo.class, Compatibilityinfoclass); Field Defaultcompatibilityinfofield = Compatibilityinfoclass.getdeclaredfield ("Default_compatibility_info"); Defaultcompatibilityinfofield.setaccessible (true); Object Defaultcompatibilityinfo = Defaultcompatibilityinfofield.get (NULL); ApplicationInfo applicationinfo = Generateapplicationinfo (apkfile); Object loadedapk = Getpackageinfonocheckmethod.invoke (Currentactivitythread, ApplicationInfo, Defaultcompatibilityinfo);
We have successfully constructed the loadedapk, then we need to replace the ClassLoader, and then add it into the Activitythread mpackages:
String Odexpath = Utils.getpluginoptdexdir (applicationinfo.packagename). GetPath (); String Libdir = Utils.getpluginlibdir (applicationinfo.packagename). GetPath (); ClassLoader ClassLoader = new CustomClassLoader (Apkfile.getpath (), Odexpath, Libdir, Classloader.getsystemclassloader ()); Field Mclassloaderfield = Loadedapk.getclass (). Getdeclaredfield ("Mclassloader"); Mclassloaderfield.setaccessible ( true); Mclassloaderfield.set (loadedapk, ClassLoader);//Because it is a weak reference, so we have to save a copy somewhere, otherwise it is easy to be GC; Then it will be wasted. Sloadedapk.put (Applicationinfo.packagename, loadedapk); WeakReference weakreference = new WeakReference (loadedapk); Mpackages.put (Applicationinfo.packagename, WeakReference );
Our customclassloader is very simple, the direct inheritance of dexclassloader, nothing to do;
Of course, you can use Dexclassloader directly here, re-create a class here to be more differentiated;
Control over class loading can also be implemented later by modifying this class:
public class CustomClassLoader extends Dexclassloader {public CustomClassLoader (string Dexpath, String Optimizeddirectory, String LibraryPath, ClassLoader parent) { super (Dexpath, Optimizeddirectory, LibraryPath, parent);} }
Here, we have successfully put the plug-in information into the activitythread, so that our plug-in class can be successfully loaded;
So the activity instance in the plugin can be created successfully; As the whole process is more complex, let's simply comb:
1, after Activitythread receives the schedulelaunchactivity remote call to IApplication, forwards the message to H
When the 2,h class is Handlemessage, the Getpackageinfonocheck method is called to get the component information to be started.
In this method, we will first find the cache information in Mpackages, and we have added the plugin information manually;
It is therefore possible to successfully hit the cache and obtain the plug-in information that exists independently.
The 3,h class then calls Handlelaunchactivity to eventually forward to the Performlaunchactivity method;
This method loads the activity class using the Mclassloader from Getpackageinfonocheck to loadedapk,
The activity instance is then created using reflection, and then the activation of the activity component is completed, such as Application,context.
It looks as if it's flawless, but it's going to be an exception to run.
The error is that application cannot be instantiated, and application is created in Performlaunchactivity, and here is a bit odd, let's take a closer look.
5.5.2 Bypassing system checks
Through the Performlaunchactivity method of Activitythread, application is created by loadedapk Makeapplication method,
We looked at this method and found the location of the exception thrown in the source code:
try { Java.lang.ClassLoader cl = getClassLoader (); if (!mpackagename.equals ("Android")) { initializejavacontextclassloader (); } Contextimpl AppContext = Contextimpl.createappcontext (Mactivitythread, this); App = MActivityThread.mInstrumentation.newApplication ( cl, Appclass, appContext); Appcontext.setoutercontext (app);} catch (Exception e) { if (!mactivitythread.minstrumentation.onexception (app, E)) { throw new RuntimeException ( "Unable to instantiate application" + Appclass + ":" + e.tostring (), e);} }
Wood has a way, we only have a row to see exactly where the exception is thrown;
Fortunately, the code is not much. (So it's a matter of how important it is to narrow the range of exceptions!!!) )
The first sentence getClassLoader () Nothing suspicious, though the method is very long, but it has thrown any exception to the wood
(Of course, the code it calls may throw an exception, in case it can only be searched further, so I think we should use the check exception here).
Then we look at the second sentence, if the package name is not the beginning of Android, then call a method called Initializejavacontextclassloader; We check this method:
private void Initializejavacontextclassloader () {Ipackagemanager pm = Activitythread.getpackagemanager (); Android.content.pm.PackageInfo Pi; try {pi = pm.getpackageinfo (mpackagename, 0, Userhandle.myuserid ()); } catch (RemoteException e) {throw new IllegalStateException ("Unable to get package info for" + MP Ackagename + "; is System dying? ", e); } if (pi = = null) {throw new IllegalStateException ("Unable to get package info for" + Mpackagen Ame + "; Is package not installed? ");} Boolean shareduseridset = (Pi.shareduserid! = null); Boolean Processnamenotdefault = (Pi.applicationinfo! = null &&!mpackagename.equals (pi.applicationi Nfo.processname)); Boolean sharable = (Shareduseridset | | processnamenotdefault); ClassLoader Contextclassloader = (sharable)? New Warningcontextclassloader (): Mclassloader; Thread.CurrentThread (). Setcontextclassloader (CONTEXTCLassloader);}
Here, we find out the source of this anomaly: it was originally called the Getpackageinfo method to get the packet information;
And our plug-in is not installed on the system, so the system will certainly think that the plug-in is not installed, this method will definitely return null.
Therefore, we also have to cheat the PMS, let the system feel that the plug-in has been installed on the system;
As to how to deceive the pms,hook mechanism of the AMS&PMS have a detailed explanation, here directly to the code, do not repeat:
private static void Hookpackagemanager () throws Exception {//This step is because Initializejavacontextclassloader this method internally inadvertently checks that the package is No in the system installation//If not installed, directly throws an exception, here need to temporarily hook off the PMS, bypassing this check. class<?> Activitythreadclass = Class.forName ("Android.app.ActivityThread"); Method Currentactivitythreadmethod = Activitythreadclass.getdeclaredmethod ("Currentactivitythread"); Currentactivitythreadmethod.setaccessible (TRUE); Object currentactivitythread = Currentactivitythreadmethod.invoke (null); Get Activitythread inside the original Spackagemanager Field Spackagemanagerfield = Activitythreadclass.getdeclaredfield (" Spackagemanager "); Spackagemanagerfield.setaccessible (TRUE); Object Spackagemanager = Spackagemanagerfield.get (Currentactivitythread); Prepare the proxy object to replace the original object class<?> ipackagemanagerinterface = Class.forName ("Android.content.pm.IPackageManager"); Object proxy = proxy.newproxyinstance (Ipackagemanagerinterface.getclassloader (), new class<?>[] {IPack AgemanAgerinterface}, New Ipackagemanagerhookhandler (Spackagemanager)); 1. Replace the Spackagemanager field inside the Activitythread spackagemanagerfield.set (currentactivitythread, proxy);}
Ok here we have been able to successfully load the simple standalone apk that exists in the external file system.
At this point about the droidplugin for the activity life cycle management has been completely explained completed;
This is a very complex activity management solution, we just write a demo to understand the hook a lot of things,
In the framework layer back and forth, this one of the ins and outs to fully grasp the reader in person to read the source.
In the scenario given above, we take over the loading process of the class in the plugin, which is a relatively violent solution.
6. Summary
In this article, we have successfully completed the task of "initiating activity in an external plug-in that has not been declared in Androidmanifest.xml" by two scenarios.
In the "radical solution" we have customized the plugin's ClassLoader and bypassed the framework's detection;
Using Activitythread's caching mechanism for loadedapk, we add the plug-in information that carries this custom classloader into the mpackages, thus completing the class loading process.
In the "conservative scheme" we delve into the process of using the Classloaderfindclass system,
Discover that the non-system classes used by the application are loaded by the same pathclassloader;
The final parent class of this class basedexclassloader the process of finding the class through dexpathlist, and we hack the lookup process to complete the loading of the plug-in class.
What are the pros and cons of these two schemes?
Obviously, the "radical scheme" is more troublesome, from the code volume and analysis process can be seen, this mechanism is very complex;
And in parsing the APK, we used the Packageparser compatibility is very poor, we have to manually handle each version of the APK parsing API;
In addition, it hooks a bit more places: not only the hook AMs and H, but also hook Activitythread mpackages and packagemanager!
The "conservative scheme" is much simpler (although the principle is not simple), not only the code is very small, and the hook is not much place;
A little radical reform means that the entire class is loaded from the top-most layer.
However, we cannot simply say that "conservative schemes" are better than "radical schemes". Fundamentally, what are the differences between the two schemes?
The "radical Solution" is a multi-classloader architecture, with each plugin having its own classloader,
So the isolation of classes is very good--if different plugins use different versions of the same library, they are all right!
The "conservative scheme" is a single ClassLoader scheme, and all of the plugins and host program classes are loaded through the clasloader of the host.
Although the code is simple, but the robustness is poor, once the plug-in and even between the plug-in and the host class library used in conflict, then directly GG.
Multi-ClassLoader also has one advantage: you can actually complete the code hot load! If the plugin needs to be upgraded,
Simply re-create a custom ClassLoader to load the new plugin, then replace the original version
(in Java, the same class loaded with different ClassLoader is considered to be a different class);
Single-ClassLoader words can be cumbersome to implement and may require a restart process.
Many ClassLoader architectures, such as the Tomcat server, are used extensively in Clasloader in the Java EE field.
The Java Modular fact standard OSGi technology; so we have every reason to think that choosing a multi-classloader architecture is wise in most cases.
Currently open source plug-in solution, Droidplugin adopted the "radical Plan", small adopted "conservative plan" so, there are two advantages of the plan??
There is a natural answer.
The common denominator between droidplugin and small is that both are non-intrusive plug-in frameworks;
What is a "non-intrusive"? For example, you start a plugin activity and use startactivity directly,
As with the development of normal apk, the development of plug-ins and ordinary programs for developers is no different.
If we abandon this "intrusive" to a certain extent, then we can achieve a combination of both advantages of the plug-in framework!
OK, the content of this article is here, the "plug-in mechanism for the handling of activity" also ended.
To illustrate, in this article, the "conservative scheme" actually only handles the code loading process, it can not load the resources of the apk!
So at present, I basically do not have a warm implementation of this, of course, I am just "code loading" for example.
Android plugin loading mechanism II