Android Dynamic Loading activity principle detailed _android

Source: Internet
Author: User

Initiation process of activity

Loading an activity will certainly not be like loading a generic class, because the activity as a component of the system has its own lifecycle, there is a lot of system callback control, so it is definitely not possible to customize a Dexclassloader class loader to load the activity in the plug-in.

First have to understand the activity of the start process, of course, just a simple look, too detailed words difficult to study clearly.

A Activityclientrecord object is created when the startactivity is launched and eventually a cross process callback is made to Applicationthread schedulelaunchactivity via AMS. This object represents a acticity and his related information, such as the Activityinfo field includes the boot mode, and loadedapk, as the name implies is loaded apk, he will be placed in a map, apply package name to loadedapk key value pairs, Contains information about an application. And then through the handler switch to the main thread of the performlaunchactivity

Private Activity Performlaunchactivity (Activityclientrecord R, Intent customintent) {Activityinfo Ainfo = R.activityinf
O 1. The Activityclientrecord object was created without assigning a value to his packageinfo, so it is null if (R.packageinfo = null) {R.packageinfo = Getpackageinfo (
Ainfo.applicationinfo, R.compatinfo, Context.context_include_code);
}
// ...
Activity activity = NULL; try {//2. Very IMPORTANT!!
This ClassLoader is stored in the Loadedapk object, which is used to load the loader java.lang.ClassLoader cl = R.packageinfo.getclassloader () for the activity we write. 3. Load the activity class with the loader, which loads the matching activity activity by different intent = minstrumentation.newactivity (cl,
Component.getclassname (), r.intent);
Strictmode.incrementexpectedactivitycount (Activity.getclass ());
R.intent.setextrasclassloader (CL); if (r.state!= null) {R.state.setclassloader (CL);}} catch (Exception e) {//4. The anomaly here is also very, very important!!!
The following is based on this tip to find a breakthrough ... if (!minstrumentation.onexception (activity, E)) {Throw the new RuntimeException ("Unable to instantiate activity" + Componen
T + ":" + e.tostring (), e);} if (activity!= null) {COntext Appcontext = createbasecontextforactivity (r, activity);
Charsequence title = R.activityinfo.loadlabel (Appcontext.getpackagemanager ());
Configuration config = new Configuration (mcompatconfiguration);
From here we will perform the oncreate within the life cycle of the activity we normally see minstrumentation.callactivityoncreate (activity, r.state);
Omitted is the execution of life cycle based on different states r.paused = true;
Mactivities.put (R.token, R);
catch (Supernotcalledexception e) {throw e;} catch (Exception e) {//...} return activity; }

The

1.getPackageInfo method eventually returns a Loadedapk object, which is taken from a HASHMAP data structure, Mpackages maintains the corresponding relationship between the package name and the loadedapk, that is, each application has a key-value pair counterpart. If NULL, a new LOADEDAPK object is created and added to the map, focusing on the ClassLoader field of the object null!

Public final loadedapk Getpackageinfo (ApplicationInfo ai, compatibilityinfo compatinfo, int flags) {//True Boolean Inc
Ludecode = (Flags&context.context_include_code)!= 0; Boolean securityviolation = Includecode && ai.uid!= 0 && ai.uid!= process.system_uid && (Mbounda Pplication!= null?!
Userhandle.issameapp (Ai.uid, MBoundApplication.appInfo.uid): TRUE);
..//Includecode to TRUE//ClassLoader to null!!!
Return Getpackageinfo (AI, compatinfo, NULL, securityviolation, Includecode); Private loadedapk Getpackageinfo (ApplicationInfo ainfo, Compatibilityinfo compatinfo, ClassLoader Baseloader, Boolean Securityviolation, Boolean Includecode) {synchronized (mpackages) {weakreference<loadedapk> ref; if (Includecode 
{//Includecode is true ref = Mpackages.get (ainfo.packagename);} else {ref = Mresourcepackages.get (Ainfo.packagename);} loadedapk PackageInfo = ref!= null?
Ref.get (): null; if (PackageInfo = null | | (Packageinfo.mresources!= null &Amp;&!packageinfo.mresources.getassets (). IsUpToDate ())) {if (LOCALLOGV)//...//PackageInfo is NULL, create a loadedapk, and add to mpackages inside PackageInfo = new loadedapk (this, Ainfo, Compatinfo, this, Baseloader, securityviolation, Includecode &A
mp;& (ainfo.flags&applicationinfo.)!= 0);
if (Includecode) {mpackages.put (Ainfo.packagename, New weakreference<loadedapk> (PackageInfo));} else {
Mresourcepackages.put (Ainfo.packagename, New weakreference<loadedapk> (PackageInfo));
} return PackageInfo; }}</loadedapk></loadedapk></loadedapk>

2. Get the class loader corresponding to this activity, as mentioned above, Mclassloader is null, then executes to Applicationloaders#getclassloader (Zip, LibraryPath, Mbaseclassloader) method.

Public ClassLoader getClassLoader () {
synchronized (a) {
if (mclassloader!= null) {return
mclassloader;
}
// ...
Create the loader, create the default loader
//zip path for apk, LibraryPath is the path to JNI
Mclassloader = Applicationloaders.getdefault (). getClassLoader (Zip, LibraryPath, mbaseclassloader);
Initializejavacontextclassloader ();
Strictmode.setthreadpolicy (Oldpolicy);
} else {
if (Mbaseclassloader = = null) {
Mclassloader = Classloader.getsystemclassloader ();
} else {
Mclassloader = Mbaseclassloader;
}
Return Mclassloader
}
}

Applicationloaders uses a single instance of its getClassLoader method to create the loader based on the path of the incoming zip path, which is actually apk, and returns a Pathclassloader. And Pathclassloader can only load installed APK. This loader was created with the current application apk path, and, to be sure, to load the other APK constructs a class loader that passes other apk.

3. Load the activity we want to start with this class loader and reflect create an activity instance

Public activity newactivity (ClassLoader cl, String classname,intent Intent) throws Instantiationexception, Illegalaccessexception, ClassNotFoundException {return
(activity) Cl.loadclass (className). newinstance ();

To sum up, the idea is that when we start an activity, the activity is loaded through the default Pathclassloader of the system, and of course the activity in this application can only be loaded by default. It is then invoked by the system into the life cycle of the activity.

4. The anomaly in this place will appear in the following example, and then we can find out the way we dynamically load the activity after analyzing the reason.

Dynamic load activity: Modifying the System class loader

Follow this idea and do an example of pressing the button to open the activity in the plugin.

Plug-in Project

Plugin.dl.pluginactivity

|--mainactivity.java

The content is very simple, is a layout above wrote this is the plug-in activity! and wrote his OnStart and OnDestroy method.

public class Mainactivity extends activity {
@Override
protected void onCreate (Bundle savedinstancestate) {
super.oncreate (savedinstancestate);
After loading into the host program, the R.layout.activity_main is the R.layout.activity_main in the host program
Setcontentview (r.layout.activity_main );
}
@Override
protected void OnStart () {
super.onstart ();
Toast.maketext (This, "OnStart", 0). Show ();
@Override
protected void OnDestroy () {
Super.ondestroy ();
Toast.maketext (This, "OnDestroy", 0). Show ();
}

Host Project

Host.dl.hostactivity

|--mainactivity.java

Includes two buttons, the first button jumps to the Mainactivity.java in the plugin, and the second button moves to the Mainactivity.java in the application

Private Button btn;
Private Button btn1;
Dexclassloader loader; @Override protected void OnCreate (Bundle savedinstancestate) {super.oncreate (savedinstancestate); Setcontentview (
R.layout.activity_main);
BTN = (Button) Findviewbyid (R.ID.BTN);
BTN1 = (Button) Findviewbyid (R.ID.BTN1);
Btn.setonclicklistener (New View.onclicklistener () {@Override public void OnClick (View v) {Class activity = null;
String Dexpath = "/pluginactivity.apk"; Loader = new Dexclassloader (Dexpath, MainActivity.this.getApplicationInfo (). DataDir, NULL, GetClass (). getClassLoader
()); try {activity = Loader.loadclass ("plugin.dl.pluginactivity.MainActivity");}
catch (ClassNotFoundException e) {log.i ("mainactivity", "ClassNotFoundException");}
Intent Intent = new Intent (mainactivity.this, activity);
MainActivity.this.startActivity (Intent);
}
}); Btn1.setonclicklistener (New View.onclicklistener () {@Override public void OnClick (View v) {Intent Intent = new Intent (Ma
Inactivity.this, Mainactivity2.class); Mainactivity. this.startactivity (Intent); }
});

First we have to register the activity in the amount of the host project Androidmanifest. Click the button to open the activity in the plugin and find the error

Java.lang.RuntimeException:Unable to instantiate activity componentinfo{host.dl.hostactivity/ Plugin.dl.pluginactivity.MainActivity}: Java.lang.ClassNotFoundException:plugin.dl.pluginactivity.MainActivity

#已经使用自定义的加载器, why can't I find the activity in the plug-in when startactivity?

The previous 4th said this anomaly. In fact, this exception is thrown in the performlaunchactivity, look at this abnormal print information, Found that it said Plugin.dl.pluginactivity.MainActivity class could not find, but we did not just define a dexclassloader, successfully loaded this class?? Why is this class not found?

Actually, that's true, remember the previous mention of the system default ClassLoader Pathclassloader? (because the Mclassloader variable of the loadedapk object is null, it is called to the Applicationloaders#getclassloader method, which returns a default Pathclassloader according to the path of the current application). When executing to Mpackages.get (ainfo.packagename), Mclassloader is not specified in the loadedapk obtained from the map, so the system default class loader is used. So when the execution of this sentence minstrumentation.newactivity (cl, Component.getclassname (), r.intent), because this class loader can not find the class in our plug-in project, so the error.

It is now clear that the reason is that using the system default class loader does not contain the plug-in engineering path and does not properly load the activity that we want to cause.

Then consider replacing the system's ClassLoader.

private void Replaceclassloader (Dexclassloader loader) {
try {
Class Clazz_ath = Class.forName (" Android.app.ActivityThread ");
Class clazz_lapk = Class.forName ("android.app.LoadedApk");
Object Currentactivitythread = Clazz_ath.getmethod ("Currentactivitythread"). Invoke (null);
Field field1 = Clazz_ath.getdeclaredfield ("Mpackages");
Field1.setaccessible (true);
Map mpackages = (map) field1.get (currentactivitead);
String PackageName = MainActivity.this.getPackageName ();
WeakReference ref = (WeakReference) mpackages.get (packagename);
Field field2 = Clazz_lapk.getdeclaredfield ("Mclassloader");
Field2.setaccessible (true);
Field2.set (Ref.get (), loader);
} catch (Exception e) {
e.printstacktrace ();
}
}

The idea of this code is to replace the mclassloader of the mpackages variable in the Activitythread class with the loadedapk value of the current package name as the key to our custom ClassLoader. The next time you want to load an activity in a plug-in that resides somewhere else, you can get it directly in the mpackages variable, so we're using the class loader we modified.
Therefore, it is OK to call Replaceclassloader (loader) before opening the activity in the plug-in, and by replacing the system ClassLoader.

The effect is as follows


It was found that the activity in the plug-in could be started because it was executed to his OnStart method, and the OnDestroy method was executed when it was closed, but it was strange that the controls on the interface did not seem to change. And start his interface exactly the same, still can't click. What's the reason?

Obviously, we just loaded the Mainactivity class in the plugin, and when it comes to his OnCreate method, the layout parameters used in the inside call Setcontentview are R.layout.activity_main, Of course, use is the current application of resources!

# #已经替换了系统的类加载器为什么加载本应用的activity却能正常运行?

But before fixing the problem, did you find a very strange phenomenon that, when the activity in the plug-in was loaded, the local activity started again? What is this for? The default ClassLoader has already been replaced, and you can view the activity that was used before opening the activity in the plug-in and clicking on the second button to open the activity before the application's activity, which is indeed the class loader we have already replaced. So why do you normally start the activity of this application? The mystery is when we create the Dexclassloader the fourth parameter, the parent loader! Setting the parent loader as the loader for the current class ensures that the parent delegation model for the class is not corrupted, and that the class is loaded first, and loaded by itself when the load is unsuccessful. No, believe it. The parent loader's parameters are set to other values, such as the system ClassLoader, when the loader is new, and the error is certainly made when running the activity.

Next to resolve the previous occurrence, jump to the plug-in activity in the interface does not show the problem. The reason for this phenomenon has been explained by the use of local resources, so you need to use the resource layout in the plug-in when Setcontentview. Therefore, the following modifications are made in the plug-in activity

public class MainActivity2 extends activity {
private static view view;
@Override
protected void onCreate (Bundle savedinstancestate) {
super.oncreate (savedinstancestate);
After loading into the host program, the R.layout.activity_main is the R.layout.activity_main//Setcontentview in the Host program
(r.layout.activity_ Main);
if (view!= null)
Setcontentview (view);
}
@Override
protected void OnStart () {
super.onstart ();
Toast.maketext (This, "OnStart", 0). Show ();
@Override
protected void OnDestroy () {
Super.ondestroy ();
Toast.maketext (This, "OnDestroy", 0). Show ();
private static void setlayout (View v) {
view = v;
}
}

The plug-in resource is then fetched in the host activity and the layout is populated into view, which is then set to the activity in the plug-in as its contentview content.

class<!--?--> Layout = Loader.loadclass ("Plugin.dl.pluginactivity.r$layout");
Field field = Layout.getfield ("Activity_main");
Integer obj = (integer) field.get (null);
Use the Resources object containing the plug-in apk to get this layout to get the interface effect defined in the plug-in properly
//view View = Layoutinflater.from (mainactivity.this). Inflate ( Resources.getlayout (obj), null);
Or so, but be sure to rewrite the Getresources method to write
view view = Layoutinflater.from (Mainactivity.this). Inflate (obj, null);
Method method = Activity.getdeclaredmethod ("setlayout", view.class);
Method.setaccessible (true);
Method.invoke (activity, view);

The complete code

public class Mainactivity extends activity {Private resources, protected assetmanager assetmanager; private Bu
Tton btn;
Private Button btn1;
Dexclassloader loader; @Override protected void OnCreate (Bundle savedinstancestate) {super.oncreate (savedinstancestate); Setcontentview (
R.layout.activity_main);
BTN = (Button) Findviewbyid (R.ID.BTN);
BTN1 = (Button) Findviewbyid (R.ID.BTN1); Btn.setonclicklistener (New View.onclicklistener () {@Override public void OnClick (View v) {String Dexpath = '/pluginactiv
ITY.APK "; Loader = new Dexclassloader (Dexpath, MainActivity.this.getApplicationInfo (). DataDir, NULL, GetClass (). getClassLoader
());
class<!--?--> activity = NULL;
class<!--?--> layout = null; try {activity = Loader.loadclass ("plugin.dl.pluginactivity.MainActivity"); layout = Loader.loadclass ("
Plugin.dl.pluginactivity.r$layout ");
}catch (ClassNotFoundException e) {log.i ("mainactivity", "ClassNotFoundException");} replaceclassloader (loader);
Loadres (Dexpath); try {FIeld field = Layout.getfield ("Activity_main");
Integer obj = (integer) field.get (null); Use the Resources object that contains the plug-in apk to get this layout to get the interface effects defined in the plug-in correctly view view = Layoutinflater.from (Mainactivity.this). Inflate (
Resources.getlayout (obj), null);
Or so, but be sure to rewrite the Getresources method in order to write//view view = Layoutinflater.from (Mainactivity.this). Inflate (obj, null);
Method method = Activity.getdeclaredmethod ("setlayout", View.class);
Method.setaccessible (TRUE);
Method.invoke (activity, view);
catch (Exception e) {e.printstacktrace ();}
Intent Intent = new Intent (mainactivity.this, activity);
MainActivity.this.startActivity (Intent);
}
}); Btn1.setonclicklistener (New View.onclicklistener () {@Override public void OnClick (View v) {Intent Intent = new Intent (Ma
Inactivity.this, Mainactivity2.class);
MainActivity.this.startActivity (Intent);
}
});
public void Loadres (String path) {try {Assetmanager = AssetManager.class.newInstance ();
Method Addassetpath = AssetManager.class.getMethod ("Addassetpath", String.class); AddASsetpath.invoke (Assetmanager, path); The catch (Exception e) {} resources = new Assetmanager, Super.getresources (). Getdisplaymetrics (), Super.getreso
Urces (). GetConfiguration ()); You can also get the topic based on the resource} private void Replaceclassloader (Dexclassloader loader) {try {Class Clazz_ath = Class.forName ("Android.ap
P.activitythread ");
Class clazz_lapk = Class.forName ("ANDROID.APP.LOADEDAPK");
Object Currentactivitythread = Clazz_ath.getmethod ("Currentactivitythread"). Invoke (null);
Field field1 = Clazz_ath.getdeclaredfield ("Mpackages");
Field1.setaccessible (TRUE);
Map mpackages = (map) field1.get (currentactivitythread);
String PackageName = MainActivity.this.getPackageName ();
WeakReference ref = (WeakReference) mpackages.get (PackageName);
Field field2 = Clazz_lapk.getdeclaredfield ("Mclassloader");
Field2.setaccessible (TRUE);
Field2.set (Ref.get (), loader);
catch (Exception e) {System.out.println ("-------------------------------------" + "click"); E.printstacktrace ();} @Override Public ResourCES getresources () {return resources = null super.getresources (): Resources, @Override public Assetmanager Getasset S () {return Assetmanager = = null super.getassets (): Assetmanager;}}

Dynamic load activity: Using a proxy

Another way to start an activity in a plug-in is to use the activity in the plug-in as a generic class, not as a component activity, and then start an agent proxyactivity at startup, which is the real activity, His life cycle is managed by the system, and we call the function in the plug-in activity within it. At the same time, a reference to the agent activity is saved in the plug-in activity, which is understood as context contexts.

The life cycle function of the plug-in activity is adjusted by the agent activity, Proxyactivity is actually a real activity that we start, instead of activating the activity in the plug-in, "to start" in the plugin Activity is treated as a common class, as an ordinary class containing some functions to understand, but the name of the function in this class is somewhat "strange". When it comes to accessing resources and updating the UI, it is obtained through the current context, the saved proxyactivity reference.

Take the demo below as an example

Host Project

Com.dl.host

|--mainactivity.java

|--proxyactivity.java

Mainactivity includes a button that presses the button to jump to the plugin activity

public class Mainactivity extends activity{
private Button btn;
@Override
protected void onCreate (Bundle savedinstancestate) {
super.oncreate (savedinstancestate);
Setcontentview (r.layout.activity_main);
BTN = (Button) Findviewbyid (R.ID.BTN);
Btn.setonclicklistener (New Onclicklistener () {
@Override public
void OnClick (View v) {
MainActivity.this.startActivity (New Intent (Mainactivity.this, Proxyactivity.class));}}

Proxyactivity is a puppet of the plug-in activity we want to start, agent. is a system-maintained activity.

public class Proxyactivity extends activity{private dexclassloader loader; private activity; private class<!
--?--> clazz = null; @Override protected void OnCreate (Bundle savedinstancestate) {super.oncreate (savedinstancestate); loader = new
Dexclassloader ("/plugin.apk", Getapplicationinfo (). DataDir, NULL, GetClass (). getClassLoader ()); 
try {clazz = Loader.loadclass ("com.dl.plugin.MainActivity");} catch (ClassNotFoundException e) {e.printstacktrace ();} Set proxy try {method setproxy = Clazz.getdeclaredmethod ("SetProxy", Activity.class) for plug-in activity; Setproxy.setaccessible (
true);
Activity = (activity) clazz.newinstance ();
Setproxy.invoke (activity, this);
Method onCreate = Clazz.getdeclaredmethod ("OnCreate", Bundle.class);
Oncreate.setaccessible (TRUE);
Oncreate.invoke (activity, savedinstancestate);
catch (Exception e) {e.printstacktrace ();}} @Override protected void OnStart () {Super.onstart ();//Invoke the OnStart method of the plug-in activity to OnStart = null; try {OnStart = CLA Zz.getDeclaredmethod ("OnStart");
Onstart.setaccessible (TRUE);
Onstart.invoke (activity);
catch (Exception e) {e.printstacktrace ();}} @Override protected void OnDestroy () {Super.onstart ();//Invoke the OnDestroy method of the plug-in activity to OnDestroy = null; try {ONDESTR
Oy = Clazz.getdeclaredmethod ("OnDestroy");
Ondestroy.setaccessible (TRUE);
Ondestroy.invoke (activity);
catch (Exception e) {e.printstacktrace ();}} }

As you can see, proxyactivity is actually a real activity, and we start with this activity, not the activity in the plug-in.

Plug-in Project

Com.dl.plugin

|--mainactivity.java

Save a reference to agent activity, it is noteworthy that because the resources in the Access plug-in require additional operations to load resources, so there is no use of resources in the plug-in project, so I use the code added TextView, but the principle is the same as the previous one.

public class Mainactivity extends activity {
private activity proxyactivity;
public void SetProxy (activity proxyactivity) { 
this.proxyactivity = proxyactivity;
}
All operations inside are operated by agent activity
@Override
protected void onCreate (Bundle savedinstancestate) {
TextView TV = new TextView (proxyactivity);
Tv.settext ("plug-in activity");
Proxyactivity.setcontentview (tv,new framelayout.layoutparams (layoutparams.wrap_content, LayoutParams.WRAP_ CONTENT));
}
@Override
protected void OnStart () {
toast.maketext (proxyactivity, plug-in OnStart, 0). Show ();
@Override
protected void OnDestroy () {
toast.maketext (proxyactivity, plug-in OnDestroy, 0). Show ();

This method compared to the previous method to modify the system loader need to maintain their own life cycle, more cumbersome, the former way by the system to maintain their own, and the start of the plug-in is a real activity.

The former way to the host in the Androidmanifest Declaration plug-in activity, so when the activity is too long to declare a lot, more cumbersome, but can not be declared escaped system inspection. In this way, you need only one proxy proxyactivity class. In his oncreate, select which activity in the plug-in is loaded based on the value passed.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.