-
Notifications
You must be signed in to change notification settings - Fork 498
原理简介
limpoxe edited this page May 9, 2017
·
4 revisions
通过构造插件apk的Dexclassloader来加载插件apk中的类。
DexClassLoader的parent设置为宿主程序的classloader,即可将主程序和插件程序的class贯通。
若是独立插件,将parent设置为宿主程序的classloader的parent,可隔离宿主class和插件class,此时宿主和插件可包含同名的class。
直接构造插件apk的AssetManager和Resouce对象即可,需要注意的是,
通过addAssetsPath方法添加资源的时候,需要同时添加插件程序的资源文件和宿主程序的资源,
以及其依赖的资源。这样可以将Resource合并到一个Context里面去,解决资源访问时需要切换上下文的问题。
完成上述第二点以后,宿主程序资源id和插件程序id可能有重复而参数冲突。
我们知道,资源id是在编译时生成的,其生成的规则是0xPPTTNNNN
PP段,是用来标记apk的,默认情况下系统资源PP是01f,应用程序的PP是07f
TT段,是用来标记资源类型的,比如图标、布局等,相同的类型TT值相同,但是同一个TT值
不代表同一种资源,例如这次编译的时候可能使用03作为layout的TT,那下次编译的时候可能
会使用06作为TT的值,具体使用那个值,实际上和当前APP使用的资源类型的个数是相关联的。
NNNN则是某种资源类型的资源id,默认从1开始,依次累加。
那么我们要解决资源id问题,就可从TT的值开始入手,只要将每次编译时的TT值固定,即可是资
源id达到分组的效果,从而避免重复。例如将宿主程序的layout资源的TT固定为33,将插件程序
资源的layout的TT值固定为03(也可不对插件程序的资源id做任何处理,使其使用编译出来的原生的值), 即可解决资源id重复的问题了。
固定资源id的TT值的办法也非常简单,提供一份public.xml,在public.xml中指定什么资源类型以
什么TT值开头即可。具体public.xml如何编写,可参考FairyPlugin/public.xml,是用来固定宿主程序资源id范围的。
还有一个方法是通过定制过的aapt在编译插件时指定id范围来解决冲突(For-gradle-with-aapt分支采用的方案)
此方案需要替换sdk原生的aapt,且要区分多平台,buildTools版本更新后需同步升级aapt。
定制的aapt由 openAtlasExtention@github 项目提供,目前的版本是基于22.0.1,将项目中的BuildTools替换
到本地Android Sdk中相应版本的BuildTools中,并指定gradle的buildTools version为对应版本即可。
构造一个Context对象即可,具体的Context实现请参考PluginContextTheme.java
关键是要重写几个获取资源、主题的方法,以及重写getClassLoader方法,再从构造粗来的context中获取LayoutInfalter
要做到这一点,主要有几点:
1、上诉第4步骤,
2、在classloader树中插入自己的Classloader,在loadclass时进行映射
3、替换ActivityThread的的Instrumentation对象和Handle CallBack对象,用来拦截组件的创建过程。
4、利用反射修改成员变量,注入Context。利用反射调用隐藏方法。
5、插件中Activity等不在宿主manifest中注册即拥有完整生命周期的方法。
由于Activity等是系统组件,必须在manifest中注册才能被系统唤起并拥有完整生命周期。
通过反射代理方式实现的实际是伪生命周期,并非完整生命周期。要实现插件组件免注册有2个方法。
前提:宿主中预注册几个组件。预注册的组件可实际存在也可不存在。
a、替换classloader。适用于所有组件。
App安装时,系统会扫描app的Manifest并缓存到一个xml中,activity启动时,系统会现在查找缓存的xml,
如果查到了,再通过classLoad去load这个class,并构造一个activity实例。那么我们只需要将classload
加载这个class的时候做一个简单的映射,让系统以为加载的是A class,而实际上加载的是B class,达到挂羊头买狗肉的效果,
即可将预注册的A组件替换为未注册的插件中的B组件,从而实现插件中的组件
完全被系统接管,而拥有完整生命周期。其他组件同理。
b、替换Instrumention。
这种方式仅适用于Activity。通过修改Instrumentation进行拦截,可以利用Intent传递参数。
如果是Receiver和Service,利用Handler Callback进行拦截,再配合Classloader在loadclass时进行映射
要实现这一点,同样是基于上述第4点,构造出插件的Context后,通过attachBaseContext的方式,
替换代理Activiyt的context即可。
另外还需要在获得插件Activity对象后,通过反射给Activity的attach()方法中attach的成员变量赋值。
更新:activity代理方式已放弃,不再支持,要了解实现可以查看历史版本
重要实现原理仍然基于上述第2、3点。
要实现插件Activity的LaunchMode,需要在宿主程序中预埋若干个(standard只需1个)相应launchMode的Activity(预注
册的组件可实际存在也可不存在),在运行时进行动态映射选择。core工程的manifest中配置
Service的启动模式类似于Activity的singleInstance,因此为了支持插件多service,采用了和上述第12像类似的做法。