Android每周一轮子(android-pluginmgr(插件化))

前言
之前所做的一个项目为一个嵌入到游戏中,具备商城,支付等功能的SDK,由于游戏动态更新的问题,SDK因此也需要具备动态更新的能力,否则每一次的SDK更新都要强制游戏发布新版本了,本着该原则,限于部分历史原因,项目中采用了一个比较老的插件化方案android-pluginmgr,对于SDK的核心功能,全部抽离出放在插件中,通过这种方式可以实现对于核心功能的动态更新。
Android每周一轮子(android-pluginmgr(插件化))
文章图片
SDK设计 Github地址
基础使用

  • 在 Application中初始化插件
@Override public void onCreate(){ PluginManager.init(this); //... }

  • 从Apk中加载插件
PluginManager mgr = PluginManager.getSingleton(); File myPlug = new File("/mnt/sdcard/Download/myplug.apk"); PlugInfo plug = pluginMgr.loadPlugin(myPlug).iterator().next();

从目录中加载相应的插件,通过PlugInfo来存储插件信息。
  • 启动插件中的Activity
start activity: mgr.startMainActivity(context, plug);

Activity的启动通过调用PluginManager的startMainActivity。
  • 插件验证功能
PluginManager.getSingleton().setPluginOverdueVerifier(new PluginOverdueVerifier() { @Override public boolean isOverdue(File originPluginFile, File targetExistFile) { //check If the plugin has expired return true; } });

提供了一个回调,我们可以实现这个回调中的方法来根据自己的需求做自定义的插件过期校验。
源码实现分析
PluginManager的初始化 1.线程的判断
if (!isMainThread()) { throw new IllegalThreadStateException("PluginManager must init in UI Thread!"); }

需要确保其初始化操作发生在主线程。
2.生成确定相应的装载优化生成文件目录
this.context = context; //插件输出路径 File optimizedDexPath = context.getDir(Globals.PRIVATE_PLUGIN_OUTPUT_DIR_NAME, Context.MODE_PRIVATE); dexOutputPath = optimizedDexPath.getAbsolutePath(); dexInternalStoragePath = context.getDir( Globals.PRIVATE_PLUGIN_ODEX_OUTPUT_DIR_NAME, Context.MODE_PRIVATE );

3.部分Hook替换操作
DelegateActivityThread delegateActivityThread = DelegateActivityThread.getSingleton(); Instrumentation originInstrumentation = delegateActivityThread.getInstrumentation(); if (!(originInstrumentation instanceof PluginInstrumentation)) { PluginInstrumentation pluginInstrumentation = new PluginInstrumentation(originInstrumentation); delegateActivityThread.setInstrumentation(pluginInstrumentation); }

此处DelegateActivityThread的作用是通过反射拿到当前的ActivityThread,同时通过反射来获取其内部的Instrumentation和对Instrumentation进行设置。
PluginInstrumentation 继承自DelegateInstrumentation,DelegateInstrumentation持有了原有的Instrumentation,对于其中的大部分方法通过代理的方式,将其转交给原有的Instrumention进行处理,对于几个Activity启动相关的核心方法进行了重写。
Android每周一轮子(android-pluginmgr(插件化))
文章图片
Instrumentation 插件装载过程
if (pluginSrcDirFile.isFile()) { PlugInfo one = buildPlugInfo(pluginSrcDirFile, null, null); if (one != null) { savePluginToMap(one); } return Collections.singletonList(one); }

此处已经省略了对于目录的一些判空操作的代码,首先判断给定文件路径是为目录还是一个文件,如果是一个文件则进行构建,如果是一个目录,则会对该目录进行遍历,然后进行单个文件执行的操作。首先根据给定的文件,构造出一个插件信息,然后将该插件信息存入到我们的内存中存放PlugInfo的一个Map之中。
Map pluginPkgToInfoMap = new ConcurrentHashMap()

所以其核心操作就是buildPlugInfo。构建过程则为创建一个PlugInfo对象出来,具体步骤为对插件进行解析,来补充PlugInfo的相关属性。
构建插件信息 1.设置PlugInfo的文件路径信息,传入的插件位置和初始化时设置的路径如果不一致,则进行拷贝操作。
PlugInfo info = new PlugInfo(); info.setId(pluginId == null ? pluginApk.getName() : pluginId); File privateFile = new File(dexInternalStoragePath, targetFileName == null ? pluginApk.getName() : targetFileName); info.setFilePath(privateFile.getAbsolutePath()); //如果文件不在相同的地方,则进行复制 if(!pluginApk.getAbsolutePath().equals(privateFile.getAbsolutePath())) { copyApkToPrivatePath(pluginApk, privateFile); }

2.装载解析Manifest
String dexPath = privateFile.getAbsolutePath(); //Load Plugin Manifest PluginManifestUtil.setManifestInfo(context, dexPath, info);

根据当前的dex路径来获得到Manifest,然后解析该文件,得到其中的Activity,Service,Receiver,Provider信息,然后将这些信息分别用来设置到PlugInfo相应的属性中。
3.装载资源文件
AssetManager am = AssetManager.class.newInstance(); am.getClass().getMethod("addAssetPath", String.class) .invoke(am, dexPath); info.setAssetManager(am); Resources hotRes = context.getResources(); Resources res = new Resources(am, hotRes.getDisplayMetrics(), hotRes.getConfiguration()); info.setResources(res);

通过反射获取到执行AssetManager的addAssetPath方法,将其设置到插件的路径中,然后利用当前的AssetManager来构造一个Resource对象。将该对象设置到PlugInfo中。用来后续对插件中资源装载时使用。
4.设置ClassLoader
PluginClassLoader pluginClassLoader = new PluginClassLoader(info, dexPath, dexOutputPath , getPluginLibPath(info).getAbsolutePath(), pluginParentClassLoader); info.setClassLoader(pluginClassLoader);

继承自DexClassLoader写的ClassLoader,相比于DexClassLoader增加了一个PlugInfo属性,同时在构造函数中为其赋值。
5.创建Application,设置Application信息
ApplicationInfo appInfo = info.getPackageInfo().applicationInfo; Application app = makeApplication(info, appInfo); attachBaseContext(info, app); info.setApplication(app);

创建Application对象,attachBaseContext,在这里为什么要用attachBaseContext呢?这就设置到Context的一些问题了,先看下代码中attachbaseContext中核心代码。
Field mBase = ContextWrapper.class.getDeclaredField("mBase"); mBase.setAccessible(true); mBase.set(app, new PluginContext(context.getApplicationContext(), info));

Application继承自ContextWrapper,其具备获取资源问及那,获取包管理器,获取应用程序上下文等等,而这些方法的实现都是通过attachBaseContext方法为在ContextWrapper设置一个context的实现类,attachBaseContext()方法其实是由系统来调用的,它会把ContextImpl对象作为参数传递到attachBaseContext()方法当中,从而赋值给mBase对象,之后ContextWrapper中的所有方法其实都是通过这种委托的机制交由ContextImpl去具体实现的。因此这里需要我们手动为Application设置上这个Context的实现类。
到此为止,我们已经完成了我们SDK的初始化过程和我们的插件的装载过程。这个时候,我们可能需要对于我们插件中一些功能类的调用,或者是启动其中的Activity。
Android每周一轮子(android-pluginmgr(插件化))
文章图片
插件信息构建 Activity的启动
//从插件中查找当前Activity信息 ActivityInfo activityInfo = plugInfo.findActivityByClassName(targetActivity); //构建创建Activiyt的相关对象 CreateActivityData createActivityData = https://www.it610.com/article/new CreateActivityData(activityInfo.name, plugInfo.getPackageName()); intent.setClass(from, activitySelector.selectDynamicActivity(activityInfo)); //设置标志启动来自插件的Activity intent.putExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN, createActivityData); from.startActivity(intent);

根据目标Activity从我们创建的PlugInfo中找到相关的Activity信息。通过Activity名和插件的包名来创建一个Activity的信息。selectDynamicActivity是我们在宿主类中设置的一个动态代理类,将其设置我们跳转的一个目标。然后通过intent携带FLAG_ACTIVITY_FROM_PLUGIN的标记下的Activity的信息,这个时候通过当前的Activity来启动。启动MainActivity则为对向其传递的Activity信息做一个改变,直接启动。
Activity的启动后面实际上是通过Instrumentation中的execStartActivity来执行启动新的Activity,Instrumentation中对于execStartActivity有许多的重载方法。在这些方法执行之前都会调用一个方法:replaceIntentTargetIfNeed,replaceIntentTargetIfNeed()用来对跳转到插件Activity进行相应的处理。在方法中进行的处理如下:
//判断是否启动来自插件的Activity if (!intent.hasExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN) && currentPlugin != null){ ComponentName componentName = intent.getComponent(); if (componentName != null){ //获取包名和Activity名 String pkgName = componentName.getPackageName(); String activityName = componentName.getClassName(); if (pkgName != null){ CreateActivityData createActivityData = https://www.it610.com/article/new CreateActivityData(activityName, currentPlugin.getPackageName()); ActivityInfo activityInfo = currentPlugin.findActivityByClassName(activityName); if (activityInfo != null) { intent.setClass(from, PluginManager.getSingleton().getActivitySelector().selectDynamicActivity(activityInfo)); intent.putExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN, createActivityData); //为Intent设置额外的classLoaderintent.setExtrasClassLoader(currentPlugin.getClassLoader()); } } } }

如果Intent中没有来自插件的标识,然后当前的插件信息不为null,则会根据插件信息提取出相关的信息,然后对Intent进行一系列的设置。
在经过一系列处理,和AMS之间交互等之后,最终会调用ActivityThreadperformLaunchActivity来进行Activity的创建和启动,首先是通过相应的类装载器创建出Activity对象,然后调用其相应的生命周期函数,这个过程都是系统自动执行。在performLaunchActivity中具体执行的任务有以下几个。
1.首先从intent中解析出目标activity的启动参数。
2.通过Activity的无参构造方法来new一个对象,对象就是在这里new出来,实际的调用是Instrumentation的newActivity函数,这个函数也是我们在Hook中要重写的。
3.然后为该Activity设置上Application,Context,Instrumentation等信息。然后通过Instrumentation的callActivityOnCreate调用Activity的onCreate函数,使得其具备了生命周期。
此处我们的实现是通过我们本地的一个Activity作为桩,也就是说我们实际调用的Activity是我们本地的一个Activity,然后对其中一些步骤做Hook,对于其中的一些信息的检测,缺失处理。
这个过程,我们要对newActivity()进行Hook,还要对callActivityOnCreate()进行Hook,newActivity的实现代码
CreateActivityData activityData = https://www.it610.com/article/(CreateActivityData) intent.getSerializableExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN); if (activityData != null && PluginManager.getSingleton().getPlugins().size()> 0) { //这里找不到插件信息就会抛异常的,不用担心空指针 PlugInfo plugInfo; plugInfo = PluginManager.getSingleton().tryGetPluginInfo(activityData.pluginPkg); plugInfo.ensureApplicationCreated(); if (activityData.activityName != null){ className = activityData.activityName; cl = plugInfo.getClassLoader(); } } return super.newActivity(cl, className, intent);

Activity的创建中,获取Intent中的内容,然后将其中的信息进行解析,然后从中解析出相关属性,配置给Activity,然后调用原有父类中的方法,这个Intent在发起的时候,我们告诉系统的是调用的是我们本地插的一个Activity,但是在实际创建的时候,通过newActivity的时候,创建出的Activity是我们插件中的Activity。
Activity的创建之后,接下来需要调用其生命周期函数,然后这个过程需要我们对其再次进行Hook,添加进我们的相关操作。对于其中的代码,我们逐步来分析。
lookupActivityInPlugin(activity);

该方法执行的操作
ClassLoader classLoader = activity.getClass().getClassLoader(); if (classLoader instanceof PluginClassLoader){ currentPlugin = ((PluginClassLoader)classLoader).getPlugInfo(); }else{ currentPlugin = null; }

执行该方法之后,会为currentPlugin赋值。当currentPlugin不为null时,也就是表明此时确定了该Activity是来自插件。
Context baseContext = activity.getBaseContext(); PluginContext pluginContext = new PluginContext(baseContext, currentPlugin);

在PluginContext中进行了对于获取资源,类装载器等一些信息方法的重写。对于其中的一些资源获取,ClassLoader的获取等,都是通过PlugInfo中的信息进行设置。然后再通过反射的方式对这些原有的获取方式进行替换。
Reflect.on(activity).set("mResources", pluginContext.getResources()); Field field = ContextWrapper.class.getDeclaredField("mBase"); field.setAccessible(true); field.set(activity, pluginContext); Reflect.on(activity).set("mApplication", currentPlugin.getApplication());

获取Activity的一些主题,
ActivityInfo activityInfo = currentPlugin.findActivityByClassName(activity.getClass().getName()); int resTheme = activityInfo.getThemeResource(); if (resTheme != 0) { boolean hasNotSetTheme = true; Field mTheme = ContextThemeWrapper.class .getDeclaredField("mTheme"); mTheme.setAccessible(true); hasNotSetTheme = mTheme.get(activity) == null; if (hasNotSetTheme) { changeActivityInfo(activityInfo, activity); activity.setTheme(resTheme); } }

如果当前Activity未设置主题,则对Activity的信息进行替换。调用了方法 changeActivityInfo
在Activity的启动过程中,对于Activity相关的内容通过之前保存在插件信息中的内容通过反射的方式进行设置。
Android每周一轮子(android-pluginmgr(插件化))
文章图片
Activity启动流程 总结
【Android每周一轮子(android-pluginmgr(插件化))】该插件的实现比较简单,通过该插件可以帮助我们回顾前两篇讲的App启动,资源装载,类装载问题,该插件在2年前已经停止更新维护,其功能上相比现有的一些成熟方案,如Replugin,VirtualApk等存在很大进步空间,但是由于其实现简单,非常方便我们去了解这一个技术的实现流程,对于后续插件化代码阅读非常有帮助。接下来是对于360 RePlugin的源码分析。

    推荐阅读