Android DL 插件化开发步骤与核心注意事项
DL(Dynamic Load)框架是 Android 插件化技术的一种实现方案,旨在通过动态加载 APK 来实现功能扩展和热更新。本文将详细介绍利用 DL 框架进行开发的完整流程、框架优势以及在实际开发中必须注意的关键问题。
一、利用 DL 框架进行开发的步骤
1. 获取项目代码
首先从官方仓库获取 DL 框架源码。解压后,主要目录结构如下:
lib 目录:包含 DL 的插件库文件。
sample 目录:包含对应的 Demo 示例工程。
2. 引入 DL 库
宿主工程配置
在宿主工程中,只需将 dl-lib.jar 加入 libs 目录,并在 build.gradle 中引用:
compile fileTree(dir: 'libs', include: ['*.jar'])
插件工程配置
插件工程需要用到 DL 库的类,因此也需要引入 dl-lib.jar。但由于插件最终会加载到宿主程序中,而宿主程序已经引入了该库,如果常规方式导入会导致重复拷贝。解决方案是让插件中的 DL 库仅参与编译,但不打包进 APK。
在 Android Studio 的插件工程中创建一个目录(如 external-jars),将 dl-lib.jar 放入其中,并在 build.gradle 中添加以下配置:
provided files('external-jars/dl-lib.jar')
同理,如果宿主程序使用了 support-v4.jar,插件中原有的 support-v4.jar 也不能被打包进去。需将其放入 external-jars 并追加配置:
provided files('external-jars/android-support-v4.jar')
3. 插件 Java 代码修改
插件中的所有 Activity 必须继承自 DLBasePluginActivity 或 DLBasePluginFragmentActivity。
- 若原有 Activity 为普通 Activity,改为继承
DLBasePluginActivity。
- 若原有 Activity 为 FragmentActivity,则继承
DLBasePluginFragmentActivity。
继承示例:
public class MainActivity extends DLBasePluginActivity
public class 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 目录,如果没有则需要创建,或者根据实际需求进行修改。
获取插件列表:
String pluginFolder = Environment.getExternalStorageDirectory() + "/DynamicLoadHost";
File file = new File(pluginFolder);
File[] plugins = file.listFiles();
if (plugins == null || plugins.length == 0) {
mNoPluginTextView.setVisibility(View.VISIBLE);
return;
}
for (File plugin : plugins) {
PluginItem item = new PluginItem();
item.pluginPath = plugin.getAbsolutePath();
item.packageInfo = DLUtils.getPackageInfo(this, item.pluginPath);
if (item.packageInfo.activities != null && item.packageInfo.activities.length > 0) {
item.launcherActivityName = item.packageInfo.activities[0].name;
}
mPluginItems.add(item);
}
调起插件 APK:
需要使用 dl-lib.jar 提供的机制:
- 通过
Class.forName 的方式获取我们需要调用的插件 APK 中 MainActivity 的 Class 对象。
- 判断该对象继承自
DLBasePluginActivity 还是 DLBasePluginFragmentActivity,得到对应的代理 Class 对象。
- 使用对应的代理 Class 对象调起插件 APK。
PluginItem item = mPluginItems.get(position);
Class<?> proxyCls = null;
try {
Class<?> cls = Class.forName(item.launcherActivityName, false,
DLClassLoader.getClassLoader(item.pluginPath, getApplicationContext(), getClassLoader()));
if (cls.asSubclass(DLBasePluginActivity.class) != null) {
proxyCls = DLProxyActivity.class;
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
Toast.makeText(this,
"load plugin apk failed, load class " + item.launcherActivityName + " failed.",
Toast.LENGTH_SHORT).show();
} catch (ClassCastException e) {
} finally {
if (proxyCls == null) {
proxyCls = DLProxyFragmentActivity.class;
}
Intent intent = new Intent(this, proxyCls);
intent.putExtra(DLConstants.EXTRA_DEX_PATH,
mPluginItems.get(position).pluginPath);
startActivity(intent);
}
二、DL 框架优秀之处
Dynamic Load APK 向我们展示了许多优秀的处理方法,主要体现在以下几个方面:
- 生命周期抽象:把 Activity 关键的生命周期方法抽象成
DLPlugin 接口,ProxyActivity 通过 DLPlugin 代理调用插件 Activity 的生命周期。
- 基础基类设计:设计一个基础的
BasePluginActivity 类,插件项目里使用这些基类进行开发,可以以接近常规 Android 开发的方式开发插件项目。
- Service 处理:以类似的方式处理 Service 的问题,确保服务组件也能正常运行。
- 兼容性处理:处理了大量常见的兼容性问题,比如使用 Theme 资源时出现的问题。
- SO 库加载:处理了插件项目里的 so 库的加载问题,确保原生代码能正确执行。
- APK 管理:使用
PluginPackage 管理插件 APK,从而可以方便地管理多个插件项目。
处理插件项目里的 SO 库的加载
这里需要把插件 APK 里面的 SO 库文件解压释放出来,然后根据当前设备 CPU 的型号选择对应的 SO 库,并使用 System.load 方法加载到当前内存中来。
多插件 APK 的管理
动态加载一个插件 APK 需要三个对应的 DexClassLoader、AssetManager、Resources 实例。可以用组合的方式创建一个 PluginPackage 类存放这三个变量,再创建一个管理类 PluginManager,用成员变量 HashMap 来维护这些包的信息,实现高效的查找和管理。
三、DL 插件开发注意事项
1. 主题设置
DL 的插件必须每个 Activity 都单独设置主题。虽然文档说明也可以在 Application 上设置主题,但实际测试表明,即使 Application 设置了主题,也必须每个 Activity 都单独设置主题才能生效。
错误的做法(仅在 Application 设置):
<application
android:allowBackup="true"
android:theme="@android:style/Theme.Holo.Light"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".SampleActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
正确的做法(Activity 单独设置):
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".SampleActivity"
android:theme="@android:style/Theme.Holo.Light.DarkActionBar"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
注意:插件只能用系统主题,不能直接定义自定义主题。例如不能这样写:
android:theme="@style/AppTheme"
只能使用系统内置样式,如:
android:theme="@android:style/Theme.Light"
虽然在某些特定插件上可能不按照此规则也可以正确运行,但绝大多数情况下都需要满足此条件以避免样式错乱。
2. 权限声明
插件所需要的所有权限必须在宿主工程的 Manifest 中进行声明。插件自身无法独立申请权限,必须依赖宿主授予。
3. 使用 DL 进行插件 APK 的开发规范
- 慎用 this:因为
this 指向的是当前对象,即 APK 中的 Activity。但是由于 Activity 已经不是常规意义上的 Activity,所以 this 是没有意义的。但是如果 this 表示的是一个接口而不是 Context,比如 Activity 实现了一个接口,那么 this 继续有效。
- 使用 that:既然
this 不能用,那就用 that。that 是 APK 中 Activity 的基类 BaseActivity 中的一个成员,它在 APK 安装运行的时候指向 this,而在未安装的时候指向宿主程序中的代理 Activity。总之,that is better than this。
- Activity 的成员方法调用问题:原则上需要通过
that 来调用成员方法,但是由于大部分常用的 API 已经被重写,所以仅仅是针对部分 API 才需要通过 that 去调用。同时,APK 安装以后仍然可以正常运行。
- 启动新 Activity 的约束:启动外部 Activity 不受限制,启动 APK 内部的 Activity 有限制。首先由于 APK 中的 Activity 没注册,所以不支持隐式调用;其次必须通过
BaseActivity 中定义的新方法 startActivityByProxy 和 startActivityForResultByProxy;还有就是不支持 LaunchMode。
- 组件支持:目前暂不支持 Service、BroadcastReceiver 等需要注册才能使用的组件,但广播可以采用代码动态注册。
4. 插件 APK 的管理后台
使用动态加载的目的,就是希望可以绕过 APK 的安装过程升级应用的功能。如果插件 APK 是打包在主项目内部的那动态加载纯粹是多次一举。更多的时候我们希望可以在线下载插件 APK,并且在插件 APK 有新版本的时候,主项目要从下载最新的插件替换本地已经存在的旧插件。为此,我们应该有一个管理后台,它大概有以下功能:
- 上传不同版本的插件 APK,并向 APP 主项目提供插件 APK 信息查询功能和下载功能。
- 管理在线的插件 APK,并能向不同版本号的 APP 主项目提供最合适的插件 APK。
- 万一最新的插件 APK 出现紧急 BUG,要提供旧版本回滚功能。
- 出于安全考虑应该对 APP 项目的请求信息做一些安全性校验。
5. 插件 APK 合法性校验
加载外部的可执行代码,一个逃不开的问题就是要确保外部代码的安全性。我们可不希望加载一些来历不明的插件 APK,因为这些插件有的时候能访问主项目的关键数据。
最简单可靠的做法就是校验插件 APK 的 MD5 值。如果插件 APK 的 MD5 与我们服务器预置的数值不同,就认为插件被改动过,弃用。具体流程如下:
- 服务端存储已发布插件的 MD5 值。
- 客户端下载插件后计算本地文件的 MD5。
- 对比两者是否一致,不一致则拒绝加载并提示用户。
四、总结
DL 框架为 Android 插件化提供了一种相对成熟的解决方案,通过代理模式、类加载器隔离等技术实现了功能的动态扩展。开发者在使用时需注意主题设置、Context 引用、权限声明及安全性校验等关键点。合理运用该框架可以有效提升应用的灵活性和可维护性,但也需要严格遵循其开发规范以避免潜在的运行错误。