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

前言

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

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启动相关的核心方法进行了重写。

Instrumentation

插件装载过程
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的启动
//从插件中查找当前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之间交互等之后,最终会调用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 = (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相关的内容通过之前保存在插件信息中的内容通过反射的方式进行设置。

Activity启动流程

总结

该插件的实现比较简单,通过该插件可以帮助我们回顾前两篇讲的App启动,资源装载,类装载问题,该插件在2年前已经停止更新维护,其功能上相比现有的一些成熟方案,如Replugin,VirtualApk等存在很大进步空间,但是由于其实现简单,非常方便我们去了解这一个技术的实现流程,对于后续插件化代码阅读非常有帮助。接下来是对于360 RePlugin的源码分析。

Android-Plugin-Framework 此项目是Android插件框架完整源码以及实例。用来开发Android插件APK,并通过动态加载的方式在宿主程序中运行。 若插件APK是完全独立的APK,那么插件apk也可独立安装运行。 若插件APK不是完全独立的apk,比如和插件宿主程序共用一些依赖库,那么插件apk只能在宿主程序中运行。不可独立运行。 因为此时插件apk的代码是不完整的。 目录结构说明: PluginCore工程是插件库核心工程。用于提供对插件功能的支持。 PluginMain是用来测试的插件宿主程序Demo工程。 PluginShareLib是用来测试的插件宿主程序的依赖库Demo工程 PluginTest是用来测试的插件Demo工程。此工程下有用来编译插件的ant脚本。 宿主程序工程可以通过ant编译或者导入eclipse后直接点击Run菜单进行安装。 插件Demo工程需要通过插件ant脚本编译。编译命令为 “ant clean debug” 原因是Demo中引用了宿主程序的依赖库。需要在编译时对共享库进行排除。 插件编译出来以后,可以将插件复制到sdcard,然后在宿主程序中调用PluginLoader.installPlugin("插件apk绝对路径")进行安装 还有一种简易的安装方式,是使用编译命令为 “ant clean debug install” 直接将插件apk安装到系统中,PluginMain工程会监听系统的应用安装广播,监听到插件apk安装广播后, 再自动调用PluginLoader.installPlugin("/data/app/插件apk文件.apk")进行插件安装。免去复制到sdcard的过程。 (虽然没有用过apkplug、以及另外一个插件框架作者singwhatiwanna写的DL框架,但是看过他们的一些介绍文档,感觉自己的这份实现应该是更简单易用更完善。。。哈哈,是不是有王婆卖瓜的嫌疑。) 已支持的功能: 1、插件apk无需安装,由宿主程序动态加载运行。 2、插件形式支持fragment和activity代理。 这两种形式是插件开发中的两种主要形式。 3、插件支持activity非代理模式,真正实现Activity无需注册,既不用反射,也不用代理,实现Activity完整生命周期。 4、插件库apk可无任何特殊代码。如插件中的fragment,activity等无需继承任何特定基类或接口。完全和普通app代码没有区别. 5、支持插件共用宿主程序依赖库提供的自定义控件 6、支持插件中使用自定义控件 7、支持插件资源和宿主资源混合使用。意即支持如下场景: 插件程序和宿主程序共用依赖库时插件中的布局文件中使用了宿主程序中的自定义控件,而宿主程序中的自定义控件中又使用 了宿主程序中的布局文件。 插件代码中无需做任何特殊处理,即可支持这种布局文件。 8、插件中的各种资源、布局、R、以及宿主程序中的各种资源、布局、R等可随意使用、也无任何特殊代码。 10、支持插件使用宿主程序主题和系统主题 11、解决资源id冲突问题。尝试做过插件开发的同学应该都遇到,插件资源id和宿主程序资源id可能相同,导致获取的资源不是想要的资源。 此问题其实在android提供的编译器aapt中早已提供了支持。 12、需要关注PluginTest工程的ant.properties文件和project.properties文件以及custom_rules.xml文件,插件使用宿主程序共享库,以及共享库R引用,和编译时排除的功能,都在这3个配置文件中体现 暂不支持的功能: 1、暂不支持使用插件中自定义主题, 2、支持在插件中通过R文件使用宿主程序中的资源,暂不支持插件资源文件中直接使用宿主程序中的资源。但是支持间接使用。 例如在上述“已支持的功能”6中描述的,实际就是间接使用。 后续需要解决的问题: 1、使支持插件自定义主题 2、使插件中对activity的支持更稳定。 由于此框架没有实际的项目应用,目前对activity的提供标准API的测试还不够完全,可能在其他开发场景中,activity的部分标准API可能会出现问题。毕竟这里使用了很多反射,会涉及到多机型多系统版本的兼容问题。后续还需要持续测试和完善 上述第2点问题已解决、请看已支持的功能第3条。 3、使支持插件资源文件中直接引用依赖库中的资源。目测可能需要重写android自带的aapt程序。 实现原理简介: 1、插件apk的class 通过构造插件apk的Dexclassloader来加载插件apk中的类。DexClassLoader的parent设置为宿主程序的classloader,即可将主程序和插件程序的class贯通 2、插件apk的资源 通过构造插件apk的AssetManager和Resouce类即可。 本项目最关键一点功能、也是和网上其他插件项目不同的地方之一,在于 通过addAssetsPath方法添加资源的时候,同时添加了插件程序的资源文件和宿主程序的资源。这样就 可以做到插件资源合并。很多资源文件都迎刃而解。 3、插件apk中的资源id 完成上述第二点以后,还有需要解决的难题,是宿主程序资源id和插件程序id重复的问题。 这个问题解决办法也很简单 我们知道,资源id是在编译时生成的,其生成的规则是0xPPTTNNNN PP段,是用来标记apk的,默认情况下系统资源PP是01,应用程序的PP是07 TT段,是用来标记资源类型的,比如图标、布局等,相同的类型TT值相同,但是同一个TT值不代表同一种资源,例如这次编译的时候可能使用03作为layout的TT,那下次编译的时候可能会使用06作为TT的值,具体使用那个值,实际上和当前APP使用的资源类型的个数是相关联的。 NNNN则是某种资源类型的资源id,默认从1开始,依次累加。 那么我们要解决资源id问题,就可从TT的值开始入手,只要将每次编译时的TT值固定,即可是资源id达到分组的效果,从而避免重复。 例如将宿主程序的layout资源的TT固定为03,将插件程序资源的layout的TT值固定为23,即可解决资源id重复的问题了。 固定资源idTT值的版本也非常简单,提供一份public.xml,在public.xml中指定什么资源类型以什么TT值开头即可。 具体public.xml如何编写,可参考PluginMain/res/values/public.xml 以及 PluginTest/res/values/public.xml俩个文件,它们是分别用来固定宿主程序和插件程序资源id的范围的。 4、插件apk的Context 构造一个Context即可,具体的Context实现请参考PluginCore/src/com/plugin/core/PluginContextTheme.java 关键是要重写几个获取资源、主题的方法,以及重写getClassLoader方法 5、插件中的LayoutInfalter 通过第4步构造出来的Context获取LayoutInfater即可 6、如何实现插件代码不依赖任何特殊代码,如继承特定的基类、接口等。 要做到这一点,最主要的是实现上述第4步,构造出插件的Context后,所有问题都可以得到解决。 7、插件中Activity无需注册,拥有完整生命周期是如何实现的。 众所周知、Activity是系统组件,必须在manifest中注册才能被系统唤起并拥有完整生命周期,通过Activity代理和放射的方式实现的 实际是伪生命周期。并非完整生命周期。那么如果才能实现activity免注册,而且拥有完整的生命周期呢,这要从activity的启动流程中 入手。 App安装时,系统会扫描app的Manifest并缓存到一个xml中,activity启动时,系统会现在查找缓存的xml,如果查到了,再通过classLoad去load这个class,并构造一个activity实例。那么我们只需要将classload加载这个class的时候做一个简单的映射,让系统以为加载的是A class,而实际上加载的是B class,达到挂羊头买狗肉的效果,即可将预注册的Aclass替换为未注册的activity,从而实现插件中的Activity 完全被系统接管,而拥有完整生命周期。 在PluginMain和PluginTest已经添加了这种实现方式的测试实例。 8、通过activity代理方式实现加载插件中的activity是如何实现的 要实现这一点,同样是基于上述第4点,构造出插件的Context后,通过attachBaseContext的方式,替换代理Activiyt的context即可。 另外还需要在获得插件Activity对象后,通过反射给Activity的attach()方法中attach的成员变量赋值。 这样可解决另外一个插件框架作者singwhatiwanna实现的代码中所谓this和that的问题。也是可以使插件Activity不需要继承任何特定基类或者接口的原因。 9、activity代理实现后,其他组件,如service等,如法炮制即可。 10、插件编译问题。 如果插件和宿主共享依赖库,那边编译插件的时候不可将共享库编译到插件当中,包括共享库的代码以及R文件,但是需要在编译时添加到classpath中,且插件中如果要使用共享依赖库中的资源,需要使用共享库的R文件来进行引用。这几点在PluginTest示例工程中有体现。 11、插件开发模式 插件UI可通过fragment或者activity来实现 如果是activity实现的插件,则最终会在PluginProxyActivity中运行 如果是fragment实现的插件,又分为两种 1种是fragment运行在任意支持fragment的activity中,这种方式,在开发fragment的时候,fragmeng中凡是要使用context的地方,都需要使用通过PluginLoader.getPluginContext()获取的context,那么这种fragment对其运行容器没有特殊要求 还有1种是,fragment运行在PluginCore提供的PluginSpecDisplayer中,这种方式,由于其运行容器PluginSpecDisplayer的Context已经被PluginLoader.getPluginContext获取的context替换,因此这种fragment的代码和普通非插件开发时开发的fragment的代码没有任何区别。 需要注意的问题 项目插件开发后,特别需要注意的是宿主程序混淆问题。宿主程序混淆后,可能会导致插件程序运行时出现classnotfound 标签:Android
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值