插件化学习 - 知识总结

资源访问

res里的每一个资源都会在R.Java里生成一个对应的Integer类型的id,APP启动时会先把R.java注册到当前的上下文环境,我们在代码里以R文件的方式使用资源时正是通过使用这些id访问res资源,然而插件的R.java并没有注册到当前的上下文环境,所以插件的res资源也就无法通过id使用了。

平时我们所访问资源一般是通过getResources().getXXX()的方式来获取的。
因为宿主程序中并没有插件的资源,所以通过R来加载插件的资源是行不通的,程序会抛出异常:无法找到某某id所对应的资源。
所以我们需要先获取到插件的Resources:

private Resources getPlugResources() {
    // 获取插件包中的资源
    AssetManager assetManager = null;
    try {
        assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, mDexPath);
    } catch (Exception e) {
        e.printStackTrace();
    }
    if (assetManager == null) {
        return null;
    }
    Resources superRes = super.getResources();
    mPlugResources = new Resources(assetManager, superRes.getDisplayMetrics(),
            superRes.getConfiguration());

    return mPlugResources;
}

public int getColor(String colorName) {
    if (mPlugResources == null) {
        getPlugResources();
    }
    try {
        return mPlugResources.getColor(mPlugResources.getIdentifier(colorName, COLOR, PLUG_NAME));

    } catch (Resources.NotFoundException e) {
        e.printStackTrace();
        return -1;
    }
}

插件化知识梳理(9) - 资源的动态加载示例及源码分析

调用插件中的Activity

apk被宿主程序调起以后,apk中的activity其实就是一个普通的对象,不具有activity的性质,因为系统启动activity是要做很多初始化工作的,而我们在应用层通过反射去启动activity是很难完成系统所做的初始化工作的,所以activity的大部分特性都无法使用包括activity的生命周期管理,这就需要我们自己去管理。当然还需要我们常说的上下文Context

需要先简单的了解一下Activity的启动流程。
在此就直接记录重要的结论:

ActivityThread.javaperformLaunchActivity方法中,该方法通过Instrumentation的newActivity创建了activity类,接着完成了application的创建(没有创建Application的情况下,在该方法中就创建了Application的context),接着通过createBaseContextForActivity方法为该activity创建context,再调用attach方法进行绑定。

正常的的Activity被AMS反射调用,在attach后就有了Context,那我们自己反射的Activity要想有Context,就要模拟AMS调用方式,构造Context,但是这相当于再写个系统,不可实现,那怎么办?

遇到问题,解决问题。
插件中被反射的activity没有了Context,我们可以把主apk的Acitvity的Context传递给插件Acitivity。
在宿主APK注册一个ProxyActivity(代理Activity),就是作为占坑使用。每次打开插件APK里的某一个Activity的时候,都是在宿主里使用启动ProxyActivity,然后在ProxyActivity的生命周期里方法中,调用插件中的Activity实例的生命周期方法,从而执行插件APK的业务逻辑。所以思路就来了:
第一、ProxyActivity中需要保存一个Activity实例,该实例记录着当前需要调用插件中哪个Activity的生命周期方法。
第二、ProxyActivity如何调用插件apk中Activity的所有生命周期的方法,使用反射呢?还是其他方式(接口)。

缺点:

  1. 插件Activity不能使用this关键字,比如this.finish()方法是无效的,真正掌管生命周期的是proxy应该调用proxy.finish(),所以百度开源框架 dynamic-load-apk使用that指向proxy,约定插件中使用that来代替this。
  2. 插件Activity无法深度演绎真正的Activity组件,可能有些高级特性无法使用。
  3. 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。

生命周期管理

  1. 反射
  2. 接口

动态创建Activity

使用代理Activity有一些限制:

  1. 实际运行的Activity实例其实都是ProxyActivity,并不是真正想要启动的Activity;
  2. ProxyActivity只能指定一种LaunchMode,所以插件里的Activity无法自定义LaunchMode;
  3. 不支持静态注册的BroadcastReceiver;
  4. 往往不是所有的apk都可作为插件被加载,插件项目需要依赖特定的框架,还有需要遵循一定的”开发规范”;

解决对策就是,在需要启动插件的某一个Activity(比如PlugActivity)的时候,动态创建一个TargetActivity,新创建的TargetActivity会继承PlugActivity的所有共有行为,而这个TargetActivity的包名与类名刚好与我们事先注册的TargetActivity一致,我们就能以标准的方式启动这个Activity。

启动Activity是一个复杂的过程,有很多环节:Activity.startActivity()->Activity.startActivityForResult()->Instrument.excuteStartActivity()->ASM.startActivity()。大概又这么几个环节,详细了解可以参考文章:《深入理解Activity的启动过程》。 所谓“占坑”在宿主端的AndroidManifest.xml注册一个不存在的Activity,可以取名为StubActivity,同样启动插件的Activity都是启动StubActivity,然后在启动Activity的某个环节,我们找个“临时”演员来代替StubActivity,这个临时演员就是插件中定义的Activity,这叫“瞒天过海”。如何找“临时”演员?本章要讲的重点:使用 dexmaker 临时改造插件Activity。

插件化研究之dexmaker动态生成Activity
Android插件化学习之路(六)之动态创建Activity

HOOK Activity

在了解了Activity的启动流程之后,我们得知是mInstrumentation.newActivity()创建的Activity。
我们要做的就是通过替换掉Instrumentation类,达到定制插件运行环境的目的。

简单的HOOK

  1. 替换Instrumentation类
// 先获取到当前的ActivityThread对象
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);

//如果没有注入过,就执行替换
if (!(mInstrumentation instanceof PluginInstrumentation)) {
    PluginInstrumentation pluginInstrumentation = new PluginInstrumentation(mInstrumentation);
    mInstrumentationField.set(currentActivityThread, pluginInstrumentation);
}
  1. 替换newActivity()
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
        throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    if (intent != null) {
        isPlugin = intent.getBooleanExtra(PluginCons.FLAG_ACTIVITY_FROM_PLUGIN, false);
    }
    if (isPlugin && intent != null) {
        className = intent.getStringExtra(PluginCons.FLAG_ACTIVITY_CLASS_NAME);
    }
    return super.newActivity(cl, className, intent);
}

HOOK:
8个类搞定插件化——Activity实现方案
Android 插件化原理解析——Activity生命周期管理

动态代理:
知识总结 插件化学习 Activity加载分析
Android插件化系列第(五)篇---Activity的插件化方案(代理模式)

Demo

PluginDemo

各类开源库比较

特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
支持四大组件 只支持Activity和普通Service 只支持Activity 只支持Activity 全支持 全支持
组件无需在宿主manifest中预注册 ×
插件可以依赖宿主 ×
支持PendingIntent × × ×
Android特性支持 大部分 大部分 大部分 几乎全部 几乎全部
兼容性适配 一般 一般 中等
插件构建 部署aapt Gradle插件 Gradle插件

各类分析文章

插件化框架android-pluginmgr全解析
Dynamic-Load-Apk源码解析
Android 全面插件化 RePlugin 流程与源码解析
Android插件化快速入门与实例解析(VirtualApk)

系列文章

Android动态加载技术 系列索引
Android插件化原理解析——概要
Android插件化入门指南

你可能感兴趣的:(插件化学习 - 知识总结)