本文涉及的PPT地址:
http://slides . vimerzhao . top/006-Android-plugin-tech-share . html
一个
前言
今天想和大家分享的是Android的插件技术,这是一门比较复杂的知识,在Android中历史悠久,内容也比较复杂,今天一个小时是完成不了的,所以今天也选择性的分享一些我认为比较重要的内容。
今天,我们将大概讨论五个部分,即:
插件化的前置知识插件化的发展历史Activity的framework源码与插件化实现各大插件化方案的借鉴与革新对于插件化的几点思考2
插件的预先知识
这部分每一个基础知识都可以单独拿出来讲一个小时,我只简单提一下,不做深入讲解。通常大家都有所了解,足以应付以下内容。
什么是插件?
依靠安装,动态加载Native函数。
为什么要插(为什么)?
动态发布,包大小减少,逻辑解耦,编译速度提高?
方向问题:这些好处背后的挑战是什么?
客户稳定性,开发经验。
插件什么时候(什么时候)出现?
2012年。
插件流行在哪里?
国内,手淘,携程,滴滴等大型应用
谁在做外挂(谁)?
早期的个人开发者,后期的大公司。
插件怎么做(怎么做)?
一般来说,插件可以分为Hook学派和Static Agent学派,Hook学派会涉及到更多的基础知识。
Apk包装流程(Gradle,aapt)
有些方案会在编译器中修改插件的字节码,有些方案会在编译器中重新排列资源,所以这部分要理解。
Apk安装过程(PMS、清单解析、dexopt)
DroidPlugin和VirutalApp之类的方案模拟了在系统中安装apk的过程,所以这部分需要理解。
四个组成部分的管理(AMS,主要是启动过程和生命周期)
插件的核心是四个组件的插件,所以这部分一定要理解。
资源加载机制与图书馆
同上。
仪表板组合仪表/活页夹
Hook发送插件来处理system_process流程,而比较成熟的方案会把插件当做一个单独的流程,所以这部分也要理解。
类别加载机制
加载插件的重要一步是加载插件类,这是任何方案都无法避免的。
反思与动态主体
Hook发送插件必须使用的Java技术。
三
插件的发展历史
在下表中,我梳理了插件技术的各种开源解决方案的出现时间,主要是参考了之前的文章和自己整理的相关新闻和代码提交记录(这部分有点研究史料的感觉)。解决方案的出现不是一蹴而就的,发布时间和开发开始时间之间必须有一个跨度。过于注重时间的准确性意义不大,但是输出时间语境也可以帮助我们获得一些信息。
另外之前网上也有一些版本,但是比较老,有一些疏漏。我相信最完美和最新的表格是我编制的。
时间方案主体2012年7月mmin18/AndroidDynamicLoader[1]大众点评/屠毅敏2013年23Code-2014年初alibaba/atlas[2]阿里/伯奎2014年7月houkx/android-pluginmgr[3]houkx2014年底singwhatiwanna/dynamic-load-apk[4]百度/任玉刚2014年11月Direct-Load-apk[5]罗迪2015年4月HiWong/OpenAtlas[6]BunnyBlue2015年8月DroidPluginTeam/DroidPlugin[7]360/张勇2015年底CtripMobile/DynamicAPK[8]-2015年底wequick/Small[9]林广亮2016年7月asLody/VirtualApp[10]罗迪2017年6月didi/VirtualAPK[11]-2017年7月Qihoo360/RePlugin[12]-2018年10月ManbangGroup/Phantom[13]-2019年6月Tencent/Shadow[14]-粗略看一下这张表,我个人有以下发现,欢迎补充:
早期野蛮成长,主要以个人名义开源;后来成熟了,主要是以公司的名义开源。前期注重方案的实现,后期注重方案的可用性(比如RePlugin口号中提到的“灵活、稳定、易用”)。
16年是一个分水岭,后续所有方案都是优化的(具体优化后面会讲到),没有突破性的创新。16年前的描述侧重于框架的特性,并没有统一使用Plugin Framework这个词。16年后,插件这个词深入人心,专注于描述方案的完备性、稳定性、灵活性等实际需求。
估计大家下载的时候都有点晕。我们所知道的插件原理其实只是几个轴。怎么才能消化这么多已经出现的方案?
所以我才把内容安排定下来。我们先来看看插件的核心能力:如何在框架层处理Activity,然后逐一分析每个方案的借鉴和创新,而不是孤立地看每个方案。我相信这样会容易很多。
四
框架源代码和活动的插件实现
为什么要从原则到实施方案重点分析活动?因为它是最重要、最常用、最复杂的组件,也是插件解决方案的核心,最简单的app只能是Activity。
开始进程
活动的激活过程是我们面试必问的问题,相信大家都清楚。这里我做了一个单步调试视频,帮助大家回忆。综上所述,Activity的激活过程可以用下图来概括:
图片来自:https://juejin.im/post/5c7f3471f265da2db5425c4b
活动管理
关于Activity,我觉得还有一点可以单独讨论,就是它的栈管理机制。相对完整的插件解决方案无法避免这个问题。例如,在DroidPlugin中有这样的代码:
......
& lt活动
Android:name = " . stub . activitysub $ P00 $ standard 00 "
Android:allowTaskReparenting = " true "
Android:excludefromrensets = " true "
android:exported= "false "
Android:hardwareAccelerated = " true "
Android:label = " @ string/stub _ name _ activity "
安卓:launchMode= "标准"
安卓:noHistory= "true "
安卓:主题= "@style/DroidPluginTheme " >;
& lt意图过滤器>。
& ltaction Android:name = " Android . intent . action . MAIN "/& gt;
& ltcategoryandroid:name = " com . mor goo . droid plugin . category . PROXY _ STUB "/& gt;
& lt/ intent-filter>。
& lt元数据
Android:name = " com . mor goo . droid plugin . ACTIVITY _ STUB _ INDEX "
android:value= "0"/>
& lt/ activity>。
& lt活动
Android:name = " . stub . activitysub $ P00 $ SingleInstance 00 "
Android:allowTaskReparenting = " true "
Android:excludefromrensets = " true "
android:exported= "false "
Android:hardwareAccelerated = " true "
Android:label = " @ string/stub _ name _ activity "
Android:launch mode = " SingleInstance "
安卓:主题= "@style/DroidPluginTheme " >;
& lt意图过滤器>。
& ltaction Android:name = " Android . intent . action . MAIN "/& gt;
& ltcategoryandroid:name = " com . mor goo . droid plugin . category . PROXY _ STUB "/& gt;
& lt/ intent-filter>。
& lt元数据
Android:name = " com . mor goo . droid plugin . ACTIVITY _ STUB _ INDEX "
android:value= "0"/>
& lt/ activity>。
......
VirtualAPK和RePlugin里也有类似的。
可见每个计划都有兼容各种推出的野心~
让我们看看框架层是如何实现的。借用gityuan和盛达的两幅图,我们可以清楚地看到它的设计:
具体源代码在StartActivityChecked [15]中,非常复杂,这里就不多做分析了,宏观的指示大家都可以知道。
看了一些源代码,发现VirtualApp的实现是最完善的。这里不想具体分析。简单来说,它也像AMS一样记录活动栈的变化(相当于在自己的进程中拍摄AMS数据的快照,不知道能不能直接获取AMS数据,应该没有这样的接口~)。根据launchMode/Flag,触发onNewIntent等回调,可以说是所有方案中最高的,其他方案只能支持相对简单的launchMode。
五
主要插件方案的借鉴与创新
在开始以下工作之前,有必要区分本质问题和衍生问题:
本质问题:如何启动插件Activity,加载资源等,实现生命周期回调等。
衍生问题:插件管理、安装、加载、页面路由等。
mmin18/AndroidDynamicLoader
插件通过分片的方式实现,资源插件通过反射addAssetPath实现
staticymyresources GetResource(MyClassLoader MCL){
......
尝试{
asset manager am =(asset manager)asset manager。class.newInstance
am . getclass . getmethod(" addAssetPath ",String。类)。invoke(am,path . GetaBSolutePath);
resources super RES = my application . instance . GetReSources;
Resources res = newResources(am,superRes.getDisplayMetrics,SuperRes . GetConfiguration);
......
}
阿里巴巴/地图集
Hook for框架开始出现,对后面的方案影响深远。
下图是博魁当时的共享开源版本是17年才公布的,共享时的设计已经大不相同了
houkx/android-pluginmgr
这个方案很有趣,但很少被提及,首先Hook掉ClassLoader,通过DexMaker在客户端修改插件类(变成一个已经在Manifest注册过的类)但是客户端做这个事情效率很低,事实上后来的RePlugin、Shadow又走回了这条路,只是放在了编译期singwhatwanna/动载荷-apk
用静态代理实现,第一个完成度比较高的插件化方案 但是包括that关键字在内的设计,对开发侵入性较大直接装载装甲运兵车
lody早期的项目,针对上两种方案的优化(开发体验) 试图通过把StubActivity的数据给插件Activity,让插件Activity能像正常Activit一样,参见下面的核心代码:publicationdispatchproxitoplugin {
尝试{
//开始将插件伪装成实体活动
pluginRef。set( "mBase ",代理);
pluginRef。set( "mDecor ",proxyRef。get(" MDecor "));
pluginRef。set( "mTitleColor ",proxyRef。get(" MTitleColor ");
......
仪器仪表= proxyRef。get(" mInstrumentation ");
pluginRef。set( "mInstrumentation ",new pluginsrument(instrumentation));
......
pluginRef。设置(“多线程”,proxyRef。get(" MuiThread ");
pluginRef。set( "mHandler ",proxyRef。get(" mHandler "));
pluginRef。set( "mInstanceTracker ",proxyRef。get(" mInstanceTracker ");
......
} catch(ReflectException e) {
e.printStackTrace
}
}
HiWong/OpenAtlas
作者通过逆向手淘代码,实现的“山寨版”atlas,由于一些原因删库了,后来变成了ACDD,算是OpenAtlas的优化版 公开了一个hook编译工具aapt来解决资源冲突的实现,对后面的方案影响深远 比较遗憾的是插件必须注册在Manifest中,这和插件化的精神有所背离,所以对外宣称是容器化,即容器内的Bundle可以升级更新•绕过Activity启动检查的方案初具雏形,开始Hook一些我们耳熟能详的方法,参考核心代码:/ ****
*
*公共活动结果执行开始活动(上下文谁,IBinder
*上下文线程、内部令牌、活动目标、意图意图、内部
* RequestCode);
* ***/
公共工具挂钩(工具,上下文上下文)
this.context = context
this.mBase =仪器仪表;
尝试{
minstrumentioninvoke = Hack . into(" Android . app . instrumentation ");
如果(构建。VERSION.SDK_INT >构建。版本_代码。冰淇淋_三明治_MR1) {
mexecstatactivity = minstrumentioninvoke . method(" ExecstartActivity ",Context。类,
IBinder。类,IBinder。班级,活动。类,意图。类,int。类,捆绑。类);
} else{
mexecstatactivity = minstrumentioninvoke . method(" ExecstartActivity ",Context。类,
IBinder。类,IBinder。班级,活动。类,意图。类,int。类);
}
......
} catch(HackassertionException e){
e.printStackTrace
} catch(IllegalargumentException e){
e.printStackTrace
}
}
来源:https://Alibaba . github . io/atlas/principal-intro/Runtime _ principal . html
DROIdplugintam/DROIdplugin
第一个Hook流派的完成度非常高的版本,几乎Hook了AMS、PMS等相关服务可以真正地加载一个完全独立的第三方apk文件从这里开始,插件化具备了成为沙盒/虚拟机的可能,而不是宿主的能力补充在呼叫AMS之前:
protected booleandlerplaceintentforstartactivityabhigh(Object[]args)throws remote exception {
IntintentoFargindex = FindFirstentinDexiargs(args);
if(args!= null & amp& ampargs.length >。1 & amp& ampintentOfArgIndex & gt。= 0) {
意图意图=(意图)参数[IntentToFargindex];
//XXX String calling package =(String)args[1];
if(!pluginpatchmanager . GetInstance . CanstartPluginactive(intent)){
pluginpatchmanager . GetInstance . StartPluginactive(意图);
returnfalse
}
activity info activity info = resolveaactivity(intent);
if(activityInfo!= null & amp& ampisPackagePlugin(activityinfo . package name)){
component name component = SelectProxyActivity(意图);
if(组件!= null) {
意图新意图=新意图;
尝试{
class loader plugin lass loader = plugin processmanager . getpluginlassloader(component . getpackagename);
setIntentClassLoader(newIntent,plugin lass loader);
}捕获(例外e) {
Log.w(TAG,“将类加载器设置为新意图失败”,e);
}
newIntent.setComponent(组件);
newIntent.putExtra(Env。EXTRA_TARGET_INTENT,INTENT);
new ENDENT . SetFlags(ENDENT . GetFlags);
字符串callingPackage =(字符串)参数[1];
if(textutils . equals(mhostcontext . getpackagename,callingPackage)) {
新意图。添加标志(意图。FLAG _ ACTIVITY _ NEW _ TASK);
args[IntentToFargindex]= NewIntent;
args[1]= mhostcontext . GetPackageName;
} else{
Log.w(TAG,“startActivity,replace selectProxyActivity fail”);
}
}
}
returntrue
}
AMS回头了
@覆盖
publicboolean handleMessage(消息消息){
long b = System.currentTimeMillis
尝试{
if(!mEnable) {
returnfalse
}
if(PlugInProcessManager . IsprugInProcess(MHostContext)){
if(!plugin manager . GetInstance . Isconnected){
Log.i(TAG,“handleMessage not is connected post and wait,msg=%s”,msg);
MoldHandle . SendMessageDelated(message . get(msg),5);
returntrue
}
}
if(msg.what == LAUNCH_ACTIVITY) {
returnHandleLaunchActivity(msg);
}
if(mCallback!= null) {
returnmcallback . handlemessage(msg);
} else{
returnfalse
}
}最后{
Log.i(TAG," handleMessage(%s,%s) cost %s ms ",msg.what,codeToString(msg.what),(System . CurrentiMemillis-b));
}
}
CtripMobile/DynamicAPK
主要借鉴了OpenAtlasaapt部分有所创新我们很快/很小
提供了一种通过脚本的方式解决资源冲突 Activity依然是通过Hook的方式,没有特别大的改动asLody/VirtualApp
是对DroidPlugin的大升级,完全实现了虚拟框架,体现在源代码命名和结构上
didi/VirtualAPK
综合考虑了大量Hook导致的不稳定问题和静态代理的开发侵入性,又开始回归到仅Hook mInstrumentation 的设计,主要是execStartActivity 和 newActivity Service主要用两个真实的Service做转调 ContentProvider通过一个代理Provider进行操作的分发奇虎360/复制器
更进一步,One Hook,结合编译期的修改,把插件变成类似静态代理的类(像是把houkx/android-pluginmgr的策略换了个地方),同时Hook了ClassLoader使之加载插件的类: 记录下目标页 ActivityA,替换成已自动注册在 AndroidManifest 中的坑位 ActivityNS。 在 ClassLoader 中拦截ActivityNS的创建,创建出ActivityA返回。 返回的ActivityA占用着 ActivityNS 这个坑位, 坑位由 Gradle 编译时自动生成在AndroidManifest中。主要的焦点是稳定性和兼容性,文档也相对完善
问:你和360年前发行的DroidPlugin的主要区别是什么?
答:这个问题问得好。很多人都有这个疑问——“为什么要为360开发两个不同的插件框架”?其实说到底,最根本的区别是——目标不同:DroidPlugin主要解决的是独立功能组装在一起,不需要任何交互就可以快速释放的问题。
目前市场上的一些双开放应用和DroidPlugin有一些共同点。当然,要实现完整的双开,还是需要大量的修改,比如Native Hook等等。RePlugin解决了每个功能模块都可以独立升级的问题,需要与主机和插件交互耦合。
另外,从技术层面来说,核心区别是一个:Hook点数。DroidPlugin可以使APK“直接在主程序中运行”,而无需任何额外的修改。但是,大量的API(包括AMS、PackageManager等。)都是Hook需要的,在改编上需要做很多工作。
RePlugin只挂接了ClassLoader,所以极其稳定,也支持大部分单一产品的特性,但是需要插件做一点修改。幸运的是,作为一个插件开发者,并不需要太过关注,因为通过“动态编译方案”,开发者可以达到“在主程序中运行,无需开发者修改Java代码”的效果。可以肯定的是,DroidPlugin也是业内公认的优秀的插件式免安装解决方案。相信随着时间的推移,RePlugin和DroidPlugin会在各自的领域(综合插件&:应用免费安装)创造属于自己的世界。被申请人:张
出发地:https://github.com/Qihoo360/RePlugin/wiki/FAQ
来自:https://github。com/奇虎360/replugin/pull/840看影的时候发现作者提出了一个改进,但是没有被接受,这个没完没了的循环也没有说明原因~个人感觉这个改变挺好的。
曼邦集团/幻影
比360的RePlugin更进一步,宣称零Hook相关资料较少,暂未深入研究。除了一篇宣传性质的README,没有什么实质资料分析其代码后发现还是拓展了RePlugin编译时替换,静态代理
腾讯/影子
同样是零Hook,同时宣称支持插件框架本身的动态化(这个其实也不难) 更像是RePlugin的另一个种实现,没有本质的不同六
关于插件的几点思考
之前我们快速阅读了各个插件的主要特性,隐约意识到了。好像是《三国演义》里的那句“长合必分,长合必成”。很多方案看似新颖,但都是在前人的基础上,根据自己的业务做一些创新。在里程碑上,仁者见仁,智者见智,但他们绝不能被花哨的项目README.md所迷惑
从上下文来看,从静态agent和Hook框架的蓬勃发展,到最近几年,两种方案取长补短。总的来说,他们似乎在朝着客户端轻Hook+编译时入侵修改的方向前进。我来拆分几个维度,详细讨论一下。
开发期或运营期
如果您想像正常活动一样启动我们的插件活动,不可避免地要进行一些侵入性的修改。在我看来,主要分为三个方向:
以dynamic-load-apk为代表的,开发期侵入 以DroidPlugin为代表的,运行期侵入 以RePlugin、shadow为代表的,编译期侵入编译时入侵是开发时入侵的自动化版本,既避免了不稳定的Hook,又保证了开发人员的体验;运行时入侵一直是追求稳定性的挑战。
独立还是合并
类加载器和资源都存在此问题。有些方案会选择通用的类加载器或资源,这样会解决插件与主机、插件与插件之间的潜在冲突。aapt神奇的修正就是处理这种情况。
尤其是Resource,独立可以避免冲突,共享可以减小插件大小。
我觉得有必要区分一下场景。DroidPlugin的每个插件都非常独立,适合单独加载;如果插件是主机功能的补充,可以共享。Shawdow甚至通过白名单控制是隔离还是共享ClassLoader。
动态或插件
插件和动态有共同点,可以减小主机大小,动态释放等。目前插件的优势是在涉及四个组件时更适合,而动态则适合纯视图级的修改。
1.《virtualapp 插件化技术的演进之路》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《virtualapp 插件化技术的演进之路》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/tiyu/1107979.html