插件化学习 - 知识总结

资源访问

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插件化入门指南

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,200评论 25 707
  • Android插件化基础的主要内容包括 Android插件化基础1-----加载SD上APKAndroid插件化基...
    隔壁老李头阅读 4,598评论 2 35
  • Android Studio JNI流程首先在java代码声明本地方法 用到native关键字 本地方法不用去实现...
    MigrationUK阅读 11,868评论 7 123
  • 大学时候曾经参加过快速记忆的培训,当然记单词一天几百个不是问题,记扑克牌最快记录大概两分钟吧,记扑克牌就是说随便抽...
    狮子Hans阅读 739评论 6 10
  • 柳枝如林,披拂在四季的歌声里 像是你柔媚的眼睛 雨已下过 在这黄昏落满的残山 绝不会开出第二朵合欢 日渐断续的蝉声...
    秋水游阅读 137评论 0 2