随着前面几篇博客的学习,相信大家对插件化已经有了比较清楚的认识,然而如何将插件化应用到项目中?网上已经有一些优秀的开源框架,这里要向大家推荐一个开源的动态加载框架DL, 该项目由任玉刚大神发起的,项目地址: https://github.com/singwhatiwanna/dynamic-load-apk,该项目结构图如下:
本篇博客,我主要向大家介绍一下利用DL框架进行开发的具体步骤,还有一些注意事项:
1.首先我们需要从github上获取项目代码: 项目地址: https://github.com/singwhatiwanna/dynamic-load-apk 解压后,项目目录如下
lib目录就是DL的插件库,sample目录是对应的demo
2.引入DL库 宿主工程中只要将dl-lib.jar加入libs即可,然后在gradle中引用
compile fileTree(dir: 'libs', include: ['*.jar'])
但是插件中则不同,因为DL插件需要用到DL库的类(),所以需要引入DL库,但是插件是最终要加载到宿主程序中的,宿主程序中也是引入了DL库的,如果常规办法导入DL库,则会有两份DL的拷贝,为了解决这个问题,我们让插件中的DL只是编译的时候用,但是不打包进apk。如何让它参与编译却不被打包进apk呢?在Android-studio中 只需要在插件工程中创建一个目录,比如external-jars,然后把dl-lib.jar和放进去,同时在gradle中追加如下代码即可:
provided files('external-jars/dl-lib.jar')
同样的如果宿主程序中用了support-v4.jar,那么插件中原有的support-v4.jar也不能被打包进去,也需要将support-v4.jar放到external-jars同时追加
provided files('external-jars/android-support-v4.jar')
3.插件的java代码修改 插件中的所有Activity 必须是继承自DLBasePluginActivity或者是DLBasePluginFragmentActivity。如果原有的为Activity,这里需要改为继承DLBasePluginActivity,如果原来为FragmentActivity,那么需要继承DLBasePluginFragmentActivity。
继承DLBasePluginActivity
1. public class MainActivity extends DLBasePluginActivity
继承DLBasePluginFragmentActivity
1. TestFragmentActivity extends DLBasePluginFragmentActivity
另外原有activity中所有代表context引用的this都必须改写为that 如果要调用另外一个activity,不能使用startActivity(),而是使用startPluginActivity,并且intent也要变为DLIntent:
DLIntent intent = new DLIntent(getPackageName(), ListActivity.class);
intent.putExtra(TYPE, item.getNavigationInfo());
startPluginActivity(intent);
4.调用的插件apk 在宿主工程中,首先我们需要获取要调用的插件apk对应的MainActivity,DL的demo中插件路径为 sd卡上的DynamicLoadHost目录,没有的话需要创建,或者根据自己需求进行修改.
1. String pluginFolder = Environment.getExternalStorageDirectory() + "/DynamicLoadHost";
2. File file = new File(pluginFolder);
3. File[] plugins = file.listFiles();
4. if (plugins == null || plugins.length == 0) {
5. mNoPluginTextView.setVisibility(View.VISIBLE);
6. return;
7. }
8.
9. for (File plugin : plugins) {
10. PluginItem item = new PluginItem();
11. item.pluginPath = plugin.getAbsolutePath();
12. item.packageInfo = DLUtils.getPackageInfo(this, item.pluginPath);
13. if (item.packageInfo.activities != null && item.packageInfo.activities.length > 0) {
14. item.launcherActivityName = item.packageInfo.activities[0].name;
15. }
16. mPluginItems.add(item);
17. }
接着是调起响应的apk,这时需要使用dl-lib.jar: 1) 通过Class.forName的方式获取我们需要调用的插件apk中MainActivity的class对象 2) 就上面提到的,我们需要判断该对象继承自DLBasePluginActivity还是DLBasePluginFragmentActivity,得到对应的代理class对象 3) 使用对应的代理class对象调起插件apk
1. PluginItem item = mPluginItems.get(position);
2. Class<?> proxyCls = null;
3.
4. try {
5. Class<?> cls = Class.forName(item.launcherActivityName, false,
6. DLClassLoader.getClassLoader(item.pluginPath, getApplicationContext(), getClassLoader()));
7. if (cls.asSubclass(DLBasePluginActivity.class) != null) {
8. proxyCls = DLProxyActivity.class;
9. }
10. } catch (ClassNotFoundException e) {
11. e.printStackTrace();
12. Toast.makeText(this,
13. "load plugin apk failed, load class " + item.launcherActivityName + " failed.",
14. Toast.LENGTH_SHORT).show();
15. } catch (ClassCastException e) {
16. // ignored
17. } finally {
18. if (proxyCls == null) {
19. proxyCls = DLProxyFragmentActivity.class;
20. }
21. Intent intent = new Intent(this, proxyCls);
22. intent.putExtra(DLConstants.EXTRA_DEX_PATH,
23. mPluginItems.get(position).pluginPath);
24. startActivity(intent);
25. }
dynamic-load-apk向我们展示了许多优秀的处理方法,比如: 1. 把Activity关键的生命周期方法抽象成DLPlugin接口,ProxyActivity通过DLPlugin代理调用插件Activity的生命周期; 2. 设计一个基础的BasePluginActivity类,插件项目里使用这些基类进行开发,可以以接近常规Android开发的方式开发插件项目; 3. 以类似的方式处理Service的问题; 4. 处理了大量常见的兼容性问题(比如使用Theme资源时出现的问题); 5. 处理了插件项目里的so库的加载问题; 6. 使用PluginPackage管理插件APK,从而可以方便地管理多个插件项目
处理插件项目里的so库的加载 这里需要把插件APK里面的SO库文件解压释放出来,在根据当前设备CPU的型号选择对应的SO库,并使用System.load方法加载到当前内存中来 多插件APK的管理 动态加载一个插件APK需要三个对应的DexClassLoader、AssetManager、Resources实例,可以用组合的方式创建一个PluginPackage类存放这三个变量,再创建一个管理类PluginManager,用成员变量HashMap
1.主题 dl的插件必须每个activity都单独设置主题(插件的作者说的是也可以在application上设置主题),但我实际测试,即使application设置了主题也必须每个activity都单独设置主题。 也就是说这样是不行的:
1. <application
2. android:allowBackup="true"
3. android:theme="@android:style/Theme.Holo.Light"
4. android:icon="@drawable/ic_launcher"
5. android:label="@string/app_name" >
6. <activity
7. android:name=".SampleActivity"
9. android:label="@string/app_name" >
10. <intent-filter>
11. <action android:name="android.intent.action.MAIN" />
12. <category android:name="android.intent.category.LAUNCHER" />
13. </intent-filter>
14. </activity>
15. </application>
必须这样:
1. <application
2. android:allowBackup="true"
3. android:icon="@drawable/ic_launcher"
4. android:label="@string/app_name" >
5. <activity
6. android:name=".SampleActivity"
7. android:theme="@android:style/Theme.Holo.Light.DarkActionBar"
8. android:label="@string/app_name" >
9. <intent-filter>
10. <action android:name="android.intent.action.MAIN" />
11. <category android:name="android.intent.category.LAUNCHER" />
12. </intent-filter>
13. </activity>
14. </application>
注意的是 插件只能用系统主题 不能直接定义主题 不能这样
1. android:theme="@style/AppTheme"
只能这样
1. android:theme="@android:style/Theme.Light"
虽然在某些插件上可能不按照此规则也可以正确运行 ,但是我试过绝大多数多需要满足此条件。
2.插件所需要权限需要在宿主工程中声明 3. 使用DL进行插件apk的开发规范
1) 慎用this(接口除外):因为this指向的是当前对象,即apk中的activity,但是由于activity已经不是常规意义上的activity,所以this是没有意义的,但是如果this表示的是一个接口而不是context,比如activity实现了而一个接口,那么this继续有效。
2) 使用that:既然this不能用,那就用that,that是apk中activity的基类BaseActivity中的一个成员,它在apk安装运行的时候指向this,而在未安装的时候指向宿主程序中的代理activity,anyway,that is better than this。
3) activity的成员方法调用问题:原则来说,需要通过that来调用成员方法,但是由于大部分常用的api已经被重写,所以仅仅是针对部分api才需要通过that去调用用。同时,apk安装以后仍然可以正常运行。
4) 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。
5) 目前暂不支持Service、BroadcastReceiver等需要注册才能使用的组件,但广播可以采用代码动态注册
4.插件APK的管理后台 使用动态加载的目的,就是希望可以绕过APK的安装过程升级应用的功能,如果插件APK是打包在主项目内部的那动态加载纯粹是多次一举。更多的时候我们希望可以在线下载插件APK,并且在插件APK有新版本的时候,主项目要从服务器下载最新的插件替换本地已经存在的旧插件。为此,我们应该有一个管理后台,它大概有以下功能:
5.插件APK合法性校验 加载外部的可执行代码,一个逃不开的问题就是要确保外部代码的安全性,我们可不希望加载一些来历不明的插件APK,因为这些插件有的时候能访问主项目的关键数据。 最简单可靠的做法就是校验插件APK的MD5值,如果插件APK的MD5与我们服务器预置的数值不同,就认为插件被改动过,弃用。