Android动态加载Activity原理详解
activity的启动流程
加载一个activity肯定不会像加载一般的类那样,因为activity作为系统的组件有自己的生命周期,有系统的很多回调控制,所以自定义一个dexclassloader类加载器来加载插件中的activity肯定是不可以的。
首先不得不了解一下activity的启动流程,当然只是简单的看一下,太详细的话很难研究清楚。
通过startactivity启动后,最终通过ams进行跨进程回调到applicationthread的schedulelaunchactivity,这时会创建一个activityclientrecord对象,这个对象表示一个acticity以及他的相关信息,比如activityinfo字段包括了启动模式等,还有loadedapk,顾名思义指的是加载过了的apk,他会被放在一个map中,应用包名到loadedapk的键值对,包含了一个应用的相关信息。然后通过handler切换到主线程执performlaunchactivity
private activity performlaunchactivity(activityclientrecord r, intent customintent) { activityinfo ainfo = r.activityinfo; // 1.创建activityclientrecord对象时没有对他的packageinfo赋值,所以它是null if (r.packageinfo == null) { r.packageinfo = getpackageinfo(ainfo.applicationinfo, r.compatinfo, context.context_include_code); } // ... activity activity = null; try { // 2.非常重要!!这个classloader保存于loadedapk对象中,它是用来加载我们写的activity的加载器 java.lang.classloader cl = r.packageinfo.getclassloader(); // 3.用加载器来加载activity类,这个会根据不同的intent加载匹配的activity activity = 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.这里的异常也是非常非常重要的!!!后面就根据这个提示找到突破口。。。 if (!minstrumentation.onexception(activity, e)) { throw new runtimeexception( "unable to instantiate activity " + component + ": " + e.tostring(), e); } } if (activity != null) { context appcontext = createbasecontextforactivity(r, activity); charsequence title = r.activityinfo.loadlabel(appcontext.getpackagemanager()); configuration config = new configuration(mcompatconfiguration); // 从这里就会执行到我们通常看到的activity的生命周期的oncreate里面 minstrumentation.callactivityoncreate(activity, r.state); // 省略的是根据不同的状态执行生命周期 } r.paused = true; mactivities.put(r.token, r); } catch (supernotcalledexception e) { throw e; } catch (exception e) { // ... } return activity; }
1.getpackageinfo方法最终返回一个loadedapk对象,它会从一个hashmap的数据结构中取,mpackages维护了包名和loadedapk的对应关系,即每一个应用有一个键值对对应。如果为null,就新创建一个loadedapk对象,并将其添加到map中,重点是这个对象的classloader字段为null!
public final loadedapk getpackageinfo(applicationinfo ai, compatibilityinfo compatinfo, int flags) { // 为true boolean includecode = (flags&context.context_include_code) != 0; boolean securityviolation = includecode && ai.uid != 0 && ai.uid != process.system_uid && (mboundapplication != null ? !userhandle.issameapp(ai.uid, mboundapplication.appinfo.uid) : true); // ... // includecode为true // classloader为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为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 && !packageinfo.mresources.getassets().isuptodate())) { if (locallogv) // ... // packageinfo为null,创建一个loadedapk,并且添加到mpackages里面 packageinfo = new loadedapk(this, ainfo, compatinfo, this, baseloader, securityviolation, includecode && (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.获取这个activity对应的类加载器,由于上面说过,mclassloader为null,那么就会执行到applicationloaders#getclassloader(zip, librarypath, mbaseclassloader)方法。
public classloader getclassloader() { synchronized (this) { if (mclassloader != null) { return mclassloader; } // ... // 创建加载器,创建默认的加载器 // zip为apk的路径,librarypath也就是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使用单例它的getclassloader方法根据传入的zip路径事实上也就是apk的路径来创建加载器,返回的是一个pathclassloader。并且pathclassloader只能加载安装过的apk。这个加载器创建的时候传入的是当前应用apk的路径,理所应当的,想加载其他的apk就构造一个传递其他apk的类加载器。
3.用该类加载器加载我们要启动的activity,并反射创建一个activity实例
public activity newactivity(classloader cl, string classname,intent intent) throws instantiationexception, illegalaccessexception, classnotfoundexception { return (activity)cl.loadclass(classname).newinstance(); }
总结一下上面的思路就是,当我们启动一个activity时,通过系统默认的pathclassloader来加载这个activity,当然默认情况下只能加载本应用里面的activity,然后就由系统调用到这个activity的生命周期中。
4.这个地方的异常在后面的示例中会出现,到时候分析到原因后就可以找出我们动态加载activity的思路了。
动态加载activity:修改系统类加载器
按照这个思路,做这样的一个示例,按下按钮,打开插件中的activity。
插件项目
plugin.dl.pluginactivity
|--mainactivity.java
内容很简单,就是一个布局上面写了这是插件中的activity!并重写了他的onstart和ondestroy方法。
public class mainactivity extends activity { @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); // 加载到宿主程序中之后,这个r.layout.activity_main就是宿主程序中的r.layout.activity_main了 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.dl.hostactivity
|--mainactivity.java
包括两个按钮,第一个按钮跳转到插件中的mainactivity.java,第二个按钮调转到本应用中的mainactivity.java
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(mainactivity.this, mainactivity2.class); mainactivity.this.startactivity(intent); } });
首先我们要将该activity在宿主工程的额androidmanifest里面注册。点击按钮打开插件中的activity,发现报错
java.lang.runtimeexception: unable to instantiate activity componentinfo{host.dl.hostactivity/plugin.dl.pluginactivity.mainactivity}: java.lang.classnotfoundexception: plugin.dl.pluginactivity.mainactivity
#已经使用自定义的加载器,当startactivity时为什么提示找不到插件中的activity?
前面第四点说过这个异常。其实这个异常就是在performlaunchactivity中抛出的,仔细看这个异常打印信息,发现它说plugin.dl.pluginactivity.mainactivity类找不到,可是我们不是刚刚定义了一个dexclassloader,成功加载了这个类的吗??怎么这里又提示这个类找不到?
实际上,确实是这样的,还记得前面说过,系统默认的类加载器pathclassloader吗?(因为loadedapk对象的mclassloader变量为null,就调用到applicationloaders#getclassloader方法,即根据当前应用的路径返回一个默认的pathclassloader),当执行到mpackages.get(ainfo.packagename);时从map获取的loadedapk中未指定mclassloader,因此会使用系统默认的类加载器。于是当执行这一句 minstrumentation.newactivity(cl, component.getclassname(), r.intent);时,由于这个类加载器找不到我们插件工程中的类,因此报错了。
现在很清楚了,原因就是使用系统默认的这个类加载器不包含插件工程路径,无法正确加载我们想要的activity造成的。
于是考虑替换系统的类加载器。
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(); } }
这段代码的思路是将activitythread类中的mpackages变量中保存的以当前包名为键的loadedapk值的mclassloader替换成我们自定义的类加载器。当下一次要加载存放在别的地方的插件中的某个activity时,直接在mpackages变量中能取到,因此用的就是我们修改了的类加载器了。
因此,在打开插件中的activity之前调用replaceclassloader(loader);方法替换系统的类加载器,就可以了。
效果如下
此时发现可以启动插件中的activity,因为执行到了他的onstart方法,并且关闭的时候执行了ondestroy方法,但是奇怪的是界面上的控件貌似没有变化?和启动他的界面一模一样,还不能点击。这是什么原因呢?
显然,我们只是把插件中的mainactivity类加载过来了,当执行到他的oncreate方法时,在里面调用setcontentview使用的布局参数是r.layout.activity_main,当然使用的就是当前应用的资源了!
##已经替换了系统的类加载器为什么加载本应用的activity却能正常运行?
不过在修正这个问题之前,有没有发现一个很奇怪的现象,当加载过插件中的activity后,再次启动本地的activity也是能正常启动的?这是为什么呢?前面已经替换了默认的类加载器了,并且可以在打开插件中的activity后再点击第二个按钮打开本应用的activity之前查看使用的activity,确实是我们已经替换了的类加载器。那这里为什么还能正常启动本应用的activity呢?玄机就在我们创建dexclassloader时的第四个参数,父加载器!设置父加载器为当前类的加载器,就能保证类的双亲委派模型不被破坏,在加载类时都是先由父加载器来加载,加载不成功时在由自己加载。不信可以在new这个加载器的时候父加载器的参数设置成其他值,比如系统类加载器,那么当运行activity时肯定会报错。
接下来解决前面出现的,跳转到插件activity中界面显示不对的问题。这个现象出现的原因已经解释过了,就是因为使用了本地的资源所导致的,因此需要在setcontentview时,使用插件中的资源布局。因此在插件activity中作如下修改
public class mainactivity2 extends activity { private static view view; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); // 加载到宿主程序中之后,这个r.layout.activity_main就是宿主程序中的r.layout.activity_main了 // setcontentview(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; } }
然后在宿主activity中获取插件资源并将布局填充成view,然后设置给插件中的activity,作为它的contentview的内容。
class<!--?--> layout = loader.loadclass("plugin.dl.pluginactivity.r$layout"); field field = layout.getfield("activity_main"); integer obj = (integer) field.get(null); // 使用包含插件apk的resources对象来获取这个布局才能正确获取插件中定义的界面效果 //view view = layoutinflater.from(mainactivity.this).inflate(resources.getlayout(obj),null); // 或者这样,但一定要重写getresources方法,才能这样写 view view = layoutinflater.from(mainactivity.this).inflate(obj, null); method method = activity.getdeclaredmethod("setlayout", view.class); method.setaccessible(true); method.invoke(activity, view);
完整的代码
public class mainactivity extends activity { private resources resources; protected assetmanager assetmanager; 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) { string dexpath = "/pluginactivity.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); // 使用包含插件apk的resources对象来获取这个布局才能正确获取插件中定义的界面效果 view view = layoutinflater.from(mainactivity.this).inflate(resources.getlayout(obj),null); // 或者这样,但一定要重写getresources方法,才能这样写 // 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(mainactivity.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); } catch (exception e) { } resources = new resources(assetmanager, super.getresources().getdisplaymetrics(), super.getresources().getconfiguration()); // 也可以根据资源获取主题 } 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(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 getassets() { return assetmanager == null ? super.getassets() : assetmanager; } }
动态加载activity:使用代理
还有一种方式启动插件中的activity的方式就是将插件中的activity当做一个一般的类,不把它当成组件activity,于是在启动的时候启动一个代理proxyactivity,它才是真正的activity,他的生命周期由系统管理,我们在它里面调用插件activity里的函数即可。同时,在插件activity里面保存一个代理activity的引用,把这个引用当做上下文环境context理解。
这里插件activity的生命周期函数均由代理activity调起,proxyactivity其实就是一个真正的我们启动的activity,而不是启动插件中的activity,插件中的“要启动”的activity就当做一个很普通的类看待,当成一个包含了一些函数的普通类来理解,只是这个类里面的函数名字起的有些“奇怪”罢了。涉及到访问资源和更新ui相关的时候通过当前上下文环境,即保存的proxyactivity引用来获取。
以下面这个demo为例
宿主项目
com.dl.host
|--mainactivity.java
|--proxyactivity.java
mainactivity包括一个按钮,按下按钮跳转到插件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就是我们要启动的插件activity的一个傀儡,代理。是系统维护的activity。
public class proxyactivity extends activity{ private dexclassloader loader; private activity 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(); } // 设置插件activity的代理 try { method setproxy = clazz.getdeclaredmethod("setproxy", activity.class); 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(); // 调用插件activity的onstart方法 method onstart = null; try { onstart = clazz.getdeclaredmethod("onstart"); onstart.setaccessible(true); onstart.invoke(activity); } catch (exception e) { e.printstacktrace(); } } @override protected void ondestroy() { super.onstart(); // 调用插件activity的ondestroy方法 method ondestroy = null; try { ondestroy = clazz.getdeclaredmethod("ondestroy"); ondestroy.setaccessible(true); ondestroy.invoke(activity); } catch (exception e) { e.printstacktrace(); } } }
可以看到,proxyactivity其实就是一个真正的activity,我们启动的就是这个activity,而不是插件中的activity。
插件项目
com.dl.plugin
|--mainactivity.java
保存了一个代理activity的引用,值得注意的是,由于访问插件中的资源需要额外的操作,要加载资源,因此这里未使用插件项目里面的资源,所以我使用代码添加的textview,但原理和前面讲的内容是一样的。
public class mainactivity extends activity { private activity proxyactivity; public void setproxy(activity proxyactivity) { this.proxyactivity = proxyactivity; } // 里面的所有操作都由代理activity来操作 @override protected void oncreate(bundle savedinstancestate) { textview tv = new textview(proxyactivity); tv.settext("插件activity"); proxyactivity.setcontentview(tv,new framelayout.layoutparams(layoutparams.wrap_content, layoutparams.wrap_content)); } @override protected void onstart() { toast.maketext(proxyactivity, "插件onstart", 0).show(); } @override protected void ondestroy() { toast.maketext(proxyactivity, "插件ondestroy", 0).show(); } }
这种方法相比较前面修改系统加载器的方法需要自己维护生命周期,比较麻烦,前一种方式由系统自己维护,并且启动的就是插件中实实在在的activity。
前一种方式要在宿主的androidmanifest里面声明插件activity,这样当activity太多时就要声明很多,比较繁琐,不过也可以不声明逃过系统检查。后面这种方式就只需要一个代理proxyactivity类即可。在他的oncreate里面根据传递的值选择加载插件中的哪个activity即可。
推荐阅读
-
Android动态加载Activity原理详解
-
详解Android Activity之间切换传递数据的方法
-
Android中利用动态加载实现手机淘宝的节日特效
-
Android中利用动态加载实现手机淘宝的节日特效
-
详解Android中Handler的实现原理
-
详解Android中Activity运行时屏幕方向与显示方式
-
详解Android的Socket通信、List加载更多、Spinner下拉列表
-
Android ListView异步加载图片方法详解
-
Android Fragment(动态,静态)碎片详解及总结
-
深入Android中BroadcastReceiver的两种注册方式(静态和动态)详解