graph LR
subgraph 解决插件组件生命周期调用问题
插件生命周期
end
subgraph 反射AssetManager.assetPath,再通过AssetManager来创建一个新的Resources对象
加载插件资源
end
subgraph newDexClassLoader加载插件apk,从插件ClassLoader中load指定的插件Activity名字,newInstance之后强转为Activity类型使用
加载插件类
end
subgraph 上下文如BaseContext,mResources,activityInfo在attach时反射替换为插件的
直接提供插件Activity给系统
end
插件生命周期-->启动插件Activity
启动插件Activity-->HookInstrumentation的execStartActivity
启动插件Activity-->|Shadow|更改启动Activity方式,调用自定义方法到插件进程,转成插代理Activity
插件生命周期-->AMS回到App端
AMS回到App端-->直接提供插件Activity给系统-->Hack修改宿主PathClassLoader
直接提供插件Activity给系统-->HackActivityThread主线程Handler
AMS回到App端-->提供代理Activity给系统-->生命周期转发给插件Activity
提供代理Activity给系统-->|Shadow|生命周期转发给插件Activity,再转调回代理Activity
Java还有两个和动态化相关的特性,一个是接口,另一个是向上转型。
Class<?> implClass = classLoader.loadClass("com.xxx.AImpl");
Object implObject = implClass.newInstance();
A a = (A) implObject;
这里假设classLoader动态加载了一些Java类,其中就有一个类叫做com.xxx.AImpl,AImpl继承自A,或者AImpl实现了A接口。注意这里用了强制类型转换,是因为代码层面是将Object类型向下转型成了A。但实际上我们知道implObject的类型是AImpl,AImpl转换成A是一个向上转型。向上转型总是安全的。所以用这种方法总是可以先定义出接口,精心设计接口,让接口足够通用和稳定。只要接口不变,它的实现总是可以修改的。我们将接口打包在宿主中,接口就轻易不能更新了。但是它的实现总是可以更新的。
https://github.com/singwhatiwanna/dynamic-load-apk/
我们知道,activity的工作主要是由ContextImpl来完成的, 它在activity中是一个叫做mBase的成员变量。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上context就是通过它们来获取资源的,这两个抽象方法的真正实现在ContextImpl中。也即是说,只要我们自己实现这两个方法,就可以解决资源问题了。
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
下面看一下如何实现这两个方法 首先要加载apk中的资源:
protected void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
说明:加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。
val packageManager = hostAppContext.packageManager
val packageArchiveInfo = packageManager.getPackageArchiveInfo(
archiveFilePath,
PackageManager.GET_ACTIVITIES
or PackageManager.GET_META_DATA
or PackageManager.GET_SERVICES
or PackageManager.GET_PROVIDERS
or PackageManager.GET_SIGNATURES
)
packageArchiveInfo.applicationInfo.nativeLibraryDir = installedApk.libraryPath
packageArchiveInfo.applicationInfo.dataDir = dataDir.absolutePath
packageArchiveInfo.applicationInfo.publicSourceDir = archiveFilePath
packageArchiveInfo.applicationInfo.sourceDir = archiveFilePath
packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo)
getResourcesForApplication控制AssetManager加载插件路径里的资源:
createAssetManager(ResourcesKey):358, ResourcesManager (android.app), ResourcesManager.java
createResourcesImpl(ResourcesKey):490, ResourcesManager (android.app), ResourcesManager.java
getOrCreateResources(IBinder, ResourcesKey, ClassLoader):787, ResourcesManager (android.app), ResourcesManager.java
getResources(IBinder, String, String[], String[], String[], int, Configuration, CompatibilityInfo, ClassLoader):853, ResourcesManager (android.app), ResourcesManager.java
getTopLevelResources(String, String[], String[], String[], int, LoadedApk):1940, ActivityThread (android.app), ActivityThread.java
getResourcesForApplication(ApplicationInfo):1420, ApplicationPackageManager (android.app), ApplicationPackageManager.java
create(PackageInfo, String, Context):32, CreateResourceBloc (com.tencent.shadow.core.loader.blocs), CreateResourceBloc.kt
call():109, LoadPluginBloc$loadPlugin$buildResources$1 (com.tencent.shadow.core.loader.blocs), LoadPluginBloc.kt
上述创建的插件Resource存放在ShadowPluginLoader中PluginPartsMap对应的entry value也就是pluginParts中,pluginParts保存了插件application,classLoader,resources等信息
上述resource被用来构造mixResource并在ShadowActivityDelegate的getResource方法中返回mixResource,默认使用插件resource
不同插件之间,宿主和插件之间的资源管理方面:
插件Activity生命周期
以下是静态代理,占坑方式偷梁换柱
有几种Hook方式可以实现
面临插件上下文信息使用的是宿主信息的问题:
需要在Activity attach方法调用后(Instrumentation#callActivityOnCreate)反射替换刚刚attach到Activity里的BaseContext,mResources,activityInfo等属性,这里可以优化的是可以在对应属性的getter方法中拦截处理,而不是反射替换到属性值,因此需要字节码插桩让插件的Activity继承自PluginActivity(插件Activity基类),在这个基类中实现各种对插件包依赖的转调(Neptune新的方式),通过装饰者模式实现
面临插件上下文信息使用的是宿主信息的问题: Shadow的处理方式和上面装饰者的方式类似,区别在于动态插桩的插件基类不是真的Activity,是一个ContextWrapper,也就是说,本身就是一个装饰功能类,并非真的Activity,中间的双向转调层可以获取插件信息并通过HostActivityDelegator转调给宿主Activity
Tencent Shadow—零反射全动态Android插件框架正式开源 那么一个重要的原则就是,如果一个组件需要安装才能使用,那么就别在没安装的情况下把它交给系统。我们已知的插件框架中,做的最好的也不符合这个原则,所以尽管它的Hook点少,但就是由于它将没有安装的Activity交给系统了,所以后面就不得不做一些Hack的事修补
实际上通过前面的分析,我们发现其实根本不需要插件Activity执行super.onCreate()方法。明确了这个方案原本的目的,就是在宿主中注册并启动一个壳子Activity,这个壳子Activity什么都不自己做,想办法让插件Activity的各个生命周期方法实现代码成为壳子Activity的各个生命周期实现方法的代码。因此我们根本不需要插件Activity是一个系统Activity的子类。我们只是因为需要插件Activity还能正常安装运行,才导致它是一个真正的系统Activity子类的。
class ShadowActivity {
ContainerActivity containerActivity;
public void onCreate(Bundle savedInstanceState) {
containerActivity.superOnCreate(savedInstanceState);
}
}
class XXXPluginActivity extends ShadowActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
savedInstanceState.clear();
super.onCreate(savedInstanceState);
}
}
class ContainerActivity extends Activity {
ShadowActivity pluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
pluginActivity.onCreate(savedInstanceState);
}
public void superOnCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
仔细思考下现在的调用结果,是不是PluginActivity在正常安装运行和插件环境下运行时行为就一致了?