|
3 | 3 |
|
4 | 4 | 此项目是Android插件框架完整源码以及实例。用来开发Android插件APK,并通过动态加载的方式在宿主程序中运行。
|
5 | 5 |
|
6 |
| -若插件APK是完全独立的APK,那么插件apk也可独立安装运行。 |
7 |
| -若插件APK不是完全独立的apk,比如和插件宿主程序共用一些依赖库,那么插件apk只能在宿主程序中运行。不可独立运行。 |
8 |
| -因为此时插件apk的代码是不完整的。 |
9 |
| - |
| 6 | +插件分两类: |
| 7 | + 1、独立插件,即插件本身可以独立安装运行。 |
| 8 | + |
| 9 | + 2、非独立插件,即插件需要依赖宿主程序的class和资源,不能独立安装运行。 |
| 10 | + 非独立插件中的class不能同时存在于宿主程序中。需要打包时排除掉。 |
| 11 | + |
| 12 | + |
10 | 13 | 目录结构说明:
|
11 | 14 |
|
12 |
| - PluginCore工程是插件库核心工程。用于提供对插件功能的支持。 |
| 15 | + 1、PluginCore工程是插件库核心工程,用于提供对插件功能的支持。 |
13 | 16 |
|
14 |
| - PluginMain是用来测试的插件宿主程序Demo工程。 |
| 17 | + 2、PluginMain是用来测试的插件宿主程序Demo工程。 |
15 | 18 |
|
16 |
| - PluginShareLib是用来测试的插件宿主程序的依赖库Demo工程 |
| 19 | + 3、PluginShareLib是用来测试的插件宿主程序的依赖库Demo工程。 |
| 20 | + |
| 21 | + 4、PluginTest是用来测试的非独立插件Demo工程。此工程下有用来编译插件的ant脚本。 |
| 22 | + |
| 23 | + |
| 24 | +demo安装说明: |
17 | 25 |
|
18 |
| - PluginTest是用来测试的插件Demo工程。此工程下有用来编译插件的ant脚本。 |
| 26 | + 1、宿主程序工程可以通过ant编译或者导入eclipse后直接点击Run菜单进行安装。 |
19 | 27 |
|
20 |
| -宿主程序工程可以通过ant编译或者导入eclipse后直接点击Run菜单进行安装。 |
| 28 | + 2、插件Demo工程需要通过插件ant脚本编译。编译命令为 “ant clean debug” |
| 29 | + |
| 30 | + 原因是Demo中引用了宿主程序的依赖库。需要在编译时对共享库进行排除,如果是独立插件不需要特定的编译脚本。 |
21 | 31 |
|
22 |
| -插件Demo工程需要通过插件ant脚本编译。编译命令为 “ant clean debug” 原因是Demo中引用了宿主程序的依赖库。需要在编译时对共享库进行排除。 |
23 |
| -插件编译出来以后,可以将插件复制到sdcard,然后在宿主程序中调用PluginLoader.installPlugin("插件apk绝对路径")进行安装 |
| 32 | + 3、插件编译出来以后,可以将插件复制到sdcard,然后在宿主程序中调用PluginLoader.installPlugin("插件apk绝对路径")进行安装 |
24 | 33 |
|
25 |
| -还有一种简易的安装方式,是使用编译命令为 “ant clean debug install” 直接将插件apk安装到系统中,PluginMain工程会监听系统的应用安装广播,监听到插件apk安装广播后, |
| 34 | + 4、简易安装方式,是使用编译命令为 “ant clean debug install” 直接将插件apk安装到系统中,PluginMain工程会监听系统的应用安装广播,监听到插件apk安装广播后, |
26 | 35 | 再自动调用PluginLoader.installPlugin("/data/app/插件apk文件.apk")进行插件安装。免去复制到sdcard的过程。
|
27 | 36 |
|
| 37 | + 5、需要关注PluginTest工程的ant.properties文件和project.properties文件以及custom_rules.xml文件, |
| 38 | + 插件使用宿主程序共享库,以及共享库R引用,和编译时排除的功能,都在这3个配置文件中体现 |
28 | 39 |
|
29 |
| -(虽然没有用过apkplug、以及另外一个插件框架作者singwhatiwanna写的DL框架,但是看过他们的一些介绍文档,感觉自己的这份实现应该是更简单易用更完善。。。哈哈,是不是有王婆卖瓜的嫌疑。) |
| 40 | +编译环境:如果编译失败,请升级androidSDK |
30 | 41 |
|
31 | 42 |
|
32 | 43 | # 已支持的功能:
|
33 | 44 | 1、插件apk无需安装,由宿主程序动态加载运行。
|
34 | 45 |
|
35 |
| - 2、插件形式支持fragment和activity代理。 |
36 |
| - 这两种形式是插件开发中的两种主要形式。 |
37 |
| - |
38 |
| - 3、插件支持activity非代理模式,真正实现Activity无需注册,既不用反射,也不用代理,实现Activity完整生命周期。 |
39 |
| - |
40 |
| - 4、插件库apk可无任何特殊代码。如插件中的fragment,activity等无需继承任何特定基类或接口。完全和普通app代码没有区别. |
41 |
| - |
42 |
| - 5、支持插件共用宿主程序依赖库提供的自定义控件 |
43 |
| - |
44 |
| - 6、支持插件中使用自定义控件 |
| 46 | + 2、支持fragment、activity、service、receiver、application组件。 |
45 | 47 |
|
46 |
| - 7、支持插件资源和宿主资源混合使用。意即支持如下场景: |
47 |
| - |
48 |
| - 插件程序和宿主程序共用依赖库时插件中的布局文件中使用了宿主程序中的自定义控件,而宿主程序中的自定义控件中又使用 |
49 |
| - 了宿主程序中的布局文件。 |
50 |
| - 插件代码中无需做任何特殊处理,即可支持这种布局文件。 |
| 48 | + 3、插件中的activity、service、receiver组件拥有真正生命周期(不是使用反射实现的伪生命周期)、同时也支持Activity反射代理。 |
51 | 49 |
|
52 |
| - 8、插件中的各种资源、布局、R、以及宿主程序中的各种资源、布局、R等可随意使用、也无任何特殊代码。 |
| 50 | + 4、插件中的组件无需继承特定基类。 |
53 | 51 |
|
54 |
| - 10、支持插件使用宿主程序主题和系统主题 |
| 52 | + 5、支持插件中使用自定义控件、使用宿主程序提供的自定义控件 |
55 | 53 |
|
56 |
| - 11、解决资源id冲突问题。尝试做过插件开发的同学应该都遇到,插件资源id和宿主程序资源id可能相同,导致获取的资源不是想要的资源。 |
57 |
| - 此问题其实在android提供的编译器aapt中早已提供了支持。 |
| 54 | + 6、支持插件资源和宿主资源混合使用。意即支持如下场景: |
58 | 55 |
|
59 |
| - 12、需要关注PluginTest工程的ant.properties文件和project.properties文件以及custom_rules.xml文件,插件使用宿主程序共享库,以及共享库R引用,和编译时排除的功能,都在这3个配置文件中体现 |
| 56 | + 插件程序和宿主程序共用依赖库时,插件中的某布局文件使用了宿主程序中的自定义控件, |
| 57 | + 而宿主程序中的自定义控件又使用了宿主程序中的布局文件。此时插件布局文件相当于即 |
| 58 | + 包含了宿主布局,有包含了插件布局。插件中无需做任何特殊处理,通过layoutinflater即可使用这种布局文件。 |
| 59 | + |
| 60 | + 7、插件中的各种资源、布局、R、以及宿主程序中的各种资源、布局、R等可随意使用。 |
60 | 61 |
|
61 |
| - 13、支持使用插件中定义的主题以及style(控件style暂不支持5.x)。 |
| 62 | + 8、支持插件使用宿主程序主题、系统主题、插件自身主题以及style(控件style暂不支持5.x)。 |
62 | 63 |
|
63 |
| - (2015.07.29) |
64 |
| - 14、添加对插件Application和Service的支持 |
65 | 64 |
|
66 | 65 | # 暂不支持的功能:
|
67 | 66 |
|
68 | 67 | 1、插件中定义的控件的style 暂不支持5.x
|
69 | 68 |
|
70 |
| - 2、支持在插件中通过R文件使用宿主程序中的资源,暂不支持插件资源文件中直接使用宿主程序中的资源。但是支持间接使用。 |
| 69 | + 2、暂不支持插件资源文件中直接使用宿主程序中的资源。但是支持间接使用。 |
71 | 70 | 例如在上述“已支持的功能”6中描述的,实际就是间接使用。
|
72 | 71 |
|
73 |
| -# 后续需要解决的问题: |
74 |
| - 1、解决暂不支持的两点问题 |
| 72 | + 3、部分ROM(如coolpad和oppo某些系统版本)只支持独立插件,非独立插件时如果使用宿主资源,需要通过指定的Context获取。 |
75 | 73 |
|
76 |
| - 2、使支持插件资源文件中直接引用依赖库中的资源。目测可能需要重写android自带的aapt程序。 |
77 | 74 |
|
78 |
| - |
79 | 75 | # 实现原理简介:
|
80 | 76 | 1、插件apk的class
|
81 | 77 |
|
82 |
| - 通过构造插件apk的Dexclassloader来加载插件apk中的类。DexClassLoader的parent设置为宿主程序的classloader,即可将主程序和插件程序的class贯通 |
| 78 | + 通过构造插件apk的Dexclassloader来加载插件apk中的类。 |
| 79 | + DexClassLoader的parent设置为宿主程序的classloader,即可将主程序和插件程序的class贯通。 |
| 80 | + 若是独立插件,将parent设置为宿主程序的classloader的parent,可隔离宿主class和插件class。 |
83 | 81 |
|
84 |
| - 2、插件apk的资源 |
| 82 | + 2、插件apk的Resource |
85 | 83 |
|
86 |
| - 通过构造插件apk的AssetManager和Resouce类即可。 |
87 |
| - |
88 |
| - 本项目最关键一点功能、也是和网上其他插件项目不同的地方之一,在于 |
89 |
| - 通过addAssetsPath方法添加资源的时候,同时添加了插件程序的资源文件和宿主程序的资源。这样就 可以做到插件资源合并。很多资源文件都迎刃而解。 |
| 84 | + 直接构造插件apk的AssetManager和Resouce对象即可,需要注意的是, |
| 85 | + 通过addAssetsPath方法添加资源的时候,需要同时添加插件程序的资源文件和宿主程序的资源, |
| 86 | + 以及其依赖的资源。这样可以讲Resource合并到一个Context里面去,很多资源问题都迎刃而解。 |
90 | 87 |
|
91 |
| - 3、插件apk中的资源id |
| 88 | + 3、插件apk中的资源id冲突 |
92 | 89 |
|
93 |
| - 完成上述第二点以后,还有需要解决的难题,是宿主程序资源id和插件程序id重复的问题。 |
94 |
| - 这个问题解决办法也很简单 |
| 90 | + 完成上述第二点以后,宿主程序资源id和插件程序id可能有重复而参数冲突。 |
95 | 91 | 我们知道,资源id是在编译时生成的,其生成的规则是0xPPTTNNNN
|
96 | 92 | PP段,是用来标记apk的,默认情况下系统资源PP是01,应用程序的PP是07
|
97 |
| - TT段,是用来标记资源类型的,比如图标、布局等,相同的类型TT值相同,但是同一个TT值不代表同一种资源,例如这次编译的时候可能使用03作为layout的TT,那下次编译的时候可能会使用06作为TT的值,具体使用那个值,实际上和当前APP使用的资源类型的个数是相关联的。 |
| 93 | + TT段,是用来标记资源类型的,比如图标、布局等,相同的类型TT值相同,但是同一个TT值 |
| 94 | + 不代表同一种资源,例如这次编译的时候可能使用03作为layout的TT,那下次编译的时候可能 |
| 95 | + 会使用06作为TT的值,具体使用那个值,实际上和当前APP使用的资源类型的个数是相关联的。 |
98 | 96 | NNNN则是某种资源类型的资源id,默认从1开始,依次累加。
|
99 | 97 |
|
100 |
| - 那么我们要解决资源id问题,就可从TT的值开始入手,只要将每次编译时的TT值固定,即可是资源id达到分组的效果,从而避免重复。 |
101 |
| - 例如将宿主程序的layout资源的TT固定为03,将插件程序资源的layout的TT值固定为23,即可解决资源id重复的问题了。 |
102 |
| - |
103 |
| - 固定资源idTT值的版本也非常简单,提供一份public.xml,在public.xml中指定什么资源类型以什么TT值开头即可。 |
104 |
| - 具体public.xml如何编写,可参考PluginMain/res/values/public.xml 以及 PluginTest/res/values/public.xml俩个文件,它们是分别用来固定宿主程序和插件程序资源id的范围的。 |
105 |
| - |
106 |
| - |
| 98 | + 那么我们要解决资源id问题,就可从TT的值开始入手,只要将每次编译时的TT值固定,即可是资 |
| 99 | + 源id达到分组的效果,从而避免重复。例如将宿主程序的layout资源的TT固定为03,将插件程序 |
| 100 | + 资源的layout的TT值固定为23,即可解决资源id重复的问题了。 |
107 | 101 |
|
108 |
| - 4、插件apk的Context |
| 102 | + 固定资源idTT值的版本也非常简单,提供一份public.xml,在public.xml中指定什么资源类型以 |
| 103 | + 什么TT值开头即可。具体public.xml如何编写,可参考PluginMain/res/values/public.xml 以及 PluginTest/res/values/public.xml俩个文件,它们是分别用来固定宿主程序和插件程序资源id的范围的。 |
109 | 104 |
|
110 |
| - 构造一个Context即可,具体的Context实现请参考PluginCore/src/com/plugin/core/PluginContextTheme.java |
111 |
| - 关键是要重写几个获取资源、主题的方法,以及重写getClassLoader方法 |
112 |
| - |
113 |
| - 5、插件中的LayoutInfalter |
| 105 | + 4、插件apk的Context和LayoutInfalter |
114 | 106 |
|
115 |
| - 通过第4步构造出来的Context获取LayoutInfater即可 |
116 |
| - |
117 |
| - |
118 |
| - 6、如何实现插件代码不依赖任何特殊代码,如继承特定的基类、接口等。 |
| 107 | + 构造一个Context对象即可,具体的Context实现请参考PluginCore/src/com/plugin/core/PluginContextTheme.java |
| 108 | + 关键是要重写几个获取资源、主题的方法,以及重写getClassLoader方法,再从构造粗来的context中获取LayoutInfalter |
| 109 | + |
| 110 | + 6、插件代码不依赖特殊代码,如继承特定的基类、接口等。 |
119 | 111 |
|
120 |
| - 要做到这一点,最主要的是实现上述第4步,构造出插件的Context后,所有问题都可以得到解决。 |
| 112 | + 要做到这一点,主要有几点: |
| 113 | + 1、上诉第4步骤, |
| 114 | + 2、在classloader树中插入自己的Classloader |
| 115 | + 3、替换ActivityThread的的Instrumentation对象和Handle CallBack对象,用来拦截组件的创建过程。 |
| 116 | + 4、利用反射修改成员变量。 |
| 117 | + 实际上如果提供特定父类给插件中的组建来继承的话,整个实现过程将会愉快很多。。。 |
| 118 | + |
121 | 119 |
|
122 |
| - 7、插件中Activity无需注册,拥有完整生命周期是如何实现的。 |
| 120 | + 7、插件中Activity、service、receiver不在宿主manifest中注册即拥有完整生命周期的方法。 |
123 | 121 |
|
124 |
| - 众所周知、Activity是系统组件,必须在manifest中注册才能被系统唤起并拥有完整生命周期,通过Activity代理和放射的方式实现的 |
125 |
| - 实际是伪生命周期。并非完整生命周期。那么如果才能实现activity免注册,而且拥有完整的生命周期呢,这要从activity的启动流程中 |
126 |
| - 入手。 |
| 122 | + 由于Activity、service、receiver是系统组件,必须在manifest中注册才能被系统唤起并拥有完整生命周期。 |
| 123 | + 通过反射代理方式实现的实际是伪生命周期,并非完整生命周期。要实现面注册有2个方法。 |
127 | 124 |
|
128 |
| - App安装时,系统会扫描app的Manifest并缓存到一个xml中,activity启动时,系统会现在查找缓存的xml,如果查到了,再通过classLoad去load这个class,并构造一个activity实例。那么我们只需要将classload加载这个class的时候做一个简单的映射,让系统以为加载的是A class,而实际上加载的是B class,达到挂羊头买狗肉的效果,即可将预注册的Aclass替换为未注册的activity,从而实现插件中的Activity |
129 |
| - 完全被系统接管,而拥有完整生命周期。 |
| 125 | + a、替换classloader。适用于所有�组件。 |
| 126 | + App安装时,系统会扫描app的Manifest并缓存到一个xml中,activity启动时,系统会现在查找缓存的xml, |
| 127 | + 如果查到了,再通过classLoad去load这个class,并构造一个activity实例。那么我们只需要将classload |
| 128 | + 加载这个class的时候做一个简单的映射,让系统以为加载的是A class,而实际上加载的是B class,达到挂羊头买狗肉的效果,即可将预注册的Aclass替换为未注册的activity,从而实现插件中的Activity |
| 129 | + 完全被系统接管,而拥有完整生命周期。其他组件同理。 |
130 | 130 |
|
131 |
| - 在PluginMain和PluginTest已经添加了这种实现方式的测试实例。 |
| 131 | + 在PluginMain和PluginTest已经添加了这种实现方式的测试实例。 |
132 | 132 |
|
133 |
| - (2015.07.29) |
134 |
| - 通过在Classloader阶段进行ClassName欺骗的方式,更适用于Service和Receiver等组件。 |
135 |
| - 而Activity的欺骗行为修改为通过Instrumentation进行,可以更好的利用Intent传递参数。 |
136 |
| - 另外增加了Handle CallBack的注入,通过callback,也可以在handleMessage时className的欺骗 |
| 133 | + b、替换Instrumention。 |
| 134 | + 这种方式仅适用于Activity。通过修改Instrumentation进行拦截,可以很好的利益Intent传递参数。 |
| 135 | + 如果是Receiver,利用Handler Callback进行拦截,也能很好的利用Intent传递参数。 |
| 136 | + 如果是Service,service在创建时是没有Intent的,只能通过Classloader在loadclass时进行拦截 |
| 137 | + |
137 | 138 |
|
138 | 139 | 8、通过activity代理方式实现加载插件中的activity是如何实现的
|
139 | 140 |
|
140 |
| - 要实现这一点,同样是基于上述第4点,构造出插件的Context后,通过attachBaseContext的方式,替换代理Activiyt的context即可。 |
| 141 | + 要实现这一点,同样是基于上述第4点,构造出插件的Context后,通过attachBaseContext的方式, |
| 142 | + 替换代理Activiyt的context即可。 |
141 | 143 | 另外还需要在获得插件Activity对象后,通过反射给Activity的attach()方法中attach的成员变量赋值。
|
142 |
| - 这样可解决另外一个插件框架作者singwhatiwanna实现的代码中所谓this和that的问题。也是可以使插件Activity不需要继承任何特定基类或者接口的原因。 |
143 | 144 |
|
144 |
| - activity代理方式不建议使用,完全可以用上述第7点替代。因为代理方式可能会有兼容问题 |
145 |
| - |
146 |
| - 9、activity代理实现后,其他组件,如service等,如法炮制即可。 |
| 145 | + activity代理方式不建议使用,完全可以用上述第7点替代。 |
147 | 146 |
|
| 147 | + 9、插件编译问题。 |
148 | 148 |
|
149 |
| - 10、插件编译问题。 |
150 |
| - |
151 |
| - 如果插件和宿主共享依赖库,那边编译插件的时候不可将共享库编译到插件当中,包括共享库的代码以及R文件,但是需要在编译时添加到classpath中,且插件中如果要使用共享依赖库中的资源,需要使用共享库的R文件来进行引用。这几点在PluginTest示例工程中有体现。 |
| 149 | + 如果插件和宿主共享依赖库,常见的如supportv4,那么编译插件的时候不可将共享库编译到插件当中, |
| 150 | + 包括共享库的代码以及R文件,只需在编译时添加到classpath中,且插件中如果要使用共享依赖库中的资源, |
| 151 | + 需要使用共享库的R文件来进行引用。这几点在PluginTest示例工程中有体现。 |
152 | 152 |
|
153 | 153 |
|
154 |
| - 11、插件开发模式 |
| 154 | + 10、插件Fragment |
155 | 155 | 插件UI可通过fragment或者activity来实现
|
156 |
| - 如果是activity实现的插件,前面介绍了,分两种模式。自由模式和代理模式 |
157 | 156 |
|
158 |
| - 如果是fragment实现的插件,又分为两种 |
159 |
| - 1种是fragment运行在任意支持fragment的activity中,这种方式,在开发fragment的时候,fragmeng中凡是要使用context的地方,都需要使用通过PluginLoader.getPluginContext()获取的context,那么这种fragment对其运行容器没有特殊要求 |
| 157 | + 如果是fragment实现的插件,又分为两种: |
| 158 | + 1种是fragment运行在任意支持fragment的activity中,这种方式,在开发fragment的时候 |
| 159 | + ,fragmeng中凡是要使用context的地方,都需要使用通过PluginLoader.getPluginContext()获取的context, |
| 160 | + 那么这种fragment对其运行容器没有特殊要求 |
160 | 161 |
|
161 |
| - 还有1种是,fragment运行在PluginCore提供的PluginSpecDisplayer中,这种方式,由于其运行容器PluginSpecDisplayer的Context已经被PluginLoader.getPluginContext获取的context替换,因此这种fragment的代码和普通非插件开发时开发的fragment的代码没有任何区别。 |
| 162 | + 另1种是,fragment运行在PluginCore提供的PluginSpecDisplayer中,这种方式, |
| 163 | + 由于其运行容器PluginSpecDisplayer的Context已经被PluginLoader.getPluginContext获取的context替换, |
| 164 | + 因此这种fragment的代码和普通非插件开发时开发的fragment的代码没有任何区别。 |
162 | 165 |
|
163 | 166 |
|
164 |
| - 12、插件主题 |
165 |
| - 轻松支持插件主题。实现原理基于上述第2点。 |
| 167 | + 11、插件主题 |
| 168 | + 轻松支持插件主题,重要实现原理仍然基于上述第2点。 |
166 | 169 |
|
167 | 170 | # 需要注意的问题
|
168 |
| - 项目插件开发后,特别需要注意的是宿主程序混淆问题。宿主程序混淆后,可能会导致插件程序运行时出现classnotfound |
169 |
| - |
| 171 | + 1、项目插件开发后,特别需要注意的是宿主程序混淆问题。宿主程序混淆后,可能会导致插件程序运行时出现classnotfound |
| 172 | + 2、android gradle插件不支持public.xml中使用padding,在android Studio可能无法编译。可以使用eclipse。 |
| 173 | + 3、android sdk中的build tools版本较低时也午饭编译public.xml 文件。 |
| 174 | + |
170 | 175 | 联系作者:
|
171 |
| - QQ:15871365851 添加时请注明插件开发。 |
| 176 | + QQ:15871365851, 添加时请注明插件开发。 |
| 177 | + Q群:207397154。 |
0 commit comments