在android的项目开发中,都会遇到后期功能拓展增强与主程序代码变更的现实矛盾,也就是程序的灵活度。

由于linux平台的安全机制,再加上dalvik的特殊机制,各种权限壁垒,使得开发一个灵活多变的程序,变得比较困难,不像pc平台下那么容易。

瞅瞅elipse的插件,瞅瞅360的插件,在android下,我们一开始很难写好一个主程序,然后通过插件机制来应对以后的功能拓展,于是程序变得不那么灵活多变了。

比如一款android下的安全软件,新版本增加了一个功能,如短信拦截,往往会因为一个模块的增加,而重新编译一个apk包,这样周而复始,哪怕只增加50kb的功能代码,用户也需要升级一个完整的apk,往往是5~6M的体积。



最近思来想去,想到一个方法,既然tencent qq在android下面可以以apk的形式来换皮肤,这资源文件的拓展都可以这样简便的搞,为何功能性的拓展就不可以?

想出来了两种解决方案。

先来说说第一种。



demo下载在最后



先说分析思路。

android下,默认的情况是,每个apk相互独立的,基本上每个应用都是一个dalvik虚拟机,都有一个uid,再配合上linux本身的权限机制,使得apk互通很难直接进行。但作为一个独立应用的集成,不管多少个apk,都可以并为一个单独的dalvik虚拟机,直观的反映给开发人员就是在shell下列出进程,那几个apk同时加载后,会一个进程存在。

这主要就是工程的清单文件 Mainfest中配置了,只需要一句话,以我的测试demo为例:

1. ....

2.

3. <manifest xmlns:android="http://schemas.android.com/apk/res/android"

4. package="org.igeek.plugintest.main"

5. <!-- 就是这句关键代码 -->

6. android:sharedUserId="org.igeek.plugintest"

7. android:versionCode="1"

8. android:versionName="1.0">

9.

10. .....

复制代码

在上面的代码中,android:sharedUserId是指共用一个uid,也就是,凡是这个属性相同的工程,都会共用同一个uid,这样,权限壁垒就消除了,dalvik也会融合为一个,可以测试一下,写几个工程,没有这个属性和有这个属性的情况下,同时运行,在列出当前进程,就直观的说明了。



程序拓展的插件化,当然需要一个主程序,主程序是实现基本功能,以及UI,还有插件的检索以及插件的调用。

这里贴出我demo中的主activity代码:

1. public class AndoirdpluginActivity extends ActivityGroup implements OnClickListener ,OnScrollCompleteListener{

2. private LinearLayout llMainLayout;

3.

4. //workspace,看看luncher的源码,这个就是桌面那个多屏的实现

5. private WorkSpace wkMain;

6. private Button btnFindPlugins;

7. private CheckBox chbAttachMain;

8.

9. private LocalActivityManager m_ActivityManager;

10.

11. //这个bean的集合,就相当于插件的描述集合

12. //每个bean也就是一个插件的各种描述

13. private List<PluginBean> plugins;

14.

15. @Override

16. public void onCreate(Bundle savedInstanceState) {

17. super.onCreate(savedInstanceState);

18. setContentView(R.layout.main);

19.

20. llMainLayout=(LinearLayout) findViewById(R.id.main_llMainLayout);

21. wkMain=(WorkSpace) findViewById(R.id.main_wkMain);

22. btnFindPlugins=(Button) findViewById(R.id.main_btnFindPlugins);

23. chbAttachMain=(CheckBox) findViewById(R.id.main_chbAttachMain);

24.

25. m_ActivityManager = getLocalActivityManager();

26.

27. wkMain.setOnScrollCompleteLinstenner(this);

28. btnFindPlugins.setOnClickListener(this);

29. }

30.

31. @Override

32. public void onClick(View v) {

33. attachPlugin(findPlugins());

34. btnFindPlugins.setVisibility(View.GONE);

35. }

36.

37. /**

38. * 加载插件列表

39. * @param plugins

40. */

41. private void attachPlugin(final List<PluginBean> plugins){

42. Log.e("ydt", " ");

43. Log.e("ydt", "----- 列出插件");

44. this.plugins=plugins;

45. for(final PluginBean plugin:plugins){

46. Button btn=new Button(this);

47. btn.setTextColor(Color.RED);

48. btn.setText(plugin.getLabel());

49.

50. llMainLayout.addView(btn);

51. //添加事件

52. btn.setOnClickListener(new OnClickListener() {

53.

54. @Override

55. public void onClick(View v) {

56. boolean isAttack=chbAttachMain.isChecked();

57.

58. Intent it=new Intent();

59. it.setAction(plugin.getPakageName());

60.

61. //是否附加为view

62. if(isAttack){

63. //这里偷下懒,这是演示插件作为view附加到主程序中的

64. for(PluginBean plugin:plugins){

65.

66. Intent itt=new Intent();

67. itt.setAction(plugin.getPakageName());

68. ViewGroup view=(ViewGroup) (m_ActivityManager.startActivity("", itt)).getDecorView();

69. wkMain.addView(view);

70. }

71. //一次性附加完毕算了,然后把按钮都删了,看着清净,这几个不是重点

72. llMainLayout.removeAllViews();

73. chbAttachMain.setVisibility(View.GONE);

74. wkMain.setToScreen(0);

75. }else{

76. //这里,不会把插件的窗体附加到主程序中,纯粹无用的演示

77. startActivity(it);

78. }

79. }

80. });

81.

82. }

83. }

84.

85. /**

86. * 查找插件

87. * @return

88. */

89. private List<PluginBean> findPlugins(){

90.

91. List<PluginBean> plugins=new ArrayList<PluginBean>();

92.

93.

94. //遍历包名,来获取插件

95. PackageManager pm=getPackageManager();

96.

97.

98. List<PackageInfo> pkgs=pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);

99. for(PackageInfo pkg :pkgs){

100. //包名

101. String packageName=pkg.packageName;

102. String sharedUserId= pkg.sharedUserId;

103.

104. //sharedUserId是开发时约定好的,这样判断是否为自己人

105. if(!"org.igeek.plugintest".equals(sharedUserId)||"org.igeek.plugintest.main".equals(packageName))

106. continue;

107.

108. //进程名

109. String prcessName=pkg.applicationInfo.processName;

110.

111. //label,也就是appName了

112. String label=pm.getApplicationLabel(pkg.applicationInfo).toString();

113.

114. PluginBean plug=new PluginBean();

115. plug.setLabel(label);

116. plug.setPakageName(packageName);

117.

118. plugins.add(plug);

119. }

120.

121.

122. return plugins;

123.

124. }

125.

126.

127. /**

128. * WorkSpace滚动到那个屏,会触发这个事件

129. * 而worksapce中每一屏又是一个插件

130. * 这个事件是用来列出当前屏幕插件所提供的应用,并且让用户调用

131. */

132. @Override

133. public void onScrollComplete(final ScrollEvent e) {

134. try {

135. final Context context = createPackageContext(plugins.get(e.curScreen).getPakageName(), Context.CONTEXT_INCLUDE_CODE|Context.CONTEXT_IGNORE_SECURITY);

136. llMainLayout.removeAllViews();

137. //这几行,通过反射获取了当前插件的描述信息,如同大部分框架的xml一样,这里算是模拟了一下IOC控制反转

138. Class clazz=context.getClassLoader().loadClass(plugins.get(e.curScreen).getPakageName()+".PluginApplication");

139. Object o=clazz.newInstance();

140. Map<String,List<String>>r=(Map<String, List<String>>) clazz.getMethod("getDesciption").invoke(o);

141. List<String> classes=r.get("classes");

142. List<String> methods=r.get("methods");

143.

144.

145. //这里,根据获得的插件所提供的功能,来生成几个按钮显示,供我们调用

146. for(final String clas:classes){

147. for(final String method:methods){

148. Button btn=new Button(this);

149.

150. btn.setText(clas+" -> "+method+" 执行");

151.

152.

153. //点击后,就执行插件所提供的方法

154. btn.setOnClickListener(new OnClickListener() {

155.

156. @Override

157. public void onClick(View v) {

158. try {

159. Class c=context.getClassLoader().loadClass(plugins.get(e.curScreen).getPakageName()+"."+clas);

160. Object o1=c.newInstance();

161.

162. //这里注意,context实际上就是句柄,这里如果涉及到窗体,plugin的句柄其实是不行的,因为它没有可以

163. //依附的窗体

164.

165. //这个context是plugin的,通过测试,dialog这类行不通,Toast是可以的,因为

166. //Toast是依附于屏幕主窗口的

167. //c.getMethod(method,Context.class).invoke(o1,context);

168.

169. //这里则传递的是主程序的句柄

170. c.getMethod(method,Context.class).invoke(o1,AndoirdpluginActivity.this);

171.

172. } catch (Exception e) {

173. // TODO Auto-generated catch block

174. e.printStackTrace();

175. }

176. }

177. });

178. llMainLayout.addView(btn);

179. }

180. }

181.

182.

183. } catch (Exception e1) {

184. // TODO Auto-generated catch block

185. e1.printStackTrace();

186. }

187.

188. }

189. }

复制代码

看注释吧,主要有两点

插件的扫描

这种方案是,每个插件以一个单独的apk发布,这样可以在程序中很灵活的知道是否有新的插件,提示用户下载安装,插件的apk清单描述为Action为非Luncher,Category为Default。

主程序侦听packgeManager的安装完成广播,之后扫描同包名(插件当然得这么定义了,只要通过packgeManager能判断是否为自己的插件就行)的apk,之后列出来,让用户选择是否加载。



插件的加载与调用

在获取包后,通过调用系统的api可以得到 sharedUserId 与主程序相同的apk的context,也就是句柄,获得了句柄,通过这个context可以得到classloader,之后就简单了,如何知道这个插件提供什么功能?

这个可以用xml描述,比如这个xml是插件apk的一个资源,就像spring这个框架一样。xml中描述了这个插件有哪些类,提供哪些方法,这些方法需要传入什么参数,返回什么类型。我的demo中为了方便,是用接口,每个插件有一个类提供一个相同的方法,来获取一个map集合,获得这个插件的描述。



ok,到这里就知道加载的插件提供什么功能了。



在上面贴出来的代码中,是循环遍历每个插件,并把每个插件提供的功能以Button的方式显示给用户,点击按钮,就执行了插件的功能,执行时,并不是activity转向(这样就无意义了),而是在主程序自身的context句柄中执行,也就是在自身的窗体中执行。

代码中有一段注释,说明,如果插件有用到context时,记得传递进去的是主程序的context,这样窗体才能附加到这个句柄中,如果传递的是插件的context,它没有一个窗体实例,是无法将一些窗体附加进去的,无任何效果。



这里只提供思路,有时间的话研究一下,看能不能搞个通用的框架出来。还有另一种方法,不通过apk形式,以后会写出来。

更多相关文章

  1. android 导入项目gradle(无法下载/下载慢/版本号与gradle plugin
  2. Android使用插件SimpleCropView
  3. Android Gradle和Gradle插件区别
  4. cordova 插件 开发添加 android 权限
  5. 【国外转】Spring Android and Maven (Maven Integration for Ec
  6. idea开发android studio插件,打印日志
  7. Andriod Studio科普篇——3.关于gradle插件的常见问题

随机推荐

  1. Android中实现类似iOS的SwitchButton控件
  2. Android数据存储方式(二)SharedPreferences
  3. android中activity无法启动的原因小结
  4. Mqtt从服务端到Android客户端搭建(Android
  5. Unity3D——android device 真机发布调试
  6. 第95章、手机服务之AudioManager服务(从零
  7. Android开发资料:Android启动优化解析
  8. 测试横竖屏切换时activity 的生命周期
  9. android 照相
  10. Android--高效地加载大图片