Android插件化快速入门与实例解析(VirtualApk)

集成一个第三方相册功能,只需集成一个插件APK到项目中,无需集成额外代码,并且支持随时更新相册功能,无需发布版本更新,无需AndroidManifest中声明四大组件,这就是插件化。

插件化可利用性很广,但事实上大多数开发者,因为未知而放弃使用,所以本篇将深入浅出带你了解插件化原理,从基础到实现,插件化不再是你陌生的领域。

本篇主要涉及到:

  • 一、Activity/Service的启动原理和流程。
  • 二、插件化实现原理。
  • 三、DiDi开源VirtualApk源码解析(Activity/Service)。
  • VirtualApk优化反射带来损耗的小技巧。

ps:如果你对此(一、二)已经十分了解,请自行略过。

一、 Activity/Service启动流程

Activity和Service的启动流程十分复杂,一个startActivity的背后是无数的逻辑实现,这里不深入讨论,但需要理解这个流程,因为插件化是在流程上动手脚,以达到绕过系统限制的目的。

下方图片是Activity启动的简化流程,可以看到,从Instrumentation开始,到ActivityManagerServiceActivityThread结束,启动一个Activity,流程并不简单。

InstrumentationexecStartActivity开始启动,到通过checkStartActivityResult校验Activity是否在Manifest中声明,从图中可以看出,流程还是相当繁琐的。
  
Activity启动流程详见图片

Activity简化启动流程图

下方图片是Service启动的简化流程,同样可以看到,ActivityManagerServiceActivityThread同样起到了关键性的作用。插件化的关键,就在于InstrumentationActivityManagerServiceActivityThread

Service启动流程详见图片

Service简化启动流程图

为了更好理解插件化,如下图,是几个关键类的对应关系与实际作用,有点S/C的味道。它们的通信是通过IBinder,实现进程通信的,可以看出,启动Activity和Service,ActivityThreadActivityManagerService是关键,并且上面我们知道,Instrumentation是Activity的启动入口,所以实现插件化的流程,便可以在这些关键类上开刀。

下图在插件化实现中起到关键作用

关键类关系图

提前说明

好了,带了一波基础姿势的节奏,稍安勿躁,先这里在补充几个概念,如果你已经习得,可以跳过:

  • Hook:拦截某个内部流程,在其中做某些修改,以实现自己的逻辑。

  • Instrumentation:每个Activity都有一个Instrumentation对象,它是在Activity启动是被赋予的Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(); 这便是startActivityForResult的启动,同时返回启动结果。

  • 占坑:声明一个不存在的Activity,如:
    <activity android:name=".A$1" android:launchMode="standard"/>
    ,这样启动.A$1这个Activity可以欺骗系统检测,然后再将插件Activity注入到.A$1这个坑位中。

二、插件化实现原理。

插件化的实现就是在于加载、绕过系统限制、启动和管理插件等过程。按照VirtualApk的实现,大致流程为:

1、初始化Hook住InstrumentationActivityThread等。通过PackageParser(插件apk包信息)、AssetManager(资源文件Resources)、ClassLoader等加载一个Apk插件。

2、启动插件Activity:提前在主APP中占有坑位,通过替换Intent中的targetActivity,打开占坑声明的A$Activity,然后绕过AndroidManifest检测,再拦截newActivity方法中恢复targetActivity。

3、启动插件Service:通过启动一个代理Service统一管理,拦截所有Service方法,修改为startService到代理Service,在代理Service的onStartCommond统一管理,创建/停止目标service 。

三、VirtualApk源码解析

1、初始化

初始化过程中,VirtualApk 创建了PluginManager ,并且hook住了Instrumentation和SystemService,如下图所示。

初始化

下图所示,VrutalApk通过Instrumentation创建了一个VAInstrumentation对象,VAInstrumentation是一个继承Instrumentation的类。

VAInstrumentation反射插入到ActivityThread中,这样系统接下来关于Instrumentation的操作,就会回到VAInstrumentation中,被VrtualApk接管。

Hook Instrumentation 图1

这里是如何拿到Instrumentation的?

因为在ActivityThread内部有一个sCurrentActivityThread静态变量。如下图,通过反射sCurrentActivityThread我们可以获取当前ActivityThread,而ActivityThread的公开方法getInstrumentation即可拿到Instrumentation对象。

Hook Instrumentation 图2

另外,上方图1还有设置HandlerCallback的流程,其实就是拦截了ActivityThread中的mH这个Handler的Callback,从【 一、 Activity/Service启动流程】流程图可以看到,mH的handleMessage处理很多Activity的启动状态。

如下图, 是Hook Service的流程,如图中注释所示,通过ActivityManagerNativegetDefault,拿到AndroidManagerService(详见启动流程图),而VirtualApk通过自定义ActivityManagerProxy,重新生成了一个IActivityManager,然后注入回AndroidManagerService中,这样接管了系统启动、管理service等操作。

Hook Service 图1

2、加载插件Apk

加载插件APK是通过PluginManagerloadPlugin方法,如下图所示,此处就是将apk拆开,解析,读取,加载,组装为LoadedPlugin并保存,以方便后面管理与使用。

加载Apk插件

此处对Apk进行了复杂的解析、加载、合并等操作,大致流程如下:

  • 解析apk的相关包信息、判断是否加载过apk。
  • 创建一些插件工具类。
  • 通过AssetManager创建Resource对象,平台用AssetManager创建出Resource,判断是否和宿主Apk合并资源。
  • ClassLoader 根据插件APK路径创建loader,判断是否合并loader中的dex,合并nativeLIbraryDirectories。
  • 将so复制到mNativeLibDir路径。
  • 保存Instrumentation、Activities、Services、Providers , 注册Broadcast等。
  • 创建出Apk的Application,并call Application onCreate。

3、启动插件Activity

那么是时候启动插件Activity了。通过startActivity便可以启动。从上面的流程我们知道启动是从Instrumentation.execStartActivity();开始的,而系统的Instrumentation已经被VAInstrumentation替换,其中VAInstrumentation重写了几个关键方法:

  • execStartActivity:入口。
  • newActivity:创建。
  • callActivityOnCreate:通知。
  • handleMessage:处理。

没错,如下图,在启动Activity的入口处,VirtualApk拦截了请求,然后根据Intent的参数,去匹配plugin中的Activity坑位,之后替换Intent中的Activity,以此来达到欺骗系统的效果。

但是,因为这个Activity的对象了实际上并不存在,最终我们需要启动的是实现了的targetActivity,所以需要拦截Instrumentation的第二个方法newActivity,因为在ActivityTreadperformLaunchActivity中,会调用InstrumentationnewActivity

启动Activity

newActivity中,如下图,类没有找到时(坑位类肯定找不到啦),那么就去获取原本保存在Intent的目标Activity,然后调用创建VAInstrumentation时保存的Instrumentation(mBase)去创建Activity。

创建Activity

Activity虽然创建好了,但是它对应的资源和context都还不对,所以我们需要在Activity的OnCreate之前完成好Resource的注入。前面加载Apk时,这些资源都保存在Plugin中。所以我们拦截callActivityOnCreate方法,如下图,将Activity的Context、Application、Reource,都替换成Plugin中对应的对象。一个Activity就这样绕过AndroidManifest启动起来了。

屏幕快照 2017-07-15 上午12.36.21.png

4、启动插件Service

startService启动Service时,还记得上面我们通过ActivityManagerProxy生成IActivityManager吗?它主要拦截了Service相关启动和停止等方法,然后将其都转化为对应的startService方法,指向代理Service。因为startService方法的特性,他们最终都会在代理Service的onStartCommand中被统一处理。

如下图,是ActivityManagerProxy,其中invoke拦截了所有相关的服务请求,并做了转化处理,下面以startService为例。

ActivityManagerProxy

startService这里,主要便是提取原本目标service信息,然后转化为代理Service,发送到代理Service,下方图片启动流程转化流程

启动流程
转化流程

如下图,在代理service中,根据请求类型,代理service会通过classLoader加载来创建service,并操作其attach、onCreate、onStartCommand等,让service工作起来。

启动真正的目标service

自此Activity和Service都成功启动了,是不是对插件化有了不一样的了解?

5、AndroidStub

容许这里插入这一块,安利下Virtual中的AndroidStub模块,如下图

AndroidStub

因为都用反射很浪费性能,所以有了AndroidStub,它是用来欺骗编译器的。正常情况下你想操作ActivityThread就会出现如下图情况,因为它是一个@hide类,这时候除了反射得到ActivityThread,你还需要再反射需要执着方法才能执行,这在一定程度会损耗一些性能。

但是如下图,VirtualApk通过AndroidStub,模拟源码创建了如ActivityThread类,这里你就可以如图正常使用ActivityThread了,而AndroidStub中的ActivityThread,其实只是定义了和原码中一摸一样的方法,并没有其他实现。

因为CoreLibrary依赖AndroidStub使用的是provided ,因为provided依赖是不打包依赖包,而是运行时提供,所以成功欺骗了编辑器,以此提高了性能。很神奇吧?

ActivityThread
CoreLibrary依赖AndroidStub

终于结束了,如果你看到了这里,相信你是一个很有耐心的同志!当然插件化还是其他实现方式,如Replugin,只Hook住了ClassLoader,流程更加复杂,如有什么建议和疑问,欢迎留言讨论。

VirtualApk:https://github.com/didi/VirtualAPK

个人github:https://github.com/CarGuo

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

推荐阅读更多精彩内容