以前所作的一个项目为一个嵌入到游戏中,具有商城,支付等功能的SDK,因为游戏动态更新的问题,SDK所以也须要具有动态更新的能力,不然每一次的SDK更新都要强制游戏发布新版本了,本着该原则,限于部分历史缘由,项目中采用了一个比较老的插件化方案android-pluginmgr,对于SDK的核心功能,所有抽离出放在插件中,经过这种方式能够实现对于核心功能的动态更新。android
Github地址git
@Override public void onCreate(){ PluginManager.init(this); //... }
PluginManager mgr = PluginManager.getSingleton(); File myPlug = new File("/mnt/sdcard/Download/myplug.apk"); PlugInfo plug = pluginMgr.loadPlugin(myPlug).iterator().next();
从目录中加载相应的插件,经过PlugInfo来存储插件信息。github
start activity: mgr.startMainActivity(context, plug);
Activity的启动经过调用PluginManager的startMainActivity。app
PluginManager.getSingleton().setPluginOverdueVerifier(new PluginOverdueVerifier() { @Override public boolean isOverdue(File originPluginFile, File targetExistFile) { //check If the plugin has expired return true; } });
提供了一个回调,咱们能够实现这个回调中的方法来根据本身的需求作自定义的插件过时校验。ide
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替换操做ui
DelegateActivityThread delegateActivityThread = DelegateActivityThread.getSingleton(); Instrumentation originInstrumentation = delegateActivityThread.getInstrumentation(); if (!(originInstrumentation instanceof PluginInstrumentation)) { PluginInstrumentation pluginInstrumentation = new PluginInstrumentation(originInstrumentation); delegateActivityThread.setInstrumentation(pluginInstrumentation); }
此处DelegateActivityThread的做用是经过反射拿到当前的ActivityThread,同时经过反射来获取其内部的Instrumentation和对Instrumentation进行设置。this
PluginInstrumentation 继承自DelegateInstrumentation,DelegateInstrumentation持有了原有的Instrumentation,对于其中的大部分方法经过代理的方式,将其转交给原有的Instrumention进行处理,对于几个Activity启动相关的核心方法进行了重写。
if (pluginSrcDirFile.isFile()) { PlugInfo one = buildPlugInfo(pluginSrcDirFile, null, null); if (one != null) { savePluginToMap(one); } return Collections.singletonList(one); }
此处已经省略了对于目录的一些判空操做的代码,首先判断给定文件路径是为目录仍是一个文件,若是是一个文件则进行构建,若是是一个目录,则会对该目录进行遍历,而后进行单个文件执行的操做。首先根据给定的文件,构造出一个插件信息,而后将该插件信息存入到咱们的内存中存放PlugInfo的一个Map之中。
Map<String, PlugInfo> pluginPkgToInfoMap = new ConcurrentHashMap<String, PlugInfo>()
因此其核心操做就是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。
//从插件中查找当前Activity信息 ActivityInfo activityInfo = plugInfo.findActivityByClassName(targetActivity); //构建建立Activiyt的相关对象 CreateActivityData createActivityData = 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 = 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设置额外的classLoader intent.setExtrasClassLoader(currentPlugin.getClassLoader()); } } } }
若是Intent中没有来自插件的标识,而后当前的插件信息不为null,则会根据插件信息提取出相关的信息,而后对Intent
进行一系列的设置。
在通过一系列处理,和AMS
之间交互等以后,最终会调用ActivityThread
的performLaunchActivity
来进行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 = (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相关的内容经过以前保存在插件信息中的内容经过反射的方式进行设置。
该插件的实现比较简单,经过该插件能够帮助咱们回顾前两篇讲的App启动,资源装载,类装载问题,该插件在2年前已经中止更新维护,其功能上相比现有的一些成熟方案,如Replugin,VirtualApk等存在很大进步空间,可是因为其实现简单,很是方便咱们去了解这一个技术的实现流程,对于后续插件化代码阅读很是有帮助。接下来是对于360 RePlugin的源码分析。