Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Android Plugin Framework 插件开发框架及示例程序,原理介绍等

License

Notifications You must be signed in to change notification settings

v2sky/Android-Plugin-Framework

Open more actions menu
 
 

Repository files navigation

Android-Plugin-Framework

此项目是Android插件开发框架完整源码及示例。用来通过动态加载的方式在宿主程序中运行插件APK。

已支持的功能:

1、插件apk无需安装,由宿主程序动态加载运行。

2、支持fragment、activity、service、receiver、contentprovider、so、application、notification。

3、支持插件自定义控件、宿主自定控件。

4、开发插件apk和开发普通apk时代码编写方式无区别。对插件apk和宿主程序来说,插件框架完全透明,开发插件apk时无约定、无规范约束。

5、插件中的组件拥有真正生命周期,完全交由系统管理、非反射代理

6、支持插件引用宿主程序的依赖库、插件资源、宿主资源、以及插件依赖插件。

7、支持插件使用宿主主题、系统主题、插件自身主题以及style、轻松支持皮肤切换

8、支持非独立插件和独立插件(非独立插件指自己编译的需要依赖宿主中的公共类和资源的插件,不可独立安装运行。独立插件又分为两种: 一种是自己编译的不需要依赖宿主中的类和资源的插件,可独立安装运行;一种是第三方发布的apk,如从应用市场下载的apk,可独立安装 运行,这种只做了简单支持。)

9、支持插件Activity的4个LaunchMode

10、支持插件资源文件中直接通过@xxx方式引用共享依赖库中的资源

11、支持插件发送notification时在RemoteViews携带插件自定义的布局资源(只支持5.x及以上)

12、支持插件热更新:即在插件模块已经被唤起的情况先安装新版本插件,无需重启进程

13、支持在插件中注册全局服务:即在某个插件中注册一个服务,在其他插件中通过LocalServiceManager或者直接使用getSystemService获取这个服务

暂不支持的功能:

1、插件Activity切换动画不支持使用插件自己的资源。

2、不支持插件申请权限,权限必须预埋到宿主中。

3、不支持第三方app试图唤起插件中的组件时直接使用插件app的Intent。即插件app不能认为自己是一个正常安装的app。 第三方app要唤起插件中的静态组件时必须由宿主程序进行桥接,方法请参看wxsdklibrary工程的用法

4、不支持android.app.NativeActivity

开发注意事项

1、非独立插件开发需要解决插件资源id和宿主资源id重复产生的冲突问题。

    解决冲突的方式有如下两种:

    a)通过在宿主中添加一个public.xml文件来解决资源id冲突(master分支采用的方案)

    b)通过定制过的aapt在编译插件时指定id范围来解决冲突(For-gradle-with-aapt分支采用的方案)
       此方案需要替换sdk原生的aapt,且要区分多平台,buildTools版本更新后需同步升级aapt。
       定制的aapt由 openAtlasExtention@github 项目提供,目前的版本是基于22.0.1,将项目中的BuildTools替换
       到本地Android Sdk中相应版本的BuildTools中,
       并指定gradle的buildTools version为对应版本即可。

2、非独立插件中的class不能同时存在于宿主和插件程序中,因此其引用的公共库仅参与编译,不参与打包,参看demo中的gradle脚本。

3、若插件中包含so,则需要在宿主中添加一个占位的so文件。占位so可随意创建,随意命名,关键在于so所在的cpuarch目录要正确。
   在pluginMain工程。pluginMain工程中的libstub.so其实只是一个txt文件。

   需要占位so的原因是,宿主在安装时,系统会扫描宿主中的so的(根据文件夹判断)类型,决定宿主在哪种cpu模式下运行、并保持到系统设置里面。
   (系统源码可查看com.android.server.pm.PackageManagerService.setBundledAppAbisAndRoots()方法)
   例如32、64、还是x86等等。如果宿主中不包含so,系统默认会选择一个最适合当前设备的模式。
   那么问题来了,如果系统默认选择的模式,和将来下载安装的插件中的so支持的模式不匹配,则会出现so异常。

   因此需要提前通知系统,宿主需要在哪些cpu模式下运行。提前通知的方式即内置占位so。

4、插件依赖插件时,被插件依赖的插件暂不支持包含资源(技术上可行,但为了降低复杂度不做支持)

5、在插件中调用getPackageName方法返回的是宿主的包名,不是插件包名。

   这一点非常重要。在插件中需要使用此api的地方应当直接换成插件包名
   这一点也是为何在上述#已支持的功能第8条中指出只对第三方应用做了简单支持的原因。
   若需要支持任意第三方应用,则“插件中调用getPackageName方法返回的是插件包名”是必要条件。
   而本项目中不返回插件包名的原因,在代码中有注释说明,不赘述。
   
6、插件默认是在插件进程中运行,如需切到宿主进程,仅需将core工程的Manifest中配置的所有组件去都去掉掉process属性即可。
   PluginMain工程下有几个插件进程的demo也需要去掉process属性

7、将配置插件为非独立插件、为插件配置依赖插件的方法。
   插件框架识别一个插件是否为独立插件,是根据插件的Manifest文件中的android:sharedUserId属性。
   将android:sharedUserId属性设置为宿主的packageName,则表示为非独立插件。不配置或配置为其他值表示为独立插件

   插件如果依赖其他基础插件,需要在插件Manifest中配置如下信息
   <uses-library android:name="XX.XX.XX" android:required="true" />
   name是被依赖的插件的packageName

8、框架中对非独立插件做了签名校验。如果宿主是release模式,要求插件的签名和宿主的签名一致才允许安装。

目录结构说明:

1、PluginCore工程是插件库核心工程,用于提供对插件功能的支持。

2、PluginMain是用来测试的宿主程序Demo工程。

3、PluginShareLib是用来测试非独立插件的公共依赖库Demo工程。

4、PluginTest是用来测试的非独立插件Demo工程。

5、PluginHelloWorld是用来测试的独立插件Demo工程。

6、PluginBase是用来测试的被PluginTest插件依赖的插件Demo工程(此插件被PluginTest、wxsdklibrary两个插件依赖),此插件不包含资源。

7、wxsdklibrary是用来测试的非独立插件Demo工程。

demo安装说明:

1、宿主程序demo工程的assets目录下已包含了编译好的独立插件demo apk和非独立插件demo apk。

2、宿主程序demo工程根目录下已包含一个已经编译好的宿主demo,可直接安装运行。

3、宿主程序demo工程源码可直接编译安装运行。

4、插件demo工程:

 1、若使用master分支:
    直接编译即可,无特别要求。

 2、若使用For-gradle-with-aapt分支:
    将openAtlasExtention@github项目提供的BuildTools替换自己的Sdk中相应版本的BuildTools。剩下的步骤照常即可。

 3、若使用For-eclipse-ide分支:
    需要使用ant编译,关注PluginTest工程的ant.properties文件和project.properties文件以及custom_rules.xml,若编译失败,请升级androidSDK。

4、编译方法

   a)如果是命令行中:
   cd  Android-Plugin-Framework
   ./gradlew clean
   ./gradlew build

   b)如果是studio中:
   打开studio右侧gradle面板区,点clean、点build。不要使用菜单栏的菜单编译。

   重要:
        1、由于编译脚本依赖build.doLast, 使用其他编译方法(如菜单编译)可能不会触发build.doLast导致编译失败
        2、必须先编译宿主,再编译非独立插件。(这也是使用菜单栏编译会失败的原因之一)
           原因很简单,既然是非独立插件,肯定是需要引用宿主的类和资源的。所以编译非独立插件时会用到编译宿主时的输出物
   
        所以如果使用其他编译方法,请务必仔细阅读build.gradle,了解编译过程和依赖关系后可以自行调整编译脚本,否则可能会失败。

   待插件编译完成后,插件的编译脚本会自动将插件demo的apk复制到PlugiMain/assets目录下(复制脚本参看插件工程的build.gradle),然后重新打包安装PluginMain。
   或者也可将插件apk复制到sdcard,然后在宿主程序中调用PluginLoader.installPlugin("插件apk绝对路径")进行安装。

实现原理简介:

1、插件apk的class

 通过构造插件apk的Dexclassloader来加载插件apk中的类。
 DexClassLoader的parent设置为宿主程序的classloader,即可将主程序和插件程序的class贯通。
 若是独立插件,将parent设置为宿主程序的classloader的parent,可隔离宿主class和插件class,此时宿主和插件可包含同名的class。

2、插件apk的Resource

 直接构造插件apk的AssetManager和Resouce对象即可,需要注意的是,
 通过addAssetsPath方法添加资源的时候,需要同时添加插件程序的资源文件和宿主程序的资源,
 以及其依赖的资源。这样可以将Resource合并到一个Context里面去,解决资源访问时需要切换上下文的问题。

3、插件apk中的资源id冲突

完成上述第二点以后,宿主程序资源id和插件程序id可能有重复而参数冲突。
我们知道,资源id是在编译时生成的,其生成的规则是0xPPTTNNNN
PP段,是用来标记apk的,默认情况下系统资源PP是01,应用程序的PP是07
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如何编写,可参考PluginMain/public.xml,是用来固定宿主程序资源id范围的。


还有一个方法是通过定制过的aapt在编译时指定插件的PP段的值来实现分组:
    参考openAtlasExtention@github项目提供的重写过的aapt指定PP段来实现id分组,代码见For-gradle-with-aapt分支

4、插件apk的Context和LayoutInfalter

构造一个Context对象即可,具体的Context实现请参考PluginCore/src/com/plugin/core/PluginContextTheme.java
关键是要重写几个获取资源、主题的方法,以及重写getClassLoader方法,再从构造粗来的context中获取LayoutInfalter

6、插件代码无约定无规范约束。

要做到这一点,主要有几点:
1、上诉第4步骤,
2、在classloader树中插入自己的Classloader,在loadclass时进行映射
3、替换ActivityThread的的Instrumentation对象和Handle CallBack对象,用来拦截组件的创建过程。
4、利用反射修改成员变量,注入Context。利用反射调用隐藏方法。

7、插件中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时进行映射

8、通过activity代理方式实现加载插件中的activity是如何实现的

 要实现这一点,同样是基于上述第4点,构造出插件的Context后,通过attachBaseContext的方式,
 替换代理Activiyt的context即可。
 另外还需要在获得插件Activity对象后,通过反射给Activity的attach()方法中attach的成员变量赋值。
 
 更新:activity代理方式已放弃,不再支持,要了解实现可以查看历史版本

9、插件编译问题。

 如果插件和宿主共享依赖库,常见的如supportv4,那么编译插件的时候不可将共享库编译到插件当中,
 包括共享库的代码以及R文件,只需在编译时添加到classpath中,且插件中如果要使用共享依赖库中的资源,
 需要使用共享库的R文件来进行引用。这几点在PluginTest示例工程中有体现。

 更新:已接入gradle,通过provided方式即可,具体可参考PluginShareLib和PluginTest的build.gradle文件

10、插件Fragment 插件UI可通过fragment或者activity来实现

如果是fragment实现的插件,又分为3种:
1、fragment运行在宿主中的普通Activity中
2、fragment运行在宿主中的特定Activity中
3、fragment运行在插件中的Activity中

对第2种和第3种,fragmet的开发方式和正常开发方式没有任何区别

对第1种,fragmeng中凡是要使用context的地方,都需要使用通过PluginLoader.getDefaultPluginContext(FragmentClass)或者
通过context.createPackageContext(插件包名)获取的插件context,
那么这种fragment对其运行容器没有特殊要求

第1种Activity和第2种Activity,两者在代码上没有任何区别。主要是插件框架在运行时需要区分注入的Context的类型。

demo中都有例子。

11、插件主题

重要实现原理仍然基于上述第2、3点。

12、插件Activity的LaunchMode

要实现插件Activity的LaunchMode,需要在宿主程序中预埋若干个(standard只需1个)相应launchMode的Activity(预注
册的组件可实际存在也可不存在),在运行时进行动态映射选择。core工程的manifest中配置

13、对多Service的支持

Service的启动模式类似于Activity的singleInstance,因此为了支持插件多service,采用了和上述第12像类似的做法。

需要注意的问题

1、项目插件化后,特别需要注意的是宿主程序混淆问题。 若公共库混淆后,可能会导致非独立插件程序运行时出现classnotfound,原因很好理解。 所以公共库一定要排除混淆或者使用稳定的mapping混淆。

  具体方法:
        1、开启混淆编译宿主,保留mapping文件
        2、将插件的build.gradle文件中的provided配置换成compile, 因为provided方式提供的包不会被混淆
        3、在插件的混淆配置中apply编译宿主时产生的mapping文件。
        4、接着在插件编译脚本中开启multdex编译。并配置multdex的mainlist,使得原先所有provided的包的class被打入到副dex中。
           这样插件编译完成后,会有2个dex,1个是插件自己需要的代码,1个是原先provided后来改成了compile的那些包。
        5、再将这个原provided的包形成的dex,也就是副dex从apk中删除,再对插件apk重新签名。
        
        上述方法作者也未试过,理论上可以解决公共库混淆问题。
        gradle插件在1.5版本以后去除了指定mainlist的功能,因此在高于这个版本时指定multidex分包需要使用其他分包插件。可使用这个项目:https://github.com/ceabie/DexKnifePlugin
  
  若需要混淆core工程的代码,请参考PluginMain工程下的混淆配置

2、android sdk中的build tools版本较低时也无法编译public.xml文件,因此如果采用public.xml的方式,应使用较新版本的buildtools。

3、本项目除master分支外,其他分支不会更新维护。

更新纪录:

2016-05-18: 添加对函数式服务localservice的跨进程支持

2016-05-07: 分离插件data目录、优化编译脚本、修复若干bug

2016-04-08: 修复若干bug,优化

2016-02-24: 1、添加插件进程
             2、添加插件MultiDex支持

2016-02-18: 增加对插件使用宿主主题的支持,例如supportV7主题

2016-01-27: 1、修复几个bug
             2、增加对插件Activity透明主题支持

2016-01-16: 对系统服务增加了一层Proxy,以支持拦截系统服务的方法调用

2016-01-01: 1、添加对插件依赖插件的支持
             2、添加localservice

2015-12-27: 添加控件插件支持。可在宿主或插件布局文件中直接嵌入其他插件中定义的控件

2015-12-05: 1、修复插件so在多cpu平台下模式选择错误的问题
             2、添加对基于主题style和自定义属性的换肤功能

2015-11-22: 1、gradle插件1.3.0以上版本不支持public.xml文件也无法识别public-padding节点的文件的问题已解决,
                因此master分支切回到利用public.xml分组的实现
             2、支持插件资源文件直接通过@package:type/name方式引用宿主资源

2015-05-04: add project

联系作者: Q:15871365851,添加时请注明插件开发

Q群:116993004、207397154(已满),重要:添加前请务必仔细阅读此ReadMe!请务必仔细阅读Demo!

About

Android Plugin Framework 插件开发框架及示例程序,原理介绍等

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 95.7%
  • HTML 4.3%
Morty Proxy This is a proxified and sanitized view of the page, visit original site.