欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android动态加载Activity原理详解

程序员文章站 2024-02-27 14:36:57
activity的启动流程 加载一个activity肯定不会像加载一般的类那样,因为activity作为系统的组件有自己的生命周期,有系统的很多回调控制,所以自定义一个d...

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();
}
}

Android动态加载Activity原理详解

宿主项目

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);
}
});

Android动态加载Activity原理详解

首先我们要将该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);方法替换系统的类加载器,就可以了。

效果如下

Android动态加载Activity原理详解

此时发现可以启动插件中的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;
}
}

Android动态加载Activity原理详解

动态加载activity:使用代理

还有一种方式启动插件中的activity的方式就是将插件中的activity当做一个一般的类,不把它当成组件activity,于是在启动的时候启动一个代理proxyactivity,它才是真正的activity,他的生命周期由系统管理,我们在它里面调用插件activity里的函数即可。同时,在插件activity里面保存一个代理activity的引用,把这个引用当做上下文环境context理解。

这里插件activity的生命周期函数均由代理activity调起,proxyactivity其实就是一个真正的我们启动的activity,而不是启动插件中的activity,插件中的“要启动”的activity就当做一个很普通的类看待,当成一个包含了一些函数的普通类来理解,只是这个类里面的函数名字起的有些“奇怪”罢了。涉及到访问资源和更新ui相关的时候通过当前上下文环境,即保存的proxyactivity引用来获取。

以下面这个demo为例

宿主项目

Android动态加载Activity原理详解

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即可。